diff --git a/Converters/SKColorTypeConverter.cs b/Converters/SKColorTypeConverter.cs new file mode 100644 index 0000000..2b10804 --- /dev/null +++ b/Converters/SKColorTypeConverter.cs @@ -0,0 +1,259 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; +using System.Globalization; +using SkiaSharp; +using Microsoft.Maui.Graphics; + +namespace Microsoft.Maui.Platform.Linux.Converters; + +/// +/// Type converter for converting between MAUI Color and SKColor. +/// Enables XAML styling with Color values that get applied to Skia controls. +/// +public class SKColorTypeConverter : TypeConverter +{ + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) + { + return sourceType == typeof(string) || + sourceType == typeof(Color) || + base.CanConvertFrom(context, sourceType); + } + + public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) + { + return destinationType == typeof(string) || + destinationType == typeof(Color) || + base.CanConvertTo(context, destinationType); + } + + public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + if (value is Color mauiColor) + { + return ToSKColor(mauiColor); + } + + if (value is string str) + { + return ParseColor(str); + } + + return base.ConvertFrom(context, culture, value); + } + + public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) + { + if (value is SKColor skColor) + { + if (destinationType == typeof(string)) + { + return $"#{skColor.Alpha:X2}{skColor.Red:X2}{skColor.Green:X2}{skColor.Blue:X2}"; + } + + if (destinationType == typeof(Color)) + { + return ToMauiColor(skColor); + } + } + + return base.ConvertTo(context, culture, value, destinationType); + } + + /// + /// Converts a MAUI Color to an SKColor. + /// + public static SKColor ToSKColor(Color mauiColor) + { + return new SKColor( + (byte)(mauiColor.Red * 255), + (byte)(mauiColor.Green * 255), + (byte)(mauiColor.Blue * 255), + (byte)(mauiColor.Alpha * 255)); + } + + /// + /// Converts an SKColor to a MAUI Color. + /// + public static Color ToMauiColor(SKColor skColor) + { + return new Color( + skColor.Red / 255f, + skColor.Green / 255f, + skColor.Blue / 255f, + skColor.Alpha / 255f); + } + + /// + /// Parses a color string (hex, named, or rgb format). + /// + private static SKColor ParseColor(string colorString) + { + if (string.IsNullOrWhiteSpace(colorString)) + return SKColors.Black; + + colorString = colorString.Trim(); + + // Try hex format + if (colorString.StartsWith("#")) + { + return SKColor.Parse(colorString); + } + + // Try named colors + var namedColor = GetNamedColor(colorString.ToLowerInvariant()); + if (namedColor.HasValue) + return namedColor.Value; + + // Try rgb/rgba format + if (colorString.StartsWith("rgb", StringComparison.OrdinalIgnoreCase)) + { + return ParseRgbColor(colorString); + } + + // Fallback to SKColor.Parse + if (SKColor.TryParse(colorString, out var parsed)) + return parsed; + + return SKColors.Black; + } + + private static SKColor? GetNamedColor(string name) => name switch + { + "transparent" => SKColors.Transparent, + "black" => SKColors.Black, + "white" => SKColors.White, + "red" => SKColors.Red, + "green" => SKColors.Green, + "blue" => SKColors.Blue, + "yellow" => SKColors.Yellow, + "cyan" => SKColors.Cyan, + "magenta" => SKColors.Magenta, + "gray" or "grey" => SKColors.Gray, + "darkgray" or "darkgrey" => SKColors.DarkGray, + "lightgray" or "lightgrey" => SKColors.LightGray, + "orange" => new SKColor(0xFF, 0xA5, 0x00), + "pink" => new SKColor(0xFF, 0xC0, 0xCB), + "purple" => new SKColor(0x80, 0x00, 0x80), + "brown" => new SKColor(0xA5, 0x2A, 0x2A), + "navy" => new SKColor(0x00, 0x00, 0x80), + "teal" => new SKColor(0x00, 0x80, 0x80), + "olive" => new SKColor(0x80, 0x80, 0x00), + "silver" => new SKColor(0xC0, 0xC0, 0xC0), + "maroon" => new SKColor(0x80, 0x00, 0x00), + "lime" => new SKColor(0x00, 0xFF, 0x00), + "aqua" => new SKColor(0x00, 0xFF, 0xFF), + "fuchsia" => new SKColor(0xFF, 0x00, 0xFF), + "gold" => new SKColor(0xFF, 0xD7, 0x00), + "coral" => new SKColor(0xFF, 0x7F, 0x50), + "salmon" => new SKColor(0xFA, 0x80, 0x72), + "crimson" => new SKColor(0xDC, 0x14, 0x3C), + "indigo" => new SKColor(0x4B, 0x00, 0x82), + "violet" => new SKColor(0xEE, 0x82, 0xEE), + "turquoise" => new SKColor(0x40, 0xE0, 0xD0), + "tan" => new SKColor(0xD2, 0xB4, 0x8C), + "chocolate" => new SKColor(0xD2, 0x69, 0x1E), + "tomato" => new SKColor(0xFF, 0x63, 0x47), + "steelblue" => new SKColor(0x46, 0x82, 0xB4), + "skyblue" => new SKColor(0x87, 0xCE, 0xEB), + "slategray" or "slategrey" => new SKColor(0x70, 0x80, 0x90), + "seagreen" => new SKColor(0x2E, 0x8B, 0x57), + "royalblue" => new SKColor(0x41, 0x69, 0xE1), + "plum" => new SKColor(0xDD, 0xA0, 0xDD), + "peru" => new SKColor(0xCD, 0x85, 0x3F), + "orchid" => new SKColor(0xDA, 0x70, 0xD6), + "orangered" => new SKColor(0xFF, 0x45, 0x00), + "olivedrab" => new SKColor(0x6B, 0x8E, 0x23), + "midnightblue" => new SKColor(0x19, 0x19, 0x70), + "mediumblue" => new SKColor(0x00, 0x00, 0xCD), + "limegreen" => new SKColor(0x32, 0xCD, 0x32), + "hotpink" => new SKColor(0xFF, 0x69, 0xB4), + "honeydew" => new SKColor(0xF0, 0xFF, 0xF0), + "greenyellow" => new SKColor(0xAD, 0xFF, 0x2F), + "forestgreen" => new SKColor(0x22, 0x8B, 0x22), + "firebrick" => new SKColor(0xB2, 0x22, 0x22), + "dodgerblue" => new SKColor(0x1E, 0x90, 0xFF), + "deeppink" => new SKColor(0xFF, 0x14, 0x93), + "deepskyblue" => new SKColor(0x00, 0xBF, 0xFF), + "darkviolet" => new SKColor(0x94, 0x00, 0xD3), + "darkturquoise" => new SKColor(0x00, 0xCE, 0xD1), + "darkslategray" or "darkslategrey" => new SKColor(0x2F, 0x4F, 0x4F), + "darkred" => new SKColor(0x8B, 0x00, 0x00), + "darkorange" => new SKColor(0xFF, 0x8C, 0x00), + "darkolivegreen" => new SKColor(0x55, 0x6B, 0x2F), + "darkmagenta" => new SKColor(0x8B, 0x00, 0x8B), + "darkkhaki" => new SKColor(0xBD, 0xB7, 0x6B), + "darkgreen" => new SKColor(0x00, 0x64, 0x00), + "darkgoldenrod" => new SKColor(0xB8, 0x86, 0x0B), + "darkcyan" => new SKColor(0x00, 0x8B, 0x8B), + "darkblue" => new SKColor(0x00, 0x00, 0x8B), + "cornflowerblue" => new SKColor(0x64, 0x95, 0xED), + "cadetblue" => new SKColor(0x5F, 0x9E, 0xA0), + "blueviolet" => new SKColor(0x8A, 0x2B, 0xE2), + "azure" => new SKColor(0xF0, 0xFF, 0xFF), + "aquamarine" => new SKColor(0x7F, 0xFF, 0xD4), + "aliceblue" => new SKColor(0xF0, 0xF8, 0xFF), + _ => null + }; + + private static SKColor ParseRgbColor(string colorString) + { + try + { + var isRgba = colorString.StartsWith("rgba", StringComparison.OrdinalIgnoreCase); + var startIndex = colorString.IndexOf('('); + var endIndex = colorString.IndexOf(')'); + + if (startIndex == -1 || endIndex == -1) + return SKColors.Black; + + var values = colorString.Substring(startIndex + 1, endIndex - startIndex - 1) + .Split(',') + .Select(v => v.Trim()) + .ToArray(); + + if (values.Length < 3) + return SKColors.Black; + + var r = byte.Parse(values[0]); + var g = byte.Parse(values[1]); + var b = byte.Parse(values[2]); + byte a = 255; + + if (isRgba && values.Length >= 4) + { + var alphaValue = float.Parse(values[3], CultureInfo.InvariantCulture); + a = (byte)(alphaValue <= 1 ? alphaValue * 255 : alphaValue); + } + + return new SKColor(r, g, b, a); + } + catch + { + return SKColors.Black; + } + } +} + +/// +/// Extension methods for color conversion. +/// +public static class ColorExtensions +{ + /// + /// Converts a MAUI Color to an SKColor. + /// + public static SKColor ToSKColor(this Color color) + { + return SKColorTypeConverter.ToSKColor(color); + } + + /// + /// Converts an SKColor to a MAUI Color. + /// + public static Color ToMauiColor(this SKColor color) + { + return SKColorTypeConverter.ToMauiColor(color); + } +} diff --git a/Converters/SKRectTypeConverter.cs b/Converters/SKRectTypeConverter.cs new file mode 100644 index 0000000..c1cf0df --- /dev/null +++ b/Converters/SKRectTypeConverter.cs @@ -0,0 +1,328 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; +using System.Globalization; +using SkiaSharp; +using Microsoft.Maui; + +namespace Microsoft.Maui.Platform.Linux.Converters; + +/// +/// Type converter for converting between MAUI Thickness and SKRect (for padding/margin). +/// Enables XAML styling with Thickness values that get applied to Skia controls. +/// +public class SKRectTypeConverter : TypeConverter +{ + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) + { + return sourceType == typeof(string) || + sourceType == typeof(Thickness) || + sourceType == typeof(double) || + sourceType == typeof(float) || + base.CanConvertFrom(context, sourceType); + } + + public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) + { + return destinationType == typeof(string) || + destinationType == typeof(Thickness) || + base.CanConvertTo(context, destinationType); + } + + public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + if (value is Thickness thickness) + { + return ThicknessToSKRect(thickness); + } + + if (value is double d) + { + return new SKRect((float)d, (float)d, (float)d, (float)d); + } + + if (value is float f) + { + return new SKRect(f, f, f, f); + } + + if (value is string str) + { + return ParseRect(str); + } + + return base.ConvertFrom(context, culture, value); + } + + public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) + { + if (value is SKRect rect) + { + if (destinationType == typeof(string)) + { + return $"{rect.Left},{rect.Top},{rect.Right},{rect.Bottom}"; + } + + if (destinationType == typeof(Thickness)) + { + return SKRectToThickness(rect); + } + } + + return base.ConvertTo(context, culture, value, destinationType); + } + + /// + /// Converts a MAUI Thickness to an SKRect (used as padding storage). + /// + public static SKRect ThicknessToSKRect(Thickness thickness) + { + return new SKRect( + (float)thickness.Left, + (float)thickness.Top, + (float)thickness.Right, + (float)thickness.Bottom); + } + + /// + /// Converts an SKRect (used as padding storage) to a MAUI Thickness. + /// + public static Thickness SKRectToThickness(SKRect rect) + { + return new Thickness(rect.Left, rect.Top, rect.Right, rect.Bottom); + } + + /// + /// Parses a string into an SKRect for padding/margin. + /// Supports formats: "uniform", "horizontal,vertical", "left,top,right,bottom" + /// + private static SKRect ParseRect(string str) + { + if (string.IsNullOrWhiteSpace(str)) + return SKRect.Empty; + + str = str.Trim(); + var parts = str.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries); + + if (parts.Length == 1) + { + // Uniform padding + if (float.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var uniform)) + { + return new SKRect(uniform, uniform, uniform, uniform); + } + } + else if (parts.Length == 2) + { + // Horizontal, Vertical + if (float.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var horizontal) && + float.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var vertical)) + { + return new SKRect(horizontal, vertical, horizontal, vertical); + } + } + else if (parts.Length == 4) + { + // Left, Top, Right, Bottom + if (float.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var left) && + float.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var top) && + float.TryParse(parts[2], NumberStyles.Float, CultureInfo.InvariantCulture, out var right) && + float.TryParse(parts[3], NumberStyles.Float, CultureInfo.InvariantCulture, out var bottom)) + { + return new SKRect(left, top, right, bottom); + } + } + + return SKRect.Empty; + } +} + +/// +/// Type converter for SKSize. +/// +public class SKSizeTypeConverter : TypeConverter +{ + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) + { + return sourceType == typeof(string) || + sourceType == typeof(Size) || + base.CanConvertFrom(context, sourceType); + } + + public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) + { + return destinationType == typeof(string) || + destinationType == typeof(Size) || + base.CanConvertTo(context, destinationType); + } + + public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + if (value is Size size) + { + return new SKSize((float)size.Width, (float)size.Height); + } + + if (value is string str) + { + return ParseSize(str); + } + + return base.ConvertFrom(context, culture, value); + } + + public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) + { + if (value is SKSize size) + { + if (destinationType == typeof(string)) + { + return $"{size.Width},{size.Height}"; + } + + if (destinationType == typeof(Size)) + { + return new Size(size.Width, size.Height); + } + } + + return base.ConvertTo(context, culture, value, destinationType); + } + + private static SKSize ParseSize(string str) + { + if (string.IsNullOrWhiteSpace(str)) + return SKSize.Empty; + + str = str.Trim(); + var parts = str.Split(new[] { ',', ' ', 'x', 'X' }, StringSplitOptions.RemoveEmptyEntries); + + if (parts.Length == 1) + { + if (float.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var uniform)) + { + return new SKSize(uniform, uniform); + } + } + else if (parts.Length == 2) + { + if (float.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var width) && + float.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var height)) + { + return new SKSize(width, height); + } + } + + return SKSize.Empty; + } +} + +/// +/// Type converter for SKPoint. +/// +public class SKPointTypeConverter : TypeConverter +{ + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) + { + return sourceType == typeof(string) || + sourceType == typeof(Point) || + base.CanConvertFrom(context, sourceType); + } + + public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) + { + return destinationType == typeof(string) || + destinationType == typeof(Point) || + base.CanConvertTo(context, destinationType); + } + + public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + if (value is Point point) + { + return new SKPoint((float)point.X, (float)point.Y); + } + + if (value is string str) + { + return ParsePoint(str); + } + + return base.ConvertFrom(context, culture, value); + } + + public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) + { + if (value is SKPoint point) + { + if (destinationType == typeof(string)) + { + return $"{point.X},{point.Y}"; + } + + if (destinationType == typeof(Point)) + { + return new Point(point.X, point.Y); + } + } + + return base.ConvertTo(context, culture, value, destinationType); + } + + private static SKPoint ParsePoint(string str) + { + if (string.IsNullOrWhiteSpace(str)) + return SKPoint.Empty; + + str = str.Trim(); + var parts = str.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries); + + if (parts.Length == 2) + { + if (float.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var x) && + float.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var y)) + { + return new SKPoint(x, y); + } + } + + return SKPoint.Empty; + } +} + +/// +/// Extension methods for SkiaSharp type conversions. +/// +public static class SKTypeExtensions +{ + public static SKRect ToSKRect(this Thickness thickness) + { + return SKRectTypeConverter.ThicknessToSKRect(thickness); + } + + public static Thickness ToThickness(this SKRect rect) + { + return SKRectTypeConverter.SKRectToThickness(rect); + } + + public static SKSize ToSKSize(this Size size) + { + return new SKSize((float)size.Width, (float)size.Height); + } + + public static Size ToSize(this SKSize size) + { + return new Size(size.Width, size.Height); + } + + public static SKPoint ToSKPoint(this Point point) + { + return new SKPoint((float)point.X, (float)point.Y); + } + + public static Point ToPoint(this SKPoint point) + { + return new Point(point.X, point.Y); + } +} diff --git a/Handlers/ActivityIndicatorHandler.Linux.cs b/Handlers/ActivityIndicatorHandler.Linux.cs index 624ea32..11b9722 100644 --- a/Handlers/ActivityIndicatorHandler.Linux.cs +++ b/Handlers/ActivityIndicatorHandler.Linux.cs @@ -15,6 +15,8 @@ public partial class ActivityIndicatorHandler : ViewHandler CommandMapper = new(ViewHandler.ViewCommandMapper); @@ -40,4 +42,22 @@ public partial class ActivityIndicatorHandler : ViewHandler +/// Handler for MAUI Application on Linux. +/// Bridges the MAUI Application lifecycle with LinuxApplication. +/// +public partial class ApplicationHandler : ElementHandler +{ + public static IPropertyMapper Mapper = + new PropertyMapper(ElementHandler.ElementMapper) + { + }; + + public static CommandMapper CommandMapper = + new(ElementHandler.ElementCommandMapper) + { + [nameof(IApplication.OpenWindow)] = MapOpenWindow, + [nameof(IApplication.CloseWindow)] = MapCloseWindow, + }; + + public ApplicationHandler() : base(Mapper, CommandMapper) + { + } + + public ApplicationHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } + + protected override LinuxApplicationContext CreatePlatformElement() + { + return new LinuxApplicationContext(); + } + + protected override void ConnectHandler(LinuxApplicationContext platformView) + { + base.ConnectHandler(platformView); + platformView.Application = VirtualView; + } + + protected override void DisconnectHandler(LinuxApplicationContext platformView) + { + platformView.Application = null; + base.DisconnectHandler(platformView); + } + + public static void MapOpenWindow(ApplicationHandler handler, IApplication application, object? args) + { + if (args is IWindow window) + { + handler.PlatformView?.OpenWindow(window); + } + } + + public static void MapCloseWindow(ApplicationHandler handler, IApplication application, object? args) + { + if (args is IWindow window) + { + handler.PlatformView?.CloseWindow(window); + } + } +} + +/// +/// Platform context for the MAUI Application on Linux. +/// Manages windows and the application lifecycle. +/// +public class LinuxApplicationContext +{ + private readonly List _windows = new(); + private IApplication? _application; + + /// + /// Gets or sets the MAUI Application. + /// + public IApplication? Application + { + get => _application; + set + { + _application = value; + if (_application != null) + { + // Initialize windows from the application + foreach (var window in _application.Windows) + { + if (!_windows.Contains(window)) + { + _windows.Add(window); + } + } + } + } + } + + /// + /// Gets the list of open windows. + /// + public IReadOnlyList Windows => _windows; + + /// + /// Opens a window and creates its handler. + /// + public void OpenWindow(IWindow window) + { + if (!_windows.Contains(window)) + { + _windows.Add(window); + } + } + + /// + /// Closes a window and cleans up its handler. + /// + public void CloseWindow(IWindow window) + { + _windows.Remove(window); + + if (_windows.Count == 0) + { + // Last window closed, stop the application + LinuxApplication.Current?.MainWindow?.Stop(); + } + } + + /// + /// Gets the main window of the application. + /// + public IWindow? MainWindow => _windows.Count > 0 ? _windows[0] : null; +} diff --git a/Handlers/BorderHandler.cs b/Handlers/BorderHandler.cs index b68dec4..a0392a9 100644 --- a/Handlers/BorderHandler.cs +++ b/Handlers/BorderHandler.cs @@ -20,7 +20,9 @@ public partial class BorderHandler : ViewHandler [nameof(IBorderView.Content)] = MapContent, [nameof(IBorderStroke.Stroke)] = MapStroke, [nameof(IBorderStroke.StrokeThickness)] = MapStrokeThickness, + ["StrokeShape"] = MapStrokeShape, // StrokeShape is on Border, not IBorderStroke [nameof(IView.Background)] = MapBackground, + ["BackgroundColor"] = MapBackgroundColor, [nameof(IPadding.Padding)] = MapPadding, }; @@ -55,13 +57,25 @@ public partial class BorderHandler : ViewHandler public static void MapContent(BorderHandler handler, IBorderView border) { - if (handler.PlatformView is null) return; + if (handler.PlatformView is null || handler.MauiContext is null) return; handler.PlatformView.ClearChildren(); - if (border.PresentedContent?.Handler?.PlatformView is SkiaView skiaContent) + var content = border.PresentedContent; + if (content != null) { - handler.PlatformView.AddChild(skiaContent); + // Create handler for content if it doesn't exist + if (content.Handler == null) + { + Console.WriteLine($"[BorderHandler] Creating handler for content: {content.GetType().Name}"); + content.Handler = content.ToHandler(handler.MauiContext); + } + + if (content.Handler?.PlatformView is SkiaView skiaContent) + { + Console.WriteLine($"[BorderHandler] Adding content: {skiaContent.GetType().Name}"); + handler.PlatformView.AddChild(skiaContent); + } } } @@ -91,6 +105,17 @@ public partial class BorderHandler : ViewHandler } } + public static void MapBackgroundColor(BorderHandler handler, IBorderView border) + { + if (handler.PlatformView is null) return; + + if (border is VisualElement ve && ve.BackgroundColor != null) + { + handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor(); + handler.PlatformView.Invalidate(); + } + } + public static void MapPadding(BorderHandler handler, IBorderView border) { if (handler.PlatformView is null) return; @@ -101,4 +126,33 @@ public partial class BorderHandler : ViewHandler handler.PlatformView.PaddingRight = (float)padding.Right; handler.PlatformView.PaddingBottom = (float)padding.Bottom; } + + public static void MapStrokeShape(BorderHandler handler, IBorderView border) + { + if (handler.PlatformView is null) return; + + // StrokeShape is on the Border control class, not IBorderView interface + if (border is not Border borderControl) return; + + var shape = borderControl.StrokeShape; + if (shape is Microsoft.Maui.Controls.Shapes.RoundRectangle roundRect) + { + // RoundRectangle can have different corner radii, but we use a uniform one + // Take the top-left corner as the uniform radius + var cornerRadius = roundRect.CornerRadius; + handler.PlatformView.CornerRadius = (float)cornerRadius.TopLeft; + } + else if (shape is Microsoft.Maui.Controls.Shapes.Rectangle) + { + handler.PlatformView.CornerRadius = 0; + } + else if (shape is Microsoft.Maui.Controls.Shapes.Ellipse) + { + // For ellipse, use half the min dimension as corner radius + // This will be applied during rendering when bounds are known + handler.PlatformView.CornerRadius = float.MaxValue; // Marker for "fully rounded" + } + + handler.PlatformView.Invalidate(); + } } diff --git a/Handlers/BoxViewHandler.cs b/Handlers/BoxViewHandler.cs new file mode 100644 index 0000000..6b9ddd5 --- /dev/null +++ b/Handlers/BoxViewHandler.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Maui.Controls; +using Microsoft.Maui.Handlers; +using SkiaSharp; + +namespace Microsoft.Maui.Platform.Linux.Handlers; + +/// +/// Handler for BoxView on Linux. +/// +public partial class BoxViewHandler : ViewHandler +{ + public static IPropertyMapper Mapper = + new PropertyMapper(ViewMapper) + { + [nameof(BoxView.Color)] = MapColor, + [nameof(BoxView.CornerRadius)] = MapCornerRadius, + [nameof(IView.Background)] = MapBackground, + ["BackgroundColor"] = MapBackgroundColor, + }; + + public BoxViewHandler() : base(Mapper) + { + } + + protected override SkiaBoxView CreatePlatformView() + { + return new SkiaBoxView(); + } + + public static void MapColor(BoxViewHandler handler, BoxView boxView) + { + if (boxView.Color != null) + { + handler.PlatformView.Color = new SKColor( + (byte)(boxView.Color.Red * 255), + (byte)(boxView.Color.Green * 255), + (byte)(boxView.Color.Blue * 255), + (byte)(boxView.Color.Alpha * 255)); + } + } + + public static void MapCornerRadius(BoxViewHandler handler, BoxView boxView) + { + handler.PlatformView.CornerRadius = (float)boxView.CornerRadius.TopLeft; + } + + public static void MapBackground(BoxViewHandler handler, BoxView boxView) + { + if (boxView.Background is SolidColorBrush solidBrush && solidBrush.Color != null) + { + handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor(); + handler.PlatformView.Invalidate(); + } + } + + public static void MapBackgroundColor(BoxViewHandler handler, BoxView boxView) + { + if (boxView.BackgroundColor != null) + { + handler.PlatformView.BackgroundColor = boxView.BackgroundColor.ToSKColor(); + handler.PlatformView.Invalidate(); + } + } +} diff --git a/Handlers/ButtonHandler.Linux.cs b/Handlers/ButtonHandler.Linux.cs index ba3e1f7..54feb11 100644 --- a/Handlers/ButtonHandler.Linux.cs +++ b/Handlers/ButtonHandler.Linux.cs @@ -60,6 +60,21 @@ public partial class ButtonHandler : ViewHandler platformView.Clicked += OnClicked; platformView.Pressed += OnPressed; platformView.Released += OnReleased; + + // Manually map all properties on connect since MAUI may not trigger updates + // for properties that were set before handler connection + if (VirtualView != null) + { + MapText(this, VirtualView); + MapTextColor(this, VirtualView); + MapBackground(this, VirtualView); + MapFont(this, VirtualView); + MapPadding(this, VirtualView); + MapCornerRadius(this, VirtualView); + MapBorderColor(this, VirtualView); + MapBorderWidth(this, VirtualView); + MapIsEnabled(this, VirtualView); + } } protected override void DisconnectHandler(SkiaButton platformView) @@ -105,7 +120,8 @@ public partial class ButtonHandler : ViewHandler var background = button.Background; if (background is SolidColorBrush solidBrush && solidBrush.Color != null) { - handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor(); + // Use ButtonBackgroundColor which is used for rendering, not base BackgroundColor + handler.PlatformView.ButtonBackgroundColor = solidBrush.Color.ToSKColor(); } handler.PlatformView.Invalidate(); } @@ -156,6 +172,7 @@ public partial class ButtonHandler : ViewHandler public static void MapIsEnabled(ButtonHandler handler, IButton button) { + Console.WriteLine($"[ButtonHandler] MapIsEnabled called - Text='{handler.PlatformView.Text}', IsEnabled={button.IsEnabled}"); handler.PlatformView.IsEnabled = button.IsEnabled; handler.PlatformView.Invalidate(); } diff --git a/Handlers/ButtonHandler.cs b/Handlers/ButtonHandler.cs index 6b58bbb..33110d9 100644 --- a/Handlers/ButtonHandler.cs +++ b/Handlers/ButtonHandler.cs @@ -20,6 +20,7 @@ public partial class ButtonHandler : ViewHandler [nameof(IButtonStroke.CornerRadius)] = MapCornerRadius, [nameof(IView.Background)] = MapBackground, [nameof(IPadding.Padding)] = MapPadding, + [nameof(IView.IsEnabled)] = MapIsEnabled, }; public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper) @@ -47,6 +48,18 @@ public partial class ButtonHandler : ViewHandler platformView.Clicked += OnClicked; platformView.Pressed += OnPressed; platformView.Released += OnReleased; + + // Manually map all properties on connect since MAUI may not trigger updates + // for properties that were set before handler connection + if (VirtualView != null) + { + MapStrokeColor(this, VirtualView); + MapStrokeThickness(this, VirtualView); + MapCornerRadius(this, VirtualView); + MapBackground(this, VirtualView); + MapPadding(this, VirtualView); + MapIsEnabled(this, VirtualView); + } } protected override void DisconnectHandler(SkiaButton platformView) @@ -88,7 +101,8 @@ public partial class ButtonHandler : ViewHandler if (button.Background is SolidPaint solidPaint && solidPaint.Color is not null) { - handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor(); + // Set ButtonBackgroundColor (used for rendering) not base BackgroundColor + handler.PlatformView.ButtonBackgroundColor = solidPaint.Color.ToSKColor(); } } @@ -103,6 +117,14 @@ public partial class ButtonHandler : ViewHandler (float)padding.Right, (float)padding.Bottom); } + + public static void MapIsEnabled(ButtonHandler handler, IButton button) + { + if (handler.PlatformView is null) return; + Console.WriteLine($"[ButtonHandler] MapIsEnabled - Text='{handler.PlatformView.Text}', IsEnabled={button.IsEnabled}"); + handler.PlatformView.IsEnabled = button.IsEnabled; + handler.PlatformView.Invalidate(); + } } /// @@ -124,6 +146,21 @@ public partial class TextButtonHandler : ButtonHandler { } + protected override void ConnectHandler(SkiaButton platformView) + { + base.ConnectHandler(platformView); + + // Manually map text properties on connect since MAUI may not trigger updates + // for properties that were set before handler connection + if (VirtualView is ITextButton textButton) + { + MapText(this, textButton); + MapTextColor(this, textButton); + MapFont(this, textButton); + MapCharacterSpacing(this, textButton); + } + } + public static void MapText(TextButtonHandler handler, ITextButton button) { if (handler.PlatformView is null) return; diff --git a/Handlers/CheckBoxHandler.Linux.cs b/Handlers/CheckBoxHandler.Linux.cs index 3ea84e0..d8d81e8 100644 --- a/Handlers/CheckBoxHandler.Linux.cs +++ b/Handlers/CheckBoxHandler.Linux.cs @@ -19,6 +19,8 @@ public partial class CheckBoxHandler : ViewHandler [nameof(ICheckBox.IsChecked)] = MapIsChecked, [nameof(ICheckBox.Foreground)] = MapForeground, [nameof(IView.IsEnabled)] = MapIsEnabled, + [nameof(IView.Background)] = MapBackground, + ["BackgroundColor"] = MapBackgroundColor, }; /// @@ -90,4 +92,22 @@ public partial class CheckBoxHandler : ViewHandler handler.PlatformView.IsEnabled = checkBox.IsEnabled; handler.PlatformView.Invalidate(); } + + public static void MapBackground(CheckBoxHandler handler, ICheckBox checkBox) + { + if (checkBox.Background is SolidColorBrush solidBrush && solidBrush.Color != null) + { + handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor(); + handler.PlatformView.Invalidate(); + } + } + + public static void MapBackgroundColor(CheckBoxHandler handler, ICheckBox checkBox) + { + if (checkBox is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null) + { + handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor(); + handler.PlatformView.Invalidate(); + } + } } diff --git a/Handlers/CheckBoxHandler.cs b/Handlers/CheckBoxHandler.cs index b18830e..cfc966b 100644 --- a/Handlers/CheckBoxHandler.cs +++ b/Handlers/CheckBoxHandler.cs @@ -18,6 +18,8 @@ public partial class CheckBoxHandler : ViewHandler [nameof(ICheckBox.IsChecked)] = MapIsChecked, [nameof(ICheckBox.Foreground)] = MapForeground, [nameof(IView.Background)] = MapBackground, + [nameof(IView.VerticalLayoutAlignment)] = MapVerticalLayoutAlignment, + [nameof(IView.HorizontalLayoutAlignment)] = MapHorizontalLayoutAlignment, }; public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper) @@ -83,4 +85,32 @@ public partial class CheckBoxHandler : ViewHandler handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor(); } } + + public static void MapVerticalLayoutAlignment(CheckBoxHandler handler, ICheckBox checkBox) + { + if (handler.PlatformView is null) return; + + handler.PlatformView.VerticalOptions = checkBox.VerticalLayoutAlignment switch + { + Primitives.LayoutAlignment.Start => LayoutOptions.Start, + Primitives.LayoutAlignment.Center => LayoutOptions.Center, + Primitives.LayoutAlignment.End => LayoutOptions.End, + Primitives.LayoutAlignment.Fill => LayoutOptions.Fill, + _ => LayoutOptions.Fill + }; + } + + public static void MapHorizontalLayoutAlignment(CheckBoxHandler handler, ICheckBox checkBox) + { + if (handler.PlatformView is null) return; + + handler.PlatformView.HorizontalOptions = checkBox.HorizontalLayoutAlignment switch + { + Primitives.LayoutAlignment.Start => LayoutOptions.Start, + Primitives.LayoutAlignment.Center => LayoutOptions.Center, + Primitives.LayoutAlignment.End => LayoutOptions.End, + Primitives.LayoutAlignment.Fill => LayoutOptions.Fill, + _ => LayoutOptions.Start + }; + } } diff --git a/Handlers/CollectionViewHandler.cs b/Handlers/CollectionViewHandler.cs index 47d0141..e76a4a6 100644 --- a/Handlers/CollectionViewHandler.cs +++ b/Handlers/CollectionViewHandler.cs @@ -15,6 +15,8 @@ namespace Microsoft.Maui.Platform.Linux.Handlers; /// public partial class CollectionViewHandler : ViewHandler { + private bool _isUpdatingSelection; + public static IPropertyMapper Mapper = new PropertyMapper(ViewHandler.ViewMapper) { @@ -36,6 +38,7 @@ public partial class CollectionViewHandler : ViewHandler CommandMapper = @@ -76,21 +79,34 @@ public partial class CollectionViewHandler : ViewHandler + { + try + { + // Create view from template + var content = template.CreateContent(); + if (content is View view) + { + // Set binding context FIRST so bindings evaluate + view.BindingContext = item; + + // Force binding evaluation by accessing the visual tree + // This ensures child bindings are evaluated before handler creation + PropagateBindingContext(view, item); + + // Create handler for the view + if (view.Handler == null && handler.MauiContext != null) + { + view.Handler = view.ToHandler(handler.MauiContext); + } + + if (view.Handler?.PlatformView is SkiaView skiaView) + { + return skiaView; + } + } + else if (content is ViewCell cell) + { + cell.BindingContext = item; + var cellView = cell.View; + if (cellView != null) + { + if (cellView.Handler == null && handler.MauiContext != null) + { + cellView.Handler = cellView.ToHandler(handler.MauiContext); + } + + if (cellView.Handler?.PlatformView is SkiaView skiaView) + { + return skiaView; + } + } + } + } + catch + { + // Ignore template creation errors + } + return null; + }; + } + + handler.PlatformView.Invalidate(); } public static void MapEmptyView(CollectionViewHandler handler, CollectionView collectionView) @@ -146,19 +220,40 @@ public partial class CollectionViewHandler : ViewHandler 0) + try { - handler.PlatformView.SelectedItem = selectedItems.First(); + handler._isUpdatingSelection = true; + + // Sync selected items + var selectedItems = collectionView.SelectedItems; + if (selectedItems != null && selectedItems.Count > 0) + { + handler.PlatformView.SelectedItem = selectedItems.First(); + } + } + finally + { + handler._isUpdatingSelection = false; } } @@ -214,12 +309,26 @@ public partial class CollectionViewHandler : ViewHandler + /// Recursively propagates binding context to all child views to force binding evaluation. + /// + private static void PropagateBindingContext(View view, object? bindingContext) + { + view.BindingContext = bindingContext; + + // Propagate to children + if (view is Layout layout) + { + foreach (var child in layout.Children) + { + if (child is View childView) + { + PropagateBindingContext(childView, bindingContext); + } + } + } + else if (view is ContentView contentView && contentView.Content != null) + { + PropagateBindingContext(contentView.Content, bindingContext); + } + else if (view is Border border && border.Content is View borderContent) + { + PropagateBindingContext(borderContent, bindingContext); + } + } } diff --git a/Handlers/EditorHandler.cs b/Handlers/EditorHandler.cs index 1e0ba7e..447eede 100644 --- a/Handlers/EditorHandler.cs +++ b/Handlers/EditorHandler.cs @@ -31,6 +31,7 @@ public partial class EditorHandler : ViewHandler [nameof(IEditor.HorizontalTextAlignment)] = MapHorizontalTextAlignment, [nameof(IEditor.VerticalTextAlignment)] = MapVerticalTextAlignment, [nameof(IView.Background)] = MapBackground, + ["BackgroundColor"] = MapBackgroundColor, }; public static CommandMapper CommandMapper = @@ -82,6 +83,7 @@ public partial class EditorHandler : ViewHandler { if (handler.PlatformView is null) return; handler.PlatformView.Text = editor.Text ?? ""; + handler.PlatformView.Invalidate(); } public static void MapPlaceholder(EditorHandler handler, IEditor editor) @@ -165,4 +167,15 @@ public partial class EditorHandler : ViewHandler handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor(); } } + + public static void MapBackgroundColor(EditorHandler handler, IEditor editor) + { + if (handler.PlatformView is null) return; + + if (editor is VisualElement ve && ve.BackgroundColor != null) + { + handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor(); + handler.PlatformView.Invalidate(); + } + } } diff --git a/Handlers/EntryHandler.Linux.cs b/Handlers/EntryHandler.Linux.cs index 9c99862..1f2eac6 100644 --- a/Handlers/EntryHandler.Linux.cs +++ b/Handlers/EntryHandler.Linux.cs @@ -30,6 +30,7 @@ public partial class EntryHandler : ViewHandler [nameof(IEntry.ReturnType)] = MapReturnType, [nameof(IView.IsEnabled)] = MapIsEnabled, [nameof(IEntry.Background)] = MapBackground, + ["BackgroundColor"] = MapBackgroundColor, }; /// @@ -186,4 +187,13 @@ public partial class EntryHandler : ViewHandler } handler.PlatformView.Invalidate(); } + + public static void MapBackgroundColor(EntryHandler handler, IEntry entry) + { + if (entry is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null) + { + handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor(); + handler.PlatformView.Invalidate(); + } + } } diff --git a/Handlers/EntryHandler.cs b/Handlers/EntryHandler.cs index 1cd054a..ac0ff82 100644 --- a/Handlers/EntryHandler.cs +++ b/Handlers/EntryHandler.cs @@ -85,7 +85,10 @@ public partial class EntryHandler : ViewHandler if (handler.PlatformView is null) return; if (handler.PlatformView.Text != entry.Text) + { handler.PlatformView.Text = entry.Text ?? string.Empty; + handler.PlatformView.Invalidate(); + } } public static void MapTextColor(EntryHandler handler, IEntry entry) diff --git a/Handlers/FrameHandler.cs b/Handlers/FrameHandler.cs new file mode 100644 index 0000000..88847b7 --- /dev/null +++ b/Handlers/FrameHandler.cs @@ -0,0 +1,104 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Maui.Controls; +using Microsoft.Maui.Handlers; +using SkiaSharp; + +namespace Microsoft.Maui.Platform.Linux.Handlers; + +/// +/// Handler for Frame on Linux using SkiaFrame. +/// +public partial class FrameHandler : ViewHandler +{ + public static IPropertyMapper Mapper = + new PropertyMapper(ViewMapper) + { + [nameof(Frame.BorderColor)] = MapBorderColor, + [nameof(Frame.CornerRadius)] = MapCornerRadius, + [nameof(Frame.HasShadow)] = MapHasShadow, + [nameof(Frame.BackgroundColor)] = MapBackgroundColor, + [nameof(Frame.Padding)] = MapPadding, + [nameof(Frame.Content)] = MapContent, + }; + + public FrameHandler() : base(Mapper) + { + } + + public FrameHandler(IPropertyMapper? mapper) + : base(mapper ?? Mapper) + { + } + + protected override SkiaFrame CreatePlatformView() + { + return new SkiaFrame(); + } + + public static void MapBorderColor(FrameHandler handler, Frame frame) + { + if (frame.BorderColor != null) + { + handler.PlatformView.Stroke = new SKColor( + (byte)(frame.BorderColor.Red * 255), + (byte)(frame.BorderColor.Green * 255), + (byte)(frame.BorderColor.Blue * 255), + (byte)(frame.BorderColor.Alpha * 255)); + } + } + + public static void MapCornerRadius(FrameHandler handler, Frame frame) + { + handler.PlatformView.CornerRadius = frame.CornerRadius; + } + + public static void MapHasShadow(FrameHandler handler, Frame frame) + { + handler.PlatformView.HasShadow = frame.HasShadow; + } + + public static void MapBackgroundColor(FrameHandler handler, Frame frame) + { + if (frame.BackgroundColor != null) + { + handler.PlatformView.BackgroundColor = new SKColor( + (byte)(frame.BackgroundColor.Red * 255), + (byte)(frame.BackgroundColor.Green * 255), + (byte)(frame.BackgroundColor.Blue * 255), + (byte)(frame.BackgroundColor.Alpha * 255)); + } + } + + public static void MapPadding(FrameHandler handler, Frame frame) + { + handler.PlatformView.SetPadding( + (float)frame.Padding.Left, + (float)frame.Padding.Top, + (float)frame.Padding.Right, + (float)frame.Padding.Bottom); + } + + public static void MapContent(FrameHandler handler, Frame frame) + { + if (handler.PlatformView is null || handler.MauiContext is null) return; + + handler.PlatformView.ClearChildren(); + + var content = frame.Content; + if (content != null) + { + // Create handler for content if it doesn't exist + if (content.Handler == null) + { + content.Handler = content.ToHandler(handler.MauiContext); + } + + if (content.Handler?.PlatformView is SkiaView skiaContent) + { + handler.PlatformView.AddChild(skiaContent); + } + } + } +} diff --git a/Handlers/LabelHandler.Linux.cs b/Handlers/LabelHandler.Linux.cs index a2bb8c2..3df3fd4 100644 --- a/Handlers/LabelHandler.Linux.cs +++ b/Handlers/LabelHandler.Linux.cs @@ -26,6 +26,8 @@ public partial class LabelHandler : ViewHandler [nameof(ILabel.Padding)] = MapPadding, [nameof(ILabel.TextDecorations)] = MapTextDecorations, [nameof(ILabel.LineHeight)] = MapLineHeight, + [nameof(ILabel.Background)] = MapBackground, + ["BackgroundColor"] = MapBackgroundColor, }; /// @@ -151,4 +153,22 @@ public partial class LabelHandler : ViewHandler handler.PlatformView.LineHeight = (float)label.LineHeight; handler.PlatformView.Invalidate(); } + + public static void MapBackground(LabelHandler handler, ILabel label) + { + if (label.Background is SolidColorBrush solidBrush && solidBrush.Color != null) + { + handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor(); + handler.PlatformView.Invalidate(); + } + } + + public static void MapBackgroundColor(LabelHandler handler, ILabel label) + { + if (label is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null) + { + handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor(); + handler.PlatformView.Invalidate(); + } + } } diff --git a/Handlers/LabelHandler.cs b/Handlers/LabelHandler.cs index 726eba6..a94953f 100644 --- a/Handlers/LabelHandler.cs +++ b/Handlers/LabelHandler.cs @@ -23,8 +23,12 @@ public partial class LabelHandler : ViewHandler [nameof(ITextAlignment.VerticalTextAlignment)] = MapVerticalTextAlignment, [nameof(ILabel.TextDecorations)] = MapTextDecorations, [nameof(ILabel.LineHeight)] = MapLineHeight, + ["LineBreakMode"] = MapLineBreakMode, + ["MaxLines"] = MapMaxLines, [nameof(IPadding.Padding)] = MapPadding, [nameof(IView.Background)] = MapBackground, + [nameof(IView.VerticalLayoutAlignment)] = MapVerticalLayoutAlignment, + [nameof(IView.HorizontalLayoutAlignment)] = MapHorizontalLayoutAlignment, }; public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper) @@ -121,6 +125,37 @@ public partial class LabelHandler : ViewHandler handler.PlatformView.LineHeight = (float)label.LineHeight; } + public static void MapLineBreakMode(LabelHandler handler, ILabel label) + { + if (handler.PlatformView is null) return; + + // LineBreakMode is on Label control, not ILabel interface + if (label is Microsoft.Maui.Controls.Label mauiLabel) + { + handler.PlatformView.LineBreakMode = mauiLabel.LineBreakMode switch + { + Microsoft.Maui.LineBreakMode.NoWrap => Platform.LineBreakMode.NoWrap, + Microsoft.Maui.LineBreakMode.WordWrap => Platform.LineBreakMode.WordWrap, + Microsoft.Maui.LineBreakMode.CharacterWrap => Platform.LineBreakMode.CharacterWrap, + Microsoft.Maui.LineBreakMode.HeadTruncation => Platform.LineBreakMode.HeadTruncation, + Microsoft.Maui.LineBreakMode.TailTruncation => Platform.LineBreakMode.TailTruncation, + Microsoft.Maui.LineBreakMode.MiddleTruncation => Platform.LineBreakMode.MiddleTruncation, + _ => Platform.LineBreakMode.TailTruncation + }; + } + } + + public static void MapMaxLines(LabelHandler handler, ILabel label) + { + if (handler.PlatformView is null) return; + + // MaxLines is on Label control, not ILabel interface + if (label is Microsoft.Maui.Controls.Label mauiLabel) + { + handler.PlatformView.MaxLines = mauiLabel.MaxLines; + } + } + public static void MapPadding(LabelHandler handler, ILabel label) { if (handler.PlatformView is null) return; @@ -142,4 +177,32 @@ public partial class LabelHandler : ViewHandler handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor(); } } + + public static void MapVerticalLayoutAlignment(LabelHandler handler, ILabel label) + { + if (handler.PlatformView is null) return; + + handler.PlatformView.VerticalOptions = label.VerticalLayoutAlignment switch + { + Primitives.LayoutAlignment.Start => LayoutOptions.Start, + Primitives.LayoutAlignment.Center => LayoutOptions.Center, + Primitives.LayoutAlignment.End => LayoutOptions.End, + Primitives.LayoutAlignment.Fill => LayoutOptions.Fill, + _ => LayoutOptions.Start + }; + } + + public static void MapHorizontalLayoutAlignment(LabelHandler handler, ILabel label) + { + if (handler.PlatformView is null) return; + + handler.PlatformView.HorizontalOptions = label.HorizontalLayoutAlignment switch + { + Primitives.LayoutAlignment.Start => LayoutOptions.Start, + Primitives.LayoutAlignment.Center => LayoutOptions.Center, + Primitives.LayoutAlignment.End => LayoutOptions.End, + Primitives.LayoutAlignment.Fill => LayoutOptions.Fill, + _ => LayoutOptions.Start + }; + } } diff --git a/Handlers/LayoutHandler.Linux.cs b/Handlers/LayoutHandler.Linux.cs index 96a200a..f5fd97a 100644 --- a/Handlers/LayoutHandler.Linux.cs +++ b/Handlers/LayoutHandler.Linux.cs @@ -17,7 +17,9 @@ public partial class LayoutHandler : ViewHandler public static IPropertyMapper Mapper = new PropertyMapper(ViewHandler.ViewMapper) { [nameof(ILayout.Background)] = MapBackground, + ["BackgroundColor"] = MapBackgroundColor, [nameof(ILayout.ClipsToBounds)] = MapClipsToBounds, + [nameof(IPadding.Padding)] = MapPadding, }; /// @@ -53,8 +55,46 @@ public partial class LayoutHandler : ViewHandler return new SkiaStackLayout(); } + protected override void ConnectHandler(SkiaLayoutView platformView) + { + base.ConnectHandler(platformView); + + // Explicitly map BackgroundColor since it may be set before handler creation + // (e.g., in ItemTemplates for CollectionView) + if (VirtualView is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null) + { + platformView.BackgroundColor = ve.BackgroundColor.ToSKColor(); + platformView.Invalidate(); + } + + // Add existing children (important for template-created views) + if (VirtualView is ILayout layout && MauiContext != null) + { + for (int i = 0; i < layout.Count; i++) + { + var child = layout[i]; + if (child == null) continue; + + // Create handler for child if it doesn't exist + if (child.Handler == null) + { + child.Handler = child.ToHandler(MauiContext); + } + + if (child.Handler?.PlatformView is SkiaView skiaChild) + { + platformView.AddChild(skiaChild); + } + } + } + } + public static void MapBackground(LayoutHandler handler, ILayout layout) { + // Don't override if BackgroundColor is explicitly set + if (layout is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null) + return; + var background = layout.Background; if (background is SolidColorBrush solidBrush && solidBrush.Color != null) { @@ -63,12 +103,36 @@ public partial class LayoutHandler : ViewHandler handler.PlatformView.Invalidate(); } + public static void MapBackgroundColor(LayoutHandler handler, ILayout layout) + { + if (layout is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null) + { + handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor(); + handler.PlatformView.Invalidate(); + } + } + public static void MapClipsToBounds(LayoutHandler handler, ILayout layout) { handler.PlatformView.ClipToBounds = layout.ClipsToBounds; handler.PlatformView.Invalidate(); } + public static void MapPadding(LayoutHandler handler, ILayout layout) + { + if (layout is IPadding paddable) + { + var padding = paddable.Padding; + handler.PlatformView.Padding = new SKRect( + (float)padding.Left, + (float)padding.Top, + (float)padding.Right, + (float)padding.Bottom); + handler.PlatformView.InvalidateMeasure(); + handler.PlatformView.Invalidate(); + } + } + public static void MapAdd(LayoutHandler handler, ILayout layout, object? arg) { if (arg is LayoutHandlerUpdate update) @@ -194,9 +258,16 @@ public partial class GridHandler : LayoutHandler { [nameof(IGridLayout.ColumnSpacing)] = MapColumnSpacing, [nameof(IGridLayout.RowSpacing)] = MapRowSpacing, + [nameof(IGridLayout.RowDefinitions)] = MapRowDefinitions, + [nameof(IGridLayout.ColumnDefinitions)] = MapColumnDefinitions, }; - public GridHandler() : base(Mapper) + public static new CommandMapper GridCommandMapper = new(LayoutHandler.CommandMapper) + { + ["Add"] = MapGridAdd, + }; + + public GridHandler() : base(Mapper, GridCommandMapper) { } @@ -205,6 +276,52 @@ public partial class GridHandler : LayoutHandler return new SkiaGrid(); } + protected override void ConnectHandler(SkiaLayoutView platformView) + { + Console.WriteLine($"[GridHandler.ConnectHandler] Called! VirtualView={VirtualView?.GetType().Name}, PlatformView={platformView?.GetType().Name}, MauiContext={(MauiContext != null ? "set" : "null")}"); + base.ConnectHandler(platformView); + + // Map definitions on connect + if (VirtualView is IGridLayout gridLayout && platformView is SkiaGrid grid && MauiContext != null) + { + Console.WriteLine($"[GridHandler.ConnectHandler] Grid has {gridLayout.Count} children, RowDefs={gridLayout.RowDefinitions?.Count ?? 0}"); + UpdateRowDefinitions(grid, gridLayout); + UpdateColumnDefinitions(grid, gridLayout); + + // Add existing children (important for template-created views) + for (int i = 0; i < gridLayout.Count; i++) + { + var child = gridLayout[i]; + if (child == null) continue; + + Console.WriteLine($"[GridHandler.ConnectHandler] Child[{i}]: {child.GetType().Name}, Handler={child.Handler?.GetType().Name ?? "null"}"); + + // Create handler for child if it doesn't exist + if (child.Handler == null) + { + child.Handler = child.ToHandler(MauiContext); + Console.WriteLine($"[GridHandler.ConnectHandler] Created handler for child[{i}]: {child.Handler?.GetType().Name ?? "failed"}"); + } + + if (child.Handler?.PlatformView is SkiaView skiaChild) + { + // Get grid position from attached properties + int row = 0, column = 0, rowSpan = 1, columnSpan = 1; + if (child is Microsoft.Maui.Controls.View mauiView) + { + row = Microsoft.Maui.Controls.Grid.GetRow(mauiView); + column = Microsoft.Maui.Controls.Grid.GetColumn(mauiView); + rowSpan = Microsoft.Maui.Controls.Grid.GetRowSpan(mauiView); + columnSpan = Microsoft.Maui.Controls.Grid.GetColumnSpan(mauiView); + } + Console.WriteLine($"[GridHandler.ConnectHandler] Adding child[{i}] at row={row}, col={column}"); + grid.AddChild(skiaChild, row, column, rowSpan, columnSpan); + } + } + Console.WriteLine($"[GridHandler.ConnectHandler] Grid now has {grid.Children.Count} SkiaView children"); + } + } + public static void MapColumnSpacing(GridHandler handler, IGridLayout layout) { if (handler.PlatformView is SkiaGrid grid) @@ -222,6 +339,79 @@ public partial class GridHandler : LayoutHandler grid.Invalidate(); } } + + public static void MapRowDefinitions(GridHandler handler, IGridLayout layout) + { + if (handler.PlatformView is SkiaGrid grid) + { + UpdateRowDefinitions(grid, layout); + grid.InvalidateMeasure(); + grid.Invalidate(); + } + } + + public static void MapColumnDefinitions(GridHandler handler, IGridLayout layout) + { + if (handler.PlatformView is SkiaGrid grid) + { + UpdateColumnDefinitions(grid, layout); + grid.InvalidateMeasure(); + grid.Invalidate(); + } + } + + private static void UpdateRowDefinitions(SkiaGrid grid, IGridLayout layout) + { + grid.RowDefinitions.Clear(); + foreach (var rowDef in layout.RowDefinitions) + { + var height = rowDef.Height; + if (height.IsAbsolute) + grid.RowDefinitions.Add(new GridLength((float)height.Value, GridUnitType.Absolute)); + else if (height.IsAuto) + grid.RowDefinitions.Add(GridLength.Auto); + else // Star + grid.RowDefinitions.Add(new GridLength((float)height.Value, GridUnitType.Star)); + } + } + + private static void UpdateColumnDefinitions(SkiaGrid grid, IGridLayout layout) + { + grid.ColumnDefinitions.Clear(); + foreach (var colDef in layout.ColumnDefinitions) + { + var width = colDef.Width; + if (width.IsAbsolute) + grid.ColumnDefinitions.Add(new GridLength((float)width.Value, GridUnitType.Absolute)); + else if (width.IsAuto) + grid.ColumnDefinitions.Add(GridLength.Auto); + else // Star + grid.ColumnDefinitions.Add(new GridLength((float)width.Value, GridUnitType.Star)); + } + } + + public static void MapGridAdd(GridHandler handler, ILayout layout, object? arg) + { + if (arg is LayoutHandlerUpdate update && handler.PlatformView is SkiaGrid grid) + { + var childHandler = update.View.Handler; + if (childHandler?.PlatformView is SkiaView skiaView) + { + // Get grid position from attached properties + int row = 0, column = 0, rowSpan = 1, columnSpan = 1; + + if (update.View is Microsoft.Maui.Controls.View mauiView) + { + row = Microsoft.Maui.Controls.Grid.GetRow(mauiView); + column = Microsoft.Maui.Controls.Grid.GetColumn(mauiView); + rowSpan = Microsoft.Maui.Controls.Grid.GetRowSpan(mauiView); + columnSpan = Microsoft.Maui.Controls.Grid.GetColumnSpan(mauiView); + } + + grid.AddChild(skiaView, row, column, rowSpan, columnSpan); + } + } + } } /// diff --git a/Handlers/LayoutHandler.cs b/Handlers/LayoutHandler.cs index 3341767..3b94823 100644 --- a/Handlers/LayoutHandler.cs +++ b/Handlers/LayoutHandler.cs @@ -17,6 +17,7 @@ public partial class LayoutHandler : ViewHandler { [nameof(ILayout.ClipsToBounds)] = MapClipsToBounds, [nameof(IView.Background)] = MapBackground, + [nameof(IPadding.Padding)] = MapPadding, }; public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper) @@ -42,6 +43,38 @@ public partial class LayoutHandler : ViewHandler return new SkiaStackLayout(); } + protected override void ConnectHandler(SkiaLayoutView platformView) + { + base.ConnectHandler(platformView); + + // Create handlers for all children and add them to the platform view + if (VirtualView == null || MauiContext == null) return; + + // Explicitly map BackgroundColor since it may be set before handler creation + if (VirtualView is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null) + { + platformView.BackgroundColor = ve.BackgroundColor.ToSKColor(); + } + + for (int i = 0; i < VirtualView.Count; i++) + { + var child = VirtualView[i]; + if (child == null) continue; + + // Create handler for child if it doesn't exist + if (child.Handler == null) + { + child.Handler = child.ToHandler(MauiContext); + } + + // Add child's platform view to our layout + if (child.Handler?.PlatformView is SkiaView skiaChild) + { + platformView.AddChild(skiaChild); + } + } + } + public static void MapClipsToBounds(LayoutHandler handler, ILayout layout) { if (handler.PlatformView == null) return; @@ -102,6 +135,23 @@ public partial class LayoutHandler : ViewHandler // Force re-layout handler.PlatformView?.InvalidateMeasure(); } + + public static void MapPadding(LayoutHandler handler, ILayout layout) + { + if (handler.PlatformView == null) return; + + if (layout is IPadding paddable) + { + var padding = paddable.Padding; + handler.PlatformView.Padding = new SKRect( + (float)padding.Left, + (float)padding.Top, + (float)padding.Right, + (float)padding.Bottom); + handler.PlatformView.InvalidateMeasure(); + handler.PlatformView.Invalidate(); + } + } } /// @@ -138,6 +188,29 @@ public partial class StackLayoutHandler : LayoutHandler return new SkiaStackLayout(); } + protected override void ConnectHandler(SkiaLayoutView platformView) + { + // Set orientation first + if (platformView is SkiaStackLayout stackLayout && VirtualView is IStackLayout stackView) + { + // Determine orientation based on view type + if (VirtualView is Microsoft.Maui.Controls.HorizontalStackLayout) + { + stackLayout.Orientation = StackOrientation.Horizontal; + } + else if (VirtualView is Microsoft.Maui.Controls.VerticalStackLayout || + VirtualView is Microsoft.Maui.Controls.StackLayout) + { + stackLayout.Orientation = StackOrientation.Vertical; + } + + stackLayout.Spacing = (float)stackView.Spacing; + } + + // Let base handle children + base.ConnectHandler(platformView); + } + public static void MapSpacing(StackLayoutHandler handler, IStackLayout layout) { if (handler.PlatformView is SkiaStackLayout stackLayout) @@ -156,6 +229,8 @@ public partial class GridHandler : LayoutHandler { [nameof(IGridLayout.RowSpacing)] = MapRowSpacing, [nameof(IGridLayout.ColumnSpacing)] = MapColumnSpacing, + [nameof(IGridLayout.RowDefinitions)] = MapRowDefinitions, + [nameof(IGridLayout.ColumnDefinitions)] = MapColumnDefinitions, }; public GridHandler() : base(Mapper) @@ -167,6 +242,80 @@ public partial class GridHandler : LayoutHandler return new SkiaGrid(); } + protected override void ConnectHandler(SkiaLayoutView platformView) + { + try + { + // Don't call base - we handle children specially for Grid + if (VirtualView is not IGridLayout gridLayout || MauiContext == null || platformView is not SkiaGrid grid) return; + + Console.WriteLine($"[GridHandler] ConnectHandler: {gridLayout.Count} children, {gridLayout.RowDefinitions.Count} rows, {gridLayout.ColumnDefinitions.Count} cols"); + + // Explicitly map BackgroundColor since it may be set before handler creation + if (VirtualView is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null) + { + platformView.BackgroundColor = ve.BackgroundColor.ToSKColor(); + } + + // Explicitly map Padding since it may be set before handler creation + if (VirtualView is IPadding paddable) + { + var padding = paddable.Padding; + platformView.Padding = new SKRect( + (float)padding.Left, + (float)padding.Top, + (float)padding.Right, + (float)padding.Bottom); + Console.WriteLine($"[GridHandler] Applied Padding: L={padding.Left}, T={padding.Top}, R={padding.Right}, B={padding.Bottom}"); + } + + // Map row/column definitions first + MapRowDefinitions(this, gridLayout); + MapColumnDefinitions(this, gridLayout); + + // Add each child with its row/column position + for (int i = 0; i < gridLayout.Count; i++) + { + var child = gridLayout[i]; + if (child == null) continue; + + Console.WriteLine($"[GridHandler] Processing child {i}: {child.GetType().Name}"); + + // Create handler for child if it doesn't exist + if (child.Handler == null) + { + child.Handler = child.ToHandler(MauiContext); + } + + // Get grid position from attached properties + int row = 0, column = 0, rowSpan = 1, columnSpan = 1; + if (child is Microsoft.Maui.Controls.View mauiView) + { + row = Microsoft.Maui.Controls.Grid.GetRow(mauiView); + column = Microsoft.Maui.Controls.Grid.GetColumn(mauiView); + rowSpan = Microsoft.Maui.Controls.Grid.GetRowSpan(mauiView); + columnSpan = Microsoft.Maui.Controls.Grid.GetColumnSpan(mauiView); + } + + Console.WriteLine($"[GridHandler] Child {i} at row={row}, col={column}, handler={child.Handler?.GetType().Name}"); + + // Add child's platform view to our grid + if (child.Handler?.PlatformView is SkiaView skiaChild) + { + grid.AddChild(skiaChild, row, column, rowSpan, columnSpan); + Console.WriteLine($"[GridHandler] Added child {i} to grid"); + } + } + Console.WriteLine($"[GridHandler] ConnectHandler complete"); + } + catch (Exception ex) + { + Console.WriteLine($"[GridHandler] EXCEPTION in ConnectHandler: {ex.GetType().Name}: {ex.Message}"); + Console.WriteLine($"[GridHandler] Stack trace: {ex.StackTrace}"); + throw; + } + } + public static void MapRowSpacing(GridHandler handler, IGridLayout layout) { if (handler.PlatformView is SkiaGrid grid) @@ -182,4 +331,38 @@ public partial class GridHandler : LayoutHandler grid.ColumnSpacing = (float)layout.ColumnSpacing; } } + + public static void MapRowDefinitions(GridHandler handler, IGridLayout layout) + { + if (handler.PlatformView is not SkiaGrid grid) return; + + grid.RowDefinitions.Clear(); + foreach (var rowDef in layout.RowDefinitions) + { + var height = rowDef.Height; + if (height.IsAbsolute) + grid.RowDefinitions.Add(new Microsoft.Maui.Platform.GridLength((float)height.Value, Microsoft.Maui.Platform.GridUnitType.Absolute)); + else if (height.IsAuto) + grid.RowDefinitions.Add(Microsoft.Maui.Platform.GridLength.Auto); + else // Star + grid.RowDefinitions.Add(new Microsoft.Maui.Platform.GridLength((float)height.Value, Microsoft.Maui.Platform.GridUnitType.Star)); + } + } + + public static void MapColumnDefinitions(GridHandler handler, IGridLayout layout) + { + if (handler.PlatformView is not SkiaGrid grid) return; + + grid.ColumnDefinitions.Clear(); + foreach (var colDef in layout.ColumnDefinitions) + { + var width = colDef.Width; + if (width.IsAbsolute) + grid.ColumnDefinitions.Add(new Microsoft.Maui.Platform.GridLength((float)width.Value, Microsoft.Maui.Platform.GridUnitType.Absolute)); + else if (width.IsAuto) + grid.ColumnDefinitions.Add(Microsoft.Maui.Platform.GridLength.Auto); + else // Star + grid.ColumnDefinitions.Add(new Microsoft.Maui.Platform.GridLength((float)width.Value, Microsoft.Maui.Platform.GridUnitType.Star)); + } + } } diff --git a/Handlers/NavigationPageHandler.cs b/Handlers/NavigationPageHandler.cs index 3ab62f2..8e899a4 100644 --- a/Handlers/NavigationPageHandler.cs +++ b/Handlers/NavigationPageHandler.cs @@ -6,6 +6,7 @@ using Microsoft.Maui.Graphics; using Microsoft.Maui.Controls; using Microsoft.Maui.Platform; using SkiaSharp; +using System.Collections.Specialized; namespace Microsoft.Maui.Platform.Linux.Handlers; @@ -50,10 +51,15 @@ public partial class NavigationPageHandler : ViewHandler _toolbarSubscriptions = new(); + + private void MapToolbarItems(SkiaPage skiaPage, Page page) + { + if (skiaPage is SkiaContentPage contentPage) + { + Console.WriteLine($"[NavigationPageHandler] MapToolbarItems for '{page.Title}', count={page.ToolbarItems.Count}"); + + contentPage.ToolbarItems.Clear(); + foreach (var item in page.ToolbarItems) + { + Console.WriteLine($"[NavigationPageHandler] Adding toolbar item: '{item.Text}', Order={item.Order}"); + // Default and Primary should both be treated as Primary (shown in toolbar) + // Only Secondary goes to overflow menu + var order = item.Order == ToolbarItemOrder.Secondary + ? SkiaToolbarItemOrder.Secondary + : SkiaToolbarItemOrder.Primary; + + // Create a command that invokes the Clicked event + var toolbarItem = item; // Capture for closure + var clickCommand = new RelayCommand(() => + { + Console.WriteLine($"[NavigationPageHandler] ToolbarItem '{toolbarItem.Text}' clicked, invoking..."); + // Use IMenuItemController to send the click + if (toolbarItem is IMenuItemController menuController) + { + menuController.Activate(); + } + else + { + // Fallback: invoke Command if set + toolbarItem.Command?.Execute(toolbarItem.CommandParameter); + } + }); + + contentPage.ToolbarItems.Add(new SkiaToolbarItem + { + Text = item.Text ?? "", + Order = order, + Command = clickCommand + }); + } + + // Subscribe to ToolbarItems changes if not already subscribed + if (page.ToolbarItems is INotifyCollectionChanged notifyCollection && !_toolbarSubscriptions.ContainsKey(page)) + { + Console.WriteLine($"[NavigationPageHandler] Subscribing to ToolbarItems changes for '{page.Title}'"); + notifyCollection.CollectionChanged += (s, e) => + { + Console.WriteLine($"[NavigationPageHandler] ToolbarItems changed for '{page.Title}', action={e.Action}"); + MapToolbarItems(skiaPage, page); + skiaPage.Invalidate(); + }; + _toolbarSubscriptions[page] = (skiaPage, notifyCollection); + } + } + } + + private void OnVirtualViewPushed(object? sender, Microsoft.Maui.Controls.NavigationEventArgs e) + { + try + { + Console.WriteLine($"[NavigationPageHandler] VirtualView Pushed: {e.Page?.Title}"); + if (e.Page == null || PlatformView == null || MauiContext == null) return; + + // Ensure the page has a handler + if (e.Page.Handler == null) + { + Console.WriteLine($"[NavigationPageHandler] Creating handler for page: {e.Page.GetType().Name}"); + e.Page.Handler = e.Page.ToHandler(MauiContext); + Console.WriteLine($"[NavigationPageHandler] Handler created: {e.Page.Handler?.GetType().Name}"); + } + + if (e.Page.Handler?.PlatformView is SkiaPage skiaPage) + { + Console.WriteLine($"[NavigationPageHandler] Setting up skiaPage, content: {skiaPage.Content?.GetType().Name ?? "null"}"); + skiaPage.ShowNavigationBar = true; + skiaPage.TitleBarColor = PlatformView.BarBackgroundColor; + skiaPage.TitleTextColor = PlatformView.BarTextColor; + Console.WriteLine($"[NavigationPageHandler] Mapping toolbar items"); + MapToolbarItems(skiaPage, e.Page); + Console.WriteLine($"[NavigationPageHandler] Pushing page to platform"); + PlatformView.Push(skiaPage, true); + Console.WriteLine($"[NavigationPageHandler] Push complete"); + } + } + catch (Exception ex) + { + Console.WriteLine($"[NavigationPageHandler] EXCEPTION in OnVirtualViewPushed: {ex.GetType().Name}: {ex.Message}"); + Console.WriteLine($"[NavigationPageHandler] Stack trace: {ex.StackTrace}"); + throw; + } + } + + private void OnVirtualViewPopped(object? sender, Microsoft.Maui.Controls.NavigationEventArgs e) + { + Console.WriteLine($"[NavigationPageHandler] VirtualView Popped: {e.Page?.Title}"); + // Pop on the platform side to sync with MAUI navigation + PlatformView?.Pop(true); + } + + private void OnVirtualViewPoppedToRoot(object? sender, Microsoft.Maui.Controls.NavigationEventArgs e) + { + Console.WriteLine($"[NavigationPageHandler] VirtualView PoppedToRoot"); + PlatformView?.PopToRoot(true); } private void OnPushed(object? sender, NavigationEventArgs e) @@ -81,7 +266,12 @@ public partial class NavigationPageHandler : ViewHandler 1) + { + // Don't trigger another pop on platform side + VirtualView.Navigation.RemovePage(VirtualView.Navigation.NavigationStack.Last()); + } } private void OnPoppedToRoot(object? sender, NavigationEventArgs e) @@ -131,14 +321,29 @@ public partial class NavigationPageHandler : ViewHandler +/// Simple relay command for invoking actions. +/// +internal class RelayCommand : System.Windows.Input.ICommand +{ + private readonly Action _execute; + private readonly Func? _canExecute; + + public RelayCommand(Action execute, Func? canExecute = null) + { + _execute = execute ?? throw new ArgumentNullException(nameof(execute)); + _canExecute = canExecute; + } + + public event EventHandler? CanExecuteChanged; + + public bool CanExecute(object? parameter) => _canExecute?.Invoke() ?? true; + + public void Execute(object? parameter) => _execute(); + + public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty); +} diff --git a/Handlers/PageHandler.cs b/Handlers/PageHandler.cs index 34aba24..67e42e7 100644 --- a/Handlers/PageHandler.cs +++ b/Handlers/PageHandler.cs @@ -58,6 +58,7 @@ public partial class PageHandler : ViewHandler private void OnAppearing(object? sender, EventArgs e) { + Console.WriteLine($"[PageHandler] OnAppearing received for: {VirtualView?.Title}"); (VirtualView as IPageController)?.SendAppearing(); } @@ -133,18 +134,29 @@ public partial class ContentPageHandler : PageHandler public static void MapContent(ContentPageHandler handler, ContentPage page) { - if (handler.PlatformView is null) return; + if (handler.PlatformView is null || handler.MauiContext is null) return; // Get the platform view for the content var content = page.Content; if (content != null) { - // The content's handler should provide the platform view - var contentHandler = content.Handler; - if (contentHandler?.PlatformView is SkiaView skiaContent) + // Create handler for content if it doesn't exist + if (content.Handler == null) { + Console.WriteLine($"[ContentPageHandler] Creating handler for content: {content.GetType().Name}"); + content.Handler = content.ToHandler(handler.MauiContext); + } + + // The content's handler should provide the platform view + if (content.Handler?.PlatformView is SkiaView skiaContent) + { + Console.WriteLine($"[ContentPageHandler] Setting content: {skiaContent.GetType().Name}"); handler.PlatformView.Content = skiaContent; } + else + { + Console.WriteLine($"[ContentPageHandler] Content handler PlatformView is not SkiaView: {content.Handler?.PlatformView?.GetType().Name ?? "null"}"); + } } else { diff --git a/Handlers/PickerHandler.cs b/Handlers/PickerHandler.cs index d53dd74..2c015be 100644 --- a/Handlers/PickerHandler.cs +++ b/Handlers/PickerHandler.cs @@ -6,6 +6,7 @@ using Microsoft.Maui.Graphics; using Microsoft.Maui.Controls; using Microsoft.Maui.Platform; using SkiaSharp; +using System.Collections.Specialized; namespace Microsoft.Maui.Platform.Linux.Handlers; @@ -25,6 +26,7 @@ public partial class PickerHandler : ViewHandler [nameof(IPicker.HorizontalTextAlignment)] = MapHorizontalTextAlignment, [nameof(IPicker.VerticalTextAlignment)] = MapVerticalTextAlignment, [nameof(IView.Background)] = MapBackground, + [nameof(Picker.ItemsSource)] = MapItemsSource, }; public static CommandMapper CommandMapper = @@ -32,6 +34,8 @@ public partial class PickerHandler : ViewHandler { }; + private INotifyCollectionChanged? _itemsCollection; + public PickerHandler() : base(Mapper, CommandMapper) { } @@ -51,6 +55,13 @@ public partial class PickerHandler : ViewHandler base.ConnectHandler(platformView); platformView.SelectedIndexChanged += OnSelectedIndexChanged; + // Subscribe to items collection changes + if (VirtualView is Picker picker && picker.Items is INotifyCollectionChanged items) + { + _itemsCollection = items; + _itemsCollection.CollectionChanged += OnItemsCollectionChanged; + } + // Load items ReloadItems(); } @@ -58,9 +69,21 @@ public partial class PickerHandler : ViewHandler protected override void DisconnectHandler(SkiaPicker platformView) { platformView.SelectedIndexChanged -= OnSelectedIndexChanged; + + if (_itemsCollection != null) + { + _itemsCollection.CollectionChanged -= OnItemsCollectionChanged; + _itemsCollection = null; + } + base.DisconnectHandler(platformView); } + private void OnItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + ReloadItems(); + } + private void OnSelectedIndexChanged(object? sender, EventArgs e) { if (VirtualView is null || PlatformView is null) return; @@ -130,4 +153,9 @@ public partial class PickerHandler : ViewHandler handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor(); } } + + public static void MapItemsSource(PickerHandler handler, IPicker picker) + { + handler.ReloadItems(); + } } diff --git a/Handlers/ProgressBarHandler.Linux.cs b/Handlers/ProgressBarHandler.Linux.cs index 23ee3b6..e12f5fe 100644 --- a/Handlers/ProgressBarHandler.Linux.cs +++ b/Handlers/ProgressBarHandler.Linux.cs @@ -15,6 +15,8 @@ public partial class ProgressBarHandler : ViewHandler CommandMapper = new(ViewHandler.ViewCommandMapper); @@ -40,4 +42,22 @@ public partial class ProgressBarHandler : ViewHandler +/// Handler for ScrollView on Linux using SkiaScrollView. +/// +public partial class ScrollViewHandler : ViewHandler +{ + public static IPropertyMapper Mapper = + new PropertyMapper(ViewMapper) + { + [nameof(IScrollView.Content)] = MapContent, + [nameof(IScrollView.HorizontalScrollBarVisibility)] = MapHorizontalScrollBarVisibility, + [nameof(IScrollView.VerticalScrollBarVisibility)] = MapVerticalScrollBarVisibility, + [nameof(IScrollView.Orientation)] = MapOrientation, + }; + + public static CommandMapper CommandMapper = + new(ViewCommandMapper) + { + [nameof(IScrollView.RequestScrollTo)] = MapRequestScrollTo + }; + + public ScrollViewHandler() : base(Mapper, CommandMapper) + { + } + + public ScrollViewHandler(IPropertyMapper? mapper) + : base(mapper ?? Mapper, CommandMapper) + { + } + + protected override SkiaScrollView CreatePlatformView() + { + return new SkiaScrollView(); + } + + public static void MapContent(ScrollViewHandler handler, IScrollView scrollView) + { + if (handler.PlatformView == null || handler.MauiContext == null) + return; + + var content = scrollView.PresentedContent; + if (content != null) + { + Console.WriteLine($"[ScrollViewHandler] MapContent: {content.GetType().Name}"); + + // Create handler for content if it doesn't exist + if (content.Handler == null) + { + content.Handler = content.ToHandler(handler.MauiContext); + } + + if (content.Handler?.PlatformView is SkiaView skiaContent) + { + Console.WriteLine($"[ScrollViewHandler] Setting content: {skiaContent.GetType().Name}"); + handler.PlatformView.Content = skiaContent; + } + } + else + { + handler.PlatformView.Content = null; + } + } + + public static void MapHorizontalScrollBarVisibility(ScrollViewHandler handler, IScrollView scrollView) + { + handler.PlatformView.HorizontalScrollBarVisibility = scrollView.HorizontalScrollBarVisibility switch + { + Microsoft.Maui.ScrollBarVisibility.Always => ScrollBarVisibility.Always, + Microsoft.Maui.ScrollBarVisibility.Never => ScrollBarVisibility.Never, + _ => ScrollBarVisibility.Default + }; + } + + public static void MapVerticalScrollBarVisibility(ScrollViewHandler handler, IScrollView scrollView) + { + handler.PlatformView.VerticalScrollBarVisibility = scrollView.VerticalScrollBarVisibility switch + { + Microsoft.Maui.ScrollBarVisibility.Always => ScrollBarVisibility.Always, + Microsoft.Maui.ScrollBarVisibility.Never => ScrollBarVisibility.Never, + _ => ScrollBarVisibility.Default + }; + } + + public static void MapOrientation(ScrollViewHandler handler, IScrollView scrollView) + { + handler.PlatformView.Orientation = scrollView.Orientation switch + { + Microsoft.Maui.ScrollOrientation.Horizontal => ScrollOrientation.Horizontal, + Microsoft.Maui.ScrollOrientation.Both => ScrollOrientation.Both, + Microsoft.Maui.ScrollOrientation.Neither => ScrollOrientation.Neither, + _ => ScrollOrientation.Vertical + }; + } + + public static void MapRequestScrollTo(ScrollViewHandler handler, IScrollView scrollView, object? args) + { + if (args is ScrollToRequest request) + { + // Instant means no animation, so we pass !Instant for animated parameter + handler.PlatformView.ScrollTo((float)request.HorizontalOffset, (float)request.VerticalOffset, !request.Instant); + } + } +} diff --git a/Handlers/ShellHandler.cs b/Handlers/ShellHandler.cs index 6c52b76..bd7a2b7 100644 --- a/Handlers/ShellHandler.cs +++ b/Handlers/ShellHandler.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Maui.Controls; using Microsoft.Maui.Handlers; using Microsoft.Maui.Graphics; using SkiaSharp; @@ -10,13 +11,13 @@ namespace Microsoft.Maui.Platform.Linux.Handlers; /// /// Handler for Shell on Linux using Skia rendering. /// -public partial class ShellHandler : ViewHandler +public partial class ShellHandler : ViewHandler { - public static IPropertyMapper Mapper = new PropertyMapper(ViewHandler.ViewMapper) + public static IPropertyMapper Mapper = new PropertyMapper(ViewHandler.ViewMapper) { }; - public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper) + public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper) { }; @@ -39,12 +40,26 @@ public partial class ShellHandler : ViewHandler base.ConnectHandler(platformView); platformView.FlyoutIsPresentedChanged += OnFlyoutIsPresentedChanged; platformView.Navigated += OnNavigated; + + // Subscribe to Shell navigation events + if (VirtualView != null) + { + VirtualView.Navigating += OnShellNavigating; + VirtualView.Navigated += OnShellNavigated; + } } protected override void DisconnectHandler(SkiaShell platformView) { platformView.FlyoutIsPresentedChanged -= OnFlyoutIsPresentedChanged; platformView.Navigated -= OnNavigated; + + if (VirtualView != null) + { + VirtualView.Navigating -= OnShellNavigating; + VirtualView.Navigated -= OnShellNavigated; + } + base.DisconnectHandler(platformView); } @@ -55,6 +70,24 @@ public partial class ShellHandler : ViewHandler private void OnNavigated(object? sender, ShellNavigationEventArgs e) { - // Handle navigation events + // Handle platform navigation events + } + + private void OnShellNavigating(object? sender, ShellNavigatingEventArgs e) + { + Console.WriteLine($"[ShellHandler] Shell Navigating to: {e.Target?.Location}"); + + // Route to platform view + if (PlatformView != null && e.Target?.Location != null) + { + var route = e.Target.Location.ToString().TrimStart('/'); + Console.WriteLine($"[ShellHandler] Routing to: {route}"); + PlatformView.GoToAsync(route); + } + } + + private void OnShellNavigated(object? sender, ShellNavigatedEventArgs e) + { + Console.WriteLine($"[ShellHandler] Shell Navigated to: {e.Current?.Location}"); } } diff --git a/Handlers/SliderHandler.Linux.cs b/Handlers/SliderHandler.Linux.cs index dcf32cd..5f76b5b 100644 --- a/Handlers/SliderHandler.Linux.cs +++ b/Handlers/SliderHandler.Linux.cs @@ -19,6 +19,8 @@ public partial class SliderHandler : ViewHandler [nameof(ISlider.MaximumTrackColor)] = MapMaximumTrackColor, [nameof(ISlider.ThumbColor)] = MapThumbColor, [nameof(IView.IsEnabled)] = MapIsEnabled, + [nameof(IView.Background)] = MapBackground, + ["BackgroundColor"] = MapBackgroundColor, }; public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper); @@ -100,4 +102,22 @@ public partial class SliderHandler : ViewHandler handler.PlatformView.IsEnabled = slider.IsEnabled; handler.PlatformView.Invalidate(); } + + public static void MapBackground(SliderHandler handler, ISlider slider) + { + if (slider.Background is SolidColorBrush solidBrush && solidBrush.Color != null) + { + handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor(); + handler.PlatformView.Invalidate(); + } + } + + public static void MapBackgroundColor(SliderHandler handler, ISlider slider) + { + if (slider is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null) + { + handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor(); + handler.PlatformView.Invalidate(); + } + } } diff --git a/Handlers/SliderHandler.cs b/Handlers/SliderHandler.cs index 31ba9fc..2eb4583 100644 --- a/Handlers/SliderHandler.cs +++ b/Handlers/SliderHandler.cs @@ -22,6 +22,7 @@ public partial class SliderHandler : ViewHandler [nameof(ISlider.MaximumTrackColor)] = MapMaximumTrackColor, [nameof(ISlider.ThumbColor)] = MapThumbColor, [nameof(IView.Background)] = MapBackground, + [nameof(IView.IsEnabled)] = MapIsEnabled, }; public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper) @@ -48,6 +49,15 @@ public partial class SliderHandler : ViewHandler platformView.ValueChanged += OnValueChanged; platformView.DragStarted += OnDragStarted; platformView.DragCompleted += OnDragCompleted; + + // Sync properties that may have been set before handler connection + if (VirtualView != null) + { + MapMinimum(this, VirtualView); + MapMaximum(this, VirtualView); + MapValue(this, VirtualView); + MapIsEnabled(this, VirtualView); + } } protected override void DisconnectHandler(SkiaSlider platformView) @@ -133,4 +143,11 @@ public partial class SliderHandler : ViewHandler handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor(); } } + + public static void MapIsEnabled(SliderHandler handler, ISlider slider) + { + if (handler.PlatformView is null) return; + handler.PlatformView.IsEnabled = slider.IsEnabled; + handler.PlatformView.Invalidate(); + } } diff --git a/Handlers/SwitchHandler.Linux.cs b/Handlers/SwitchHandler.Linux.cs index fd90d66..fd9873e 100644 --- a/Handlers/SwitchHandler.Linux.cs +++ b/Handlers/SwitchHandler.Linux.cs @@ -16,6 +16,8 @@ public partial class SwitchHandler : ViewHandler [nameof(ISwitch.TrackColor)] = MapTrackColor, [nameof(ISwitch.ThumbColor)] = MapThumbColor, [nameof(IView.IsEnabled)] = MapIsEnabled, + [nameof(IView.Background)] = MapBackground, + ["BackgroundColor"] = MapBackgroundColor, }; public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper); @@ -71,4 +73,22 @@ public partial class SwitchHandler : ViewHandler handler.PlatformView.IsEnabled = @switch.IsEnabled; handler.PlatformView.Invalidate(); } + + public static void MapBackground(SwitchHandler handler, ISwitch @switch) + { + if (@switch.Background is SolidColorBrush solidBrush && solidBrush.Color != null) + { + handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor(); + handler.PlatformView.Invalidate(); + } + } + + public static void MapBackgroundColor(SwitchHandler handler, ISwitch @switch) + { + if (@switch is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null) + { + handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor(); + handler.PlatformView.Invalidate(); + } + } } diff --git a/Handlers/WebViewHandler.cs b/Handlers/WebViewHandler.cs new file mode 100644 index 0000000..d4e5e79 --- /dev/null +++ b/Handlers/WebViewHandler.cs @@ -0,0 +1,96 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Maui.Controls; +using Microsoft.Maui.Handlers; +using Microsoft.Maui.Platform; + +namespace Microsoft.Maui.Platform.Linux.Handlers; + +/// +/// Handler for WebView control on Linux using WebKitGTK. +/// +public partial class WebViewHandler : ViewHandler +{ + public static IPropertyMapper Mapper = new PropertyMapper(ViewHandler.ViewMapper) + { + [nameof(IWebView.Source)] = MapSource, + }; + + public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper) + { + [nameof(IWebView.GoBack)] = MapGoBack, + [nameof(IWebView.GoForward)] = MapGoForward, + [nameof(IWebView.Reload)] = MapReload, + }; + + public WebViewHandler() : base(Mapper, CommandMapper) + { + } + + public WebViewHandler(IPropertyMapper? mapper = null, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } + + protected override SkiaWebView CreatePlatformView() + { + return new SkiaWebView(); + } + + protected override void ConnectHandler(SkiaWebView platformView) + { + base.ConnectHandler(platformView); + + platformView.Navigating += OnNavigating; + platformView.Navigated += OnNavigated; + } + + protected override void DisconnectHandler(SkiaWebView platformView) + { + platformView.Navigating -= OnNavigating; + platformView.Navigated -= OnNavigated; + + base.DisconnectHandler(platformView); + } + + private void OnNavigating(object? sender, WebNavigatingEventArgs e) + { + // Forward to virtual view if needed + } + + private void OnNavigated(object? sender, WebNavigatedEventArgs e) + { + // Forward to virtual view if needed + } + + public static void MapSource(WebViewHandler handler, IWebView webView) + { + if (handler.PlatformView == null) return; + + var source = webView.Source; + if (source is UrlWebViewSource urlSource) + { + handler.PlatformView.Source = urlSource.Url ?? ""; + } + else if (source is HtmlWebViewSource htmlSource) + { + handler.PlatformView.Html = htmlSource.Html ?? ""; + } + } + + public static void MapGoBack(WebViewHandler handler, IWebView webView, object? args) + { + handler.PlatformView?.GoBack(); + } + + public static void MapGoForward(WebViewHandler handler, IWebView webView, object? args) + { + handler.PlatformView?.GoForward(); + } + + public static void MapReload(WebViewHandler handler, IWebView webView, object? args) + { + handler.PlatformView?.Reload(); + } +} diff --git a/Handlers/WindowHandler.cs b/Handlers/WindowHandler.cs index 62b25b2..7dc08aa 100644 --- a/Handlers/WindowHandler.cs +++ b/Handlers/WindowHandler.cs @@ -141,6 +141,7 @@ public partial class WindowHandler : ElementHandler /// /// Skia window wrapper for Linux display servers. +/// Handles rendering of content and popup overlays automatically. /// public class SkiaWindow { @@ -164,6 +165,28 @@ public class SkiaWindow } } + /// + /// Renders the window content and popup overlays to the canvas. + /// This should be called by the platform rendering loop. + /// + public void Render(SKCanvas canvas) + { + // Clear background + canvas.Clear(SKColors.White); + + // Draw main content + if (_content != null) + { + _content.Measure(new SKSize(_width, _height)); + _content.Arrange(new SKRect(0, 0, _width, _height)); + _content.Draw(canvas); + } + + // Draw popup overlays on top (dropdowns, date pickers, etc.) + // This ensures popups always render above all other content + SkiaView.DrawPopupOverlays(canvas); + } + public string Title { get => _title; diff --git a/Hosting/LinuxMauiAppBuilderExtensions.cs b/Hosting/LinuxMauiAppBuilderExtensions.cs index 62b0530..baa6339 100644 --- a/Hosting/LinuxMauiAppBuilderExtensions.cs +++ b/Hosting/LinuxMauiAppBuilderExtensions.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.ComponentModel; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Maui.ApplicationModel; @@ -8,9 +9,11 @@ using Microsoft.Maui.ApplicationModel.Communication; using Microsoft.Maui.ApplicationModel.DataTransfer; using Microsoft.Maui.Hosting; using Microsoft.Maui.Platform.Linux.Services; +using Microsoft.Maui.Platform.Linux.Converters; using Microsoft.Maui.Storage; using Microsoft.Maui.Platform.Linux.Handlers; using Microsoft.Maui.Controls; +using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Hosting; @@ -47,51 +50,69 @@ public static class LinuxMauiAppBuilderExtensions builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); + // Register type converters for XAML support + RegisterTypeConverters(); + // Register Linux-specific handlers builder.ConfigureMauiHandlers(handlers => { - // Phase 1 - MVP controls - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); + // Application handler + handlers.AddHandler(); - // Phase 2 - Input controls - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); + // Core controls + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); - // Phase 2 - Image & Graphics - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); + // Layout controls + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); - // Phase 3 - Collection Views + // Picker controls + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + + // Progress & Activity + handlers.AddHandler(); + handlers.AddHandler(); + + // Image & Graphics + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + + // Collection Views handlers.AddHandler(); + handlers.AddHandler(); - // Phase 4 - Pages & Navigation + // Pages & Navigation handlers.AddHandler(); handlers.AddHandler(); handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); - // Phase 5 - Advanced Controls - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - - // Phase 7 - Additional Controls - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - - // Window handler - handlers.AddHandler(); + // Application & Window + handlers.AddHandler(); + handlers.AddHandler(); }); // Store options for later use @@ -99,6 +120,18 @@ public static class LinuxMauiAppBuilderExtensions return builder; } + + /// + /// Registers custom type converters for Linux platform. + /// + private static void RegisterTypeConverters() + { + // Register SkiaSharp type converters for XAML styling support + TypeDescriptor.AddAttributes(typeof(SKColor), new TypeConverterAttribute(typeof(SKColorTypeConverter))); + TypeDescriptor.AddAttributes(typeof(SKRect), new TypeConverterAttribute(typeof(SKRectTypeConverter))); + TypeDescriptor.AddAttributes(typeof(SKSize), new TypeConverterAttribute(typeof(SKSizeTypeConverter))); + TypeDescriptor.AddAttributes(typeof(SKPoint), new TypeConverterAttribute(typeof(SKPointTypeConverter))); + } } /// diff --git a/Hosting/LinuxMauiContext.cs b/Hosting/LinuxMauiContext.cs new file mode 100644 index 0000000..995c22b --- /dev/null +++ b/Hosting/LinuxMauiContext.cs @@ -0,0 +1,299 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Maui.Animations; +using Microsoft.Maui.Dispatching; +using Microsoft.Maui.Platform; +using SkiaSharp; + +namespace Microsoft.Maui.Platform.Linux.Hosting; + +/// +/// Linux-specific implementation of IMauiContext. +/// Provides the infrastructure for creating handlers and accessing platform services. +/// +public class LinuxMauiContext : IMauiContext +{ + private readonly IServiceProvider _services; + private readonly IMauiHandlersFactory _handlers; + private readonly LinuxApplication _linuxApp; + private IAnimationManager? _animationManager; + private IDispatcher? _dispatcher; + + public LinuxMauiContext(IServiceProvider services, LinuxApplication linuxApp) + { + _services = services ?? throw new ArgumentNullException(nameof(services)); + _linuxApp = linuxApp ?? throw new ArgumentNullException(nameof(linuxApp)); + _handlers = services.GetRequiredService(); + } + + /// + public IServiceProvider Services => _services; + + /// + public IMauiHandlersFactory Handlers => _handlers; + + /// + /// Gets the Linux application instance. + /// + public LinuxApplication LinuxApp => _linuxApp; + + /// + /// Gets the animation manager. + /// + public IAnimationManager AnimationManager + { + get + { + _animationManager ??= _services.GetService() + ?? new LinuxAnimationManager(new LinuxTicker()); + return _animationManager; + } + } + + /// + /// Gets the dispatcher for UI thread operations. + /// + public IDispatcher Dispatcher + { + get + { + _dispatcher ??= _services.GetService() + ?? new LinuxDispatcher(); + return _dispatcher; + } + } +} + +/// +/// Scoped MAUI context for a specific window or view hierarchy. +/// +public class ScopedLinuxMauiContext : IMauiContext +{ + private readonly LinuxMauiContext _parent; + + public ScopedLinuxMauiContext(LinuxMauiContext parent) + { + _parent = parent ?? throw new ArgumentNullException(nameof(parent)); + } + + public IServiceProvider Services => _parent.Services; + public IMauiHandlersFactory Handlers => _parent.Handlers; +} + +/// +/// Linux dispatcher for UI thread operations. +/// +internal class LinuxDispatcher : IDispatcher +{ + private readonly object _lock = new(); + private readonly Queue _queue = new(); + private bool _isDispatching; + + public bool IsDispatchRequired => false; // Linux uses single-threaded event loop + + public IDispatcherTimer CreateTimer() + { + return new LinuxDispatcherTimer(); + } + + public bool Dispatch(Action action) + { + if (action == null) + return false; + + lock (_lock) + { + _queue.Enqueue(action); + } + + ProcessQueue(); + return true; + } + + public bool DispatchDelayed(TimeSpan delay, Action action) + { + if (action == null) + return false; + + Task.Delay(delay).ContinueWith(_ => Dispatch(action)); + return true; + } + + private void ProcessQueue() + { + if (_isDispatching) + return; + + _isDispatching = true; + try + { + while (true) + { + Action? action; + lock (_lock) + { + if (_queue.Count == 0) + break; + action = _queue.Dequeue(); + } + action?.Invoke(); + } + } + finally + { + _isDispatching = false; + } + } +} + +/// +/// Linux dispatcher timer implementation. +/// +internal class LinuxDispatcherTimer : IDispatcherTimer +{ + private Timer? _timer; + private TimeSpan _interval = TimeSpan.FromMilliseconds(16); // ~60fps default + private bool _isRunning; + private bool _isRepeating = true; + + public TimeSpan Interval + { + get => _interval; + set => _interval = value; + } + + public bool IsRunning => _isRunning; + + public bool IsRepeating + { + get => _isRepeating; + set => _isRepeating = value; + } + + public event EventHandler? Tick; + + public void Start() + { + if (_isRunning) + return; + + _isRunning = true; + _timer = new Timer(OnTimerCallback, null, _interval, _isRepeating ? _interval : Timeout.InfiniteTimeSpan); + } + + public void Stop() + { + _isRunning = false; + _timer?.Dispose(); + _timer = null; + } + + private void OnTimerCallback(object? state) + { + Tick?.Invoke(this, EventArgs.Empty); + + if (!_isRepeating) + { + Stop(); + } + } +} + +/// +/// Linux animation manager. +/// +internal class LinuxAnimationManager : IAnimationManager +{ + private readonly List _animations = new(); + private readonly ITicker _ticker; + + public LinuxAnimationManager(ITicker ticker) + { + _ticker = ticker; + _ticker.Fire = OnTickerFire; + } + + public double SpeedModifier { get; set; } = 1.0; + public bool AutoStartTicker { get; set; } = true; + + public ITicker Ticker => _ticker; + + public void Add(Microsoft.Maui.Animations.Animation animation) + { + _animations.Add(animation); + + if (AutoStartTicker && !_ticker.IsRunning) + { + _ticker.Start(); + } + } + + public void Remove(Microsoft.Maui.Animations.Animation animation) + { + _animations.Remove(animation); + + if (_animations.Count == 0 && _ticker.IsRunning) + { + _ticker.Stop(); + } + } + + private void OnTickerFire() + { + var animations = _animations.ToArray(); + foreach (var animation in animations) + { + animation.Tick(16.0 / 1000.0 * SpeedModifier); // ~60fps + if (animation.HasFinished) + { + Remove(animation); + } + } + } +} + +/// +/// Linux ticker for animation timing. +/// +internal class LinuxTicker : ITicker +{ + private Timer? _timer; + private bool _isRunning; + private int _maxFps = 60; + + public bool IsRunning => _isRunning; + + public bool SystemEnabled => true; + + public int MaxFps + { + get => _maxFps; + set => _maxFps = Math.Max(1, Math.Min(120, value)); + } + + public Action? Fire { get; set; } + + public void Start() + { + if (_isRunning) + return; + + _isRunning = true; + var interval = TimeSpan.FromMilliseconds(1000.0 / _maxFps); + _timer = new Timer(OnTimerCallback, null, TimeSpan.Zero, interval); + } + + public void Stop() + { + _isRunning = false; + _timer?.Dispose(); + _timer = null; + } + + private void OnTimerCallback(object? state) + { + Fire?.Invoke(); + } +} diff --git a/Hosting/LinuxProgramHost.cs b/Hosting/LinuxProgramHost.cs index 7e11d32..54e61ee 100644 --- a/Hosting/LinuxProgramHost.cs +++ b/Hosting/LinuxProgramHost.cs @@ -4,39 +4,151 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Maui.Hosting; using Microsoft.Maui.Controls; +using Microsoft.Maui.Platform; using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Hosting; +/// +/// Entry point for running MAUI applications on Linux. +/// public static class LinuxProgramHost { + /// + /// Runs the MAUI application on Linux. + /// + /// The application type. + /// Command line arguments. public static void Run(string[] args) where TApp : class, IApplication, new() { Run(args, null); } + /// + /// Runs the MAUI application on Linux with additional configuration. + /// + /// The application type. + /// Command line arguments. + /// Optional builder configuration action. public static void Run(string[] args, Action? configure) where TApp : class, IApplication, new() { + // Build the MAUI application var builder = MauiApp.CreateBuilder(); builder.UseLinux(); configure?.Invoke(builder); builder.UseMauiApp(); var mauiApp = builder.Build(); + // Get application options var options = mauiApp.Services.GetService() ?? new LinuxApplicationOptions(); ParseCommandLineOptions(args, options); + // Create Linux application using var linuxApp = new LinuxApplication(); linuxApp.Initialize(options); - // Create comprehensive demo UI with ALL controls - var rootView = CreateComprehensiveDemo(); - linuxApp.RootView = rootView; + // Create MAUI context + var mauiContext = new LinuxMauiContext(mauiApp.Services, linuxApp); + // Get the MAUI application instance + var application = mauiApp.Services.GetService(); + + // Ensure Application.Current is set - required for Shell.Current to work + if (application is Application app && Application.Current == null) + { + // Use reflection to set Current since it has a protected setter + var currentProperty = typeof(Application).GetProperty("Current"); + currentProperty?.SetValue(null, app); + } + + // Try to render the application's main page + SkiaView? rootView = null; + + if (application != null) + { + rootView = RenderApplication(application, mauiContext, options); + } + + // Fallback to demo if no application view is available + if (rootView == null) + { + Console.WriteLine("No application page found. Showing demo UI."); + rootView = CreateDemoView(); + } + + linuxApp.RootView = rootView; linuxApp.Run(); } + /// + /// Renders the MAUI application and returns the root SkiaView. + /// + private static SkiaView? RenderApplication(IApplication application, LinuxMauiContext mauiContext, LinuxApplicationOptions options) + { + try + { + // For Applications, we need to create a window + if (application is Application app) + { + Page? mainPage = app.MainPage; + + // If no MainPage set, check for windows + if (mainPage == null && application.Windows.Count > 0) + { + var existingWindow = application.Windows[0]; + if (existingWindow.Content is Page page) + { + mainPage = page; + } + } + + if (mainPage != null) + { + // Create a MAUI Window and add it to the application + // This ensures Shell.Current works properly (it reads from Application.Current.Windows[0].Page) + if (app.Windows.Count == 0) + { + var mauiWindow = new Microsoft.Maui.Controls.Window(mainPage); + + // Try OpenWindow first + app.OpenWindow(mauiWindow); + + // If that didn't work, use reflection to add directly to _windows + if (app.Windows.Count == 0) + { + var windowsField = typeof(Application).GetField("_windows", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + if (windowsField?.GetValue(app) is System.Collections.IList windowsList) + { + windowsList.Add(mauiWindow); + } + } + } + + return RenderPage(mainPage, mauiContext); + } + } + + return null; + } + catch (Exception ex) + { + Console.WriteLine($"Error rendering application: {ex.Message}"); + Console.WriteLine(ex.StackTrace); + return null; + } + } + + /// + /// Renders a MAUI Page to a SkiaView. + /// + private static SkiaView? RenderPage(Page page, LinuxMauiContext mauiContext) + { + var renderer = new LinuxViewRenderer(mauiContext); + return renderer.RenderPage(page); + } + private static void ParseCommandLineOptions(string[] args, LinuxApplicationOptions options) { for (int i = 0; i < args.Length; i++) @@ -54,15 +166,22 @@ public static class LinuxProgramHost options.Height = h; i++; break; + case "--demo": + // Force demo mode + options.ForceDemo = true; + break; } } } - private static SkiaView CreateComprehensiveDemo() + /// + /// Creates a demo view showcasing all controls. + /// + public static SkiaView CreateDemoView() { // Create scrollable container var scroll = new SkiaScrollView(); - + var root = new SkiaStackLayout { Orientation = StackOrientation.Vertical, @@ -72,18 +191,18 @@ public static class LinuxProgramHost root.Padding = new SKRect(20, 20, 20, 20); // ========== TITLE ========== - root.AddChild(new SkiaLabel - { - Text = "MAUI Linux Control Demo", - FontSize = 28, + root.AddChild(new SkiaLabel + { + Text = "OpenMaui Linux Control Demo", + FontSize = 28, TextColor = new SKColor(0x1A, 0x23, 0x7E), IsBold = true }); - root.AddChild(new SkiaLabel - { - Text = "All controls rendered using SkiaSharp on X11", - FontSize = 14, - TextColor = SKColors.Gray + root.AddChild(new SkiaLabel + { + Text = "All controls rendered using SkiaSharp on X11", + FontSize = 14, + TextColor = SKColors.Gray }); // ========== LABELS SECTION ========== @@ -100,7 +219,7 @@ public static class LinuxProgramHost root.AddChild(CreateSeparator()); root.AddChild(CreateSectionHeader("Buttons")); var buttonSection = new SkiaStackLayout { Orientation = StackOrientation.Horizontal, Spacing = 10 }; - + var btnPrimary = new SkiaButton { Text = "Primary", FontSize = 14 }; btnPrimary.BackgroundColor = new SKColor(0x21, 0x96, 0xF3); btnPrimary.TextColor = SKColors.White; @@ -117,7 +236,7 @@ public static class LinuxProgramHost btnDanger.BackgroundColor = new SKColor(0xF4, 0x43, 0x36); btnDanger.TextColor = SKColors.White; buttonSection.AddChild(btnDanger); - + root.AddChild(buttonSection); // ========== ENTRY SECTION ========== @@ -139,9 +258,9 @@ public static class LinuxProgramHost // ========== EDITOR SECTION ========== root.AddChild(CreateSeparator()); root.AddChild(CreateSectionHeader("Editor (Multi-line)")); - var editor = new SkiaEditor - { - Placeholder = "Enter multiple lines of text...", + var editor = new SkiaEditor + { + Placeholder = "Enter multiple lines of text...", FontSize = 14, BackgroundColor = SKColors.White }; @@ -277,7 +396,7 @@ public static class LinuxProgramHost }; collectionView.ItemsSource =(new object[] { "Apple", "Banana", "Cherry", "Date", "Elderberry", "Fig", "Grape", "Honeydew" }); var collectionLabel = new SkiaLabel { Text = "Selected: (none)", FontSize = 12, TextColor = SKColors.Gray }; - collectionView.SelectionChanged += (s, e) => + collectionView.SelectionChanged += (s, e) => { var selected = e.CurrentSelection.FirstOrDefault(); collectionLabel.Text = $"Selected: {selected}"; @@ -289,7 +408,7 @@ public static class LinuxProgramHost root.AddChild(CreateSeparator()); root.AddChild(CreateSectionHeader("ImageButton")); var imageButtonSection = new SkiaStackLayout { Orientation = StackOrientation.Horizontal, Spacing = 10 }; - + // Create ImageButton with a generated icon (since we don't have image files) var imgBtn = new SkiaImageButton { @@ -315,7 +434,7 @@ public static class LinuxProgramHost root.AddChild(CreateSeparator()); root.AddChild(CreateSectionHeader("Image")); var imageSection = new SkiaStackLayout { Orientation = StackOrientation.Horizontal, Spacing = 10 }; - + // Create Image with a generated sample image var img = new SkiaImage(); var sampleBitmap = CreateSampleImage(80, 60); @@ -326,17 +445,17 @@ public static class LinuxProgramHost // ========== FOOTER ========== root.AddChild(CreateSeparator()); - root.AddChild(new SkiaLabel - { - Text = "All 25+ controls are interactive - try them all!", - FontSize = 16, + root.AddChild(new SkiaLabel + { + Text = "All 25+ controls are interactive - try them all!", + FontSize = 16, TextColor = new SKColor(0x4C, 0xAF, 0x50), IsBold = true }); - root.AddChild(new SkiaLabel - { - Text = "Scroll down to see more controls", - FontSize = 12, + root.AddChild(new SkiaLabel + { + Text = "Scroll down to see more controls", + FontSize = 12, TextColor = SKColors.Gray }); diff --git a/Hosting/LinuxViewRenderer.cs b/Hosting/LinuxViewRenderer.cs new file mode 100644 index 0000000..f7d4184 --- /dev/null +++ b/Hosting/LinuxViewRenderer.cs @@ -0,0 +1,586 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Maui.Controls; +using Microsoft.Maui.Platform; +using SkiaSharp; + +namespace Microsoft.Maui.Platform.Linux.Hosting; + +/// +/// Renders MAUI views to Skia platform views. +/// Handles the conversion of the view hierarchy. +/// +public class LinuxViewRenderer +{ + private readonly IMauiContext _mauiContext; + + /// + /// Static reference to the current MAUI Shell for navigation support. + /// Used when Shell.Current is not available through normal lifecycle. + /// + public static Shell? CurrentMauiShell { get; private set; } + + /// + /// Static reference to the current SkiaShell for navigation updates. + /// + public static SkiaShell? CurrentSkiaShell { get; private set; } + + /// + /// Navigate to a route using the SkiaShell directly. + /// Use this instead of Shell.Current.GoToAsync on Linux. + /// + /// The route to navigate to (e.g., "Buttons" or "//Buttons") + /// True if navigation succeeded + public static bool NavigateToRoute(string route) + { + if (CurrentSkiaShell == null) + { + Console.WriteLine($"[NavigateToRoute] CurrentSkiaShell is null"); + return false; + } + + // Clean up the route - remove leading // or / + var cleanRoute = route.TrimStart('/'); + Console.WriteLine($"[NavigateToRoute] Navigating to: {cleanRoute}"); + + for (int i = 0; i < CurrentSkiaShell.Sections.Count; i++) + { + var section = CurrentSkiaShell.Sections[i]; + if (section.Route.Equals(cleanRoute, StringComparison.OrdinalIgnoreCase) || + section.Title.Equals(cleanRoute, StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine($"[NavigateToRoute] Found section {i}: {section.Title}"); + CurrentSkiaShell.NavigateToSection(i); + return true; + } + } + + Console.WriteLine($"[NavigateToRoute] Route not found: {cleanRoute}"); + return false; + } + + /// + /// Current renderer instance for page rendering. + /// + public static LinuxViewRenderer? CurrentRenderer { get; set; } + + /// + /// Pushes a page onto the navigation stack. + /// + /// The page to push + /// True if successful + public static bool PushPage(Page page) + { + Console.WriteLine($"[PushPage] Pushing page: {page.GetType().Name}"); + + if (CurrentSkiaShell == null) + { + Console.WriteLine($"[PushPage] CurrentSkiaShell is null"); + return false; + } + + if (CurrentRenderer == null) + { + Console.WriteLine($"[PushPage] CurrentRenderer is null"); + return false; + } + + try + { + // Render the page content + SkiaView? pageContent = null; + if (page is ContentPage contentPage && contentPage.Content != null) + { + pageContent = CurrentRenderer.RenderView(contentPage.Content); + } + + if (pageContent == null) + { + Console.WriteLine($"[PushPage] Failed to render page content"); + return false; + } + + // Wrap in ScrollView if needed + if (pageContent is not SkiaScrollView) + { + var scrollView = new SkiaScrollView { Content = pageContent }; + pageContent = scrollView; + } + + // Push onto SkiaShell's navigation stack + CurrentSkiaShell.PushAsync(pageContent, page.Title ?? "Detail"); + Console.WriteLine($"[PushPage] Successfully pushed page"); + return true; + } + catch (Exception ex) + { + Console.WriteLine($"[PushPage] Error: {ex.Message}"); + return false; + } + } + + /// + /// Pops the current page from the navigation stack. + /// + /// True if successful + public static bool PopPage() + { + Console.WriteLine($"[PopPage] Popping page"); + + if (CurrentSkiaShell == null) + { + Console.WriteLine($"[PopPage] CurrentSkiaShell is null"); + return false; + } + + return CurrentSkiaShell.PopAsync(); + } + + public LinuxViewRenderer(IMauiContext mauiContext) + { + _mauiContext = mauiContext ?? throw new ArgumentNullException(nameof(mauiContext)); + // Store reference for push/pop navigation + CurrentRenderer = this; + } + + /// + /// Renders a MAUI page and returns the corresponding SkiaView. + /// + public SkiaView? RenderPage(Page page) + { + if (page == null) + return null; + + // Special handling for Shell - Shell is our navigation container + if (page is Shell shell) + { + return RenderShell(shell); + } + + // Set handler context + page.Handler?.DisconnectHandler(); + var handler = page.ToHandler(_mauiContext); + + if (handler.PlatformView is SkiaView skiaPage) + { + // For ContentPage, render the content + if (page is ContentPage contentPage && contentPage.Content != null) + { + var contentView = RenderView(contentPage.Content); + if (skiaPage is SkiaPage sp && contentView != null) + { + sp.Content = contentView; + } + } + + return skiaPage; + } + + return null; + } + + /// + /// Renders a MAUI Shell with all its navigation structure. + /// + private SkiaShell RenderShell(Shell shell) + { + // Store reference for navigation - Shell.Current is computed from Application.Current.Windows + // Our platform handles navigation through SkiaShell directly + CurrentMauiShell = shell; + + var skiaShell = new SkiaShell + { + Title = shell.Title ?? "App", + FlyoutBehavior = shell.FlyoutBehavior switch + { + FlyoutBehavior.Flyout => ShellFlyoutBehavior.Flyout, + FlyoutBehavior.Locked => ShellFlyoutBehavior.Locked, + FlyoutBehavior.Disabled => ShellFlyoutBehavior.Disabled, + _ => ShellFlyoutBehavior.Flyout + } + }; + + // Process shell items into sections + foreach (var item in shell.Items) + { + ProcessShellItem(skiaShell, item); + } + + // Store reference to SkiaShell for navigation + CurrentSkiaShell = skiaShell; + + // Subscribe to MAUI Shell navigation events to update SkiaShell + shell.Navigated += OnShellNavigated; + shell.Navigating += (s, e) => Console.WriteLine($"[Navigation] Navigating: {e.Target}"); + + Console.WriteLine($"[Navigation] Shell navigation events subscribed. Sections: {skiaShell.Sections.Count}"); + for (int i = 0; i < skiaShell.Sections.Count; i++) + { + Console.WriteLine($"[Navigation] Section {i}: Route='{skiaShell.Sections[i].Route}', Title='{skiaShell.Sections[i].Title}'"); + } + + return skiaShell; + } + + /// + /// Handles MAUI Shell navigation events and updates SkiaShell accordingly. + /// + private static void OnShellNavigated(object? sender, ShellNavigatedEventArgs e) + { + Console.WriteLine($"[Navigation] OnShellNavigated called - Source: {e.Source}, Current: {e.Current?.Location}, Previous: {e.Previous?.Location}"); + + if (CurrentSkiaShell == null || CurrentMauiShell == null) + { + Console.WriteLine($"[Navigation] CurrentSkiaShell or CurrentMauiShell is null"); + return; + } + + // Get the current route from the Shell + var currentState = CurrentMauiShell.CurrentState; + var location = currentState?.Location?.OriginalString ?? ""; + Console.WriteLine($"[Navigation] Location: {location}, Sections: {CurrentSkiaShell.Sections.Count}"); + + // Find the matching section in SkiaShell by route + for (int i = 0; i < CurrentSkiaShell.Sections.Count; i++) + { + var section = CurrentSkiaShell.Sections[i]; + Console.WriteLine($"[Navigation] Checking section {i}: Route='{section.Route}', Title='{section.Title}'"); + if (!string.IsNullOrEmpty(section.Route) && location.Contains(section.Route, StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine($"[Navigation] Match found by route! Navigating to section {i}"); + if (i != CurrentSkiaShell.CurrentSectionIndex) + { + CurrentSkiaShell.NavigateToSection(i); + } + return; + } + if (!string.IsNullOrEmpty(section.Title) && location.Contains(section.Title, StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine($"[Navigation] Match found by title! Navigating to section {i}"); + if (i != CurrentSkiaShell.CurrentSectionIndex) + { + CurrentSkiaShell.NavigateToSection(i); + } + return; + } + } + Console.WriteLine($"[Navigation] No matching section found for location: {location}"); + } + + /// + /// Process a ShellItem (FlyoutItem, TabBar, etc.) into SkiaShell sections. + /// + private void ProcessShellItem(SkiaShell skiaShell, ShellItem item) + { + if (item is FlyoutItem flyoutItem) + { + // Each FlyoutItem becomes a section + var section = new ShellSection + { + Title = flyoutItem.Title ?? "", + Route = flyoutItem.Route ?? flyoutItem.Title ?? "" + }; + + // Process the items within the FlyoutItem + foreach (var shellSection in flyoutItem.Items) + { + foreach (var content in shellSection.Items) + { + var shellContent = new ShellContent + { + Title = content.Title ?? shellSection.Title ?? flyoutItem.Title ?? "", + Route = content.Route ?? "" + }; + + // Create the page content + var pageContent = CreateShellContentPage(content); + if (pageContent != null) + { + shellContent.Content = pageContent; + } + + section.Items.Add(shellContent); + } + } + + // If there's only one item, use it as the main section content + if (section.Items.Count == 1) + { + section.Title = section.Items[0].Title; + } + + skiaShell.AddSection(section); + } + else if (item is TabBar tabBar) + { + // TabBar items get their own sections + foreach (var tab in tabBar.Items) + { + var section = new ShellSection + { + Title = tab.Title ?? "", + Route = tab.Route ?? "" + }; + + foreach (var content in tab.Items) + { + var shellContent = new ShellContent + { + Title = content.Title ?? tab.Title ?? "", + Route = content.Route ?? "" + }; + + var pageContent = CreateShellContentPage(content); + if (pageContent != null) + { + shellContent.Content = pageContent; + } + + section.Items.Add(shellContent); + } + + skiaShell.AddSection(section); + } + } + else + { + // Generic ShellItem + var section = new ShellSection + { + Title = item.Title ?? "", + Route = item.Route ?? "" + }; + + foreach (var shellSection in item.Items) + { + foreach (var content in shellSection.Items) + { + var shellContent = new ShellContent + { + Title = content.Title ?? "", + Route = content.Route ?? "" + }; + + var pageContent = CreateShellContentPage(content); + if (pageContent != null) + { + shellContent.Content = pageContent; + } + + section.Items.Add(shellContent); + } + } + + skiaShell.AddSection(section); + } + } + + /// + /// Creates the page content for a ShellContent. + /// + private SkiaView? CreateShellContentPage(Controls.ShellContent content) + { + try + { + // Try to create the page from the content template + Page? page = null; + + if (content.ContentTemplate != null) + { + page = content.ContentTemplate.CreateContent() as Page; + } + + if (page == null && content.Content is Page contentPage) + { + page = contentPage; + } + + if (page is ContentPage cp && cp.Content != null) + { + // Wrap in a scroll view if not already scrollable + var contentView = RenderView(cp.Content); + if (contentView != null) + { + if (contentView is SkiaScrollView) + { + return contentView; + } + else + { + var scrollView = new SkiaScrollView + { + Content = contentView + }; + return scrollView; + } + } + } + } + catch (Exception) + { + // Silently handle template creation errors + } + + return null; + } + + /// + /// Renders a MAUI view and returns the corresponding SkiaView. + /// + public SkiaView? RenderView(IView view) + { + if (view == null) + return null; + + try + { + // Disconnect any existing handler + if (view is Element element && element.Handler != null) + { + element.Handler.DisconnectHandler(); + } + + // Create handler for the view + var handler = view.ToHandler(_mauiContext); + + if (handler?.PlatformView is not SkiaView skiaView) + { + // If no Skia handler, create a fallback + return CreateFallbackView(view); + } + + // Recursively render children for layout views + if (view is ILayout layout && skiaView is SkiaLayoutView layoutView) + { + + // For StackLayout, copy orientation and spacing + if (layoutView is SkiaStackLayout skiaStack) + { + if (view is Controls.VerticalStackLayout) + { + skiaStack.Orientation = StackOrientation.Vertical; + } + else if (view is Controls.HorizontalStackLayout) + { + skiaStack.Orientation = StackOrientation.Horizontal; + } + else if (view is Controls.StackLayout sl) + { + skiaStack.Orientation = sl.Orientation == Microsoft.Maui.Controls.StackOrientation.Vertical + ? StackOrientation.Vertical : StackOrientation.Horizontal; + } + + if (view is IStackLayout stackLayout) + { + skiaStack.Spacing = (float)stackLayout.Spacing; + } + } + + // For Grid, set up row/column definitions + if (view is Controls.Grid mauiGrid && layoutView is SkiaGrid skiaGrid) + { + // Copy row definitions + foreach (var rowDef in mauiGrid.RowDefinitions) + { + skiaGrid.RowDefinitions.Add(new GridLength((float)rowDef.Height.Value, + rowDef.Height.IsAbsolute ? GridUnitType.Absolute : + rowDef.Height.IsStar ? GridUnitType.Star : GridUnitType.Auto)); + } + // Copy column definitions + foreach (var colDef in mauiGrid.ColumnDefinitions) + { + skiaGrid.ColumnDefinitions.Add(new GridLength((float)colDef.Width.Value, + colDef.Width.IsAbsolute ? GridUnitType.Absolute : + colDef.Width.IsStar ? GridUnitType.Star : GridUnitType.Auto)); + } + skiaGrid.RowSpacing = (float)mauiGrid.RowSpacing; + skiaGrid.ColumnSpacing = (float)mauiGrid.ColumnSpacing; + } + + foreach (var child in layout) + { + if (child is IView childViewElement) + { + var childView = RenderView(childViewElement); + if (childView != null) + { + // For Grid, get attached properties for position + if (layoutView is SkiaGrid grid && child is BindableObject bindable) + { + var row = Controls.Grid.GetRow(bindable); + var col = Controls.Grid.GetColumn(bindable); + var rowSpan = Controls.Grid.GetRowSpan(bindable); + var colSpan = Controls.Grid.GetColumnSpan(bindable); + grid.AddChild(childView, row, col, rowSpan, colSpan); + } + else + { + layoutView.AddChild(childView); + } + } + } + } + } + else if (view is IContentView contentView && contentView.Content is IView contentElement) + { + var content = RenderView(contentElement); + if (content != null) + { + if (skiaView is SkiaBorder border) + { + border.AddChild(content); + } + else if (skiaView is SkiaFrame frame) + { + frame.AddChild(content); + } + else if (skiaView is SkiaScrollView scrollView) + { + scrollView.Content = content; + } + } + } + + return skiaView; + } + catch (Exception) + { + return CreateFallbackView(view); + } + } + + /// + /// Creates a fallback view for unsupported view types. + /// + private SkiaView CreateFallbackView(IView view) + { + // For views without handlers, create a placeholder + return new SkiaLabel + { + Text = $"[{view.GetType().Name}]", + TextColor = SKColors.Gray, + FontSize = 12 + }; + } +} + +/// +/// Extension methods for MAUI handler creation. +/// +public static class MauiHandlerExtensions +{ + /// + /// Creates a handler for the view and returns it. + /// + public static IElementHandler ToHandler(this IElement element, IMauiContext mauiContext) + { + var handler = mauiContext.Handlers.GetHandler(element.GetType()); + if (handler != null) + { + handler.SetMauiContext(mauiContext); + handler.SetVirtualView(element); + } + return handler!; + } +} diff --git a/Hosting/MauiAppBuilderExtensions.cs b/Hosting/MauiAppBuilderExtensions.cs index 4cc3b75..b1809ed 100644 --- a/Hosting/MauiAppBuilderExtensions.cs +++ b/Hosting/MauiAppBuilderExtensions.cs @@ -6,7 +6,8 @@ using Microsoft.Maui; using Microsoft.Maui.Controls; using Microsoft.Maui.Controls.Hosting; using Microsoft.Maui.Hosting; -using OpenMaui.Platform.Linux.Handlers; +using Microsoft.Maui.Platform.Linux; +using Microsoft.Maui.Platform.Linux.Handlers; namespace OpenMaui.Platform.Linux.Hosting; @@ -59,17 +60,10 @@ public static class MauiAppBuilderExtensions handlers.AddHandler(); // Layout Controls - handlers.AddHandler(); handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); // Collection Controls handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); // Navigation Controls handlers.AddHandler(); @@ -87,6 +81,9 @@ public static class MauiAppBuilderExtensions // Search handlers.AddHandler(); + // Web + handlers.AddHandler(); + // Window handlers.AddHandler(); }); diff --git a/Input/KeyMapping.cs b/Input/KeyMapping.cs index 7695130..12ca106 100644 --- a/Input/KeyMapping.cs +++ b/Input/KeyMapping.cs @@ -153,4 +153,154 @@ public static class KeyMapping return modifiers; } + + // Linux evdev keycode to Key mapping (used by Wayland) + private static readonly Dictionary LinuxKeycodeToKey = new() + { + // Top row + [1] = Key.Escape, + [2] = Key.D1, [3] = Key.D2, [4] = Key.D3, [5] = Key.D4, [6] = Key.D5, + [7] = Key.D6, [8] = Key.D7, [9] = Key.D8, [10] = Key.D9, [11] = Key.D0, + [12] = Key.Minus, [13] = Key.Equals, [14] = Key.Backspace, [15] = Key.Tab, + + // QWERTY row + [16] = Key.Q, [17] = Key.W, [18] = Key.E, [19] = Key.R, [20] = Key.T, + [21] = Key.Y, [22] = Key.U, [23] = Key.I, [24] = Key.O, [25] = Key.P, + [26] = Key.LeftBracket, [27] = Key.RightBracket, [28] = Key.Enter, + + // Control and ASDF row + [29] = Key.Control, + [30] = Key.A, [31] = Key.S, [32] = Key.D, [33] = Key.F, [34] = Key.G, + [35] = Key.H, [36] = Key.J, [37] = Key.K, [38] = Key.L, + [39] = Key.Semicolon, [40] = Key.Quote, [41] = Key.Grave, + + // Shift and ZXCV row + [42] = Key.Shift, [43] = Key.Backslash, + [44] = Key.Z, [45] = Key.X, [46] = Key.C, [47] = Key.V, [48] = Key.B, + [49] = Key.N, [50] = Key.M, + [51] = Key.Comma, [52] = Key.Period, [53] = Key.Slash, [54] = Key.Shift, + + // Bottom row + [55] = Key.NumPadMultiply, [56] = Key.Alt, [57] = Key.Space, + [58] = Key.CapsLock, + + // Function keys + [59] = Key.F1, [60] = Key.F2, [61] = Key.F3, [62] = Key.F4, + [63] = Key.F5, [64] = Key.F6, [65] = Key.F7, [66] = Key.F8, + [67] = Key.F9, [68] = Key.F10, + + // NumLock and numpad + [69] = Key.NumLock, [70] = Key.ScrollLock, + [71] = Key.NumPad7, [72] = Key.NumPad8, [73] = Key.NumPad9, [74] = Key.NumPadSubtract, + [75] = Key.NumPad4, [76] = Key.NumPad5, [77] = Key.NumPad6, [78] = Key.NumPadAdd, + [79] = Key.NumPad1, [80] = Key.NumPad2, [81] = Key.NumPad3, + [82] = Key.NumPad0, [83] = Key.NumPadDecimal, + + // More function keys + [87] = Key.F11, [88] = Key.F12, + + // Extended keys + [96] = Key.Enter, // NumPad Enter + [97] = Key.Control, // Right Control + [98] = Key.NumPadDivide, + [99] = Key.PrintScreen, + [100] = Key.Alt, // Right Alt + [102] = Key.Home, + [103] = Key.Up, + [104] = Key.PageUp, + [105] = Key.Left, + [106] = Key.Right, + [107] = Key.End, + [108] = Key.Down, + [109] = Key.PageDown, + [110] = Key.Insert, + [111] = Key.Delete, + [119] = Key.Pause, + [125] = Key.Super, // Left Super (Windows key) + [126] = Key.Super, // Right Super + [127] = Key.Menu, + }; + + /// + /// Converts a Linux evdev keycode to a MAUI Key. + /// Used for Wayland input where keycodes are offset by 8 from X11 keycodes. + /// + public static Key FromLinuxKeycode(uint keycode) + { + // Wayland uses evdev keycodes, X11 uses keycodes + 8 + // If caller added 8, subtract it + var evdevCode = keycode >= 8 ? keycode - 8 : keycode; + + if (LinuxKeycodeToKey.TryGetValue(evdevCode, out var key)) + return key; + + return Key.Unknown; + } + + /// + /// Converts a Key to its character representation, if applicable. + /// + public static char? ToChar(Key key, KeyModifiers modifiers) + { + bool shift = modifiers.HasFlag(KeyModifiers.Shift); + bool capsLock = modifiers.HasFlag(KeyModifiers.CapsLock); + bool upper = shift ^ capsLock; + + // Letters + if (key >= Key.A && key <= Key.Z) + { + char ch = (char)('a' + (key - Key.A)); + return upper ? char.ToUpper(ch) : ch; + } + + // Numbers (with shift gives symbols) + if (key >= Key.D0 && key <= Key.D9) + { + if (shift) + { + return (key - Key.D0) switch + { + 0 => ')', + 1 => '!', + 2 => '@', + 3 => '#', + 4 => '$', + 5 => '%', + 6 => '^', + 7 => '&', + 8 => '*', + 9 => '(', + _ => null + }; + } + return (char)('0' + (key - Key.D0)); + } + + // NumPad numbers + if (key >= Key.NumPad0 && key <= Key.NumPad9) + return (char)('0' + (key - Key.NumPad0)); + + // Punctuation + return key switch + { + Key.Space => ' ', + Key.Comma => shift ? '<' : ',', + Key.Period => shift ? '>' : '.', + Key.Slash => shift ? '?' : '/', + Key.Semicolon => shift ? ':' : ';', + Key.Quote => shift ? '"' : '\'', + Key.LeftBracket => shift ? '{' : '[', + Key.RightBracket => shift ? '}' : ']', + Key.Backslash => shift ? '|' : '\\', + Key.Minus => shift ? '_' : '-', + Key.Equals => shift ? '+' : '=', + Key.Grave => shift ? '~' : '`', + Key.NumPadAdd => '+', + Key.NumPadSubtract => '-', + Key.NumPadMultiply => '*', + Key.NumPadDivide => '/', + Key.NumPadDecimal => '.', + _ => null + }; + } } diff --git a/LinuxApplication.cs b/LinuxApplication.cs index 4468010..fbb0fc6 100644 --- a/LinuxApplication.cs +++ b/LinuxApplication.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Maui.Hosting; using Microsoft.Maui.Platform.Linux.Rendering; using Microsoft.Maui.Platform.Linux.Window; using Microsoft.Maui.Platform.Linux.Services; @@ -18,6 +20,7 @@ public class LinuxApplication : IDisposable private SkiaView? _rootView; private SkiaView? _focusedView; private SkiaView? _hoveredView; + private SkiaView? _capturedView; // View that has captured pointer events during drag private bool _disposed; /// @@ -85,6 +88,129 @@ public class LinuxApplication : IDisposable public LinuxApplication() { Current = this; + + // Set up dialog service invalidation callback + LinuxDialogService.SetInvalidateCallback(() => _renderingEngine?.InvalidateAll()); + } + + /// + /// Runs a MAUI application on Linux. + /// This is the main entry point for Linux apps. + /// + /// The MauiApp to run. + /// Command line arguments. + public static void Run(MauiApp app, string[] args) + { + Run(app, args, null); + } + + /// + /// Runs a MAUI application on Linux with options. + /// + /// The MauiApp to run. + /// Command line arguments. + /// Optional configuration action. + public static void Run(MauiApp app, string[] args, Action? configure) + { + var options = app.Services.GetService() + ?? new LinuxApplicationOptions(); + configure?.Invoke(options); + ParseCommandLineOptions(args, options); + + using var linuxApp = new LinuxApplication(); + linuxApp.Initialize(options); + + // Create MAUI context + var mauiContext = new Hosting.LinuxMauiContext(app.Services, linuxApp); + + // Get the application and render it + var application = app.Services.GetService(); + SkiaView? rootView = null; + + if (application is Microsoft.Maui.Controls.Application mauiApplication) + { + // Force Application.Current to be this instance + // The constructor sets Current = this, but we ensure it here + var currentProperty = typeof(Microsoft.Maui.Controls.Application).GetProperty("Current"); + if (currentProperty != null && currentProperty.CanWrite) + { + currentProperty.SetValue(null, mauiApplication); + } + + if (mauiApplication.MainPage != null) + { + // Create a MAUI Window and add it to the application + // This ensures Shell.Current works (it reads from Application.Current.Windows[0].Page) + var mainPage = mauiApplication.MainPage; + + // Always ensure we have a window with the Shell/Page + var windowsField = typeof(Microsoft.Maui.Controls.Application).GetField("_windows", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + var windowsList = windowsField?.GetValue(mauiApplication) as System.Collections.Generic.List; + + if (windowsList != null && windowsList.Count == 0) + { + var mauiWindow = new Microsoft.Maui.Controls.Window(mainPage); + windowsList.Add(mauiWindow); + mauiWindow.Parent = mauiApplication; + } + else if (windowsList != null && windowsList.Count > 0 && windowsList[0].Page == null) + { + // Window exists but has no page - set it + windowsList[0].Page = mainPage; + } + + var renderer = new Hosting.LinuxViewRenderer(mauiContext); + rootView = renderer.RenderPage(mainPage); + + // Update window title based on app name (NavigationPage.Title takes precedence) + string windowTitle = "OpenMaui App"; + if (mainPage is Microsoft.Maui.Controls.NavigationPage navPage) + { + // Prefer NavigationPage.Title (app name) over CurrentPage.Title (page name) for window title + windowTitle = navPage.Title ?? windowTitle; + } + else if (mainPage is Microsoft.Maui.Controls.Shell shell) + { + windowTitle = shell.Title ?? windowTitle; + } + else + { + windowTitle = mainPage.Title ?? windowTitle; + } + linuxApp.SetWindowTitle(windowTitle); + } + } + + // Fallback to demo if no view + if (rootView == null) + { + rootView = Hosting.LinuxProgramHost.CreateDemoView(); + } + + linuxApp.RootView = rootView; + linuxApp.Run(); + } + + private static void ParseCommandLineOptions(string[] args, LinuxApplicationOptions options) + { + for (int i = 0; i < args.Length; i++) + { + switch (args[i].ToLowerInvariant()) + { + case "--title" when i + 1 < args.Length: + options.Title = args[++i]; + break; + case "--width" when i + 1 < args.Length && int.TryParse(args[i + 1], out var w): + options.Width = w; + i++; + break; + case "--height" when i + 1 < args.Length && int.TryParse(args[i + 1], out var h): + options.Height = h; + i++; + break; + } + } } /// @@ -123,6 +249,14 @@ public class LinuxApplication : IDisposable // For now, we create singleton instances } + /// + /// Sets the window title. + /// + public void SetWindowTitle(string title) + { + _mainWindow?.SetTitle(title); + } + /// /// Shows the main window and runs the event loop. /// @@ -171,6 +305,9 @@ public class LinuxApplication : IDisposable { if (_rootView != null) { + // Re-measure with new available size, then arrange + var availableSize = new SkiaSharp.SKSize(size.Width, size.Height); + _rootView.Measure(availableSize); _rootView.Arrange(new SkiaSharp.SKRect(0, 0, size.Width, size.Height)); } _renderingEngine?.InvalidateAll(); @@ -183,6 +320,13 @@ public class LinuxApplication : IDisposable private void OnKeyDown(object? sender, KeyEventArgs e) { + // Route to dialog if one is active + if (LinuxDialogService.HasActiveDialog) + { + LinuxDialogService.TopDialog?.OnKeyDown(e); + return; + } + if (_focusedView != null) { _focusedView.OnKeyDown(e); @@ -191,6 +335,13 @@ public class LinuxApplication : IDisposable private void OnKeyUp(object? sender, KeyEventArgs e) { + // Route to dialog if one is active + if (LinuxDialogService.HasActiveDialog) + { + LinuxDialogService.TopDialog?.OnKeyUp(e); + return; + } + if (_focusedView != null) { _focusedView.OnKeyUp(e); @@ -207,10 +358,26 @@ public class LinuxApplication : IDisposable private void OnPointerMoved(object? sender, PointerEventArgs e) { + // Route to dialog if one is active + if (LinuxDialogService.HasActiveDialog) + { + LinuxDialogService.TopDialog?.OnPointerMoved(e); + return; + } + if (_rootView != null) { - var hitView = _rootView.HitTest(e.X, e.Y); - + // If a view has captured the pointer, send all events to it + if (_capturedView != null) + { + _capturedView.OnPointerMoved(e); + return; + } + + // Check for popup overlay first + var popupOwner = SkiaView.GetPopupOwnerAt(e.X, e.Y); + var hitView = popupOwner ?? _rootView.HitTest(e.X, e.Y); + // Track hover state changes if (hitView != _hoveredView) { @@ -218,28 +385,50 @@ public class LinuxApplication : IDisposable _hoveredView = hitView; _hoveredView?.OnPointerEntered(e); } - + hitView?.OnPointerMoved(e); } } private void OnPointerPressed(object? sender, PointerEventArgs e) { + Console.WriteLine($"[LinuxApplication] OnPointerPressed at ({e.X}, {e.Y})"); + + // Route to dialog if one is active + if (LinuxDialogService.HasActiveDialog) + { + LinuxDialogService.TopDialog?.OnPointerPressed(e); + return; + } + if (_rootView != null) { - var hitView = _rootView.HitTest(e.X, e.Y); + // Check for popup overlay first + var popupOwner = SkiaView.GetPopupOwnerAt(e.X, e.Y); + var hitView = popupOwner ?? _rootView.HitTest(e.X, e.Y); + Console.WriteLine($"[LinuxApplication] HitView: {hitView?.GetType().Name ?? "null"}, rootView: {_rootView.GetType().Name}"); + if (hitView != null) { + // Capture pointer to this view for drag operations + _capturedView = hitView; + // Update focus if (hitView.IsFocusable) { FocusedView = hitView; } + Console.WriteLine($"[LinuxApplication] Calling OnPointerPressed on {hitView.GetType().Name}"); hitView.OnPointerPressed(e); } else { + // Close any open popups when clicking outside + if (SkiaView.HasActivePopup && _focusedView != null) + { + _focusedView.OnFocusLost(); + } FocusedView = null; } } @@ -247,22 +436,42 @@ public class LinuxApplication : IDisposable private void OnPointerReleased(object? sender, PointerEventArgs e) { + // Route to dialog if one is active + if (LinuxDialogService.HasActiveDialog) + { + LinuxDialogService.TopDialog?.OnPointerReleased(e); + return; + } + if (_rootView != null) { - var hitView = _rootView.HitTest(e.X, e.Y); + // If a view has captured the pointer, send release to it + if (_capturedView != null) + { + _capturedView.OnPointerReleased(e); + _capturedView = null; // Release capture + return; + } + + // Check for popup overlay first + var popupOwner = SkiaView.GetPopupOwnerAt(e.X, e.Y); + var hitView = popupOwner ?? _rootView.HitTest(e.X, e.Y); hitView?.OnPointerReleased(e); } } private void OnScroll(object? sender, ScrollEventArgs e) { + Console.WriteLine($"[LinuxApplication] OnScroll - X={e.X}, Y={e.Y}, DeltaX={e.DeltaX}, DeltaY={e.DeltaY}"); if (_rootView != null) { var hitView = _rootView.HitTest(e.X, e.Y); + Console.WriteLine($"[LinuxApplication] HitView: {hitView?.GetType().Name ?? "null"}"); // Bubble scroll events up to find a ScrollView var view = hitView; while (view != null) { + Console.WriteLine($"[LinuxApplication] Bubbling to: {view.GetType().Name}"); if (view is SkiaScrollView scrollView) { scrollView.OnScroll(e); @@ -324,6 +533,11 @@ public class LinuxApplicationOptions /// Gets or sets the display server type. /// public DisplayServerType DisplayServer { get; set; } = DisplayServerType.Auto; + + /// + /// Gets or sets whether to force demo mode instead of loading the application's pages. + /// + public bool ForceDemo { get; set; } = false; } /// diff --git a/OpenMaui.Controls.Linux.csproj b/OpenMaui.Controls.Linux.csproj index 80aa495..c3d7e50 100644 --- a/OpenMaui.Controls.Linux.csproj +++ b/OpenMaui.Controls.Linux.csproj @@ -35,14 +35,14 @@ - - - - + + + + - - + + @@ -50,12 +50,19 @@ - + + + + + + + + diff --git a/Rendering/SkiaRenderingEngine.cs b/Rendering/SkiaRenderingEngine.cs index b9a6e8c..4b4b28c 100644 --- a/Rendering/SkiaRenderingEngine.cs +++ b/Rendering/SkiaRenderingEngine.cs @@ -3,6 +3,7 @@ using SkiaSharp; using Microsoft.Maui.Platform.Linux.Window; +using Microsoft.Maui.Platform; using System.Runtime.InteropServices; namespace Microsoft.Maui.Platform.Linux.Rendering; @@ -86,7 +87,13 @@ public class SkiaRenderingEngine : IDisposable // Draw popup overlays (dropdowns, calendars, etc.) on top SkiaView.DrawPopupOverlays(_canvas); - + + // Draw modal dialogs on top of everything + if (LinuxDialogService.HasActiveDialog) + { + LinuxDialogService.DrawDialogs(_canvas, new SKRect(0, 0, Width, Height)); + } + _canvas.Flush(); // Present to X11 window diff --git a/Services/DisplayServerFactory.cs b/Services/DisplayServerFactory.cs index 38917e4..58a8e19 100644 --- a/Services/DisplayServerFactory.cs +++ b/Services/DisplayServerFactory.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Runtime.InteropServices; -using SkiaSharp; using Microsoft.Maui.Platform.Linux.Window; using Microsoft.Maui.Platform.Linux.Rendering; @@ -216,113 +215,25 @@ public class X11DisplayWindow : IDisplayWindow /// /// Wayland display window wrapper implementing IDisplayWindow. -/// Uses wl_shm for software rendering with SkiaSharp. +/// Uses the full WaylandWindow implementation with xdg-shell protocol. /// public class WaylandDisplayWindow : IDisplayWindow { - #region Native Interop + private readonly WaylandWindow _window; - private const string LibWaylandClient = "libwayland-client.so.0"; + public int Width => _window.Width; + public int Height => _window.Height; + public bool IsRunning => _window.IsRunning; - [DllImport(LibWaylandClient)] - private static extern IntPtr wl_display_connect(string? name); + /// + /// Gets the pixel data pointer for rendering. + /// + public IntPtr PixelData => _window.PixelData; - [DllImport(LibWaylandClient)] - private static extern void wl_display_disconnect(IntPtr display); - - [DllImport(LibWaylandClient)] - private static extern int wl_display_dispatch(IntPtr display); - - [DllImport(LibWaylandClient)] - private static extern int wl_display_dispatch_pending(IntPtr display); - - [DllImport(LibWaylandClient)] - private static extern int wl_display_roundtrip(IntPtr display); - - [DllImport(LibWaylandClient)] - private static extern int wl_display_flush(IntPtr display); - - [DllImport(LibWaylandClient)] - private static extern IntPtr wl_display_get_registry(IntPtr display); - - [DllImport(LibWaylandClient)] - private static extern IntPtr wl_compositor_create_surface(IntPtr compositor); - - [DllImport(LibWaylandClient)] - private static extern void wl_surface_attach(IntPtr surface, IntPtr buffer, int x, int y); - - [DllImport(LibWaylandClient)] - private static extern void wl_surface_damage(IntPtr surface, int x, int y, int width, int height); - - [DllImport(LibWaylandClient)] - private static extern void wl_surface_commit(IntPtr surface); - - [DllImport(LibWaylandClient)] - private static extern void wl_surface_destroy(IntPtr surface); - - [DllImport(LibWaylandClient)] - private static extern IntPtr wl_shm_create_pool(IntPtr shm, int fd, int size); - - [DllImport(LibWaylandClient)] - private static extern void wl_shm_pool_destroy(IntPtr pool); - - [DllImport(LibWaylandClient)] - private static extern IntPtr wl_shm_pool_create_buffer(IntPtr pool, int offset, int width, int height, int stride, uint format); - - [DllImport(LibWaylandClient)] - private static extern void wl_buffer_destroy(IntPtr buffer); - - [DllImport("libc", EntryPoint = "shm_open")] - private static extern int shm_open([MarshalAs(UnmanagedType.LPStr)] string name, int oflag, int mode); - - [DllImport("libc", EntryPoint = "shm_unlink")] - private static extern int shm_unlink([MarshalAs(UnmanagedType.LPStr)] string name); - - [DllImport("libc", EntryPoint = "ftruncate")] - private static extern int ftruncate(int fd, long length); - - [DllImport("libc", EntryPoint = "mmap")] - private static extern IntPtr mmap(IntPtr addr, nuint length, int prot, int flags, int fd, long offset); - - [DllImport("libc", EntryPoint = "munmap")] - private static extern int munmap(IntPtr addr, nuint length); - - [DllImport("libc", EntryPoint = "close")] - private static extern int close(int fd); - - private const int O_RDWR = 2; - private const int O_CREAT = 0x40; - private const int O_EXCL = 0x80; - private const int PROT_READ = 1; - private const int PROT_WRITE = 2; - private const int MAP_SHARED = 1; - private const uint WL_SHM_FORMAT_XRGB8888 = 1; - - #endregion - - private IntPtr _display; - private IntPtr _registry; - private IntPtr _compositor; - private IntPtr _shm; - private IntPtr _surface; - private IntPtr _shmPool; - private IntPtr _buffer; - private IntPtr _pixelData; - private int _shmFd = -1; - private int _bufferSize; - - private int _width; - private int _height; - private string _title; - private bool _isRunning; - private bool _disposed; - - private SKBitmap? _bitmap; - private SKCanvas? _canvas; - - public int Width => _width; - public int Height => _height; - public bool IsRunning => _isRunning; + /// + /// Gets the stride (bytes per row) of the pixel buffer. + /// + public int Stride => _window.Stride; public event EventHandler? KeyDown; public event EventHandler? KeyUp; @@ -337,213 +248,27 @@ public class WaylandDisplayWindow : IDisplayWindow public WaylandDisplayWindow(string title, int width, int height) { - _title = title; - _width = width; - _height = height; + _window = new WaylandWindow(title, width, height); - Initialize(); + // Wire up events + _window.KeyDown += (s, e) => KeyDown?.Invoke(this, e); + _window.KeyUp += (s, e) => KeyUp?.Invoke(this, e); + _window.TextInput += (s, e) => TextInput?.Invoke(this, e); + _window.PointerMoved += (s, e) => PointerMoved?.Invoke(this, e); + _window.PointerPressed += (s, e) => PointerPressed?.Invoke(this, e); + _window.PointerReleased += (s, e) => PointerReleased?.Invoke(this, e); + _window.Scroll += (s, e) => Scroll?.Invoke(this, e); + _window.Exposed += (s, e) => Exposed?.Invoke(this, e); + _window.Resized += (s, e) => Resized?.Invoke(this, e); + _window.CloseRequested += (s, e) => CloseRequested?.Invoke(this, e); } - private void Initialize() - { - _display = wl_display_connect(null); - if (_display == IntPtr.Zero) - { - throw new InvalidOperationException("Failed to connect to Wayland display. Is WAYLAND_DISPLAY set?"); - } - - _registry = wl_display_get_registry(_display); - if (_registry == IntPtr.Zero) - { - throw new InvalidOperationException("Failed to get Wayland registry"); - } - - // Note: A full implementation would set up registry listeners to get - // compositor and shm handles. For now, we throw an informative error - // and fall back to X11 via XWayland in DisplayServerFactory. - - // This is a placeholder - proper Wayland support requires: - // 1. Setting up wl_registry_listener with callbacks - // 2. Binding to wl_compositor, wl_shm, wl_seat, xdg_wm_base - // 3. Implementing the xdg-shell protocol for toplevel windows - - wl_display_roundtrip(_display); - - // For now, signal that native Wayland isn't fully implemented - throw new NotSupportedException( - "Native Wayland support is experimental. " + - "Set MAUI_PREFER_X11=1 to use XWayland, or run with DISPLAY set."); - } - - private void CreateShmBuffer() - { - int stride = _width * 4; - _bufferSize = stride * _height; - - string shmName = $"/maui-shm-{Environment.ProcessId}-{DateTime.Now.Ticks}"; - _shmFd = shm_open(shmName, O_RDWR | O_CREAT | O_EXCL, 0600); - - if (_shmFd < 0) - { - throw new InvalidOperationException("Failed to create shared memory file"); - } - - shm_unlink(shmName); - - if (ftruncate(_shmFd, _bufferSize) < 0) - { - close(_shmFd); - throw new InvalidOperationException("Failed to resize shared memory"); - } - - _pixelData = mmap(IntPtr.Zero, (nuint)_bufferSize, PROT_READ | PROT_WRITE, MAP_SHARED, _shmFd, 0); - if (_pixelData == IntPtr.Zero || _pixelData == new IntPtr(-1)) - { - close(_shmFd); - throw new InvalidOperationException("Failed to mmap shared memory"); - } - - _shmPool = wl_shm_create_pool(_shm, _shmFd, _bufferSize); - if (_shmPool == IntPtr.Zero) - { - munmap(_pixelData, (nuint)_bufferSize); - close(_shmFd); - throw new InvalidOperationException("Failed to create wl_shm_pool"); - } - - _buffer = wl_shm_pool_create_buffer(_shmPool, 0, _width, _height, stride, WL_SHM_FORMAT_XRGB8888); - if (_buffer == IntPtr.Zero) - { - wl_shm_pool_destroy(_shmPool); - munmap(_pixelData, (nuint)_bufferSize); - close(_shmFd); - throw new InvalidOperationException("Failed to create wl_buffer"); - } - - // Create Skia bitmap backed by shared memory - var info = new SKImageInfo(_width, _height, SKColorType.Bgra8888, SKAlphaType.Opaque); - _bitmap = new SKBitmap(); - _bitmap.InstallPixels(info, _pixelData, stride); - _canvas = new SKCanvas(_bitmap); - } - - public void Show() - { - if (_surface == IntPtr.Zero || _buffer == IntPtr.Zero) return; - - wl_surface_attach(_surface, _buffer, 0, 0); - wl_surface_damage(_surface, 0, 0, _width, _height); - wl_surface_commit(_surface); - wl_display_flush(_display); - } - - public void Hide() - { - if (_surface == IntPtr.Zero) return; - - wl_surface_attach(_surface, IntPtr.Zero, 0, 0); - wl_surface_commit(_surface); - wl_display_flush(_display); - } - - public void SetTitle(string title) - { - _title = title; - } - - public void Resize(int width, int height) - { - if (width == _width && height == _height) return; - - _canvas?.Dispose(); - _bitmap?.Dispose(); - - if (_buffer != IntPtr.Zero) - wl_buffer_destroy(_buffer); - if (_shmPool != IntPtr.Zero) - wl_shm_pool_destroy(_shmPool); - if (_pixelData != IntPtr.Zero) - munmap(_pixelData, (nuint)_bufferSize); - if (_shmFd >= 0) - close(_shmFd); - - _width = width; - _height = height; - - CreateShmBuffer(); - Resized?.Invoke(this, (width, height)); - } - - public void ProcessEvents() - { - if (!_isRunning || _display == IntPtr.Zero) return; - - wl_display_dispatch_pending(_display); - wl_display_flush(_display); - } - - public void Stop() - { - _isRunning = false; - } - - public SKCanvas? GetCanvas() => _canvas; - - public void CommitFrame() - { - if (_surface != IntPtr.Zero && _buffer != IntPtr.Zero) - { - wl_surface_attach(_surface, _buffer, 0, 0); - wl_surface_damage(_surface, 0, 0, _width, _height); - wl_surface_commit(_surface); - wl_display_flush(_display); - } - } - - public void Dispose() - { - if (_disposed) return; - _disposed = true; - - _isRunning = false; - - _canvas?.Dispose(); - _bitmap?.Dispose(); - - if (_buffer != IntPtr.Zero) - { - wl_buffer_destroy(_buffer); - _buffer = IntPtr.Zero; - } - - if (_shmPool != IntPtr.Zero) - { - wl_shm_pool_destroy(_shmPool); - _shmPool = IntPtr.Zero; - } - - if (_pixelData != IntPtr.Zero && _pixelData != new IntPtr(-1)) - { - munmap(_pixelData, (nuint)_bufferSize); - _pixelData = IntPtr.Zero; - } - - if (_shmFd >= 0) - { - close(_shmFd); - _shmFd = -1; - } - - if (_surface != IntPtr.Zero) - { - wl_surface_destroy(_surface); - _surface = IntPtr.Zero; - } - - if (_display != IntPtr.Zero) - { - wl_display_disconnect(_display); - _display = IntPtr.Zero; - } - } + public void Show() => _window.Show(); + public void Hide() => _window.Hide(); + public void SetTitle(string title) => _window.SetTitle(title); + public void Resize(int width, int height) => _window.Resize(width, height); + public void ProcessEvents() => _window.ProcessEvents(); + public void Stop() => _window.Stop(); + public void CommitFrame() => _window.CommitFrame(); + public void Dispose() => _window.Dispose(); } diff --git a/Services/Gtk4InteropService.cs b/Services/Gtk4InteropService.cs new file mode 100644 index 0000000..80079fb --- /dev/null +++ b/Services/Gtk4InteropService.cs @@ -0,0 +1,821 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +namespace Microsoft.Maui.Platform.Linux.Services; + +/// +/// GTK4 dialog response codes. +/// +public enum GtkResponseType +{ + None = -1, + Reject = -2, + Accept = -3, + DeleteEvent = -4, + Ok = -5, + Cancel = -6, + Close = -7, + Yes = -8, + No = -9, + Apply = -10, + Help = -11 +} + +/// +/// GTK4 message dialog types. +/// +public enum GtkMessageType +{ + Info = 0, + Warning = 1, + Question = 2, + Error = 3, + Other = 4 +} + +/// +/// GTK4 button layouts for dialogs. +/// +public enum GtkButtonsType +{ + None = 0, + Ok = 1, + Close = 2, + Cancel = 3, + YesNo = 4, + OkCancel = 5 +} + +/// +/// GTK4 file chooser actions. +/// +public enum GtkFileChooserAction +{ + Open = 0, + Save = 1, + SelectFolder = 2, + CreateFolder = 3 +} + +/// +/// Result from a file dialog. +/// +public class FileDialogResult +{ + public bool Accepted { get; init; } + public string[] SelectedFiles { get; init; } = Array.Empty(); + public string? SelectedFile => SelectedFiles.Length > 0 ? SelectedFiles[0] : null; +} + +/// +/// Result from a color dialog. +/// +public class ColorDialogResult +{ + public bool Accepted { get; init; } + public float Red { get; init; } + public float Green { get; init; } + public float Blue { get; init; } + public float Alpha { get; init; } +} + +/// +/// GTK4 interop layer for native Linux dialogs. +/// Provides native file pickers, message boxes, and color choosers. +/// +public class Gtk4InteropService : IDisposable +{ + #region GTK4 Native Interop + + private const string LibGtk4 = "libgtk-4.so.1"; + private const string LibGio = "libgio-2.0.so.0"; + private const string LibGlib = "libglib-2.0.so.0"; + private const string LibGObject = "libgobject-2.0.so.0"; + + // GTK initialization + [DllImport(LibGtk4)] + private static extern bool gtk_init_check(); + + [DllImport(LibGtk4)] + private static extern bool gtk_is_initialized(); + + // Main loop + [DllImport(LibGtk4)] + private static extern IntPtr g_main_context_default(); + + [DllImport(LibGtk4)] + private static extern bool g_main_context_iteration(IntPtr context, bool mayBlock); + + [DllImport(LibGlib)] + private static extern void g_free(IntPtr mem); + + // GObject + [DllImport(LibGObject)] + private static extern void g_object_unref(IntPtr obj); + + [DllImport(LibGObject)] + private static extern void g_object_ref(IntPtr obj); + + // Window + [DllImport(LibGtk4)] + private static extern IntPtr gtk_window_new(); + + [DllImport(LibGtk4)] + private static extern void gtk_window_set_title(IntPtr window, [MarshalAs(UnmanagedType.LPStr)] string title); + + [DllImport(LibGtk4)] + private static extern void gtk_window_set_modal(IntPtr window, bool modal); + + [DllImport(LibGtk4)] + private static extern void gtk_window_set_transient_for(IntPtr window, IntPtr parent); + + [DllImport(LibGtk4)] + private static extern void gtk_window_destroy(IntPtr window); + + [DllImport(LibGtk4)] + private static extern void gtk_window_present(IntPtr window); + + [DllImport(LibGtk4)] + private static extern void gtk_window_close(IntPtr window); + + // Widget + [DllImport(LibGtk4)] + private static extern void gtk_widget_show(IntPtr widget); + + [DllImport(LibGtk4)] + private static extern void gtk_widget_hide(IntPtr widget); + + [DllImport(LibGtk4)] + private static extern void gtk_widget_set_visible(IntPtr widget, bool visible); + + [DllImport(LibGtk4)] + private static extern bool gtk_widget_get_visible(IntPtr widget); + + // Alert Dialog (GTK4) + [DllImport(LibGtk4)] + private static extern IntPtr gtk_alert_dialog_new([MarshalAs(UnmanagedType.LPStr)] string format); + + [DllImport(LibGtk4)] + private static extern void gtk_alert_dialog_set_message(IntPtr dialog, [MarshalAs(UnmanagedType.LPStr)] string message); + + [DllImport(LibGtk4)] + private static extern void gtk_alert_dialog_set_detail(IntPtr dialog, [MarshalAs(UnmanagedType.LPStr)] string detail); + + [DllImport(LibGtk4)] + private static extern void gtk_alert_dialog_set_buttons(IntPtr dialog, string[] labels); + + [DllImport(LibGtk4)] + private static extern void gtk_alert_dialog_set_cancel_button(IntPtr dialog, int button); + + [DllImport(LibGtk4)] + private static extern void gtk_alert_dialog_set_default_button(IntPtr dialog, int button); + + [DllImport(LibGtk4)] + private static extern void gtk_alert_dialog_show(IntPtr dialog, IntPtr parent); + + // File Dialog (GTK4) + [DllImport(LibGtk4)] + private static extern IntPtr gtk_file_dialog_new(); + + [DllImport(LibGtk4)] + private static extern void gtk_file_dialog_set_title(IntPtr dialog, [MarshalAs(UnmanagedType.LPStr)] string title); + + [DllImport(LibGtk4)] + private static extern void gtk_file_dialog_set_modal(IntPtr dialog, bool modal); + + [DllImport(LibGtk4)] + private static extern void gtk_file_dialog_set_accept_label(IntPtr dialog, [MarshalAs(UnmanagedType.LPStr)] string label); + + [DllImport(LibGtk4)] + private static extern void gtk_file_dialog_open(IntPtr dialog, IntPtr parent, IntPtr cancellable, GAsyncReadyCallback callback, IntPtr userData); + + [DllImport(LibGtk4)] + private static extern IntPtr gtk_file_dialog_open_finish(IntPtr dialog, IntPtr result, out IntPtr error); + + [DllImport(LibGtk4)] + private static extern void gtk_file_dialog_save(IntPtr dialog, IntPtr parent, IntPtr cancellable, GAsyncReadyCallback callback, IntPtr userData); + + [DllImport(LibGtk4)] + private static extern IntPtr gtk_file_dialog_save_finish(IntPtr dialog, IntPtr result, out IntPtr error); + + [DllImport(LibGtk4)] + private static extern void gtk_file_dialog_select_folder(IntPtr dialog, IntPtr parent, IntPtr cancellable, GAsyncReadyCallback callback, IntPtr userData); + + [DllImport(LibGtk4)] + private static extern IntPtr gtk_file_dialog_select_folder_finish(IntPtr dialog, IntPtr result, out IntPtr error); + + [DllImport(LibGtk4)] + private static extern void gtk_file_dialog_open_multiple(IntPtr dialog, IntPtr parent, IntPtr cancellable, GAsyncReadyCallback callback, IntPtr userData); + + [DllImport(LibGtk4)] + private static extern IntPtr gtk_file_dialog_open_multiple_finish(IntPtr dialog, IntPtr result, out IntPtr error); + + // File filters + [DllImport(LibGtk4)] + private static extern IntPtr gtk_file_filter_new(); + + [DllImport(LibGtk4)] + private static extern void gtk_file_filter_set_name(IntPtr filter, [MarshalAs(UnmanagedType.LPStr)] string name); + + [DllImport(LibGtk4)] + private static extern void gtk_file_filter_add_pattern(IntPtr filter, [MarshalAs(UnmanagedType.LPStr)] string pattern); + + [DllImport(LibGtk4)] + private static extern void gtk_file_filter_add_mime_type(IntPtr filter, [MarshalAs(UnmanagedType.LPStr)] string mimeType); + + [DllImport(LibGtk4)] + private static extern void gtk_file_dialog_set_default_filter(IntPtr dialog, IntPtr filter); + + // GFile + [DllImport(LibGio)] + private static extern IntPtr g_file_get_path(IntPtr file); + + // GListModel for multiple files + [DllImport(LibGio)] + private static extern uint g_list_model_get_n_items(IntPtr list); + + [DllImport(LibGio)] + private static extern IntPtr g_list_model_get_item(IntPtr list, uint position); + + // Color Dialog (GTK4) + [DllImport(LibGtk4)] + private static extern IntPtr gtk_color_dialog_new(); + + [DllImport(LibGtk4)] + private static extern void gtk_color_dialog_set_title(IntPtr dialog, [MarshalAs(UnmanagedType.LPStr)] string title); + + [DllImport(LibGtk4)] + private static extern void gtk_color_dialog_set_modal(IntPtr dialog, bool modal); + + [DllImport(LibGtk4)] + private static extern void gtk_color_dialog_set_with_alpha(IntPtr dialog, bool withAlpha); + + [DllImport(LibGtk4)] + private static extern void gtk_color_dialog_choose_rgba(IntPtr dialog, IntPtr parent, IntPtr initialColor, IntPtr cancellable, GAsyncReadyCallback callback, IntPtr userData); + + [DllImport(LibGtk4)] + private static extern IntPtr gtk_color_dialog_choose_rgba_finish(IntPtr dialog, IntPtr result, out IntPtr error); + + // GdkRGBA + [StructLayout(LayoutKind.Sequential)] + private struct GdkRGBA + { + public float Red; + public float Green; + public float Blue; + public float Alpha; + } + + // Async callback delegate + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void GAsyncReadyCallback(IntPtr sourceObject, IntPtr result, IntPtr userData); + + // Legacy GTK3 fallbacks + private const string LibGtk3 = "libgtk-3.so.0"; + + [DllImport(LibGtk3, EntryPoint = "gtk_init_check")] + private static extern bool gtk3_init_check(ref int argc, ref IntPtr argv); + + [DllImport(LibGtk3, EntryPoint = "gtk_file_chooser_dialog_new")] + private static extern IntPtr gtk3_file_chooser_dialog_new( + [MarshalAs(UnmanagedType.LPStr)] string title, + IntPtr parent, + int action, + [MarshalAs(UnmanagedType.LPStr)] string firstButtonText, + int firstButtonResponse, + [MarshalAs(UnmanagedType.LPStr)] string secondButtonText, + int secondButtonResponse, + IntPtr terminator); + + [DllImport(LibGtk3, EntryPoint = "gtk_dialog_run")] + private static extern int gtk3_dialog_run(IntPtr dialog); + + [DllImport(LibGtk3, EntryPoint = "gtk_widget_destroy")] + private static extern void gtk3_widget_destroy(IntPtr widget); + + [DllImport(LibGtk3, EntryPoint = "gtk_file_chooser_get_filename")] + private static extern IntPtr gtk3_file_chooser_get_filename(IntPtr chooser); + + [DllImport(LibGtk3, EntryPoint = "gtk_file_chooser_get_filenames")] + private static extern IntPtr gtk3_file_chooser_get_filenames(IntPtr chooser); + + [DllImport(LibGtk3, EntryPoint = "gtk_file_chooser_set_select_multiple")] + private static extern void gtk3_file_chooser_set_select_multiple(IntPtr chooser, bool selectMultiple); + + [DllImport(LibGtk3, EntryPoint = "gtk_message_dialog_new")] + private static extern IntPtr gtk3_message_dialog_new( + IntPtr parent, + int flags, + int type, + int buttons, + [MarshalAs(UnmanagedType.LPStr)] string message); + + [DllImport(LibGlib, EntryPoint = "g_slist_length")] + private static extern uint g_slist_length(IntPtr list); + + [DllImport(LibGlib, EntryPoint = "g_slist_nth_data")] + private static extern IntPtr g_slist_nth_data(IntPtr list, uint n); + + [DllImport(LibGlib, EntryPoint = "g_slist_free")] + private static extern void g_slist_free(IntPtr list); + + #endregion + + #region Fields + + private bool _initialized; + private bool _useGtk4; + private bool _disposed; + private readonly object _lock = new(); + + // Store callbacks to prevent GC + private GAsyncReadyCallback? _currentCallback; + private TaskCompletionSource? _fileDialogTcs; + private TaskCompletionSource? _colorDialogTcs; + private IntPtr _currentDialog; + + #endregion + + #region Properties + + /// + /// Gets whether GTK is initialized. + /// + public bool IsInitialized => _initialized; + + /// + /// Gets whether GTK4 is being used (vs GTK3 fallback). + /// + public bool IsGtk4 => _useGtk4; + + #endregion + + #region Initialization + + /// + /// Initializes the GTK4 interop service. + /// Falls back to GTK3 if GTK4 is not available. + /// + public bool Initialize() + { + if (_initialized) + return true; + + lock (_lock) + { + if (_initialized) + return true; + + // Try GTK4 first + try + { + if (gtk_init_check()) + { + _useGtk4 = true; + _initialized = true; + Console.WriteLine("[GTK4] Initialized GTK4"); + return true; + } + } + catch (DllNotFoundException) + { + Console.WriteLine("[GTK4] GTK4 not found, trying GTK3"); + } + catch (Exception ex) + { + Console.WriteLine($"[GTK4] GTK4 init failed: {ex.Message}"); + } + + // Fall back to GTK3 + try + { + int argc = 0; + IntPtr argv = IntPtr.Zero; + if (gtk3_init_check(ref argc, ref argv)) + { + _useGtk4 = false; + _initialized = true; + Console.WriteLine("[GTK4] Initialized GTK3 (fallback)"); + return true; + } + } + catch (DllNotFoundException) + { + Console.WriteLine("[GTK4] GTK3 not found"); + } + catch (Exception ex) + { + Console.WriteLine($"[GTK4] GTK3 init failed: {ex.Message}"); + } + + return false; + } + } + + #endregion + + #region Message Dialogs + + /// + /// Shows an alert message dialog. + /// + public void ShowAlert(string title, string message, GtkMessageType type = GtkMessageType.Info) + { + if (!EnsureInitialized()) + return; + + if (_useGtk4) + { + var dialog = gtk_alert_dialog_new(title); + gtk_alert_dialog_set_detail(dialog, message); + string[] buttons = { "OK" }; + gtk_alert_dialog_set_buttons(dialog, buttons); + gtk_alert_dialog_show(dialog, IntPtr.Zero); + g_object_unref(dialog); + } + else + { + var dialog = gtk3_message_dialog_new( + IntPtr.Zero, + 1, // GTK_DIALOG_MODAL + (int)type, + (int)GtkButtonsType.Ok, + message); + + gtk3_dialog_run(dialog); + gtk3_widget_destroy(dialog); + } + + ProcessPendingEvents(); + } + + /// + /// Shows a confirmation dialog. + /// + public bool ShowConfirmation(string title, string message) + { + if (!EnsureInitialized()) + return false; + + if (_useGtk4) + { + // GTK4 async dialogs are more complex - use synchronous approach + var dialog = gtk_alert_dialog_new(title); + gtk_alert_dialog_set_detail(dialog, message); + string[] buttons = { "No", "Yes" }; + gtk_alert_dialog_set_buttons(dialog, buttons); + gtk_alert_dialog_set_default_button(dialog, 1); + gtk_alert_dialog_set_cancel_button(dialog, 0); + gtk_alert_dialog_show(dialog, IntPtr.Zero); + g_object_unref(dialog); + // Note: GTK4 alert dialogs are async, this is simplified + return true; + } + else + { + var dialog = gtk3_message_dialog_new( + IntPtr.Zero, + 1, // GTK_DIALOG_MODAL + (int)GtkMessageType.Question, + (int)GtkButtonsType.YesNo, + message); + + int response = gtk3_dialog_run(dialog); + gtk3_widget_destroy(dialog); + ProcessPendingEvents(); + + return response == (int)GtkResponseType.Yes; + } + } + + #endregion + + #region File Dialogs + + /// + /// Shows an open file dialog. + /// + public FileDialogResult ShowOpenFileDialog( + string title = "Open File", + string? initialFolder = null, + bool allowMultiple = false, + params (string Name, string Pattern)[] filters) + { + if (!EnsureInitialized()) + return new FileDialogResult { Accepted = false }; + + if (_useGtk4) + { + return ShowGtk4FileDialog(title, GtkFileChooserAction.Open, allowMultiple, filters); + } + else + { + return ShowGtk3FileDialog(title, 0, allowMultiple, filters); // GTK_FILE_CHOOSER_ACTION_OPEN = 0 + } + } + + /// + /// Shows a save file dialog. + /// + public FileDialogResult ShowSaveFileDialog( + string title = "Save File", + string? suggestedName = null, + params (string Name, string Pattern)[] filters) + { + if (!EnsureInitialized()) + return new FileDialogResult { Accepted = false }; + + if (_useGtk4) + { + return ShowGtk4FileDialog(title, GtkFileChooserAction.Save, false, filters); + } + else + { + return ShowGtk3FileDialog(title, 1, false, filters); // GTK_FILE_CHOOSER_ACTION_SAVE = 1 + } + } + + /// + /// Shows a folder picker dialog. + /// + public FileDialogResult ShowFolderDialog(string title = "Select Folder") + { + if (!EnsureInitialized()) + return new FileDialogResult { Accepted = false }; + + if (_useGtk4) + { + return ShowGtk4FileDialog(title, GtkFileChooserAction.SelectFolder, false, Array.Empty<(string, string)>()); + } + else + { + return ShowGtk3FileDialog(title, 2, false, Array.Empty<(string, string)>()); // GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER = 2 + } + } + + private FileDialogResult ShowGtk4FileDialog( + string title, + GtkFileChooserAction action, + bool allowMultiple, + (string Name, string Pattern)[] filters) + { + var dialog = gtk_file_dialog_new(); + gtk_file_dialog_set_title(dialog, title); + gtk_file_dialog_set_modal(dialog, true); + + // Set up filters + if (filters.Length > 0) + { + var filter = gtk_file_filter_new(); + gtk_file_filter_set_name(filter, filters[0].Name); + gtk_file_filter_add_pattern(filter, filters[0].Pattern); + gtk_file_dialog_set_default_filter(dialog, filter); + } + + // For GTK4, we need async handling - simplified synchronous version + // In a full implementation, this would use proper async/await + _fileDialogTcs = new TaskCompletionSource(); + _currentDialog = dialog; + + _currentCallback = (source, result, userData) => + { + IntPtr error = IntPtr.Zero; + IntPtr file = IntPtr.Zero; + + try + { + if (action == GtkFileChooserAction.Open && !allowMultiple) + file = gtk_file_dialog_open_finish(dialog, result, out error); + else if (action == GtkFileChooserAction.Save) + file = gtk_file_dialog_save_finish(dialog, result, out error); + else if (action == GtkFileChooserAction.SelectFolder) + file = gtk_file_dialog_select_folder_finish(dialog, result, out error); + + if (file != IntPtr.Zero && error == IntPtr.Zero) + { + IntPtr pathPtr = g_file_get_path(file); + string path = Marshal.PtrToStringUTF8(pathPtr) ?? ""; + g_free(pathPtr); + g_object_unref(file); + + _fileDialogTcs?.TrySetResult(new FileDialogResult + { + Accepted = true, + SelectedFiles = new[] { path } + }); + } + else + { + _fileDialogTcs?.TrySetResult(new FileDialogResult { Accepted = false }); + } + } + catch + { + _fileDialogTcs?.TrySetResult(new FileDialogResult { Accepted = false }); + } + }; + + // Start the dialog + if (action == GtkFileChooserAction.Open && !allowMultiple) + gtk_file_dialog_open(dialog, IntPtr.Zero, IntPtr.Zero, _currentCallback, IntPtr.Zero); + else if (action == GtkFileChooserAction.Open && allowMultiple) + gtk_file_dialog_open_multiple(dialog, IntPtr.Zero, IntPtr.Zero, _currentCallback, IntPtr.Zero); + else if (action == GtkFileChooserAction.Save) + gtk_file_dialog_save(dialog, IntPtr.Zero, IntPtr.Zero, _currentCallback, IntPtr.Zero); + else if (action == GtkFileChooserAction.SelectFolder) + gtk_file_dialog_select_folder(dialog, IntPtr.Zero, IntPtr.Zero, _currentCallback, IntPtr.Zero); + + // Process events until dialog completes + while (!_fileDialogTcs.Task.IsCompleted) + { + ProcessPendingEvents(); + Thread.Sleep(10); + } + + g_object_unref(dialog); + return _fileDialogTcs.Task.Result; + } + + private FileDialogResult ShowGtk3FileDialog( + string title, + int action, + bool allowMultiple, + (string Name, string Pattern)[] filters) + { + var dialog = gtk3_file_chooser_dialog_new( + title, + IntPtr.Zero, + action, + "_Cancel", (int)GtkResponseType.Cancel, + action == 1 ? "_Save" : "_Open", (int)GtkResponseType.Accept, + IntPtr.Zero); + + if (allowMultiple) + gtk3_file_chooser_set_select_multiple(dialog, true); + + int response = gtk3_dialog_run(dialog); + + var result = new FileDialogResult { Accepted = false }; + + if (response == (int)GtkResponseType.Accept) + { + if (allowMultiple) + { + IntPtr list = gtk3_file_chooser_get_filenames(dialog); + uint count = g_slist_length(list); + var files = new List(); + + for (uint i = 0; i < count; i++) + { + IntPtr pathPtr = g_slist_nth_data(list, i); + string? path = Marshal.PtrToStringUTF8(pathPtr); + if (!string.IsNullOrEmpty(path)) + { + files.Add(path); + g_free(pathPtr); + } + } + + g_slist_free(list); + result = new FileDialogResult { Accepted = true, SelectedFiles = files.ToArray() }; + } + else + { + IntPtr pathPtr = gtk3_file_chooser_get_filename(dialog); + string? path = Marshal.PtrToStringUTF8(pathPtr); + g_free(pathPtr); + + if (!string.IsNullOrEmpty(path)) + result = new FileDialogResult { Accepted = true, SelectedFiles = new[] { path } }; + } + } + + gtk3_widget_destroy(dialog); + ProcessPendingEvents(); + + return result; + } + + #endregion + + #region Color Dialog + + /// + /// Shows a color picker dialog. + /// + public ColorDialogResult ShowColorDialog( + string title = "Choose Color", + float initialRed = 1f, + float initialGreen = 1f, + float initialBlue = 1f, + float initialAlpha = 1f, + bool withAlpha = true) + { + if (!EnsureInitialized()) + return new ColorDialogResult { Accepted = false }; + + if (_useGtk4) + { + return ShowGtk4ColorDialog(title, initialRed, initialGreen, initialBlue, initialAlpha, withAlpha); + } + else + { + // GTK3 color dialog would go here + return new ColorDialogResult { Accepted = false }; + } + } + + private ColorDialogResult ShowGtk4ColorDialog( + string title, + float r, float g, float b, float a, + bool withAlpha) + { + var dialog = gtk_color_dialog_new(); + gtk_color_dialog_set_title(dialog, title); + gtk_color_dialog_set_modal(dialog, true); + gtk_color_dialog_set_with_alpha(dialog, withAlpha); + + _colorDialogTcs = new TaskCompletionSource(); + + _currentCallback = (source, result, userData) => + { + IntPtr error = IntPtr.Zero; + try + { + IntPtr rgbaPtr = gtk_color_dialog_choose_rgba_finish(dialog, result, out error); + if (rgbaPtr != IntPtr.Zero && error == IntPtr.Zero) + { + var rgba = Marshal.PtrToStructure(rgbaPtr); + _colorDialogTcs?.TrySetResult(new ColorDialogResult + { + Accepted = true, + Red = rgba.Red, + Green = rgba.Green, + Blue = rgba.Blue, + Alpha = rgba.Alpha + }); + } + else + { + _colorDialogTcs?.TrySetResult(new ColorDialogResult { Accepted = false }); + } + } + catch + { + _colorDialogTcs?.TrySetResult(new ColorDialogResult { Accepted = false }); + } + }; + + gtk_color_dialog_choose_rgba(dialog, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, _currentCallback, IntPtr.Zero); + + while (!_colorDialogTcs.Task.IsCompleted) + { + ProcessPendingEvents(); + Thread.Sleep(10); + } + + g_object_unref(dialog); + return _colorDialogTcs.Task.Result; + } + + #endregion + + #region Helpers + + private bool EnsureInitialized() + { + if (!_initialized) + Initialize(); + return _initialized; + } + + private void ProcessPendingEvents() + { + var context = g_main_context_default(); + while (g_main_context_iteration(context, false)) { } + } + + #endregion + + #region IDisposable + + public void Dispose() + { + if (_disposed) + return; + + _disposed = true; + _initialized = false; + + GC.SuppressFinalize(this); + } + + ~Gtk4InteropService() + { + Dispose(); + } + + #endregion +} diff --git a/Services/HardwareVideoService.cs b/Services/HardwareVideoService.cs new file mode 100644 index 0000000..dc8d343 --- /dev/null +++ b/Services/HardwareVideoService.cs @@ -0,0 +1,722 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using SkiaSharp; + +namespace Microsoft.Maui.Platform.Linux.Services; + +/// +/// Supported hardware video acceleration APIs. +/// +public enum VideoAccelerationApi +{ + /// + /// Automatically select the best available API. + /// + Auto, + + /// + /// VA-API (Video Acceleration API) - Intel, AMD, and some NVIDIA. + /// + VaApi, + + /// + /// VDPAU (Video Decode and Presentation API for Unix) - NVIDIA. + /// + Vdpau, + + /// + /// Software decoding fallback. + /// + Software +} + +/// +/// Video codec profiles supported by hardware acceleration. +/// +public enum VideoProfile +{ + H264Baseline, + H264Main, + H264High, + H265Main, + H265Main10, + Vp8, + Vp9Profile0, + Vp9Profile2, + Av1Main +} + +/// +/// Information about a decoded video frame. +/// +public class VideoFrame : IDisposable +{ + public int Width { get; init; } + public int Height { get; init; } + public IntPtr DataY { get; init; } + public IntPtr DataU { get; init; } + public IntPtr DataV { get; init; } + public int StrideY { get; init; } + public int StrideU { get; init; } + public int StrideV { get; init; } + public long Timestamp { get; init; } + public bool IsKeyFrame { get; init; } + + private bool _disposed; + private Action? _releaseCallback; + + internal void SetReleaseCallback(Action callback) => _releaseCallback = callback; + + public void Dispose() + { + if (!_disposed) + { + _releaseCallback?.Invoke(); + _disposed = true; + } + } +} + +/// +/// Hardware-accelerated video decoding service using VA-API or VDPAU. +/// Provides efficient video decode for media playback on Linux. +/// +public class HardwareVideoService : IDisposable +{ + #region VA-API Native Interop + + private const string LibVa = "libva.so.2"; + private const string LibVaDrm = "libva-drm.so.2"; + private const string LibVaX11 = "libva-x11.so.2"; + + // VA-API error codes + private const int VA_STATUS_SUCCESS = 0; + + // VA-API profile constants + private const int VAProfileH264Baseline = 5; + private const int VAProfileH264Main = 6; + private const int VAProfileH264High = 7; + private const int VAProfileHEVCMain = 12; + private const int VAProfileHEVCMain10 = 13; + private const int VAProfileVP8Version0_3 = 14; + private const int VAProfileVP9Profile0 = 15; + private const int VAProfileVP9Profile2 = 17; + private const int VAProfileAV1Profile0 = 20; + + // VA-API entrypoint + private const int VAEntrypointVLD = 1; // Video Decode + + // Surface formats + private const uint VA_RT_FORMAT_YUV420 = 0x00000001; + private const uint VA_RT_FORMAT_YUV420_10 = 0x00000100; + + [DllImport(LibVa)] + private static extern IntPtr vaGetDisplayDRM(int fd); + + [DllImport(LibVaX11)] + private static extern IntPtr vaGetDisplay(IntPtr x11Display); + + [DllImport(LibVa)] + private static extern int vaInitialize(IntPtr display, out int majorVersion, out int minorVersion); + + [DllImport(LibVa)] + private static extern int vaTerminate(IntPtr display); + + [DllImport(LibVa)] + private static extern IntPtr vaErrorStr(int errorCode); + + [DllImport(LibVa)] + private static extern int vaQueryConfigProfiles(IntPtr display, [Out] int[] profileList, out int numProfiles); + + [DllImport(LibVa)] + private static extern int vaQueryConfigEntrypoints(IntPtr display, int profile, [Out] int[] entrypoints, out int numEntrypoints); + + [DllImport(LibVa)] + private static extern int vaCreateConfig(IntPtr display, int profile, int entrypoint, IntPtr attribList, int numAttribs, out uint configId); + + [DllImport(LibVa)] + private static extern int vaDestroyConfig(IntPtr display, uint configId); + + [DllImport(LibVa)] + private static extern int vaCreateContext(IntPtr display, uint configId, int pictureWidth, int pictureHeight, int flag, IntPtr renderTargets, int numRenderTargets, out uint contextId); + + [DllImport(LibVa)] + private static extern int vaDestroyContext(IntPtr display, uint contextId); + + [DllImport(LibVa)] + private static extern int vaCreateSurfaces(IntPtr display, uint format, uint width, uint height, [Out] uint[] surfaces, uint numSurfaces, IntPtr attribList, uint numAttribs); + + [DllImport(LibVa)] + private static extern int vaDestroySurfaces(IntPtr display, [In] uint[] surfaces, int numSurfaces); + + [DllImport(LibVa)] + private static extern int vaSyncSurface(IntPtr display, uint surfaceId); + + [DllImport(LibVa)] + private static extern int vaMapBuffer(IntPtr display, uint bufferId, out IntPtr data); + + [DllImport(LibVa)] + private static extern int vaUnmapBuffer(IntPtr display, uint bufferId); + + [DllImport(LibVa)] + private static extern int vaDeriveImage(IntPtr display, uint surfaceId, out VaImage image); + + [DllImport(LibVa)] + private static extern int vaDestroyImage(IntPtr display, uint imageId); + + [StructLayout(LayoutKind.Sequential)] + private struct VaImage + { + public uint ImageId; + public uint Format; // VAImageFormat (simplified) + public uint FormatFourCC; + public int Width; + public int Height; + public uint DataSize; + public uint NumPlanes; + public uint PitchesPlane0; + public uint PitchesPlane1; + public uint PitchesPlane2; + public uint PitchesPlane3; + public uint OffsetsPlane0; + public uint OffsetsPlane1; + public uint OffsetsPlane2; + public uint OffsetsPlane3; + public uint BufferId; + } + + #endregion + + #region VDPAU Native Interop + + private const string LibVdpau = "libvdpau.so.1"; + + [DllImport(LibVdpau)] + private static extern int vdp_device_create_x11(IntPtr display, int screen, out IntPtr device, out IntPtr getProcAddress); + + #endregion + + #region DRM Interop + + [DllImport("libc", EntryPoint = "open")] + private static extern int open([MarshalAs(UnmanagedType.LPStr)] string path, int flags); + + [DllImport("libc", EntryPoint = "close")] + private static extern int close(int fd); + + private const int O_RDWR = 2; + + #endregion + + #region Fields + + private IntPtr _vaDisplay; + private uint _vaConfigId; + private uint _vaContextId; + private uint[] _vaSurfaces = Array.Empty(); + private int _drmFd = -1; + private bool _initialized; + private bool _disposed; + + private VideoAccelerationApi _currentApi = VideoAccelerationApi.Software; + private int _width; + private int _height; + private VideoProfile _profile; + + private readonly HashSet _supportedProfiles = new(); + private readonly object _lock = new(); + + #endregion + + #region Properties + + /// + /// Gets the currently active video acceleration API. + /// + public VideoAccelerationApi CurrentApi => _currentApi; + + /// + /// Gets whether hardware acceleration is available and initialized. + /// + public bool IsHardwareAccelerated => _currentApi != VideoAccelerationApi.Software && _initialized; + + /// + /// Gets the supported video profiles. + /// + public IReadOnlySet SupportedProfiles => _supportedProfiles; + + #endregion + + #region Initialization + + /// + /// Creates a new hardware video service. + /// + public HardwareVideoService() + { + } + + /// + /// Initializes the hardware video acceleration. + /// + /// The preferred API to use. + /// Optional X11 display for VA-API X11 backend. + /// True if initialization succeeded. + public bool Initialize(VideoAccelerationApi api = VideoAccelerationApi.Auto, IntPtr x11Display = default) + { + if (_initialized) + return true; + + lock (_lock) + { + if (_initialized) + return true; + + // Try VA-API first (works with Intel, AMD, and some NVIDIA) + if (api == VideoAccelerationApi.Auto || api == VideoAccelerationApi.VaApi) + { + if (TryInitializeVaApi(x11Display)) + { + _currentApi = VideoAccelerationApi.VaApi; + _initialized = true; + Console.WriteLine($"[HardwareVideo] Initialized VA-API with {_supportedProfiles.Count} supported profiles"); + return true; + } + } + + // Try VDPAU (NVIDIA proprietary) + if (api == VideoAccelerationApi.Auto || api == VideoAccelerationApi.Vdpau) + { + if (TryInitializeVdpau(x11Display)) + { + _currentApi = VideoAccelerationApi.Vdpau; + _initialized = true; + Console.WriteLine("[HardwareVideo] Initialized VDPAU"); + return true; + } + } + + Console.WriteLine("[HardwareVideo] No hardware acceleration available, using software"); + _currentApi = VideoAccelerationApi.Software; + return false; + } + } + + private bool TryInitializeVaApi(IntPtr x11Display) + { + try + { + // Try DRM backend first (works in Wayland and headless) + string[] drmDevices = { "/dev/dri/renderD128", "/dev/dri/renderD129", "/dev/dri/card0" }; + foreach (var device in drmDevices) + { + _drmFd = open(device, O_RDWR); + if (_drmFd >= 0) + { + _vaDisplay = vaGetDisplayDRM(_drmFd); + if (_vaDisplay != IntPtr.Zero) + { + if (InitializeVaDisplay()) + return true; + } + close(_drmFd); + _drmFd = -1; + } + } + + // Fall back to X11 backend if display provided + if (x11Display != IntPtr.Zero) + { + _vaDisplay = vaGetDisplay(x11Display); + if (_vaDisplay != IntPtr.Zero && InitializeVaDisplay()) + return true; + } + + return false; + } + catch (DllNotFoundException) + { + Console.WriteLine("[HardwareVideo] VA-API libraries not found"); + return false; + } + catch (Exception ex) + { + Console.WriteLine($"[HardwareVideo] VA-API initialization failed: {ex.Message}"); + return false; + } + } + + private bool InitializeVaDisplay() + { + int status = vaInitialize(_vaDisplay, out int major, out int minor); + if (status != VA_STATUS_SUCCESS) + { + Console.WriteLine($"[HardwareVideo] vaInitialize failed: {GetVaError(status)}"); + return false; + } + + Console.WriteLine($"[HardwareVideo] VA-API {major}.{minor} initialized"); + + // Query supported profiles + int[] profiles = new int[32]; + status = vaQueryConfigProfiles(_vaDisplay, profiles, out int numProfiles); + if (status == VA_STATUS_SUCCESS) + { + for (int i = 0; i < numProfiles; i++) + { + if (TryMapVaProfile(profiles[i], out var videoProfile)) + { + // Check if VLD (decode) entrypoint is supported + int[] entrypoints = new int[8]; + if (vaQueryConfigEntrypoints(_vaDisplay, profiles[i], entrypoints, out int numEntrypoints) == VA_STATUS_SUCCESS) + { + for (int j = 0; j < numEntrypoints; j++) + { + if (entrypoints[j] == VAEntrypointVLD) + { + _supportedProfiles.Add(videoProfile); + break; + } + } + } + } + } + } + + return true; + } + + private bool TryInitializeVdpau(IntPtr x11Display) + { + if (x11Display == IntPtr.Zero) + return false; + + try + { + int result = vdp_device_create_x11(x11Display, 0, out IntPtr device, out IntPtr getProcAddress); + if (result == 0 && device != IntPtr.Zero) + { + // VDPAU initialized - would need additional setup for actual use + // For now, just mark as available + _supportedProfiles.Add(VideoProfile.H264Baseline); + _supportedProfiles.Add(VideoProfile.H264Main); + _supportedProfiles.Add(VideoProfile.H264High); + return true; + } + } + catch (DllNotFoundException) + { + Console.WriteLine("[HardwareVideo] VDPAU libraries not found"); + } + catch (Exception ex) + { + Console.WriteLine($"[HardwareVideo] VDPAU initialization failed: {ex.Message}"); + } + + return false; + } + + #endregion + + #region Decoder Creation + + /// + /// Creates a decoder context for the specified profile and dimensions. + /// + public bool CreateDecoder(VideoProfile profile, int width, int height) + { + if (!_initialized || _currentApi == VideoAccelerationApi.Software) + return false; + + if (!_supportedProfiles.Contains(profile)) + { + Console.WriteLine($"[HardwareVideo] Profile {profile} not supported"); + return false; + } + + lock (_lock) + { + // Destroy existing context + DestroyDecoder(); + + _width = width; + _height = height; + _profile = profile; + + if (_currentApi == VideoAccelerationApi.VaApi) + return CreateVaApiDecoder(profile, width, height); + + return false; + } + } + + private bool CreateVaApiDecoder(VideoProfile profile, int width, int height) + { + int vaProfile = MapToVaProfile(profile); + + // Create config + int status = vaCreateConfig(_vaDisplay, vaProfile, VAEntrypointVLD, IntPtr.Zero, 0, out _vaConfigId); + if (status != VA_STATUS_SUCCESS) + { + Console.WriteLine($"[HardwareVideo] vaCreateConfig failed: {GetVaError(status)}"); + return false; + } + + // Create surfaces for decoded frames (use a pool of 8) + uint format = profile == VideoProfile.H265Main10 || profile == VideoProfile.Vp9Profile2 + ? VA_RT_FORMAT_YUV420_10 + : VA_RT_FORMAT_YUV420; + + _vaSurfaces = new uint[8]; + status = vaCreateSurfaces(_vaDisplay, format, (uint)width, (uint)height, _vaSurfaces, 8, IntPtr.Zero, 0); + if (status != VA_STATUS_SUCCESS) + { + Console.WriteLine($"[HardwareVideo] vaCreateSurfaces failed: {GetVaError(status)}"); + vaDestroyConfig(_vaDisplay, _vaConfigId); + return false; + } + + // Create context + status = vaCreateContext(_vaDisplay, _vaConfigId, width, height, 0, IntPtr.Zero, 0, out _vaContextId); + if (status != VA_STATUS_SUCCESS) + { + Console.WriteLine($"[HardwareVideo] vaCreateContext failed: {GetVaError(status)}"); + vaDestroySurfaces(_vaDisplay, _vaSurfaces, _vaSurfaces.Length); + vaDestroyConfig(_vaDisplay, _vaConfigId); + return false; + } + + Console.WriteLine($"[HardwareVideo] Created decoder: {profile} {width}x{height}"); + return true; + } + + /// + /// Destroys the current decoder context. + /// + public void DestroyDecoder() + { + lock (_lock) + { + if (_currentApi == VideoAccelerationApi.VaApi && _vaDisplay != IntPtr.Zero) + { + if (_vaContextId != 0) + { + vaDestroyContext(_vaDisplay, _vaContextId); + _vaContextId = 0; + } + + if (_vaSurfaces.Length > 0) + { + vaDestroySurfaces(_vaDisplay, _vaSurfaces, _vaSurfaces.Length); + _vaSurfaces = Array.Empty(); + } + + if (_vaConfigId != 0) + { + vaDestroyConfig(_vaDisplay, _vaConfigId); + _vaConfigId = 0; + } + } + } + } + + #endregion + + #region Frame Retrieval + + /// + /// Retrieves a decoded frame from the specified surface. + /// + public VideoFrame? GetDecodedFrame(int surfaceIndex, long timestamp, bool isKeyFrame) + { + if (!_initialized || _currentApi != VideoAccelerationApi.VaApi) + return null; + + if (surfaceIndex < 0 || surfaceIndex >= _vaSurfaces.Length) + return null; + + uint surfaceId = _vaSurfaces[surfaceIndex]; + + // Wait for decode to complete + int status = vaSyncSurface(_vaDisplay, surfaceId); + if (status != VA_STATUS_SUCCESS) + return null; + + // Derive image from surface + status = vaDeriveImage(_vaDisplay, surfaceId, out VaImage image); + if (status != VA_STATUS_SUCCESS) + return null; + + // Map the buffer + status = vaMapBuffer(_vaDisplay, image.BufferId, out IntPtr data); + if (status != VA_STATUS_SUCCESS) + { + vaDestroyImage(_vaDisplay, image.ImageId); + return null; + } + + var frame = new VideoFrame + { + Width = image.Width, + Height = image.Height, + DataY = data + (int)image.OffsetsPlane0, + DataU = data + (int)image.OffsetsPlane1, + DataV = data + (int)image.OffsetsPlane2, + StrideY = (int)image.PitchesPlane0, + StrideU = (int)image.PitchesPlane1, + StrideV = (int)image.PitchesPlane2, + Timestamp = timestamp, + IsKeyFrame = isKeyFrame + }; + + // Set cleanup callback + frame.SetReleaseCallback(() => + { + vaUnmapBuffer(_vaDisplay, image.BufferId); + vaDestroyImage(_vaDisplay, image.ImageId); + }); + + return frame; + } + + /// + /// Converts a decoded frame to an SKBitmap for display. + /// + public SKBitmap? ConvertFrameToSkia(VideoFrame frame) + { + if (frame == null) + return null; + + // Create BGRA bitmap + var bitmap = new SKBitmap(frame.Width, frame.Height, SKColorType.Bgra8888, SKAlphaType.Opaque); + + // Convert YUV to BGRA + unsafe + { + byte* yPtr = (byte*)frame.DataY; + byte* uPtr = (byte*)frame.DataU; + byte* vPtr = (byte*)frame.DataV; + byte* dst = (byte*)bitmap.GetPixels(); + + for (int y = 0; y < frame.Height; y++) + { + for (int x = 0; x < frame.Width; x++) + { + int yIndex = y * frame.StrideY + x; + int uvIndex = (y / 2) * frame.StrideU + (x / 2); + + int yVal = yPtr[yIndex]; + int uVal = uPtr[uvIndex] - 128; + int vVal = vPtr[uvIndex] - 128; + + // YUV to RGB conversion + int r = (int)(yVal + 1.402 * vVal); + int g = (int)(yVal - 0.344 * uVal - 0.714 * vVal); + int b = (int)(yVal + 1.772 * uVal); + + r = Math.Clamp(r, 0, 255); + g = Math.Clamp(g, 0, 255); + b = Math.Clamp(b, 0, 255); + + int dstIndex = (y * frame.Width + x) * 4; + dst[dstIndex] = (byte)b; + dst[dstIndex + 1] = (byte)g; + dst[dstIndex + 2] = (byte)r; + dst[dstIndex + 3] = 255; + } + } + } + + return bitmap; + } + + #endregion + + #region Helpers + + private static bool TryMapVaProfile(int vaProfile, out VideoProfile profile) + { + profile = vaProfile switch + { + VAProfileH264Baseline => VideoProfile.H264Baseline, + VAProfileH264Main => VideoProfile.H264Main, + VAProfileH264High => VideoProfile.H264High, + VAProfileHEVCMain => VideoProfile.H265Main, + VAProfileHEVCMain10 => VideoProfile.H265Main10, + VAProfileVP8Version0_3 => VideoProfile.Vp8, + VAProfileVP9Profile0 => VideoProfile.Vp9Profile0, + VAProfileVP9Profile2 => VideoProfile.Vp9Profile2, + VAProfileAV1Profile0 => VideoProfile.Av1Main, + _ => VideoProfile.H264Main + }; + + return vaProfile >= VAProfileH264Baseline && vaProfile <= VAProfileAV1Profile0; + } + + private static int MapToVaProfile(VideoProfile profile) + { + return profile switch + { + VideoProfile.H264Baseline => VAProfileH264Baseline, + VideoProfile.H264Main => VAProfileH264Main, + VideoProfile.H264High => VAProfileH264High, + VideoProfile.H265Main => VAProfileHEVCMain, + VideoProfile.H265Main10 => VAProfileHEVCMain10, + VideoProfile.Vp8 => VAProfileVP8Version0_3, + VideoProfile.Vp9Profile0 => VAProfileVP9Profile0, + VideoProfile.Vp9Profile2 => VAProfileVP9Profile2, + VideoProfile.Av1Main => VAProfileAV1Profile0, + _ => VAProfileH264Main + }; + } + + private static string GetVaError(int status) + { + try + { + IntPtr errPtr = vaErrorStr(status); + return Marshal.PtrToStringAnsi(errPtr) ?? $"Unknown error {status}"; + } + catch + { + return $"Error code {status}"; + } + } + + #endregion + + #region IDisposable + + public void Dispose() + { + if (_disposed) + return; + + _disposed = true; + + DestroyDecoder(); + + if (_currentApi == VideoAccelerationApi.VaApi && _vaDisplay != IntPtr.Zero) + { + vaTerminate(_vaDisplay); + _vaDisplay = IntPtr.Zero; + } + + if (_drmFd >= 0) + { + close(_drmFd); + _drmFd = -1; + } + + GC.SuppressFinalize(this); + } + + ~HardwareVideoService() + { + Dispose(); + } + + #endregion +} diff --git a/Services/LinuxResourcesProvider.cs b/Services/LinuxResourcesProvider.cs new file mode 100644 index 0000000..7b4d52e --- /dev/null +++ b/Services/LinuxResourcesProvider.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Maui.Controls; +using Microsoft.Maui.Controls.Internals; + +[assembly: Dependency(typeof(Microsoft.Maui.Platform.Linux.Services.LinuxResourcesProvider))] + +namespace Microsoft.Maui.Platform.Linux.Services; + +/// +/// Provides system resources for the Linux platform. +/// +internal sealed class LinuxResourcesProvider : ISystemResourcesProvider +{ + private ResourceDictionary? _dictionary; + + public IResourceDictionary GetSystemResources() + { + _dictionary ??= CreateResourceDictionary(); + return _dictionary; + } + + private ResourceDictionary CreateResourceDictionary() + { + var dictionary = new ResourceDictionary(); + + // Add default styles + dictionary[Device.Styles.BodyStyleKey] = new Style(typeof(Label)); + dictionary[Device.Styles.TitleStyleKey] = CreateTitleStyle(); + dictionary[Device.Styles.SubtitleStyleKey] = CreateSubtitleStyle(); + dictionary[Device.Styles.CaptionStyleKey] = CreateCaptionStyle(); + dictionary[Device.Styles.ListItemTextStyleKey] = new Style(typeof(Label)); + dictionary[Device.Styles.ListItemDetailTextStyleKey] = CreateCaptionStyle(); + + return dictionary; + } + + private static Style CreateTitleStyle() => new(typeof(Label)) + { + Setters = { new Setter { Property = Label.FontSizeProperty, Value = 24.0 } } + }; + + private static Style CreateSubtitleStyle() => new(typeof(Label)) + { + Setters = { new Setter { Property = Label.FontSizeProperty, Value = 18.0 } } + }; + + private static Style CreateCaptionStyle() => new(typeof(Label)) + { + Setters = { new Setter { Property = Label.FontSizeProperty, Value = 12.0 } } + }; +} diff --git a/Services/SystemClipboard.cs b/Services/SystemClipboard.cs new file mode 100644 index 0000000..4e70ccd --- /dev/null +++ b/Services/SystemClipboard.cs @@ -0,0 +1,248 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; + +namespace Microsoft.Maui.Platform; + +/// +/// Static helper for system clipboard access using xclip/xsel. +/// Provides synchronous access for use in UI event handlers. +/// +public static class SystemClipboard +{ + /// + /// Gets text from the system clipboard. + /// + public static string? GetText() + { + // Try xclip first + var result = TryGetWithXclip(); + if (result != null) return result; + + // Try xsel as fallback + result = TryGetWithXsel(); + if (result != null) return result; + + // Try wl-paste for Wayland + return TryGetWithWlPaste(); + } + + /// + /// Sets text to the system clipboard. + /// + public static void SetText(string? text) + { + if (string.IsNullOrEmpty(text)) + { + ClearClipboard(); + return; + } + + // Try xclip first + if (TrySetWithXclip(text)) return; + + // Try xsel as fallback + if (TrySetWithXsel(text)) return; + + // Try wl-copy for Wayland + TrySetWithWlCopy(text); + } + + private static string? TryGetWithXclip() + { + try + { + var startInfo = new ProcessStartInfo + { + FileName = "xclip", + Arguments = "-selection clipboard -o", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using var process = Process.Start(startInfo); + if (process == null) return null; + + var output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(1000); + + return process.ExitCode == 0 ? output : null; + } + catch + { + return null; + } + } + + private static string? TryGetWithXsel() + { + try + { + var startInfo = new ProcessStartInfo + { + FileName = "xsel", + Arguments = "--clipboard --output", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using var process = Process.Start(startInfo); + if (process == null) return null; + + var output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(1000); + + return process.ExitCode == 0 ? output : null; + } + catch + { + return null; + } + } + + private static string? TryGetWithWlPaste() + { + try + { + var startInfo = new ProcessStartInfo + { + FileName = "wl-paste", + Arguments = "--no-newline", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using var process = Process.Start(startInfo); + if (process == null) return null; + + var output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(1000); + + return process.ExitCode == 0 ? output : null; + } + catch + { + return null; + } + } + + private static bool TrySetWithXclip(string text) + { + try + { + var startInfo = new ProcessStartInfo + { + FileName = "xclip", + Arguments = "-selection clipboard", + UseShellExecute = false, + RedirectStandardInput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using var process = Process.Start(startInfo); + if (process == null) return false; + + process.StandardInput.Write(text); + process.StandardInput.Close(); + + process.WaitForExit(1000); + return process.ExitCode == 0; + } + catch + { + return false; + } + } + + private static bool TrySetWithXsel(string text) + { + try + { + var startInfo = new ProcessStartInfo + { + FileName = "xsel", + Arguments = "--clipboard --input", + UseShellExecute = false, + RedirectStandardInput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using var process = Process.Start(startInfo); + if (process == null) return false; + + process.StandardInput.Write(text); + process.StandardInput.Close(); + + process.WaitForExit(1000); + return process.ExitCode == 0; + } + catch + { + return false; + } + } + + private static bool TrySetWithWlCopy(string text) + { + try + { + var startInfo = new ProcessStartInfo + { + FileName = "wl-copy", + UseShellExecute = false, + RedirectStandardInput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using var process = Process.Start(startInfo); + if (process == null) return false; + + process.StandardInput.Write(text); + process.StandardInput.Close(); + + process.WaitForExit(1000); + return process.ExitCode == 0; + } + catch + { + return false; + } + } + + private static void ClearClipboard() + { + try + { + // Try xclip + var startInfo = new ProcessStartInfo + { + FileName = "xclip", + Arguments = "-selection clipboard", + UseShellExecute = false, + RedirectStandardInput = true, + CreateNoWindow = true + }; + + using var process = Process.Start(startInfo); + if (process != null) + { + process.StandardInput.Close(); + process.WaitForExit(1000); + } + } + catch + { + // Ignore errors when clearing + } + } +} diff --git a/Views/SkiaActivityIndicator.cs b/Views/SkiaActivityIndicator.cs index 881237e..bdf374c 100644 --- a/Views/SkiaActivityIndicator.cs +++ b/Views/SkiaActivityIndicator.cs @@ -6,38 +6,169 @@ using SkiaSharp; namespace Microsoft.Maui.Platform; /// -/// Skia-rendered activity indicator (spinner) control. +/// Skia-rendered activity indicator (spinner) control with full XAML styling support. /// public class SkiaActivityIndicator : SkiaView { - private bool _isRunning; + #region BindableProperties + + /// + /// Bindable property for IsRunning. + /// + public static readonly BindableProperty IsRunningProperty = + BindableProperty.Create( + nameof(IsRunning), + typeof(bool), + typeof(SkiaActivityIndicator), + false, + propertyChanged: (b, o, n) => ((SkiaActivityIndicator)b).OnIsRunningChanged()); + + /// + /// Bindable property for Color. + /// + public static readonly BindableProperty ColorProperty = + BindableProperty.Create( + nameof(Color), + typeof(SKColor), + typeof(SkiaActivityIndicator), + new SKColor(0x21, 0x96, 0xF3), + propertyChanged: (b, o, n) => ((SkiaActivityIndicator)b).Invalidate()); + + /// + /// Bindable property for DisabledColor. + /// + public static readonly BindableProperty DisabledColorProperty = + BindableProperty.Create( + nameof(DisabledColor), + typeof(SKColor), + typeof(SkiaActivityIndicator), + new SKColor(0xBD, 0xBD, 0xBD), + propertyChanged: (b, o, n) => ((SkiaActivityIndicator)b).Invalidate()); + + /// + /// Bindable property for Size. + /// + public static readonly BindableProperty SizeProperty = + BindableProperty.Create( + nameof(Size), + typeof(float), + typeof(SkiaActivityIndicator), + 32f, + propertyChanged: (b, o, n) => ((SkiaActivityIndicator)b).InvalidateMeasure()); + + /// + /// Bindable property for StrokeWidth. + /// + public static readonly BindableProperty StrokeWidthProperty = + BindableProperty.Create( + nameof(StrokeWidth), + typeof(float), + typeof(SkiaActivityIndicator), + 3f, + propertyChanged: (b, o, n) => ((SkiaActivityIndicator)b).InvalidateMeasure()); + + /// + /// Bindable property for RotationSpeed. + /// + public static readonly BindableProperty RotationSpeedProperty = + BindableProperty.Create( + nameof(RotationSpeed), + typeof(float), + typeof(SkiaActivityIndicator), + 360f); + + /// + /// Bindable property for ArcCount. + /// + public static readonly BindableProperty ArcCountProperty = + BindableProperty.Create( + nameof(ArcCount), + typeof(int), + typeof(SkiaActivityIndicator), + 12, + propertyChanged: (b, o, n) => ((SkiaActivityIndicator)b).Invalidate()); + + #endregion + + #region Properties + + /// + /// Gets or sets whether the indicator is running. + /// + public bool IsRunning + { + get => (bool)GetValue(IsRunningProperty); + set => SetValue(IsRunningProperty, value); + } + + /// + /// Gets or sets the indicator color. + /// + public SKColor Color + { + get => (SKColor)GetValue(ColorProperty); + set => SetValue(ColorProperty, value); + } + + /// + /// Gets or sets the disabled color. + /// + public SKColor DisabledColor + { + get => (SKColor)GetValue(DisabledColorProperty); + set => SetValue(DisabledColorProperty, value); + } + + /// + /// Gets or sets the indicator size. + /// + public float Size + { + get => (float)GetValue(SizeProperty); + set => SetValue(SizeProperty, value); + } + + /// + /// Gets or sets the stroke width. + /// + public float StrokeWidth + { + get => (float)GetValue(StrokeWidthProperty); + set => SetValue(StrokeWidthProperty, value); + } + + /// + /// Gets or sets the rotation speed in degrees per second. + /// + public float RotationSpeed + { + get => (float)GetValue(RotationSpeedProperty); + set => SetValue(RotationSpeedProperty, value); + } + + /// + /// Gets or sets the number of arcs. + /// + public int ArcCount + { + get => (int)GetValue(ArcCountProperty); + set => SetValue(ArcCountProperty, value); + } + + #endregion + private float _rotationAngle; private DateTime _lastUpdateTime = DateTime.UtcNow; - public bool IsRunning + private void OnIsRunningChanged() { - get => _isRunning; - set + if (IsRunning) { - if (_isRunning != value) - { - _isRunning = value; - if (value) - { - _lastUpdateTime = DateTime.UtcNow; - } - Invalidate(); - } + _lastUpdateTime = DateTime.UtcNow; } + Invalidate(); } - public SKColor Color { get; set; } = new SKColor(0x21, 0x96, 0xF3); - public SKColor DisabledColor { get; set; } = new SKColor(0xBD, 0xBD, 0xBD); - public float Size { get; set; } = 32; - public float StrokeWidth { get; set; } = 3; - public float RotationSpeed { get; set; } = 360; // Degrees per second - public int ArcCount { get; set; } = 12; - protected override void OnDraw(SKCanvas canvas, SKRect bounds) { if (!IsRunning && !IsEnabled) diff --git a/Views/SkiaAlertDialog.cs b/Views/SkiaAlertDialog.cs new file mode 100644 index 0000000..92ba9d6 --- /dev/null +++ b/Views/SkiaAlertDialog.cs @@ -0,0 +1,385 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using SkiaSharp; + +namespace Microsoft.Maui.Platform; + +/// +/// A modal alert dialog rendered with Skia. +/// Supports title, message, and up to two buttons (cancel/accept). +/// +public class SkiaAlertDialog : SkiaView +{ + private readonly string _title; + private readonly string _message; + private readonly string? _cancel; + private readonly string? _accept; + private readonly TaskCompletionSource _tcs; + + private SKRect _cancelButtonBounds; + private SKRect _acceptButtonBounds; + private bool _cancelHovered; + private bool _acceptHovered; + + // Dialog styling + private static readonly SKColor OverlayColor = new SKColor(0, 0, 0, 128); + private static readonly SKColor DialogBackground = SKColors.White; + private static readonly SKColor TitleColor = new SKColor(0x21, 0x21, 0x21); + private static readonly SKColor MessageColor = new SKColor(0x61, 0x61, 0x61); + private static readonly SKColor ButtonColor = new SKColor(0x21, 0x96, 0xF3); + private static readonly SKColor ButtonHoverColor = new SKColor(0x19, 0x76, 0xD2); + private static readonly SKColor ButtonTextColor = SKColors.White; + private static readonly SKColor CancelButtonColor = new SKColor(0x9E, 0x9E, 0x9E); + private static readonly SKColor CancelButtonHoverColor = new SKColor(0x75, 0x75, 0x75); + private static readonly SKColor BorderColor = new SKColor(0xE0, 0xE0, 0xE0); + + private const float DialogWidth = 400; + private const float DialogPadding = 24; + private const float ButtonHeight = 44; + private const float ButtonSpacing = 12; + private const float CornerRadius = 12; + + /// + /// Creates a new alert dialog. + /// + public SkiaAlertDialog(string title, string message, string? accept, string? cancel) + { + _title = title; + _message = message; + _accept = accept; + _cancel = cancel; + _tcs = new TaskCompletionSource(); + IsFocusable = true; + } + + /// + /// Gets the task that completes when the dialog is dismissed. + /// Returns true if accept was clicked, false if cancel was clicked. + /// + public Task Result => _tcs.Task; + + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + // Draw semi-transparent overlay covering entire screen + using var overlayPaint = new SKPaint + { + Color = OverlayColor, + Style = SKPaintStyle.Fill + }; + canvas.DrawRect(bounds, overlayPaint); + + // Calculate dialog dimensions + var messageLines = WrapText(_message, DialogWidth - DialogPadding * 2, 16); + var dialogHeight = CalculateDialogHeight(messageLines.Count); + + var dialogLeft = bounds.MidX - DialogWidth / 2; + var dialogTop = bounds.MidY - dialogHeight / 2; + var dialogBounds = new SKRect(dialogLeft, dialogTop, dialogLeft + DialogWidth, dialogTop + dialogHeight); + + // Draw dialog shadow + using var shadowPaint = new SKPaint + { + Color = new SKColor(0, 0, 0, 60), + MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 8), + Style = SKPaintStyle.Fill + }; + var shadowRect = new SKRect(dialogBounds.Left + 4, dialogBounds.Top + 4, + dialogBounds.Right + 4, dialogBounds.Bottom + 4); + canvas.DrawRoundRect(shadowRect, CornerRadius, CornerRadius, shadowPaint); + + // Draw dialog background + using var bgPaint = new SKPaint + { + Color = DialogBackground, + Style = SKPaintStyle.Fill, + IsAntialias = true + }; + canvas.DrawRoundRect(dialogBounds, CornerRadius, CornerRadius, bgPaint); + + // Draw title + var yOffset = dialogBounds.Top + DialogPadding; + if (!string.IsNullOrEmpty(_title)) + { + using var titleFont = new SKFont(SKTypeface.Default, 20) { Embolden = true }; + using var titlePaint = new SKPaint(titleFont) + { + Color = TitleColor, + IsAntialias = true + }; + canvas.DrawText(_title, dialogBounds.Left + DialogPadding, yOffset + 20, titlePaint); + yOffset += 36; + } + + // Draw message + if (!string.IsNullOrEmpty(_message)) + { + using var messageFont = new SKFont(SKTypeface.Default, 16); + using var messagePaint = new SKPaint(messageFont) + { + Color = MessageColor, + IsAntialias = true + }; + + foreach (var line in messageLines) + { + canvas.DrawText(line, dialogBounds.Left + DialogPadding, yOffset + 16, messagePaint); + yOffset += 22; + } + yOffset += 8; + } + + // Draw buttons + yOffset = dialogBounds.Bottom - DialogPadding - ButtonHeight; + var buttonY = yOffset; + + var buttonCount = (_accept != null ? 1 : 0) + (_cancel != null ? 1 : 0); + var totalButtonWidth = DialogWidth - DialogPadding * 2; + + if (buttonCount == 2) + { + var singleButtonWidth = (totalButtonWidth - ButtonSpacing) / 2; + + // Cancel button (left) + _cancelButtonBounds = new SKRect( + dialogBounds.Left + DialogPadding, + buttonY, + dialogBounds.Left + DialogPadding + singleButtonWidth, + buttonY + ButtonHeight); + DrawButton(canvas, _cancelButtonBounds, _cancel!, + _cancelHovered ? CancelButtonHoverColor : CancelButtonColor); + + // Accept button (right) + _acceptButtonBounds = new SKRect( + dialogBounds.Right - DialogPadding - singleButtonWidth, + buttonY, + dialogBounds.Right - DialogPadding, + buttonY + ButtonHeight); + DrawButton(canvas, _acceptButtonBounds, _accept!, + _acceptHovered ? ButtonHoverColor : ButtonColor); + } + else if (_accept != null) + { + _acceptButtonBounds = new SKRect( + dialogBounds.Left + DialogPadding, + buttonY, + dialogBounds.Right - DialogPadding, + buttonY + ButtonHeight); + DrawButton(canvas, _acceptButtonBounds, _accept, + _acceptHovered ? ButtonHoverColor : ButtonColor); + } + else if (_cancel != null) + { + _cancelButtonBounds = new SKRect( + dialogBounds.Left + DialogPadding, + buttonY, + dialogBounds.Right - DialogPadding, + buttonY + ButtonHeight); + DrawButton(canvas, _cancelButtonBounds, _cancel, + _cancelHovered ? CancelButtonHoverColor : CancelButtonColor); + } + } + + private void DrawButton(SKCanvas canvas, SKRect bounds, string text, SKColor bgColor) + { + // Button background + using var bgPaint = new SKPaint + { + Color = bgColor, + Style = SKPaintStyle.Fill, + IsAntialias = true + }; + canvas.DrawRoundRect(bounds, 8, 8, bgPaint); + + // Button text + using var font = new SKFont(SKTypeface.Default, 16) { Embolden = true }; + using var textPaint = new SKPaint(font) + { + Color = ButtonTextColor, + IsAntialias = true + }; + + var textBounds = new SKRect(); + textPaint.MeasureText(text, ref textBounds); + + var x = bounds.MidX - textBounds.MidX; + var y = bounds.MidY - textBounds.MidY; + canvas.DrawText(text, x, y, textPaint); + } + + private float CalculateDialogHeight(int messageLineCount) + { + var height = DialogPadding * 2; // Top and bottom padding + + if (!string.IsNullOrEmpty(_title)) + height += 36; // Title height + + if (!string.IsNullOrEmpty(_message)) + height += messageLineCount * 22 + 8; // Message lines + spacing + + height += ButtonHeight; // Buttons + + return Math.Max(height, 180); // Minimum height + } + + private List WrapText(string text, float maxWidth, float fontSize) + { + var lines = new List(); + if (string.IsNullOrEmpty(text)) + return lines; + + using var font = new SKFont(SKTypeface.Default, fontSize); + using var paint = new SKPaint(font); + + var words = text.Split(' '); + var currentLine = ""; + + foreach (var word in words) + { + var testLine = string.IsNullOrEmpty(currentLine) ? word : currentLine + " " + word; + var width = paint.MeasureText(testLine); + + if (width > maxWidth && !string.IsNullOrEmpty(currentLine)) + { + lines.Add(currentLine); + currentLine = word; + } + else + { + currentLine = testLine; + } + } + + if (!string.IsNullOrEmpty(currentLine)) + lines.Add(currentLine); + + return lines; + } + + public override void OnPointerMoved(PointerEventArgs e) + { + var wasHovered = _cancelHovered || _acceptHovered; + + _cancelHovered = _cancel != null && _cancelButtonBounds.Contains(e.X, e.Y); + _acceptHovered = _accept != null && _acceptButtonBounds.Contains(e.X, e.Y); + + if (wasHovered != (_cancelHovered || _acceptHovered)) + Invalidate(); + } + + public override void OnPointerPressed(PointerEventArgs e) + { + // Check if clicking on buttons + if (_cancel != null && _cancelButtonBounds.Contains(e.X, e.Y)) + { + Dismiss(false); + return; + } + + if (_accept != null && _acceptButtonBounds.Contains(e.X, e.Y)) + { + Dismiss(true); + return; + } + + // Clicking outside dialog doesn't dismiss it (it's modal) + } + + public override void OnKeyDown(KeyEventArgs e) + { + // Handle Escape to cancel + if (e.Key == Key.Escape && _cancel != null) + { + Dismiss(false); + e.Handled = true; + return; + } + + // Handle Enter to accept + if (e.Key == Key.Enter && _accept != null) + { + Dismiss(true); + e.Handled = true; + return; + } + } + + private void Dismiss(bool result) + { + // Remove from dialog system + LinuxDialogService.HideDialog(this); + _tcs.TrySetResult(result); + } + + protected override SKSize MeasureOverride(SKSize availableSize) + { + // Dialog takes full screen for the overlay + return availableSize; + } + + public override SkiaView? HitTest(float x, float y) + { + // Modal dialogs capture all input + return this; + } +} + +/// +/// Service for showing modal dialogs in OpenMaui Linux. +/// +public static class LinuxDialogService +{ + private static readonly List _activeDialogs = new(); + private static Action? _invalidateCallback; + + /// + /// Registers the invalidation callback (called by LinuxApplication). + /// + public static void SetInvalidateCallback(Action callback) + { + _invalidateCallback = callback; + } + + /// + /// Shows an alert dialog and returns when dismissed. + /// + public static Task ShowAlertAsync(string title, string message, string? accept, string? cancel) + { + var dialog = new SkiaAlertDialog(title, message, accept, cancel); + _activeDialogs.Add(dialog); + _invalidateCallback?.Invoke(); + return dialog.Result; + } + + /// + /// Hides a dialog. + /// + internal static void HideDialog(SkiaAlertDialog dialog) + { + _activeDialogs.Remove(dialog); + _invalidateCallback?.Invoke(); + } + + /// + /// Gets whether there are active dialogs. + /// + public static bool HasActiveDialog => _activeDialogs.Count > 0; + + /// + /// Gets the topmost dialog. + /// + public static SkiaAlertDialog? TopDialog => _activeDialogs.Count > 0 ? _activeDialogs[^1] : null; + + /// + /// Draws all active dialogs. + /// + public static void DrawDialogs(SKCanvas canvas, SKRect bounds) + { + foreach (var dialog in _activeDialogs) + { + dialog.Measure(new SKSize(bounds.Width, bounds.Height)); + dialog.Arrange(bounds); + dialog.Draw(canvas); + } + } +} diff --git a/Views/SkiaBorder.cs b/Views/SkiaBorder.cs index 34fcfd3..79b5268 100644 --- a/Views/SkiaBorder.cs +++ b/Views/SkiaBorder.cs @@ -6,99 +6,192 @@ using SkiaSharp; namespace Microsoft.Maui.Platform; /// -/// Skia-rendered border/frame container control. +/// Skia-rendered border/frame container control with full XAML styling support. /// public class SkiaBorder : SkiaLayoutView { - private float _strokeThickness = 1; - private float _cornerRadius = 0; - private SKColor _stroke = SKColors.Black; - private float _paddingLeft = 0; - private float _paddingTop = 0; - private float _paddingRight = 0; - private float _paddingBottom = 0; - private bool _hasShadow; + #region BindableProperties + + public static readonly BindableProperty StrokeThicknessProperty = + BindableProperty.Create(nameof(StrokeThickness), typeof(float), typeof(SkiaBorder), 1f, + propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate()); + + public static readonly BindableProperty CornerRadiusProperty = + BindableProperty.Create(nameof(CornerRadius), typeof(float), typeof(SkiaBorder), 0f, + propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate()); + + public static readonly BindableProperty StrokeProperty = + BindableProperty.Create(nameof(Stroke), typeof(SKColor), typeof(SkiaBorder), SKColors.Black, + propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate()); + + public static readonly BindableProperty PaddingLeftProperty = + BindableProperty.Create(nameof(PaddingLeft), typeof(float), typeof(SkiaBorder), 0f, + propertyChanged: (b, o, n) => ((SkiaBorder)b).InvalidateMeasure()); + + public static readonly BindableProperty PaddingTopProperty = + BindableProperty.Create(nameof(PaddingTop), typeof(float), typeof(SkiaBorder), 0f, + propertyChanged: (b, o, n) => ((SkiaBorder)b).InvalidateMeasure()); + + public static readonly BindableProperty PaddingRightProperty = + BindableProperty.Create(nameof(PaddingRight), typeof(float), typeof(SkiaBorder), 0f, + propertyChanged: (b, o, n) => ((SkiaBorder)b).InvalidateMeasure()); + + public static readonly BindableProperty PaddingBottomProperty = + BindableProperty.Create(nameof(PaddingBottom), typeof(float), typeof(SkiaBorder), 0f, + propertyChanged: (b, o, n) => ((SkiaBorder)b).InvalidateMeasure()); + + public static readonly BindableProperty HasShadowProperty = + BindableProperty.Create(nameof(HasShadow), typeof(bool), typeof(SkiaBorder), false, + propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate()); + + public static readonly BindableProperty ShadowColorProperty = + BindableProperty.Create(nameof(ShadowColor), typeof(SKColor), typeof(SkiaBorder), new SKColor(0, 0, 0, 40), + propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate()); + + public static readonly BindableProperty ShadowBlurRadiusProperty = + BindableProperty.Create(nameof(ShadowBlurRadius), typeof(float), typeof(SkiaBorder), 4f, + propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate()); + + public static readonly BindableProperty ShadowOffsetXProperty = + BindableProperty.Create(nameof(ShadowOffsetX), typeof(float), typeof(SkiaBorder), 2f, + propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate()); + + public static readonly BindableProperty ShadowOffsetYProperty = + BindableProperty.Create(nameof(ShadowOffsetY), typeof(float), typeof(SkiaBorder), 2f, + propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate()); + + #endregion + + #region Properties public float StrokeThickness { - get => _strokeThickness; - set { _strokeThickness = value; Invalidate(); } + get => (float)GetValue(StrokeThicknessProperty); + set => SetValue(StrokeThicknessProperty, value); } public float CornerRadius { - get => _cornerRadius; - set { _cornerRadius = value; Invalidate(); } + get => (float)GetValue(CornerRadiusProperty); + set => SetValue(CornerRadiusProperty, value); } public SKColor Stroke { - get => _stroke; - set { _stroke = value; Invalidate(); } + get => (SKColor)GetValue(StrokeProperty); + set => SetValue(StrokeProperty, value); } public float PaddingLeft { - get => _paddingLeft; - set { _paddingLeft = value; InvalidateMeasure(); } + get => (float)GetValue(PaddingLeftProperty); + set => SetValue(PaddingLeftProperty, value); } public float PaddingTop { - get => _paddingTop; - set { _paddingTop = value; InvalidateMeasure(); } + get => (float)GetValue(PaddingTopProperty); + set => SetValue(PaddingTopProperty, value); } public float PaddingRight { - get => _paddingRight; - set { _paddingRight = value; InvalidateMeasure(); } + get => (float)GetValue(PaddingRightProperty); + set => SetValue(PaddingRightProperty, value); } public float PaddingBottom { - get => _paddingBottom; - set { _paddingBottom = value; InvalidateMeasure(); } + get => (float)GetValue(PaddingBottomProperty); + set => SetValue(PaddingBottomProperty, value); } public bool HasShadow { - get => _hasShadow; - set { _hasShadow = value; Invalidate(); } + get => (bool)GetValue(HasShadowProperty); + set => SetValue(HasShadowProperty, value); } + public SKColor ShadowColor + { + get => (SKColor)GetValue(ShadowColorProperty); + set => SetValue(ShadowColorProperty, value); + } + + public float ShadowBlurRadius + { + get => (float)GetValue(ShadowBlurRadiusProperty); + set => SetValue(ShadowBlurRadiusProperty, value); + } + + public float ShadowOffsetX + { + get => (float)GetValue(ShadowOffsetXProperty); + set => SetValue(ShadowOffsetXProperty, value); + } + + public float ShadowOffsetY + { + get => (float)GetValue(ShadowOffsetYProperty); + set => SetValue(ShadowOffsetYProperty, value); + } + + #endregion + + /// + /// Sets uniform padding on all sides. + /// public void SetPadding(float all) { - _paddingLeft = _paddingTop = _paddingRight = _paddingBottom = all; - InvalidateMeasure(); + PaddingLeft = PaddingTop = PaddingRight = PaddingBottom = all; } + /// + /// Sets padding with horizontal and vertical values. + /// public void SetPadding(float horizontal, float vertical) { - _paddingLeft = _paddingRight = horizontal; - _paddingTop = _paddingBottom = vertical; - InvalidateMeasure(); + PaddingLeft = PaddingRight = horizontal; + PaddingTop = PaddingBottom = vertical; + } + + /// + /// Sets padding with individual values for each side. + /// + public void SetPadding(float left, float top, float right, float bottom) + { + PaddingLeft = left; + PaddingTop = top; + PaddingRight = right; + PaddingBottom = bottom; } protected override void OnDraw(SKCanvas canvas, SKRect bounds) { + var strokeThickness = StrokeThickness; + var cornerRadius = CornerRadius; + var borderRect = new SKRect( - bounds.Left + _strokeThickness / 2, - bounds.Top + _strokeThickness / 2, - bounds.Right - _strokeThickness / 2, - bounds.Bottom - _strokeThickness / 2); + bounds.Left + strokeThickness / 2, + bounds.Top + strokeThickness / 2, + bounds.Right - strokeThickness / 2, + bounds.Bottom - strokeThickness / 2); // Draw shadow if enabled - if (_hasShadow) + if (HasShadow) { using var shadowPaint = new SKPaint { - Color = new SKColor(0, 0, 0, 40), - MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 4), + Color = ShadowColor, + MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, ShadowBlurRadius), Style = SKPaintStyle.Fill }; - var shadowRect = new SKRect(borderRect.Left + 2, borderRect.Top + 2, borderRect.Right + 2, borderRect.Bottom + 2); - canvas.DrawRoundRect(new SKRoundRect(shadowRect, _cornerRadius), shadowPaint); + var shadowRect = new SKRect( + borderRect.Left + ShadowOffsetX, + borderRect.Top + ShadowOffsetY, + borderRect.Right + ShadowOffsetX, + borderRect.Bottom + ShadowOffsetY); + canvas.DrawRoundRect(new SKRoundRect(shadowRect, cornerRadius), shadowPaint); } // Draw background @@ -108,22 +201,22 @@ public class SkiaBorder : SkiaLayoutView Style = SKPaintStyle.Fill, IsAntialias = true }; - canvas.DrawRoundRect(new SKRoundRect(borderRect, _cornerRadius), bgPaint); + canvas.DrawRoundRect(new SKRoundRect(borderRect, cornerRadius), bgPaint); // Draw border - if (_strokeThickness > 0) + if (strokeThickness > 0) { using var borderPaint = new SKPaint { - Color = _stroke, + Color = Stroke, Style = SKPaintStyle.Stroke, - StrokeWidth = _strokeThickness, + StrokeWidth = strokeThickness, IsAntialias = true }; - canvas.DrawRoundRect(new SKRoundRect(borderRect, _cornerRadius), borderPaint); + canvas.DrawRoundRect(new SKRoundRect(borderRect, cornerRadius), borderPaint); } - // Draw children (call base which draws children) + // Draw children foreach (var child in Children) { if (child.IsVisible) @@ -140,21 +233,27 @@ public class SkiaBorder : SkiaLayoutView protected new SKRect GetContentBounds(SKRect bounds) { + var strokeThickness = StrokeThickness; return new SKRect( - bounds.Left + _paddingLeft + _strokeThickness, - bounds.Top + _paddingTop + _strokeThickness, - bounds.Right - _paddingRight - _strokeThickness, - bounds.Bottom - _paddingBottom - _strokeThickness); + bounds.Left + PaddingLeft + strokeThickness, + bounds.Top + PaddingTop + strokeThickness, + bounds.Right - PaddingRight - strokeThickness, + bounds.Bottom - PaddingBottom - strokeThickness); } protected override SKSize MeasureOverride(SKSize availableSize) { - var paddingWidth = _paddingLeft + _paddingRight + _strokeThickness * 2; - var paddingHeight = _paddingTop + _paddingBottom + _strokeThickness * 2; + var strokeThickness = StrokeThickness; + var paddingWidth = PaddingLeft + PaddingRight + strokeThickness * 2; + var paddingHeight = PaddingTop + PaddingBottom + strokeThickness * 2; + + // Respect explicit size requests + var requestedWidth = WidthRequest >= 0 ? (float)WidthRequest : availableSize.Width; + var requestedHeight = HeightRequest >= 0 ? (float)HeightRequest : availableSize.Height; var childAvailable = new SKSize( - availableSize.Width - paddingWidth, - availableSize.Height - paddingHeight); + Math.Max(0, requestedWidth - paddingWidth), + Math.Max(0, requestedHeight - paddingHeight)); var maxChildSize = SKSize.Empty; @@ -166,19 +265,27 @@ public class SkiaBorder : SkiaLayoutView Math.Max(maxChildSize.Height, childSize.Height)); } - return new SKSize( - maxChildSize.Width + paddingWidth, - maxChildSize.Height + paddingHeight); + // Use requested size if set, otherwise use child size + padding + var width = WidthRequest >= 0 ? (float)WidthRequest : maxChildSize.Width + paddingWidth; + var height = HeightRequest >= 0 ? (float)HeightRequest : maxChildSize.Height + paddingHeight; + + return new SKSize(width, height); } protected override SKRect ArrangeOverride(SKRect bounds) { - var contentBounds = GetContentBounds(bounds); foreach (var child in Children) { - child.Arrange(contentBounds); + // Apply child's margin + var margin = child.Margin; + var marginedBounds = new SKRect( + contentBounds.Left + (float)margin.Left, + contentBounds.Top + (float)margin.Top, + contentBounds.Right - (float)margin.Right, + contentBounds.Bottom - (float)margin.Bottom); + child.Arrange(marginedBounds); } return bounds; @@ -186,7 +293,8 @@ public class SkiaBorder : SkiaLayoutView } /// -/// Frame control (alias for Border with shadow enabled). +/// Frame control - a Border with shadow enabled by default. +/// Mimics the MAUI Frame control appearance. /// public class SkiaFrame : SkiaBorder { @@ -196,5 +304,7 @@ public class SkiaFrame : SkiaBorder CornerRadius = 4; SetPadding(10); BackgroundColor = SKColors.White; + Stroke = SKColors.Transparent; + StrokeThickness = 0; } } diff --git a/Views/SkiaBoxView.cs b/Views/SkiaBoxView.cs new file mode 100644 index 0000000..3a8f100 --- /dev/null +++ b/Views/SkiaBoxView.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using SkiaSharp; + +namespace Microsoft.Maui.Platform; + +/// +/// Skia-rendered BoxView - a simple colored rectangle. +/// +public class SkiaBoxView : SkiaView +{ + public static readonly BindableProperty ColorProperty = + BindableProperty.Create(nameof(Color), typeof(SKColor), typeof(SkiaBoxView), SKColors.Transparent, + propertyChanged: (b, o, n) => ((SkiaBoxView)b).Invalidate()); + + public static readonly BindableProperty CornerRadiusProperty = + BindableProperty.Create(nameof(CornerRadius), typeof(float), typeof(SkiaBoxView), 0f, + propertyChanged: (b, o, n) => ((SkiaBoxView)b).Invalidate()); + + public SKColor Color + { + get => (SKColor)GetValue(ColorProperty); + set => SetValue(ColorProperty, value); + } + + public float CornerRadius + { + get => (float)GetValue(CornerRadiusProperty); + set => SetValue(CornerRadiusProperty, value); + } + + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + using var paint = new SKPaint + { + Color = Color, + Style = SKPaintStyle.Fill, + IsAntialias = true + }; + + if (CornerRadius > 0) + { + canvas.DrawRoundRect(bounds, CornerRadius, CornerRadius, paint); + } + else + { + canvas.DrawRect(bounds, paint); + } + } + + protected override SKSize MeasureOverride(SKSize availableSize) + { + // BoxView uses explicit size or a default size when in unbounded context + var width = WidthRequest >= 0 ? (float)WidthRequest : + (float.IsInfinity(availableSize.Width) ? 40f : availableSize.Width); + var height = HeightRequest >= 0 ? (float)HeightRequest : + (float.IsInfinity(availableSize.Height) ? 40f : availableSize.Height); + + // Ensure no NaN values + if (float.IsNaN(width)) width = 40f; + if (float.IsNaN(height)) height = 40f; + + return new SKSize(width, height); + } +} diff --git a/Views/SkiaButton.cs b/Views/SkiaButton.cs index 1cadafa..5e584f2 100644 --- a/Views/SkiaButton.cs +++ b/Views/SkiaButton.cs @@ -7,32 +7,382 @@ using Microsoft.Maui.Platform.Linux.Rendering; namespace Microsoft.Maui.Platform; /// -/// Skia-rendered button control. +/// Skia-rendered button control with full XAML styling support. /// public class SkiaButton : SkiaView { - public string Text { get; set; } = ""; - public SKColor TextColor { get; set; } = SKColors.White; - public new SKColor BackgroundColor { get; set; } = new SKColor(0x21, 0x96, 0xF3); // Material Blue - public SKColor PressedBackgroundColor { get; set; } = new SKColor(0x19, 0x76, 0xD2); - public SKColor DisabledBackgroundColor { get; set; } = new SKColor(0xBD, 0xBD, 0xBD); - public SKColor HoveredBackgroundColor { get; set; } = new SKColor(0x42, 0xA5, 0xF5); - public SKColor BorderColor { get; set; } = SKColors.Transparent; - public string FontFamily { get; set; } = "Sans"; - public float FontSize { get; set; } = 14; - public bool IsBold { get; set; } - public bool IsItalic { get; set; } - public float CharacterSpacing { get; set; } - public float CornerRadius { get; set; } = 4; - public float BorderWidth { get; set; } = 0; - public SKRect Padding { get; set; } = new SKRect(16, 8, 16, 8); + #region BindableProperties + /// + /// Bindable property for Text. + /// + public static readonly BindableProperty TextProperty = + BindableProperty.Create( + nameof(Text), + typeof(string), + typeof(SkiaButton), + "", + propertyChanged: (b, o, n) => ((SkiaButton)b).OnTextChanged()); + + /// + /// Bindable property for TextColor. + /// + public static readonly BindableProperty TextColorProperty = + BindableProperty.Create( + nameof(TextColor), + typeof(SKColor), + typeof(SkiaButton), + SKColors.White, + propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate()); + + /// + /// Bindable property for ButtonBackgroundColor (distinct from base BackgroundColor). + /// + public static readonly BindableProperty ButtonBackgroundColorProperty = + BindableProperty.Create( + nameof(ButtonBackgroundColor), + typeof(SKColor), + typeof(SkiaButton), + new SKColor(0x21, 0x96, 0xF3), // Material Blue + propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate()); + + /// + /// Bindable property for PressedBackgroundColor. + /// + public static readonly BindableProperty PressedBackgroundColorProperty = + BindableProperty.Create( + nameof(PressedBackgroundColor), + typeof(SKColor), + typeof(SkiaButton), + new SKColor(0x19, 0x76, 0xD2), + propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate()); + + /// + /// Bindable property for DisabledBackgroundColor. + /// + public static readonly BindableProperty DisabledBackgroundColorProperty = + BindableProperty.Create( + nameof(DisabledBackgroundColor), + typeof(SKColor), + typeof(SkiaButton), + new SKColor(0xBD, 0xBD, 0xBD), + propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate()); + + /// + /// Bindable property for HoveredBackgroundColor. + /// + public static readonly BindableProperty HoveredBackgroundColorProperty = + BindableProperty.Create( + nameof(HoveredBackgroundColor), + typeof(SKColor), + typeof(SkiaButton), + new SKColor(0x42, 0xA5, 0xF5), + propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate()); + + /// + /// Bindable property for BorderColor. + /// + public static readonly BindableProperty BorderColorProperty = + BindableProperty.Create( + nameof(BorderColor), + typeof(SKColor), + typeof(SkiaButton), + SKColors.Transparent, + propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate()); + + /// + /// Bindable property for FontFamily. + /// + public static readonly BindableProperty FontFamilyProperty = + BindableProperty.Create( + nameof(FontFamily), + typeof(string), + typeof(SkiaButton), + "Sans", + propertyChanged: (b, o, n) => ((SkiaButton)b).OnFontChanged()); + + /// + /// Bindable property for FontSize. + /// + public static readonly BindableProperty FontSizeProperty = + BindableProperty.Create( + nameof(FontSize), + typeof(float), + typeof(SkiaButton), + 14f, + propertyChanged: (b, o, n) => ((SkiaButton)b).OnFontChanged()); + + /// + /// Bindable property for IsBold. + /// + public static readonly BindableProperty IsBoldProperty = + BindableProperty.Create( + nameof(IsBold), + typeof(bool), + typeof(SkiaButton), + false, + propertyChanged: (b, o, n) => ((SkiaButton)b).OnFontChanged()); + + /// + /// Bindable property for IsItalic. + /// + public static readonly BindableProperty IsItalicProperty = + BindableProperty.Create( + nameof(IsItalic), + typeof(bool), + typeof(SkiaButton), + false, + propertyChanged: (b, o, n) => ((SkiaButton)b).OnFontChanged()); + + /// + /// Bindable property for CharacterSpacing. + /// + public static readonly BindableProperty CharacterSpacingProperty = + BindableProperty.Create( + nameof(CharacterSpacing), + typeof(float), + typeof(SkiaButton), + 0f, + propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate()); + + /// + /// Bindable property for CornerRadius. + /// + public static readonly BindableProperty CornerRadiusProperty = + BindableProperty.Create( + nameof(CornerRadius), + typeof(float), + typeof(SkiaButton), + 4f, + propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate()); + + /// + /// Bindable property for BorderWidth. + /// + public static readonly BindableProperty BorderWidthProperty = + BindableProperty.Create( + nameof(BorderWidth), + typeof(float), + typeof(SkiaButton), + 0f, + propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate()); + + /// + /// Bindable property for Padding. + /// + public static readonly BindableProperty PaddingProperty = + BindableProperty.Create( + nameof(Padding), + typeof(SKRect), + typeof(SkiaButton), + new SKRect(16, 8, 16, 8), + propertyChanged: (b, o, n) => ((SkiaButton)b).InvalidateMeasure()); + + /// + /// Bindable property for Command. + /// + public static readonly BindableProperty CommandProperty = + BindableProperty.Create( + nameof(Command), + typeof(System.Windows.Input.ICommand), + typeof(SkiaButton), + null, + propertyChanged: (b, o, n) => ((SkiaButton)b).OnCommandChanged((System.Windows.Input.ICommand?)o, (System.Windows.Input.ICommand?)n)); + + /// + /// Bindable property for CommandParameter. + /// + public static readonly BindableProperty CommandParameterProperty = + BindableProperty.Create( + nameof(CommandParameter), + typeof(object), + typeof(SkiaButton), + null); + + #endregion + + #region Properties + + /// + /// Gets or sets the button text. + /// + public string Text + { + get => (string)GetValue(TextProperty); + set => SetValue(TextProperty, value); + } + + /// + /// Gets or sets the text color. + /// + public SKColor TextColor + { + get => (SKColor)GetValue(TextColorProperty); + set => SetValue(TextColorProperty, value); + } + + /// + /// Gets or sets the button background color. + /// + public SKColor ButtonBackgroundColor + { + get => (SKColor)GetValue(ButtonBackgroundColorProperty); + set => SetValue(ButtonBackgroundColorProperty, value); + } + + /// + /// Gets or sets the pressed background color. + /// + public SKColor PressedBackgroundColor + { + get => (SKColor)GetValue(PressedBackgroundColorProperty); + set => SetValue(PressedBackgroundColorProperty, value); + } + + /// + /// Gets or sets the disabled background color. + /// + public SKColor DisabledBackgroundColor + { + get => (SKColor)GetValue(DisabledBackgroundColorProperty); + set => SetValue(DisabledBackgroundColorProperty, value); + } + + /// + /// Gets or sets the hovered background color. + /// + public SKColor HoveredBackgroundColor + { + get => (SKColor)GetValue(HoveredBackgroundColorProperty); + set => SetValue(HoveredBackgroundColorProperty, value); + } + + /// + /// Gets or sets the border color. + /// + public SKColor BorderColor + { + get => (SKColor)GetValue(BorderColorProperty); + set => SetValue(BorderColorProperty, value); + } + + /// + /// Gets or sets the font family. + /// + public string FontFamily + { + get => (string)GetValue(FontFamilyProperty); + set => SetValue(FontFamilyProperty, value); + } + + /// + /// Gets or sets the font size. + /// + public float FontSize + { + get => (float)GetValue(FontSizeProperty); + set => SetValue(FontSizeProperty, value); + } + + /// + /// Gets or sets whether the text is bold. + /// + public bool IsBold + { + get => (bool)GetValue(IsBoldProperty); + set => SetValue(IsBoldProperty, value); + } + + /// + /// Gets or sets whether the text is italic. + /// + public bool IsItalic + { + get => (bool)GetValue(IsItalicProperty); + set => SetValue(IsItalicProperty, value); + } + + /// + /// Gets or sets the character spacing. + /// + public float CharacterSpacing + { + get => (float)GetValue(CharacterSpacingProperty); + set => SetValue(CharacterSpacingProperty, value); + } + + /// + /// Gets or sets the corner radius. + /// + public float CornerRadius + { + get => (float)GetValue(CornerRadiusProperty); + set => SetValue(CornerRadiusProperty, value); + } + + /// + /// Gets or sets the border width. + /// + public float BorderWidth + { + get => (float)GetValue(BorderWidthProperty); + set => SetValue(BorderWidthProperty, value); + } + + /// + /// Gets or sets the padding. + /// + public SKRect Padding + { + get => (SKRect)GetValue(PaddingProperty); + set => SetValue(PaddingProperty, value); + } + + /// + /// Gets or sets the command to execute when clicked. + /// + public System.Windows.Input.ICommand? Command + { + get => (System.Windows.Input.ICommand?)GetValue(CommandProperty); + set => SetValue(CommandProperty, value); + } + + /// + /// Gets or sets the command parameter. + /// + public object? CommandParameter + { + get => GetValue(CommandParameterProperty); + set => SetValue(CommandParameterProperty, value); + } + + /// + /// Gets whether the button is currently pressed. + /// public bool IsPressed { get; private set; } + + /// + /// Gets whether the pointer is currently over the button. + /// public bool IsHovered { get; private set; } + + #endregion + private bool _focusFromKeyboard; + /// + /// Event raised when the button is clicked. + /// public event EventHandler? Clicked; + + /// + /// Event raised when the button is pressed. + /// public event EventHandler? Pressed; + + /// + /// Event raised when the button is released. + /// public event EventHandler? Released; public SkiaButton() @@ -40,30 +390,91 @@ public class SkiaButton : SkiaView IsFocusable = true; } + private void OnTextChanged() + { + InvalidateMeasure(); + Invalidate(); + } + + private void OnFontChanged() + { + InvalidateMeasure(); + Invalidate(); + } + + private void OnCommandChanged(System.Windows.Input.ICommand? oldCommand, System.Windows.Input.ICommand? newCommand) + { + if (oldCommand != null) + { + oldCommand.CanExecuteChanged -= OnCanExecuteChanged; + } + + if (newCommand != null) + { + newCommand.CanExecuteChanged += OnCanExecuteChanged; + UpdateIsEnabled(); + } + } + + private void OnCanExecuteChanged(object? sender, EventArgs e) + { + UpdateIsEnabled(); + } + + private void UpdateIsEnabled() + { + if (Command != null) + { + IsEnabled = Command.CanExecute(CommandParameter); + } + } + protected override void OnDraw(SKCanvas canvas, SKRect bounds) { - // Determine background color based on state - var bgColor = !IsEnabled ? DisabledBackgroundColor - : IsPressed ? PressedBackgroundColor - : IsHovered ? HoveredBackgroundColor - : BackgroundColor; + // Check if this is a "text only" button (transparent background) + var isTextOnly = ButtonBackgroundColor.Alpha == 0; - // Draw shadow (for elevation effect) - if (IsEnabled && !IsPressed) + // Determine background color based on state + SKColor bgColor; + if (!IsEnabled) + { + bgColor = isTextOnly ? SKColors.Transparent : DisabledBackgroundColor; + } + else if (IsPressed) + { + // For text-only buttons, use a subtle press effect + bgColor = isTextOnly ? new SKColor(0, 0, 0, 20) : PressedBackgroundColor; + } + else if (IsHovered) + { + // For text-only buttons, use a subtle hover effect instead of full background + bgColor = isTextOnly ? new SKColor(0, 0, 0, 10) : HoveredBackgroundColor; + } + else + { + bgColor = ButtonBackgroundColor; + } + + // Draw shadow (for elevation effect) - skip for text-only buttons + if (IsEnabled && !IsPressed && !isTextOnly) { DrawShadow(canvas, bounds); } - // Draw background with rounded corners - using var bgPaint = new SKPaint - { - Color = bgColor, - IsAntialias = true, - Style = SKPaintStyle.Fill - }; - + // Create rounded rect for background and border var rect = new SKRoundRect(bounds, CornerRadius); - canvas.DrawRoundRect(rect, bgPaint); + + // Draw background with rounded corners (skip if fully transparent) + if (bgColor.Alpha > 0) + { + using var bgPaint = new SKPaint + { + Color = bgColor, + IsAntialias = true, + Style = SKPaintStyle.Fill + }; + canvas.DrawRoundRect(rect, bgPaint); + } // Draw border if (BorderWidth > 0 && BorderColor != SKColors.Transparent) @@ -104,9 +515,30 @@ public class SkiaButton : SkiaView ?? SKTypeface.Default; using var font = new SKFont(typeface, FontSize); + + // For text-only buttons, darken text on hover/press for feedback + SKColor textColorToUse; + if (!IsEnabled) + { + textColorToUse = TextColor.WithAlpha(128); + } + else if (isTextOnly && (IsHovered || IsPressed)) + { + // Darken the text color slightly for hover/press feedback + textColorToUse = new SKColor( + (byte)Math.Max(0, TextColor.Red - 40), + (byte)Math.Max(0, TextColor.Green - 40), + (byte)Math.Max(0, TextColor.Blue - 40), + TextColor.Alpha); + } + else + { + textColorToUse = TextColor; + } + using var paint = new SKPaint(font) { - Color = IsEnabled ? TextColor : TextColor.WithAlpha(128), + Color = textColorToUse, IsAntialias = true }; @@ -145,6 +577,7 @@ public class SkiaButton : SkiaView { if (!IsEnabled) return; IsHovered = true; + SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.PointerOver); Invalidate(); } @@ -155,15 +588,18 @@ public class SkiaButton : SkiaView { IsPressed = false; } + SkiaVisualStateManager.GoToState(this, IsEnabled ? SkiaVisualStateManager.CommonStates.Normal : SkiaVisualStateManager.CommonStates.Disabled); Invalidate(); } public override void OnPointerPressed(PointerEventArgs e) { + Console.WriteLine($"[SkiaButton] OnPointerPressed - Text='{Text}', IsEnabled={IsEnabled}"); if (!IsEnabled) return; IsPressed = true; _focusFromKeyboard = false; + SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.Pressed); Invalidate(); Pressed?.Invoke(this, EventArgs.Empty); } @@ -174,14 +610,18 @@ public class SkiaButton : SkiaView var wasPressed = IsPressed; IsPressed = false; + SkiaVisualStateManager.GoToState(this, IsHovered ? SkiaVisualStateManager.CommonStates.PointerOver : SkiaVisualStateManager.CommonStates.Normal); Invalidate(); Released?.Invoke(this, EventArgs.Empty); - // Fire click if released within bounds - if (wasPressed && Bounds.Contains(new SKPoint(e.X, e.Y))) + // Fire click if button was pressed + // Note: Hit testing already verified the pointer is over this button, + // so we don't need to re-check bounds (which would fail due to coordinate system differences) + if (wasPressed) { Clicked?.Invoke(this, EventArgs.Empty); + Command?.Execute(CommandParameter); } } @@ -193,7 +633,8 @@ public class SkiaButton : SkiaView if (e.Key == Key.Enter || e.Key == Key.Space) { IsPressed = true; - _focusFromKeyboard = true; + _focusFromKeyboard = true; + SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.Pressed); Invalidate(); Pressed?.Invoke(this, EventArgs.Empty); e.Handled = true; @@ -209,21 +650,36 @@ public class SkiaButton : SkiaView if (IsPressed) { IsPressed = false; + SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.Normal); Invalidate(); Released?.Invoke(this, EventArgs.Empty); Clicked?.Invoke(this, EventArgs.Empty); + Command?.Execute(CommandParameter); } e.Handled = true; } } + protected override void OnEnabledChanged() + { + base.OnEnabledChanged(); + SkiaVisualStateManager.GoToState(this, IsEnabled ? SkiaVisualStateManager.CommonStates.Normal : SkiaVisualStateManager.CommonStates.Disabled); + } + protected override SKSize MeasureOverride(SKSize availableSize) { + // Ensure we never return NaN - use safe defaults + var paddingLeft = float.IsNaN(Padding.Left) ? 16f : Padding.Left; + var paddingRight = float.IsNaN(Padding.Right) ? 16f : Padding.Right; + var paddingTop = float.IsNaN(Padding.Top) ? 8f : Padding.Top; + var paddingBottom = float.IsNaN(Padding.Bottom) ? 8f : Padding.Bottom; + var fontSize = float.IsNaN(FontSize) || FontSize <= 0 ? 14f : FontSize; + if (string.IsNullOrEmpty(Text)) { return new SKSize( - Padding.Left + Padding.Right + 40, // Minimum width - Padding.Top + Padding.Bottom + FontSize); + paddingLeft + paddingRight + 40, // Minimum width + paddingTop + paddingBottom + fontSize); } var fontStyle = new SKFontStyle( @@ -233,14 +689,25 @@ public class SkiaButton : SkiaView var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(FontFamily, fontStyle) ?? SKTypeface.Default; - using var font = new SKFont(typeface, FontSize); + using var font = new SKFont(typeface, fontSize); using var paint = new SKPaint(font); var textBounds = new SKRect(); paint.MeasureText(Text, ref textBounds); - return new SKSize( - textBounds.Width + Padding.Left + Padding.Right, - textBounds.Height + Padding.Top + Padding.Bottom); + var width = textBounds.Width + paddingLeft + paddingRight; + var height = textBounds.Height + paddingTop + paddingBottom; + + // Ensure valid, non-NaN return values + if (float.IsNaN(width) || width < 0) width = 72f; + if (float.IsNaN(height) || height < 0) height = 30f; + + // Respect WidthRequest and HeightRequest when set + if (WidthRequest >= 0) + width = (float)WidthRequest; + if (HeightRequest >= 0) + height = (float)HeightRequest; + + return new SKSize(width, height); } } diff --git a/Views/SkiaCheckBox.cs b/Views/SkiaCheckBox.cs index 31bc1b7..7fddd92 100644 --- a/Views/SkiaCheckBox.cs +++ b/Views/SkiaCheckBox.cs @@ -7,39 +7,247 @@ using Microsoft.Maui.Platform.Linux.Rendering; namespace Microsoft.Maui.Platform; /// -/// Skia-rendered checkbox control. +/// Skia-rendered checkbox control with full XAML styling support. /// public class SkiaCheckBox : SkiaView { - private bool _isChecked; + #region BindableProperties + /// + /// Bindable property for IsChecked. + /// + public static readonly BindableProperty IsCheckedProperty = + BindableProperty.Create( + nameof(IsChecked), + typeof(bool), + typeof(SkiaCheckBox), + false, + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaCheckBox)b).OnIsCheckedChanged()); + + /// + /// Bindable property for CheckColor. + /// + public static readonly BindableProperty CheckColorProperty = + BindableProperty.Create( + nameof(CheckColor), + typeof(SKColor), + typeof(SkiaCheckBox), + SKColors.White, + propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate()); + + /// + /// Bindable property for BoxColor. + /// + public static readonly BindableProperty BoxColorProperty = + BindableProperty.Create( + nameof(BoxColor), + typeof(SKColor), + typeof(SkiaCheckBox), + new SKColor(0x21, 0x96, 0xF3), + propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate()); + + /// + /// Bindable property for UncheckedBoxColor. + /// + public static readonly BindableProperty UncheckedBoxColorProperty = + BindableProperty.Create( + nameof(UncheckedBoxColor), + typeof(SKColor), + typeof(SkiaCheckBox), + SKColors.White, + propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate()); + + /// + /// Bindable property for BorderColor. + /// + public static readonly BindableProperty BorderColorProperty = + BindableProperty.Create( + nameof(BorderColor), + typeof(SKColor), + typeof(SkiaCheckBox), + new SKColor(0x75, 0x75, 0x75), + propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate()); + + /// + /// Bindable property for DisabledColor. + /// + public static readonly BindableProperty DisabledColorProperty = + BindableProperty.Create( + nameof(DisabledColor), + typeof(SKColor), + typeof(SkiaCheckBox), + new SKColor(0xBD, 0xBD, 0xBD), + propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate()); + + /// + /// Bindable property for HoveredBorderColor. + /// + public static readonly BindableProperty HoveredBorderColorProperty = + BindableProperty.Create( + nameof(HoveredBorderColor), + typeof(SKColor), + typeof(SkiaCheckBox), + new SKColor(0x21, 0x96, 0xF3), + propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate()); + + /// + /// Bindable property for BoxSize. + /// + public static readonly BindableProperty BoxSizeProperty = + BindableProperty.Create( + nameof(BoxSize), + typeof(float), + typeof(SkiaCheckBox), + 20f, + propertyChanged: (b, o, n) => ((SkiaCheckBox)b).InvalidateMeasure()); + + /// + /// Bindable property for CornerRadius. + /// + public static readonly BindableProperty CornerRadiusProperty = + BindableProperty.Create( + nameof(CornerRadius), + typeof(float), + typeof(SkiaCheckBox), + 3f, + propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate()); + + /// + /// Bindable property for BorderWidth. + /// + public static readonly BindableProperty BorderWidthProperty = + BindableProperty.Create( + nameof(BorderWidth), + typeof(float), + typeof(SkiaCheckBox), + 2f, + propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate()); + + /// + /// Bindable property for CheckStrokeWidth. + /// + public static readonly BindableProperty CheckStrokeWidthProperty = + BindableProperty.Create( + nameof(CheckStrokeWidth), + typeof(float), + typeof(SkiaCheckBox), + 2.5f, + propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate()); + + #endregion + + #region Properties + + /// + /// Gets or sets whether the checkbox is checked. + /// public bool IsChecked { - get => _isChecked; - set - { - if (_isChecked != value) - { - _isChecked = value; - CheckedChanged?.Invoke(this, new CheckedChangedEventArgs(value)); - Invalidate(); - } - } + get => (bool)GetValue(IsCheckedProperty); + set => SetValue(IsCheckedProperty, value); } - public SKColor CheckColor { get; set; } = SKColors.White; - public SKColor BoxColor { get; set; } = new SKColor(0x21, 0x96, 0xF3); // Material Blue - public SKColor UncheckedBoxColor { get; set; } = SKColors.White; - public SKColor BorderColor { get; set; } = new SKColor(0x75, 0x75, 0x75); - public SKColor DisabledColor { get; set; } = new SKColor(0xBD, 0xBD, 0xBD); - public SKColor HoveredBorderColor { get; set; } = new SKColor(0x21, 0x96, 0xF3); - public float BoxSize { get; set; } = 20; - public float CornerRadius { get; set; } = 3; - public float BorderWidth { get; set; } = 2; - public float CheckStrokeWidth { get; set; } = 2.5f; + /// + /// Gets or sets the check color. + /// + public SKColor CheckColor + { + get => (SKColor)GetValue(CheckColorProperty); + set => SetValue(CheckColorProperty, value); + } + /// + /// Gets or sets the box color when checked. + /// + public SKColor BoxColor + { + get => (SKColor)GetValue(BoxColorProperty); + set => SetValue(BoxColorProperty, value); + } + + /// + /// Gets or sets the box color when unchecked. + /// + public SKColor UncheckedBoxColor + { + get => (SKColor)GetValue(UncheckedBoxColorProperty); + set => SetValue(UncheckedBoxColorProperty, value); + } + + /// + /// Gets or sets the border color. + /// + public SKColor BorderColor + { + get => (SKColor)GetValue(BorderColorProperty); + set => SetValue(BorderColorProperty, value); + } + + /// + /// Gets or sets the disabled color. + /// + public SKColor DisabledColor + { + get => (SKColor)GetValue(DisabledColorProperty); + set => SetValue(DisabledColorProperty, value); + } + + /// + /// Gets or sets the hovered border color. + /// + public SKColor HoveredBorderColor + { + get => (SKColor)GetValue(HoveredBorderColorProperty); + set => SetValue(HoveredBorderColorProperty, value); + } + + /// + /// Gets or sets the box size. + /// + public float BoxSize + { + get => (float)GetValue(BoxSizeProperty); + set => SetValue(BoxSizeProperty, value); + } + + /// + /// Gets or sets the corner radius. + /// + public float CornerRadius + { + get => (float)GetValue(CornerRadiusProperty); + set => SetValue(CornerRadiusProperty, value); + } + + /// + /// Gets or sets the border width. + /// + public float BorderWidth + { + get => (float)GetValue(BorderWidthProperty); + set => SetValue(BorderWidthProperty, value); + } + + /// + /// Gets or sets the check stroke width. + /// + public float CheckStrokeWidth + { + get => (float)GetValue(CheckStrokeWidthProperty); + set => SetValue(CheckStrokeWidthProperty, value); + } + + /// + /// Gets whether the pointer is over the checkbox. + /// public bool IsHovered { get; private set; } + #endregion + + /// + /// Event raised when checked state changes. + /// public event EventHandler? CheckedChanged; public SkiaCheckBox() @@ -47,6 +255,13 @@ public class SkiaCheckBox : SkiaView IsFocusable = true; } + private void OnIsCheckedChanged() + { + CheckedChanged?.Invoke(this, new CheckedChangedEventArgs(IsChecked)); + SkiaVisualStateManager.GoToState(this, IsChecked ? SkiaVisualStateManager.CommonStates.Checked : SkiaVisualStateManager.CommonStates.Unchecked); + Invalidate(); + } + protected override void OnDraw(SKCanvas canvas, SKRect bounds) { // Center the checkbox box in bounds @@ -136,12 +351,14 @@ public class SkiaCheckBox : SkiaView { if (!IsEnabled) return; IsHovered = true; + SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.PointerOver); Invalidate(); } public override void OnPointerExited(PointerEventArgs e) { IsHovered = false; + SkiaVisualStateManager.GoToState(this, IsEnabled ? SkiaVisualStateManager.CommonStates.Normal : SkiaVisualStateManager.CommonStates.Disabled); Invalidate(); } @@ -169,6 +386,12 @@ public class SkiaCheckBox : SkiaView } } + protected override void OnEnabledChanged() + { + base.OnEnabledChanged(); + SkiaVisualStateManager.GoToState(this, IsEnabled ? SkiaVisualStateManager.CommonStates.Normal : SkiaVisualStateManager.CommonStates.Disabled); + } + protected override SKSize MeasureOverride(SKSize availableSize) { // Add some padding around the box for touch targets diff --git a/Views/SkiaCollectionView.cs b/Views/SkiaCollectionView.cs index 297a9aa..94f06e9 100644 --- a/Views/SkiaCollectionView.cs +++ b/Views/SkiaCollectionView.cs @@ -47,6 +47,21 @@ public class SkiaCollectionView : SkiaItemsView private float _headerHeight = 0; private float _footerHeight = 0; + // Track if heights changed during draw (requires redraw for correct positioning) + private bool _heightsChangedDuringDraw; + + // Uses parent's _itemViewCache for virtualization + + protected override void RefreshItems() + { + // Clear selection when items change to avoid stale references + _selectedItems.Clear(); + _selectedItem = null; + _selectedIndex = -1; + + base.RefreshItems(); + } + public SkiaSelectionMode SelectionMode { get => _selectionMode; @@ -175,7 +190,7 @@ public class SkiaCollectionView : SkiaItemsView } } - public SKColor SelectionColor { get; set; } = new SKColor(0x21, 0x96, 0xF3, 0x40); + public SKColor SelectionColor { get; set; } = new SKColor(0x21, 0x96, 0xF3, 0x59); // 35% opacity public SKColor HeaderBackgroundColor { get; set; } = new SKColor(0xF5, 0xF5, 0xF5); public SKColor FooterBackgroundColor { get; set; } = new SKColor(0xF5, 0xF5, 0xF5); @@ -261,14 +276,7 @@ public class SkiaCollectionView : SkiaItemsView protected override void DrawItem(SKCanvas canvas, object item, int index, SKRect bounds, SKPaint paint) { - // Draw selection highlight bool isSelected = _selectedItems.Contains(item); - if (isSelected) - { - paint.Color = SelectionColor; - paint.Style = SKPaintStyle.Fill; - canvas.DrawRect(bounds, paint); - } // Draw separator (only for vertical list layout) if (_orientation == ItemsLayoutOrientation.Vertical && _spanCount == 1) @@ -279,6 +287,70 @@ public class SkiaCollectionView : SkiaItemsView canvas.DrawLine(bounds.Left, bounds.Bottom, bounds.Right, bounds.Bottom, paint); } + // Try to use ItemViewCreator for templated rendering (from DataTemplate) + if (ItemViewCreator != null) + { + // Get or create cached view for this index + if (!_itemViewCache.TryGetValue(index, out var itemView) || itemView == null) + { + itemView = ItemViewCreator(item); + if (itemView != null) + { + itemView.Parent = this; + _itemViewCache[index] = itemView; + } + } + + if (itemView != null) + { + try + { + // Measure with large height to get natural size + var availableSize = new SKSize(bounds.Width, float.MaxValue); + var measuredSize = itemView.Measure(availableSize); + + // Cap measured height - if item returns infinity/MaxValue, use ItemHeight as default + // This happens with Star-sized Grids that have no natural height preference + var rawHeight = measuredSize.Height; + if (float.IsNaN(rawHeight) || float.IsInfinity(rawHeight) || rawHeight > 10000) + { + rawHeight = ItemHeight; + } + // Ensure minimum height + var measuredHeight = Math.Max(rawHeight, ItemHeight); + if (!_itemHeights.TryGetValue(index, out var cachedHeight) || Math.Abs(cachedHeight - measuredHeight) > 1) + { + _itemHeights[index] = measuredHeight; + _heightsChangedDuringDraw = true; // Flag for redraw with correct positions + } + + // Arrange with the actual measured height + var actualBounds = new SKRect(bounds.Left, bounds.Top, bounds.Right, bounds.Top + measuredHeight); + itemView.Arrange(actualBounds); + itemView.Draw(canvas); + + // Draw selection highlight ON TOP of the item content (semi-transparent overlay) + if (isSelected) + { + paint.Color = SelectionColor; + paint.Style = SKPaintStyle.Fill; + canvas.DrawRoundRect(actualBounds, 12, 12, paint); + } + + // Draw checkmark for selected items in multiple selection mode + if (isSelected && _selectionMode == SkiaSelectionMode.Multiple) + { + DrawCheckmark(canvas, new SKRect(actualBounds.Right - 32, actualBounds.MidY - 8, actualBounds.Right - 16, actualBounds.MidY + 8)); + } + } + catch (Exception ex) + { + Console.WriteLine($"[SkiaCollectionView.DrawItem] EXCEPTION: {ex.Message}\n{ex.StackTrace}"); + } + return; + } + } + // Use custom renderer if provided if (ItemRenderer != null) { @@ -286,7 +358,7 @@ public class SkiaCollectionView : SkiaItemsView return; } - // Default rendering + // Default rendering - fall back to ToString paint.Color = SKColors.Black; paint.Style = SKPaintStyle.Fill; @@ -333,7 +405,10 @@ public class SkiaCollectionView : SkiaItemsView protected override void OnDraw(SKCanvas canvas, SKRect bounds) { - // Draw background + // Reset the heights-changed flag at the start of each draw + _heightsChangedDuringDraw = false; + + // Draw background if set if (BackgroundColor != SKColors.Transparent) { using var bgPaint = new SKPaint @@ -381,40 +456,67 @@ public class SkiaCollectionView : SkiaItemsView { DrawListItems(canvas, contentBounds); } + + // If heights changed during this draw, schedule a redraw with correct positions + // This will queue another frame to be drawn with the correct cached heights + if (_heightsChangedDuringDraw) + { + _heightsChangedDuringDraw = false; + Invalidate(); + } } private void DrawListItems(SKCanvas canvas, SKRect bounds) { - // Standard list drawing (delegate to base implementation via manual drawing) + // Standard list drawing with variable item heights canvas.Save(); canvas.ClipRect(bounds); using var paint = new SKPaint { IsAntialias = true }; var scrollOffset = GetScrollOffset(); - var firstVisible = Math.Max(0, (int)(scrollOffset / (ItemHeight + ItemSpacing))); - var lastVisible = Math.Min(ItemCount - 1, - (int)((scrollOffset + bounds.Height) / (ItemHeight + ItemSpacing)) + 1); - for (int i = firstVisible; i <= lastVisible; i++) + // Find first visible item by walking through items + int firstVisible = 0; + float cumulativeOffset = 0; + for (int i = 0; i < ItemCount; i++) { - var itemY = bounds.Top + (i * (ItemHeight + ItemSpacing)) - scrollOffset; - var itemRect = new SKRect(bounds.Left, itemY, bounds.Right - 8, itemY + ItemHeight); - - if (itemRect.Bottom < bounds.Top || itemRect.Top > bounds.Bottom) - continue; - - var item = GetItemAt(i); - if (item != null) + var itemH = GetItemHeight(i); + if (cumulativeOffset + itemH > scrollOffset) { - DrawItem(canvas, item, i, itemRect, paint); + firstVisible = i; + break; } + cumulativeOffset += itemH + ItemSpacing; + } + + // Draw visible items using variable heights + float currentY = bounds.Top + GetItemOffset(firstVisible) - scrollOffset; + for (int i = firstVisible; i < ItemCount; i++) + { + var itemH = GetItemHeight(i); + var itemRect = new SKRect(bounds.Left, currentY, bounds.Right - 8, currentY + itemH); + + // Stop if we've passed the visible area + if (itemRect.Top > bounds.Bottom) + break; + + if (itemRect.Bottom >= bounds.Top) + { + var item = GetItemAt(i); + if (item != null) + { + DrawItem(canvas, item, i, itemRect, paint); + } + } + + currentY += itemH + ItemSpacing; } canvas.Restore(); // Draw scrollbar - var totalHeight = ItemCount * (ItemHeight + ItemSpacing) - ItemSpacing; + var totalHeight = TotalContentHeight; if (totalHeight > bounds.Height) { DrawScrollBarInternal(canvas, bounds, scrollOffset, totalHeight); @@ -480,35 +582,41 @@ public class SkiaCollectionView : SkiaItemsView private void DrawScrollBarInternal(SKCanvas canvas, SKRect bounds, float scrollOffset, float totalHeight) { - var scrollBarWidth = 8f; + var scrollBarWidth = 6f; + var scrollBarMargin = 2f; + + // Draw scrollbar track (subtle) var trackRect = new SKRect( - bounds.Right - scrollBarWidth, - bounds.Top, - bounds.Right, - bounds.Bottom); + bounds.Right - scrollBarWidth - scrollBarMargin, + bounds.Top + scrollBarMargin, + bounds.Right - scrollBarMargin, + bounds.Bottom - scrollBarMargin); using var trackPaint = new SKPaint { - Color = new SKColor(200, 200, 200, 64), + Color = new SKColor(0, 0, 0, 20), Style = SKPaintStyle.Fill }; - canvas.DrawRect(trackRect, trackPaint); + canvas.DrawRoundRect(new SKRoundRect(trackRect, 3), trackPaint); + // Calculate thumb position and size var maxOffset = Math.Max(0, totalHeight - bounds.Height); var viewportRatio = bounds.Height / totalHeight; - var thumbHeight = Math.Max(20, bounds.Height * viewportRatio); + var availableTrackHeight = trackRect.Height; + var thumbHeight = Math.Max(30, availableTrackHeight * viewportRatio); var scrollRatio = maxOffset > 0 ? scrollOffset / maxOffset : 0; - var thumbY = bounds.Top + (bounds.Height - thumbHeight) * scrollRatio; + var thumbY = trackRect.Top + (availableTrackHeight - thumbHeight) * scrollRatio; var thumbRect = new SKRect( - bounds.Right - scrollBarWidth + 1, + trackRect.Left, thumbY, - bounds.Right - 1, + trackRect.Right, thumbY + thumbHeight); + // Draw thumb with more visible color using var thumbPaint = new SKPaint { - Color = new SKColor(128, 128, 128, 128), + Color = new SKColor(100, 100, 100, 180), Style = SKPaintStyle.Fill, IsAntialias = true }; diff --git a/Views/SkiaContentPresenter.cs b/Views/SkiaContentPresenter.cs new file mode 100644 index 0000000..4c38c78 --- /dev/null +++ b/Views/SkiaContentPresenter.cs @@ -0,0 +1,257 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using SkiaSharp; + +namespace Microsoft.Maui.Platform; + +/// +/// Presents content within a ControlTemplate. +/// This control acts as a placeholder that gets replaced with the actual content +/// when the template is applied to a control. +/// +public class SkiaContentPresenter : SkiaView +{ + #region BindableProperties + + public static readonly BindableProperty ContentProperty = + BindableProperty.Create(nameof(Content), typeof(SkiaView), typeof(SkiaContentPresenter), null, + propertyChanged: (b, o, n) => ((SkiaContentPresenter)b).OnContentChanged((SkiaView?)o, (SkiaView?)n)); + + public static readonly BindableProperty HorizontalContentAlignmentProperty = + BindableProperty.Create(nameof(HorizontalContentAlignment), typeof(LayoutAlignment), typeof(SkiaContentPresenter), LayoutAlignment.Fill, + propertyChanged: (b, o, n) => ((SkiaContentPresenter)b).InvalidateMeasure()); + + public static readonly BindableProperty VerticalContentAlignmentProperty = + BindableProperty.Create(nameof(VerticalContentAlignment), typeof(LayoutAlignment), typeof(SkiaContentPresenter), LayoutAlignment.Fill, + propertyChanged: (b, o, n) => ((SkiaContentPresenter)b).InvalidateMeasure()); + + public static readonly BindableProperty PaddingProperty = + BindableProperty.Create(nameof(Padding), typeof(SKRect), typeof(SkiaContentPresenter), SKRect.Empty, + propertyChanged: (b, o, n) => ((SkiaContentPresenter)b).InvalidateMeasure()); + + #endregion + + #region Properties + + /// + /// Gets or sets the content to present. + /// + public SkiaView? Content + { + get => (SkiaView?)GetValue(ContentProperty); + set => SetValue(ContentProperty, value); + } + + /// + /// Gets or sets the horizontal alignment of the content. + /// + public LayoutAlignment HorizontalContentAlignment + { + get => (LayoutAlignment)GetValue(HorizontalContentAlignmentProperty); + set => SetValue(HorizontalContentAlignmentProperty, value); + } + + /// + /// Gets or sets the vertical alignment of the content. + /// + public LayoutAlignment VerticalContentAlignment + { + get => (LayoutAlignment)GetValue(VerticalContentAlignmentProperty); + set => SetValue(VerticalContentAlignmentProperty, value); + } + + /// + /// Gets or sets the padding around the content. + /// + public SKRect Padding + { + get => (SKRect)GetValue(PaddingProperty); + set => SetValue(PaddingProperty, value); + } + + #endregion + + private void OnContentChanged(SkiaView? oldContent, SkiaView? newContent) + { + if (oldContent != null) + { + oldContent.Parent = null; + } + + if (newContent != null) + { + newContent.Parent = this; + + // Propagate binding context to new content + if (BindingContext != null) + { + SetInheritedBindingContext(newContent, BindingContext); + } + } + + InvalidateMeasure(); + } + + /// + /// Called when binding context changes. Propagates to content. + /// + protected override void OnBindingContextChanged() + { + base.OnBindingContextChanged(); + + // Propagate binding context to content + if (Content != null) + { + SetInheritedBindingContext(Content, BindingContext); + } + } + + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + // Draw background if set + if (BackgroundColor != SKColors.Transparent) + { + using var bgPaint = new SKPaint + { + Color = BackgroundColor, + Style = SKPaintStyle.Fill + }; + canvas.DrawRect(bounds, bgPaint); + } + + // Draw content + Content?.Draw(canvas); + } + + protected override SKSize MeasureOverride(SKSize availableSize) + { + var padding = Padding; + + if (Content == null) + return new SKSize(padding.Left + padding.Right, padding.Top + padding.Bottom); + + // When alignment is not Fill, give content unlimited size in that dimension + // so it can measure its natural size without truncation + var measureWidth = HorizontalContentAlignment == LayoutAlignment.Fill + ? Math.Max(0, availableSize.Width - padding.Left - padding.Right) + : float.PositiveInfinity; + var measureHeight = VerticalContentAlignment == LayoutAlignment.Fill + ? Math.Max(0, availableSize.Height - padding.Top - padding.Bottom) + : float.PositiveInfinity; + + var contentSize = Content.Measure(new SKSize(measureWidth, measureHeight)); + return new SKSize( + contentSize.Width + padding.Left + padding.Right, + contentSize.Height + padding.Top + padding.Bottom); + } + + protected override SKRect ArrangeOverride(SKRect bounds) + { + if (Content != null) + { + var padding = Padding; + var contentBounds = new SKRect( + bounds.Left + padding.Left, + bounds.Top + padding.Top, + bounds.Right - padding.Right, + bounds.Bottom - padding.Bottom); + + // Apply alignment + var contentSize = Content.DesiredSize; + var arrangedBounds = ApplyAlignment(contentBounds, contentSize, HorizontalContentAlignment, VerticalContentAlignment); + Content.Arrange(arrangedBounds); + } + + return bounds; + } + + private static SKRect ApplyAlignment(SKRect availableBounds, SKSize contentSize, LayoutAlignment horizontal, LayoutAlignment vertical) + { + float x = availableBounds.Left; + float y = availableBounds.Top; + float width = horizontal == LayoutAlignment.Fill ? availableBounds.Width : contentSize.Width; + float height = vertical == LayoutAlignment.Fill ? availableBounds.Height : contentSize.Height; + + // Horizontal alignment + switch (horizontal) + { + case LayoutAlignment.Center: + x = availableBounds.Left + (availableBounds.Width - width) / 2; + break; + case LayoutAlignment.End: + x = availableBounds.Right - width; + break; + } + + // Vertical alignment + switch (vertical) + { + case LayoutAlignment.Center: + y = availableBounds.Top + (availableBounds.Height - height) / 2; + break; + case LayoutAlignment.End: + y = availableBounds.Bottom - height; + break; + } + + return new SKRect(x, y, x + width, y + height); + } + + public override SkiaView? HitTest(float x, float y) + { + if (!IsVisible || !Bounds.Contains(x, y)) + return null; + + // Check content first + if (Content != null) + { + var hit = Content.HitTest(x, y); + if (hit != null) + return hit; + } + + return this; + } + + public override void OnPointerPressed(PointerEventArgs e) + { + Content?.OnPointerPressed(e); + } + + public override void OnPointerMoved(PointerEventArgs e) + { + Content?.OnPointerMoved(e); + } + + public override void OnPointerReleased(PointerEventArgs e) + { + Content?.OnPointerReleased(e); + } +} + +/// +/// Layout alignment options. +/// +public enum LayoutAlignment +{ + /// + /// Fill the available space. + /// + Fill, + + /// + /// Align to the start (left or top). + /// + Start, + + /// + /// Align to the center. + /// + Center, + + /// + /// Align to the end (right or bottom). + /// + End +} diff --git a/Views/SkiaDatePicker.cs b/Views/SkiaDatePicker.cs index d0a3454..163adaa 100644 --- a/Views/SkiaDatePicker.cs +++ b/Views/SkiaDatePicker.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using SkiaSharp; +using Microsoft.Maui.Platform.Linux; namespace Microsoft.Maui.Platform; @@ -10,97 +11,234 @@ namespace Microsoft.Maui.Platform; /// public class SkiaDatePicker : SkiaView { - private DateTime _date = DateTime.Today; - private DateTime _minimumDate = new DateTime(1900, 1, 1); - private DateTime _maximumDate = new DateTime(2100, 12, 31); - private DateTime _displayMonth; - private bool _isOpen; - private string _format = "d"; + #region BindableProperties - // Styling - public SKColor TextColor { get; set; } = SKColors.Black; - public SKColor BorderColor { get; set; } = new SKColor(0xBD, 0xBD, 0xBD); - public SKColor CalendarBackgroundColor { get; set; } = SKColors.White; - public SKColor SelectedDayColor { get; set; } = new SKColor(0x21, 0x96, 0xF3); - public SKColor TodayColor { get; set; } = new SKColor(0x21, 0x96, 0xF3, 0x40); - public SKColor HeaderColor { get; set; } = new SKColor(0x21, 0x96, 0xF3); - public SKColor DisabledDayColor { get; set; } = new SKColor(0xBD, 0xBD, 0xBD); - public float FontSize { get; set; } = 14; - public float CornerRadius { get; set; } = 4; + public static readonly BindableProperty DateProperty = + BindableProperty.Create(nameof(Date), typeof(DateTime), typeof(SkiaDatePicker), DateTime.Today, BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaDatePicker)b).OnDatePropertyChanged()); - private const float CalendarWidth = 280; - private const float CalendarHeight = 320; - private const float DayCellSize = 36; - private const float HeaderHeight = 48; + public static readonly BindableProperty MinimumDateProperty = + BindableProperty.Create(nameof(MinimumDate), typeof(DateTime), typeof(SkiaDatePicker), new DateTime(1900, 1, 1), + propertyChanged: (b, o, n) => ((SkiaDatePicker)b).Invalidate()); + + public static readonly BindableProperty MaximumDateProperty = + BindableProperty.Create(nameof(MaximumDate), typeof(DateTime), typeof(SkiaDatePicker), new DateTime(2100, 12, 31), + propertyChanged: (b, o, n) => ((SkiaDatePicker)b).Invalidate()); + + public static readonly BindableProperty FormatProperty = + BindableProperty.Create(nameof(Format), typeof(string), typeof(SkiaDatePicker), "d", + propertyChanged: (b, o, n) => ((SkiaDatePicker)b).Invalidate()); + + public static readonly BindableProperty TextColorProperty = + BindableProperty.Create(nameof(TextColor), typeof(SKColor), typeof(SkiaDatePicker), SKColors.Black, + propertyChanged: (b, o, n) => ((SkiaDatePicker)b).Invalidate()); + + public static readonly BindableProperty BorderColorProperty = + BindableProperty.Create(nameof(BorderColor), typeof(SKColor), typeof(SkiaDatePicker), new SKColor(0xBD, 0xBD, 0xBD), + propertyChanged: (b, o, n) => ((SkiaDatePicker)b).Invalidate()); + + public static readonly BindableProperty CalendarBackgroundColorProperty = + BindableProperty.Create(nameof(CalendarBackgroundColor), typeof(SKColor), typeof(SkiaDatePicker), SKColors.White, + propertyChanged: (b, o, n) => ((SkiaDatePicker)b).Invalidate()); + + public static readonly BindableProperty SelectedDayColorProperty = + BindableProperty.Create(nameof(SelectedDayColor), typeof(SKColor), typeof(SkiaDatePicker), new SKColor(0x21, 0x96, 0xF3), + propertyChanged: (b, o, n) => ((SkiaDatePicker)b).Invalidate()); + + public static readonly BindableProperty TodayColorProperty = + BindableProperty.Create(nameof(TodayColor), typeof(SKColor), typeof(SkiaDatePicker), new SKColor(0x21, 0x96, 0xF3, 0x40), + propertyChanged: (b, o, n) => ((SkiaDatePicker)b).Invalidate()); + + public static readonly BindableProperty HeaderColorProperty = + BindableProperty.Create(nameof(HeaderColor), typeof(SKColor), typeof(SkiaDatePicker), new SKColor(0x21, 0x96, 0xF3), + propertyChanged: (b, o, n) => ((SkiaDatePicker)b).Invalidate()); + + public static readonly BindableProperty DisabledDayColorProperty = + BindableProperty.Create(nameof(DisabledDayColor), typeof(SKColor), typeof(SkiaDatePicker), new SKColor(0xBD, 0xBD, 0xBD), + propertyChanged: (b, o, n) => ((SkiaDatePicker)b).Invalidate()); + + public static readonly BindableProperty FontSizeProperty = + BindableProperty.Create(nameof(FontSize), typeof(float), typeof(SkiaDatePicker), 14f, + propertyChanged: (b, o, n) => ((SkiaDatePicker)b).InvalidateMeasure()); + + public static readonly BindableProperty CornerRadiusProperty = + BindableProperty.Create(nameof(CornerRadius), typeof(float), typeof(SkiaDatePicker), 4f, + propertyChanged: (b, o, n) => ((SkiaDatePicker)b).Invalidate()); + + #endregion + + #region Properties public DateTime Date { - get => _date; - set - { - var clamped = ClampDate(value); - if (_date != clamped) - { - _date = clamped; - _displayMonth = new DateTime(_date.Year, _date.Month, 1); - DateSelected?.Invoke(this, EventArgs.Empty); - Invalidate(); - } - } + get => (DateTime)GetValue(DateProperty); + set => SetValue(DateProperty, ClampDate(value)); } public DateTime MinimumDate { - get => _minimumDate; - set { _minimumDate = value; Invalidate(); } + get => (DateTime)GetValue(MinimumDateProperty); + set => SetValue(MinimumDateProperty, value); } public DateTime MaximumDate { - get => _maximumDate; - set { _maximumDate = value; Invalidate(); } + get => (DateTime)GetValue(MaximumDateProperty); + set => SetValue(MaximumDateProperty, value); } public string Format { - get => _format; - set { _format = value; Invalidate(); } + get => (string)GetValue(FormatProperty); + set => SetValue(FormatProperty, value); + } + + public SKColor TextColor + { + get => (SKColor)GetValue(TextColorProperty); + set => SetValue(TextColorProperty, value); + } + + public SKColor BorderColor + { + get => (SKColor)GetValue(BorderColorProperty); + set => SetValue(BorderColorProperty, value); + } + + public SKColor CalendarBackgroundColor + { + get => (SKColor)GetValue(CalendarBackgroundColorProperty); + set => SetValue(CalendarBackgroundColorProperty, value); + } + + public SKColor SelectedDayColor + { + get => (SKColor)GetValue(SelectedDayColorProperty); + set => SetValue(SelectedDayColorProperty, value); + } + + public SKColor TodayColor + { + get => (SKColor)GetValue(TodayColorProperty); + set => SetValue(TodayColorProperty, value); + } + + public SKColor HeaderColor + { + get => (SKColor)GetValue(HeaderColorProperty); + set => SetValue(HeaderColorProperty, value); + } + + public SKColor DisabledDayColor + { + get => (SKColor)GetValue(DisabledDayColorProperty); + set => SetValue(DisabledDayColorProperty, value); + } + + public float FontSize + { + get => (float)GetValue(FontSizeProperty); + set => SetValue(FontSizeProperty, value); + } + + public float CornerRadius + { + get => (float)GetValue(CornerRadiusProperty); + set => SetValue(CornerRadiusProperty, value); } public bool IsOpen { get => _isOpen; - set { _isOpen = value; Invalidate(); } + set + { + if (_isOpen != value) + { + _isOpen = value; + if (_isOpen) + RegisterPopupOverlay(this, DrawCalendarOverlay); + else + UnregisterPopupOverlay(this); + Invalidate(); + } + } } + #endregion + + private DateTime _displayMonth; + private bool _isOpen; + + private const float CalendarWidth = 280; + private const float CalendarHeight = 320; + private const float HeaderHeight = 48; + public event EventHandler? DateSelected; + /// + /// Gets the calendar popup rectangle with edge detection applied. + /// + private SKRect GetCalendarRect(SKRect pickerBounds) + { + // Get window dimensions for edge detection + var windowWidth = LinuxApplication.Current?.MainWindow?.Width ?? 800; + var windowHeight = LinuxApplication.Current?.MainWindow?.Height ?? 600; + + // Calculate default position (below the picker) + var calendarLeft = pickerBounds.Left; + var calendarTop = pickerBounds.Bottom + 4; + + // Edge detection: adjust horizontal position if popup would go off-screen + if (calendarLeft + CalendarWidth > windowWidth) + { + calendarLeft = windowWidth - CalendarWidth - 4; + } + if (calendarLeft < 0) calendarLeft = 4; + + // Edge detection: show above if popup would go off-screen vertically + if (calendarTop + CalendarHeight > windowHeight) + { + calendarTop = pickerBounds.Top - CalendarHeight - 4; + } + if (calendarTop < 0) calendarTop = 4; + + return new SKRect(calendarLeft, calendarTop, calendarLeft + CalendarWidth, calendarTop + CalendarHeight); + } + public SkiaDatePicker() { IsFocusable = true; - _displayMonth = new DateTime(_date.Year, _date.Month, 1); + _displayMonth = new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1); + } + + private void OnDatePropertyChanged() + { + _displayMonth = new DateTime(Date.Year, Date.Month, 1); + DateSelected?.Invoke(this, EventArgs.Empty); + Invalidate(); } private DateTime ClampDate(DateTime date) { - if (date < _minimumDate) return _minimumDate; - if (date > _maximumDate) return _maximumDate; + if (date < MinimumDate) return MinimumDate; + if (date > MaximumDate) return MaximumDate; return date; } + private void DrawCalendarOverlay(SKCanvas canvas) + { + if (!_isOpen) return; + // Use ScreenBounds for popup drawing (accounts for scroll offset) + DrawCalendar(canvas, ScreenBounds); + } + protected override void OnDraw(SKCanvas canvas, SKRect bounds) { DrawPickerButton(canvas, bounds); - - if (_isOpen) - { - DrawCalendar(canvas, bounds); - } } private void DrawPickerButton(SKCanvas canvas, SKRect bounds) { - // Draw background using var bgPaint = new SKPaint { Color = IsEnabled ? BackgroundColor : new SKColor(0xF5, 0xF5, 0xF5), @@ -109,7 +247,6 @@ public class SkiaDatePicker : SkiaView }; canvas.DrawRoundRect(new SKRoundRect(bounds, CornerRadius), bgPaint); - // Draw border using var borderPaint = new SKPaint { Color = IsFocused ? SelectedDayColor : BorderColor, @@ -119,7 +256,6 @@ public class SkiaDatePicker : SkiaView }; canvas.DrawRoundRect(new SKRoundRect(bounds, CornerRadius), borderPaint); - // Draw date text using var font = new SKFont(SKTypeface.Default, FontSize); using var textPaint = new SKPaint(font) { @@ -127,15 +263,11 @@ public class SkiaDatePicker : SkiaView IsAntialias = true }; - var dateText = _date.ToString(_format); + var dateText = Date.ToString(Format); var textBounds = new SKRect(); textPaint.MeasureText(dateText, ref textBounds); + canvas.DrawText(dateText, bounds.Left + 12, bounds.MidY - textBounds.MidY, textPaint); - var textX = bounds.Left + 12; - var textY = bounds.MidY - textBounds.MidY; - canvas.DrawText(dateText, textX, textY, textPaint); - - // Draw calendar icon DrawCalendarIcon(canvas, new SKRect(bounds.Right - 36, bounds.MidY - 10, bounds.Right - 12, bounds.MidY + 10)); } @@ -149,40 +281,22 @@ public class SkiaDatePicker : SkiaView IsAntialias = true }; - // Calendar outline var calRect = new SKRect(bounds.Left, bounds.Top + 3, bounds.Right, bounds.Bottom); canvas.DrawRoundRect(new SKRoundRect(calRect, 2), paint); - - // Top tabs canvas.DrawLine(bounds.Left + 5, bounds.Top, bounds.Left + 5, bounds.Top + 5, paint); canvas.DrawLine(bounds.Right - 5, bounds.Top, bounds.Right - 5, bounds.Top + 5, paint); - - // Header line canvas.DrawLine(bounds.Left, bounds.Top + 8, bounds.Right, bounds.Top + 8, paint); - // Dots for days paint.Style = SKPaintStyle.Fill; - paint.StrokeWidth = 0; for (int row = 0; row < 2; row++) - { for (int col = 0; col < 3; col++) - { - var dotX = bounds.Left + 4 + col * 6; - var dotY = bounds.Top + 12 + row * 4; - canvas.DrawCircle(dotX, dotY, 1, paint); - } - } + canvas.DrawCircle(bounds.Left + 4 + col * 6, bounds.Top + 12 + row * 4, 1, paint); } private void DrawCalendar(SKCanvas canvas, SKRect bounds) { - var calendarRect = new SKRect( - bounds.Left, - bounds.Bottom + 4, - bounds.Left + CalendarWidth, - bounds.Bottom + 4 + CalendarHeight); + var calendarRect = GetCalendarRect(bounds); - // Draw shadow using var shadowPaint = new SKPaint { Color = new SKColor(0, 0, 0, 40), @@ -191,88 +305,44 @@ public class SkiaDatePicker : SkiaView }; canvas.DrawRoundRect(new SKRoundRect(new SKRect(calendarRect.Left + 2, calendarRect.Top + 2, calendarRect.Right + 2, calendarRect.Bottom + 2), CornerRadius), shadowPaint); - // Draw background - using var bgPaint = new SKPaint - { - Color = CalendarBackgroundColor, - Style = SKPaintStyle.Fill, - IsAntialias = true - }; + using var bgPaint = new SKPaint { Color = CalendarBackgroundColor, Style = SKPaintStyle.Fill, IsAntialias = true }; canvas.DrawRoundRect(new SKRoundRect(calendarRect, CornerRadius), bgPaint); - // Draw border - using var borderPaint = new SKPaint - { - Color = BorderColor, - Style = SKPaintStyle.Stroke, - StrokeWidth = 1, - IsAntialias = true - }; + using var borderPaint = new SKPaint { Color = BorderColor, Style = SKPaintStyle.Stroke, StrokeWidth = 1, IsAntialias = true }; canvas.DrawRoundRect(new SKRoundRect(calendarRect, CornerRadius), borderPaint); - // Draw header DrawCalendarHeader(canvas, new SKRect(calendarRect.Left, calendarRect.Top, calendarRect.Right, calendarRect.Top + HeaderHeight)); - - // Draw weekday headers DrawWeekdayHeaders(canvas, new SKRect(calendarRect.Left, calendarRect.Top + HeaderHeight, calendarRect.Right, calendarRect.Top + HeaderHeight + 30)); - - // Draw days DrawDays(canvas, new SKRect(calendarRect.Left, calendarRect.Top + HeaderHeight + 30, calendarRect.Right, calendarRect.Bottom)); } private void DrawCalendarHeader(SKCanvas canvas, SKRect bounds) { - // Draw header background - using var headerPaint = new SKPaint - { - Color = HeaderColor, - Style = SKPaintStyle.Fill - }; - - var headerRect = new SKRect(bounds.Left, bounds.Top, bounds.Right, bounds.Bottom); + using var headerPaint = new SKPaint { Color = HeaderColor, Style = SKPaintStyle.Fill }; canvas.Save(); canvas.ClipRoundRect(new SKRoundRect(new SKRect(bounds.Left, bounds.Top, bounds.Right, bounds.Top + CornerRadius * 2), CornerRadius)); - canvas.DrawRect(headerRect, headerPaint); + canvas.DrawRect(bounds, headerPaint); canvas.Restore(); canvas.DrawRect(new SKRect(bounds.Left, bounds.Top + CornerRadius, bounds.Right, bounds.Bottom), headerPaint); - // Draw month/year text using var font = new SKFont(SKTypeface.Default, 16); - using var textPaint = new SKPaint(font) - { - Color = SKColors.White, - IsAntialias = true - }; - + using var textPaint = new SKPaint(font) { Color = SKColors.White, IsAntialias = true }; var monthYear = _displayMonth.ToString("MMMM yyyy"); var textBounds = new SKRect(); textPaint.MeasureText(monthYear, ref textBounds); canvas.DrawText(monthYear, bounds.MidX - textBounds.MidX, bounds.MidY - textBounds.MidY, textPaint); - // Draw navigation arrows - using var arrowPaint = new SKPaint - { - Color = SKColors.White, - Style = SKPaintStyle.Stroke, - StrokeWidth = 2, - IsAntialias = true, - StrokeCap = SKStrokeCap.Round - }; - - // Left arrow - var leftArrowX = bounds.Left + 20; + using var arrowPaint = new SKPaint { Color = SKColors.White, Style = SKPaintStyle.Stroke, StrokeWidth = 2, IsAntialias = true, StrokeCap = SKStrokeCap.Round }; using var leftPath = new SKPath(); - leftPath.MoveTo(leftArrowX + 6, bounds.MidY - 6); - leftPath.LineTo(leftArrowX, bounds.MidY); - leftPath.LineTo(leftArrowX + 6, bounds.MidY + 6); + leftPath.MoveTo(bounds.Left + 26, bounds.MidY - 6); + leftPath.LineTo(bounds.Left + 20, bounds.MidY); + leftPath.LineTo(bounds.Left + 26, bounds.MidY + 6); canvas.DrawPath(leftPath, arrowPaint); - // Right arrow - var rightArrowX = bounds.Right - 20; using var rightPath = new SKPath(); - rightPath.MoveTo(rightArrowX - 6, bounds.MidY - 6); - rightPath.LineTo(rightArrowX, bounds.MidY); - rightPath.LineTo(rightArrowX - 6, bounds.MidY + 6); + rightPath.MoveTo(bounds.Right - 26, bounds.MidY - 6); + rightPath.LineTo(bounds.Right - 20, bounds.MidY); + rightPath.LineTo(bounds.Right - 26, bounds.MidY + 6); canvas.DrawPath(rightPath, arrowPaint); } @@ -280,21 +350,13 @@ public class SkiaDatePicker : SkiaView { var dayNames = new[] { "Su", "Mo", "Tu", "We", "Th", "Fr", "Sa" }; var cellWidth = bounds.Width / 7; - using var font = new SKFont(SKTypeface.Default, 12); - using var paint = new SKPaint(font) - { - Color = new SKColor(0x80, 0x80, 0x80), - IsAntialias = true - }; - + using var paint = new SKPaint(font) { Color = new SKColor(0x80, 0x80, 0x80), IsAntialias = true }; for (int i = 0; i < 7; i++) { var textBounds = new SKRect(); paint.MeasureText(dayNames[i], ref textBounds); - var x = bounds.Left + i * cellWidth + cellWidth / 2 - textBounds.MidX; - var y = bounds.MidY - textBounds.MidY; - canvas.DrawText(dayNames[i], x, y, paint); + canvas.DrawText(dayNames[i], bounds.Left + i * cellWidth + cellWidth / 2 - textBounds.MidX, bounds.MidY - textBounds.MidY, paint); } } @@ -303,14 +365,11 @@ public class SkiaDatePicker : SkiaView var firstDay = new DateTime(_displayMonth.Year, _displayMonth.Month, 1); var daysInMonth = DateTime.DaysInMonth(_displayMonth.Year, _displayMonth.Month); var startDayOfWeek = (int)firstDay.DayOfWeek; - var cellWidth = bounds.Width / 7; var cellHeight = (bounds.Height - 10) / 6; - using var font = new SKFont(SKTypeface.Default, 14); using var textPaint = new SKPaint(font) { IsAntialias = true }; using var bgPaint = new SKPaint { Style = SKPaintStyle.Fill, IsAntialias = true }; - var today = DateTime.Today; for (int day = 1; day <= daysInMonth; day++) @@ -319,16 +378,12 @@ public class SkiaDatePicker : SkiaView var cellIndex = startDayOfWeek + day - 1; var row = cellIndex / 7; var col = cellIndex % 7; + var cellRect = new SKRect(bounds.Left + col * cellWidth + 2, bounds.Top + row * cellHeight + 2, bounds.Left + (col + 1) * cellWidth - 2, bounds.Top + (row + 1) * cellHeight - 2); - var cellX = bounds.Left + col * cellWidth; - var cellY = bounds.Top + row * cellHeight; - var cellRect = new SKRect(cellX + 2, cellY + 2, cellX + cellWidth - 2, cellY + cellHeight - 2); - - var isSelected = dayDate.Date == _date.Date; + var isSelected = dayDate.Date == Date.Date; var isToday = dayDate.Date == today; - var isDisabled = dayDate < _minimumDate || dayDate > _maximumDate; + var isDisabled = dayDate < MinimumDate || dayDate > MaximumDate; - // Draw day background if (isSelected) { bgPaint.Color = SelectedDayColor; @@ -340,7 +395,6 @@ public class SkiaDatePicker : SkiaView canvas.DrawCircle(cellRect.MidX, cellRect.MidY, Math.Min(cellRect.Width, cellRect.Height) / 2 - 2, bgPaint); } - // Draw day text textPaint.Color = isSelected ? SKColors.White : isDisabled ? DisabledDayColor : TextColor; var dayText = day.ToString(); var textBounds = new SKRect(); @@ -353,115 +407,104 @@ public class SkiaDatePicker : SkiaView { if (!IsEnabled) return; - if (_isOpen) + if (IsOpen) { - var calendarTop = Bounds.Bottom + 4; + // Use ScreenBounds for popup coordinate calculations (accounts for scroll offset) + var screenBounds = ScreenBounds; + var calendarRect = GetCalendarRect(screenBounds); - // Check header navigation - if (e.Y >= calendarTop && e.Y < calendarTop + HeaderHeight) + // Check if click is in header area (navigation arrows) + var headerRect = new SKRect(calendarRect.Left, calendarRect.Top, calendarRect.Right, calendarRect.Top + HeaderHeight); + if (headerRect.Contains(e.X, e.Y)) { - if (e.X < Bounds.Left + 40) - { - // Previous month - _displayMonth = _displayMonth.AddMonths(-1); - Invalidate(); - return; - } - else if (e.X > Bounds.Left + CalendarWidth - 40) - { - // Next month - _displayMonth = _displayMonth.AddMonths(1); - Invalidate(); - return; - } + if (e.X < calendarRect.Left + 40) { _displayMonth = _displayMonth.AddMonths(-1); Invalidate(); return; } + if (e.X > calendarRect.Right - 40) { _displayMonth = _displayMonth.AddMonths(1); Invalidate(); return; } + return; } - // Check day selection - var daysTop = calendarTop + HeaderHeight + 30; - if (e.Y >= daysTop && e.Y < calendarTop + CalendarHeight) + // Check if click is in days area + var daysTop = calendarRect.Top + HeaderHeight + 30; + var daysRect = new SKRect(calendarRect.Left, daysTop, calendarRect.Right, calendarRect.Bottom); + if (daysRect.Contains(e.X, e.Y)) { var cellWidth = CalendarWidth / 7; var cellHeight = (CalendarHeight - HeaderHeight - 40) / 6; - - var col = (int)((e.X - Bounds.Left) / cellWidth); + var col = (int)((e.X - calendarRect.Left) / cellWidth); var row = (int)((e.Y - daysTop) / cellHeight); - var firstDay = new DateTime(_displayMonth.Year, _displayMonth.Month, 1); - var startDayOfWeek = (int)firstDay.DayOfWeek; - var dayIndex = row * 7 + col - startDayOfWeek + 1; + var dayIndex = row * 7 + col - (int)firstDay.DayOfWeek + 1; var daysInMonth = DateTime.DaysInMonth(_displayMonth.Year, _displayMonth.Month); - if (dayIndex >= 1 && dayIndex <= daysInMonth) { var selectedDate = new DateTime(_displayMonth.Year, _displayMonth.Month, dayIndex); - if (selectedDate >= _minimumDate && selectedDate <= _maximumDate) + if (selectedDate >= MinimumDate && selectedDate <= MaximumDate) { Date = selectedDate; - _isOpen = false; + IsOpen = false; } } + return; } - else if (e.Y < calendarTop) - { - _isOpen = false; - } - } - else - { - _isOpen = true; - } + // Click is outside calendar - check if it's on the picker itself + if (screenBounds.Contains(e.X, e.Y)) + { + IsOpen = false; + } + } + else IsOpen = true; Invalidate(); } public override void OnKeyDown(KeyEventArgs e) { if (!IsEnabled) return; - switch (e.Key) { - case Key.Enter: - case Key.Space: - _isOpen = !_isOpen; - e.Handled = true; - break; - - case Key.Escape: - if (_isOpen) - { - _isOpen = false; - e.Handled = true; - } - break; - - case Key.Left: - Date = _date.AddDays(-1); - e.Handled = true; - break; - - case Key.Right: - Date = _date.AddDays(1); - e.Handled = true; - break; - - case Key.Up: - Date = _date.AddDays(-7); - e.Handled = true; - break; - - case Key.Down: - Date = _date.AddDays(7); - e.Handled = true; - break; + case Key.Enter: case Key.Space: IsOpen = !IsOpen; e.Handled = true; break; + case Key.Escape: if (IsOpen) { IsOpen = false; e.Handled = true; } break; + case Key.Left: Date = Date.AddDays(-1); e.Handled = true; break; + case Key.Right: Date = Date.AddDays(1); e.Handled = true; break; + case Key.Up: Date = Date.AddDays(-7); e.Handled = true; break; + case Key.Down: Date = Date.AddDays(7); e.Handled = true; break; } - Invalidate(); } + public override void OnFocusLost() + { + base.OnFocusLost(); + // Close popup when focus is lost (clicking outside) + if (IsOpen) + { + IsOpen = false; + } + } + protected override SKSize MeasureOverride(SKSize availableSize) { - return new SKSize( - availableSize.Width < float.MaxValue ? Math.Min(availableSize.Width, 200) : 200, - 40); + return new SKSize(availableSize.Width < float.MaxValue ? Math.Min(availableSize.Width, 200) : 200, 40); + } + + /// + /// Override to include calendar popup area in hit testing. + /// + protected override bool HitTestPopupArea(float x, float y) + { + // Use ScreenBounds for hit testing (accounts for scroll offset) + var screenBounds = ScreenBounds; + + // Always include the picker button itself + if (screenBounds.Contains(x, y)) + return true; + + // When open, also include the calendar area (with edge detection) + if (_isOpen) + { + var calendarRect = GetCalendarRect(screenBounds); + return calendarRect.Contains(x, y); + } + + return false; } } diff --git a/Views/SkiaEditor.cs b/Views/SkiaEditor.cs index f4d513c..daa04c1 100644 --- a/Views/SkiaEditor.cs +++ b/Views/SkiaEditor.cs @@ -6,90 +6,354 @@ using SkiaSharp; namespace Microsoft.Maui.Platform; /// -/// Skia-rendered multiline text editor control. +/// Skia-rendered multiline text editor control with full XAML styling support. /// public class SkiaEditor : SkiaView { - private string _text = ""; - private string _placeholder = ""; - private int _cursorPosition; - private int _selectionStart = -1; - private int _selectionLength; - private float _scrollOffsetY; - private bool _isReadOnly; - private int _maxLength = -1; - private bool _cursorVisible = true; - private DateTime _lastCursorBlink = DateTime.Now; + #region BindableProperties - // Cached line information - private List _lines = new() { "" }; - private List _lineHeights = new(); + /// + /// Bindable property for Text. + /// + public static readonly BindableProperty TextProperty = + BindableProperty.Create( + nameof(Text), + typeof(string), + typeof(SkiaEditor), + "", + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaEditor)b).OnTextPropertyChanged((string)o, (string)n)); - // Styling - public SKColor TextColor { get; set; } = SKColors.Black; - public SKColor PlaceholderColor { get; set; } = new SKColor(0x80, 0x80, 0x80); - public SKColor BorderColor { get; set; } = new SKColor(0xBD, 0xBD, 0xBD); - public SKColor SelectionColor { get; set; } = new SKColor(0x21, 0x96, 0xF3, 0x60); - public SKColor CursorColor { get; set; } = new SKColor(0x21, 0x96, 0xF3); - public string FontFamily { get; set; } = "Sans"; - public float FontSize { get; set; } = 14; - public float LineHeight { get; set; } = 1.4f; - public float CornerRadius { get; set; } = 4; - public float Padding { get; set; } = 12; - public bool AutoSize { get; set; } + /// + /// Bindable property for Placeholder. + /// + public static readonly BindableProperty PlaceholderProperty = + BindableProperty.Create( + nameof(Placeholder), + typeof(string), + typeof(SkiaEditor), + "", + propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate()); + /// + /// Bindable property for TextColor. + /// + public static readonly BindableProperty TextColorProperty = + BindableProperty.Create( + nameof(TextColor), + typeof(SKColor), + typeof(SkiaEditor), + SKColors.Black, + propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate()); + + /// + /// Bindable property for PlaceholderColor. + /// + public static readonly BindableProperty PlaceholderColorProperty = + BindableProperty.Create( + nameof(PlaceholderColor), + typeof(SKColor), + typeof(SkiaEditor), + new SKColor(0x80, 0x80, 0x80), + propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate()); + + /// + /// Bindable property for BorderColor. + /// + public static readonly BindableProperty BorderColorProperty = + BindableProperty.Create( + nameof(BorderColor), + typeof(SKColor), + typeof(SkiaEditor), + new SKColor(0xBD, 0xBD, 0xBD), + propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate()); + + /// + /// Bindable property for SelectionColor. + /// + public static readonly BindableProperty SelectionColorProperty = + BindableProperty.Create( + nameof(SelectionColor), + typeof(SKColor), + typeof(SkiaEditor), + new SKColor(0x21, 0x96, 0xF3, 0x60), + propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate()); + + /// + /// Bindable property for CursorColor. + /// + public static readonly BindableProperty CursorColorProperty = + BindableProperty.Create( + nameof(CursorColor), + typeof(SKColor), + typeof(SkiaEditor), + new SKColor(0x21, 0x96, 0xF3), + propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate()); + + /// + /// Bindable property for FontFamily. + /// + public static readonly BindableProperty FontFamilyProperty = + BindableProperty.Create( + nameof(FontFamily), + typeof(string), + typeof(SkiaEditor), + "Sans", + propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure()); + + /// + /// Bindable property for FontSize. + /// + public static readonly BindableProperty FontSizeProperty = + BindableProperty.Create( + nameof(FontSize), + typeof(float), + typeof(SkiaEditor), + 14f, + propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure()); + + /// + /// Bindable property for LineHeight. + /// + public static readonly BindableProperty LineHeightProperty = + BindableProperty.Create( + nameof(LineHeight), + typeof(float), + typeof(SkiaEditor), + 1.4f, + propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure()); + + /// + /// Bindable property for CornerRadius. + /// + public static readonly BindableProperty CornerRadiusProperty = + BindableProperty.Create( + nameof(CornerRadius), + typeof(float), + typeof(SkiaEditor), + 4f, + propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate()); + + /// + /// Bindable property for Padding. + /// + public static readonly BindableProperty PaddingProperty = + BindableProperty.Create( + nameof(Padding), + typeof(float), + typeof(SkiaEditor), + 12f, + propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure()); + + /// + /// Bindable property for IsReadOnly. + /// + public static readonly BindableProperty IsReadOnlyProperty = + BindableProperty.Create( + nameof(IsReadOnly), + typeof(bool), + typeof(SkiaEditor), + false, + propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate()); + + /// + /// Bindable property for MaxLength. + /// + public static readonly BindableProperty MaxLengthProperty = + BindableProperty.Create( + nameof(MaxLength), + typeof(int), + typeof(SkiaEditor), + -1); + + /// + /// Bindable property for AutoSize. + /// + public static readonly BindableProperty AutoSizeProperty = + BindableProperty.Create( + nameof(AutoSize), + typeof(bool), + typeof(SkiaEditor), + false, + propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure()); + + #endregion + + #region Properties + + /// + /// Gets or sets the text content. + /// public string Text { - get => _text; - set - { - var newText = value ?? ""; - if (_maxLength > 0 && newText.Length > _maxLength) - { - newText = newText.Substring(0, _maxLength); - } - - if (_text != newText) - { - _text = newText; - UpdateLines(); - _cursorPosition = Math.Min(_cursorPosition, _text.Length); - TextChanged?.Invoke(this, EventArgs.Empty); - Invalidate(); - } - } + get => (string)GetValue(TextProperty); + set => SetValue(TextProperty, value); } + /// + /// Gets or sets the placeholder text. + /// public string Placeholder { - get => _placeholder; - set { _placeholder = value ?? ""; Invalidate(); } + get => (string)GetValue(PlaceholderProperty); + set => SetValue(PlaceholderProperty, value); } + /// + /// Gets or sets the text color. + /// + public SKColor TextColor + { + get => (SKColor)GetValue(TextColorProperty); + set => SetValue(TextColorProperty, value); + } + + /// + /// Gets or sets the placeholder color. + /// + public SKColor PlaceholderColor + { + get => (SKColor)GetValue(PlaceholderColorProperty); + set => SetValue(PlaceholderColorProperty, value); + } + + /// + /// Gets or sets the border color. + /// + public SKColor BorderColor + { + get => (SKColor)GetValue(BorderColorProperty); + set => SetValue(BorderColorProperty, value); + } + + /// + /// Gets or sets the selection color. + /// + public SKColor SelectionColor + { + get => (SKColor)GetValue(SelectionColorProperty); + set => SetValue(SelectionColorProperty, value); + } + + /// + /// Gets or sets the cursor color. + /// + public SKColor CursorColor + { + get => (SKColor)GetValue(CursorColorProperty); + set => SetValue(CursorColorProperty, value); + } + + /// + /// Gets or sets the font family. + /// + public string FontFamily + { + get => (string)GetValue(FontFamilyProperty); + set => SetValue(FontFamilyProperty, value); + } + + /// + /// Gets or sets the font size. + /// + public float FontSize + { + get => (float)GetValue(FontSizeProperty); + set => SetValue(FontSizeProperty, value); + } + + /// + /// Gets or sets the line height multiplier. + /// + public float LineHeight + { + get => (float)GetValue(LineHeightProperty); + set => SetValue(LineHeightProperty, value); + } + + /// + /// Gets or sets the corner radius. + /// + public float CornerRadius + { + get => (float)GetValue(CornerRadiusProperty); + set => SetValue(CornerRadiusProperty, value); + } + + /// + /// Gets or sets the padding. + /// + public float Padding + { + get => (float)GetValue(PaddingProperty); + set => SetValue(PaddingProperty, value); + } + + /// + /// Gets or sets whether the editor is read-only. + /// public bool IsReadOnly { - get => _isReadOnly; - set { _isReadOnly = value; Invalidate(); } + get => (bool)GetValue(IsReadOnlyProperty); + set => SetValue(IsReadOnlyProperty, value); } + /// + /// Gets or sets the maximum length. -1 for unlimited. + /// public int MaxLength { - get => _maxLength; - set { _maxLength = value; } + get => (int)GetValue(MaxLengthProperty); + set => SetValue(MaxLengthProperty, value); } + /// + /// Gets or sets whether the editor auto-sizes to content. + /// + public bool AutoSize + { + get => (bool)GetValue(AutoSizeProperty); + set => SetValue(AutoSizeProperty, value); + } + + /// + /// Gets or sets the cursor position. + /// public int CursorPosition { get => _cursorPosition; set { - _cursorPosition = Math.Clamp(value, 0, _text.Length); + _cursorPosition = Math.Clamp(value, 0, Text.Length); EnsureCursorVisible(); Invalidate(); } } + #endregion + + private int _cursorPosition; + private int _selectionStart = -1; + private int _selectionLength; + private float _scrollOffsetY; + private bool _cursorVisible = true; + private DateTime _lastCursorBlink = DateTime.Now; + private List _lines = new() { "" }; + private float _wrapWidth = 0; // Available width for word wrapping + private bool _isSelecting; // For mouse-based text selection + private DateTime _lastClickTime = DateTime.MinValue; + private float _lastClickX; + private float _lastClickY; + private const double DoubleClickThresholdMs = 400; + + /// + /// Event raised when text changes. + /// public event EventHandler? TextChanged; + + /// + /// Event raised when editing is completed. + /// public event EventHandler? Completed; public SkiaEditor() @@ -97,29 +361,92 @@ public class SkiaEditor : SkiaView IsFocusable = true; } + private void OnTextPropertyChanged(string oldText, string newText) + { + var text = newText ?? ""; + + if (MaxLength > 0 && text.Length > MaxLength) + { + text = text.Substring(0, MaxLength); + SetValue(TextProperty, text); + return; + } + + UpdateLines(); + _cursorPosition = Math.Min(_cursorPosition, text.Length); + _scrollOffsetY = 0; // Reset scroll when text changes externally + _selectionLength = 0; + TextChanged?.Invoke(this, EventArgs.Empty); + Invalidate(); + } + private void UpdateLines() { _lines.Clear(); - if (string.IsNullOrEmpty(_text)) + var text = Text ?? ""; + if (string.IsNullOrEmpty(text)) { _lines.Add(""); return; } - var currentLine = ""; - foreach (var ch in _text) + using var font = new SKFont(SKTypeface.Default, FontSize); + + // Split by actual newlines first + var paragraphs = text.Split('\n'); + + foreach (var paragraph in paragraphs) { - if (ch == '\n') + if (string.IsNullOrEmpty(paragraph)) { - _lines.Add(currentLine); - currentLine = ""; + _lines.Add(""); + continue; + } + + // Word wrap this paragraph if we have a known width + if (_wrapWidth > 0) + { + WrapParagraph(paragraph, font, _wrapWidth); } else { - currentLine += ch; + _lines.Add(paragraph); } } - _lines.Add(currentLine); + + if (_lines.Count == 0) + { + _lines.Add(""); + } + } + + private void WrapParagraph(string paragraph, SKFont font, float maxWidth) + { + var words = paragraph.Split(' '); + var currentLine = ""; + + foreach (var word in words) + { + var testLine = string.IsNullOrEmpty(currentLine) ? word : currentLine + " " + word; + var lineWidth = MeasureText(testLine, font); + + if (lineWidth > maxWidth && !string.IsNullOrEmpty(currentLine)) + { + // Line too long, save current and start new + _lines.Add(currentLine); + currentLine = word; + } + else + { + currentLine = testLine; + } + } + + // Add remaining text + if (!string.IsNullOrEmpty(currentLine)) + { + _lines.Add(currentLine); + } } private (int line, int column) GetLineColumn(int position) @@ -132,7 +459,7 @@ public class SkiaEditor : SkiaView { return (i, position - pos); } - pos += lineLength + 1; // +1 for newline + pos += lineLength + 1; } return (_lines.Count - 1, _lines[^1].Length); } @@ -148,11 +475,19 @@ public class SkiaEditor : SkiaView { pos += Math.Min(column, _lines[line].Length); } - return Math.Min(pos, _text.Length); + return Math.Min(pos, Text.Length); } protected override void OnDraw(SKCanvas canvas, SKRect bounds) { + // Update wrap width if bounds changed and re-wrap text + var newWrapWidth = bounds.Width - Padding * 2; + if (Math.Abs(newWrapWidth - _wrapWidth) > 1) + { + _wrapWidth = newWrapWidth; + UpdateLines(); + } + // Handle cursor blinking if (IsFocused && (DateTime.Now - _lastCursorBlink).TotalMilliseconds > 500) { @@ -192,21 +527,20 @@ public class SkiaEditor : SkiaView canvas.Save(); canvas.ClipRect(contentRect); - canvas.Translate(0, -_scrollOffsetY); + // Don't translate - let the text draw at absolute positions + // canvas.Translate(0, -_scrollOffsetY); - if (string.IsNullOrEmpty(_text) && !string.IsNullOrEmpty(_placeholder)) + if (string.IsNullOrEmpty(Text) && !string.IsNullOrEmpty(Placeholder)) { - // Draw placeholder using var placeholderPaint = new SKPaint(font) { Color = PlaceholderColor, IsAntialias = true }; - canvas.DrawText(_placeholder, contentRect.Left, contentRect.Top + FontSize, placeholderPaint); + canvas.DrawText(Placeholder, contentRect.Left, contentRect.Top + FontSize, placeholderPaint); } else { - // Draw text with selection using var textPaint = new SKPaint(font) { Color = IsEnabled ? TextColor : TextColor.WithAlpha(128), @@ -227,15 +561,17 @@ public class SkiaEditor : SkiaView var x = contentRect.Left; // Draw selection for this line if applicable - if (_selectionStart >= 0 && _selectionLength > 0) + if (_selectionStart >= 0 && _selectionLength != 0) { - var selEnd = _selectionStart + _selectionLength; + // Handle both positive and negative selection lengths + var selStart = _selectionLength > 0 ? _selectionStart : _selectionStart + _selectionLength; + var selEnd = _selectionLength > 0 ? _selectionStart + _selectionLength : _selectionStart; var lineStart = charIndex; var lineEnd = charIndex + line.Length; - if (selEnd > lineStart && _selectionStart < lineEnd) + if (selEnd > lineStart && selStart < lineEnd) { - var selStartInLine = Math.Max(0, _selectionStart - lineStart); + var selStartInLine = Math.Max(0, selStart - lineStart); var selEndInLine = Math.Min(line.Length, selEnd - lineStart); var startX = x + MeasureText(line.Substring(0, selStartInLine), font); @@ -245,7 +581,6 @@ public class SkiaEditor : SkiaView } } - // Draw line text canvas.DrawText(line, x, y, textPaint); // Draw cursor if on this line @@ -267,7 +602,7 @@ public class SkiaEditor : SkiaView } y += lineSpacing; - charIndex += line.Length + 1; // +1 for newline + charIndex += line.Length + 1; } } @@ -332,12 +667,12 @@ public class SkiaEditor : SkiaView { if (!IsEnabled) return; - // Request focus by notifying parent IsFocused = true; - // Calculate cursor position from click - var contentX = e.X - Bounds.Left - Padding; - var contentY = e.Y - Bounds.Top - Padding + _scrollOffsetY; + // Use screen coordinates for proper hit detection + var screenBounds = ScreenBounds; + var contentX = e.X - screenBounds.Left - Padding; + var contentY = e.Y - screenBounds.Top - Padding + _scrollOffsetY; var lineSpacing = FontSize * LineHeight; var clickedLine = Math.Clamp((int)(contentY / lineSpacing), 0, _lines.Count - 1); @@ -346,7 +681,6 @@ public class SkiaEditor : SkiaView var line = _lines[clickedLine]; var clickedCol = 0; - // Find closest character position for (int i = 0; i <= line.Length; i++) { var charX = MeasureText(line.Substring(0, i), font); @@ -359,14 +693,79 @@ public class SkiaEditor : SkiaView } _cursorPosition = GetPosition(clickedLine, clickedCol); - _selectionStart = -1; - _selectionLength = 0; + + // Check for double-click (select word) + var now = DateTime.UtcNow; + var timeSinceLastClick = (now - _lastClickTime).TotalMilliseconds; + var distanceFromLastClick = Math.Sqrt(Math.Pow(e.X - _lastClickX, 2) + Math.Pow(e.Y - _lastClickY, 2)); + + if (timeSinceLastClick < DoubleClickThresholdMs && distanceFromLastClick < 10) + { + // Double-click: select the word at cursor + SelectWordAtCursor(); + _lastClickTime = DateTime.MinValue; // Reset to prevent triple-click issues + _isSelecting = false; + } + else + { + // Single click: start selection + _selectionStart = _cursorPosition; + _selectionLength = 0; + _isSelecting = true; + _lastClickTime = now; + _lastClickX = e.X; + _lastClickY = e.Y; + } + _cursorVisible = true; _lastCursorBlink = DateTime.Now; Invalidate(); } + public override void OnPointerMoved(PointerEventArgs e) + { + if (!IsEnabled || !_isSelecting) return; + + // Calculate position from mouse coordinates + var screenBounds = ScreenBounds; + var contentX = e.X - screenBounds.Left - Padding; + var contentY = e.Y - screenBounds.Top - Padding + _scrollOffsetY; + + var lineSpacing = FontSize * LineHeight; + var clickedLine = Math.Clamp((int)(contentY / lineSpacing), 0, _lines.Count - 1); + + using var font = new SKFont(SKTypeface.Default, FontSize); + var line = _lines[clickedLine]; + var clickedCol = 0; + + for (int i = 0; i <= line.Length; i++) + { + var charX = MeasureText(line.Substring(0, i), font); + if (charX > contentX) + { + clickedCol = i > 0 ? i - 1 : 0; + break; + } + clickedCol = i; + } + + var newPosition = GetPosition(clickedLine, clickedCol); + if (newPosition != _cursorPosition) + { + _cursorPosition = newPosition; + _selectionLength = _cursorPosition - _selectionStart; + _cursorVisible = true; + _lastCursorBlink = DateTime.Now; + Invalidate(); + } + } + + public override void OnPointerReleased(PointerEventArgs e) + { + _isSelecting = false; + } + public override void OnKeyDown(KeyEventArgs e) { if (!IsEnabled) return; @@ -387,7 +786,7 @@ public class SkiaEditor : SkiaView break; case Key.Right: - if (_cursorPosition < _text.Length) + if (_cursorPosition < Text.Length) { _cursorPosition++; EnsureCursorVisible(); @@ -426,7 +825,7 @@ public class SkiaEditor : SkiaView break; case Key.Enter: - if (!_isReadOnly) + if (!IsReadOnly) { InsertText("\n"); } @@ -434,30 +833,76 @@ public class SkiaEditor : SkiaView break; case Key.Backspace: - if (!_isReadOnly && _cursorPosition > 0) + if (!IsReadOnly) { - Text = _text.Remove(_cursorPosition - 1, 1); - _cursorPosition--; + if (_selectionLength != 0) + { + DeleteSelection(); + } + else if (_cursorPosition > 0) + { + Text = Text.Remove(_cursorPosition - 1, 1); + _cursorPosition--; + } EnsureCursorVisible(); } e.Handled = true; break; case Key.Delete: - if (!_isReadOnly && _cursorPosition < _text.Length) + if (!IsReadOnly) { - Text = _text.Remove(_cursorPosition, 1); + if (_selectionLength != 0) + { + DeleteSelection(); + } + else if (_cursorPosition < Text.Length) + { + Text = Text.Remove(_cursorPosition, 1); + } } e.Handled = true; break; case Key.Tab: - if (!_isReadOnly) + if (!IsReadOnly) { - InsertText(" "); // 4 spaces for tab + InsertText(" "); } e.Handled = true; break; + + case Key.A: + if (e.Modifiers.HasFlag(KeyModifiers.Control)) + { + SelectAll(); + e.Handled = true; + } + break; + + case Key.C: + if (e.Modifiers.HasFlag(KeyModifiers.Control)) + { + CopyToClipboard(); + e.Handled = true; + } + break; + + case Key.V: + if (e.Modifiers.HasFlag(KeyModifiers.Control) && !IsReadOnly) + { + PasteFromClipboard(); + e.Handled = true; + } + break; + + case Key.X: + if (e.Modifiers.HasFlag(KeyModifiers.Control) && !IsReadOnly) + { + CutToClipboard(); + e.Handled = true; + } + break; } Invalidate(); @@ -465,7 +910,11 @@ public class SkiaEditor : SkiaView public override void OnTextInput(TextInputEventArgs e) { - if (!IsEnabled || _isReadOnly) return; + if (!IsEnabled || IsReadOnly) return; + + // Ignore control characters (Ctrl+key combinations send ASCII control codes) + if (!string.IsNullOrEmpty(e.Text) && e.Text.Length == 1 && e.Text[0] < 32) + return; if (!string.IsNullOrEmpty(e.Text)) { @@ -478,21 +927,21 @@ public class SkiaEditor : SkiaView { if (_selectionLength > 0) { - // Replace selection - _text = _text.Remove(_selectionStart, _selectionLength); + var currentText = Text; + Text = currentText.Remove(_selectionStart, _selectionLength); _cursorPosition = _selectionStart; _selectionStart = -1; _selectionLength = 0; } - if (_maxLength > 0 && _text.Length + text.Length > _maxLength) + if (MaxLength > 0 && Text.Length + text.Length > MaxLength) { - text = text.Substring(0, Math.Max(0, _maxLength - _text.Length)); + text = text.Substring(0, Math.Max(0, MaxLength - Text.Length)); } if (!string.IsNullOrEmpty(text)) { - Text = _text.Insert(_cursorPosition, text); + Text = Text.Insert(_cursorPosition, text); _cursorPosition += text.Length; EnsureCursorVisible(); } @@ -509,6 +958,102 @@ public class SkiaEditor : SkiaView Invalidate(); } + public override void OnFocusGained() + { + base.OnFocusGained(); + SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.Focused); + } + + public override void OnFocusLost() + { + base.OnFocusLost(); + SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.Normal); + } + + #region Selection and Clipboard + + public void SelectAll() + { + _selectionStart = 0; + _cursorPosition = Text.Length; + _selectionLength = Text.Length; + Invalidate(); + } + + private void SelectWordAtCursor() + { + if (string.IsNullOrEmpty(Text)) return; + + // Find word boundaries + int start = _cursorPosition; + int end = _cursorPosition; + + // Move start backwards to beginning of word + while (start > 0 && IsWordChar(Text[start - 1])) + start--; + + // Move end forwards to end of word + while (end < Text.Length && IsWordChar(Text[end])) + end++; + + _selectionStart = start; + _cursorPosition = end; + _selectionLength = end - start; + } + + private static bool IsWordChar(char c) + { + return char.IsLetterOrDigit(c) || c == '_'; + } + + private void CopyToClipboard() + { + if (_selectionLength == 0) return; + + var start = Math.Min(_selectionStart, _selectionStart + _selectionLength); + var length = Math.Abs(_selectionLength); + var selectedText = Text.Substring(start, length); + + // Use system clipboard via xclip/xsel + SystemClipboard.SetText(selectedText); + } + + private void CutToClipboard() + { + CopyToClipboard(); + DeleteSelection(); + Invalidate(); + } + + private void PasteFromClipboard() + { + // Get from system clipboard + var text = SystemClipboard.GetText(); + if (string.IsNullOrEmpty(text)) return; + + if (_selectionLength != 0) + { + DeleteSelection(); + } + + InsertText(text); + } + + private void DeleteSelection() + { + if (_selectionLength == 0) return; + + var start = Math.Min(_selectionStart, _selectionStart + _selectionLength); + var length = Math.Abs(_selectionLength); + + Text = Text.Remove(start, length); + _cursorPosition = start; + _selectionStart = -1; + _selectionLength = 0; + } + + #endregion + protected override SKSize MeasureOverride(SKSize availableSize) { if (AutoSize) diff --git a/Views/SkiaEntry.cs b/Views/SkiaEntry.cs index 869e315..22fb8f0 100644 --- a/Views/SkiaEntry.cs +++ b/Views/SkiaEntry.cs @@ -7,69 +7,513 @@ using Microsoft.Maui.Platform.Linux.Rendering; namespace Microsoft.Maui.Platform; /// -/// Skia-rendered text entry control. +/// Skia-rendered text entry control with full XAML styling and data binding support. /// public class SkiaEntry : SkiaView { - private string _text = ""; - private int _cursorPosition; - private int _selectionStart; - private int _selectionLength; - private float _scrollOffset; - private DateTime _cursorBlinkTime = DateTime.UtcNow; - private bool _cursorVisible = true; + #region BindableProperties + /// + /// Bindable property for Text. + /// + public static readonly BindableProperty TextProperty = + BindableProperty.Create( + nameof(Text), + typeof(string), + typeof(SkiaEntry), + "", + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaEntry)b).OnTextPropertyChanged((string)o, (string)n)); + + /// + /// Bindable property for Placeholder. + /// + public static readonly BindableProperty PlaceholderProperty = + BindableProperty.Create( + nameof(Placeholder), + typeof(string), + typeof(SkiaEntry), + "", + propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); + + /// + /// Bindable property for PlaceholderColor. + /// + public static readonly BindableProperty PlaceholderColorProperty = + BindableProperty.Create( + nameof(PlaceholderColor), + typeof(SKColor), + typeof(SkiaEntry), + new SKColor(0x9E, 0x9E, 0x9E), + propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); + + /// + /// Bindable property for TextColor. + /// + public static readonly BindableProperty TextColorProperty = + BindableProperty.Create( + nameof(TextColor), + typeof(SKColor), + typeof(SkiaEntry), + SKColors.Black, + propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); + + /// + /// Bindable property for EntryBackgroundColor. + /// + public static readonly BindableProperty EntryBackgroundColorProperty = + BindableProperty.Create( + nameof(EntryBackgroundColor), + typeof(SKColor), + typeof(SkiaEntry), + SKColors.White, + propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); + + /// + /// Bindable property for BorderColor. + /// + public static readonly BindableProperty BorderColorProperty = + BindableProperty.Create( + nameof(BorderColor), + typeof(SKColor), + typeof(SkiaEntry), + new SKColor(0xBD, 0xBD, 0xBD), + propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); + + /// + /// Bindable property for FocusedBorderColor. + /// + public static readonly BindableProperty FocusedBorderColorProperty = + BindableProperty.Create( + nameof(FocusedBorderColor), + typeof(SKColor), + typeof(SkiaEntry), + new SKColor(0x21, 0x96, 0xF3), + propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); + + /// + /// Bindable property for SelectionColor. + /// + public static readonly BindableProperty SelectionColorProperty = + BindableProperty.Create( + nameof(SelectionColor), + typeof(SKColor), + typeof(SkiaEntry), + new SKColor(0x21, 0x96, 0xF3, 0x80), + propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); + + /// + /// Bindable property for CursorColor. + /// + public static readonly BindableProperty CursorColorProperty = + BindableProperty.Create( + nameof(CursorColor), + typeof(SKColor), + typeof(SkiaEntry), + new SKColor(0x21, 0x96, 0xF3), + propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); + + /// + /// Bindable property for FontFamily. + /// + public static readonly BindableProperty FontFamilyProperty = + BindableProperty.Create( + nameof(FontFamily), + typeof(string), + typeof(SkiaEntry), + "Sans", + propertyChanged: (b, o, n) => ((SkiaEntry)b).InvalidateMeasure()); + + /// + /// Bindable property for FontSize. + /// + public static readonly BindableProperty FontSizeProperty = + BindableProperty.Create( + nameof(FontSize), + typeof(float), + typeof(SkiaEntry), + 14f, + propertyChanged: (b, o, n) => ((SkiaEntry)b).InvalidateMeasure()); + + /// + /// Bindable property for IsBold. + /// + public static readonly BindableProperty IsBoldProperty = + BindableProperty.Create( + nameof(IsBold), + typeof(bool), + typeof(SkiaEntry), + false, + propertyChanged: (b, o, n) => ((SkiaEntry)b).InvalidateMeasure()); + + /// + /// Bindable property for IsItalic. + /// + public static readonly BindableProperty IsItalicProperty = + BindableProperty.Create( + nameof(IsItalic), + typeof(bool), + typeof(SkiaEntry), + false, + propertyChanged: (b, o, n) => ((SkiaEntry)b).InvalidateMeasure()); + + /// + /// Bindable property for CornerRadius. + /// + public static readonly BindableProperty CornerRadiusProperty = + BindableProperty.Create( + nameof(CornerRadius), + typeof(float), + typeof(SkiaEntry), + 4f, + propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); + + /// + /// Bindable property for BorderWidth. + /// + public static readonly BindableProperty BorderWidthProperty = + BindableProperty.Create( + nameof(BorderWidth), + typeof(float), + typeof(SkiaEntry), + 1f, + propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); + + /// + /// Bindable property for Padding. + /// + public static readonly BindableProperty PaddingProperty = + BindableProperty.Create( + nameof(Padding), + typeof(SKRect), + typeof(SkiaEntry), + new SKRect(12, 8, 12, 8), + propertyChanged: (b, o, n) => ((SkiaEntry)b).InvalidateMeasure()); + + /// + /// Bindable property for IsPassword. + /// + public static readonly BindableProperty IsPasswordProperty = + BindableProperty.Create( + nameof(IsPassword), + typeof(bool), + typeof(SkiaEntry), + false, + propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); + + /// + /// Bindable property for PasswordChar. + /// + public static readonly BindableProperty PasswordCharProperty = + BindableProperty.Create( + nameof(PasswordChar), + typeof(char), + typeof(SkiaEntry), + '*', // Use asterisk for universal font compatibility + propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); + + /// + /// Bindable property for MaxLength. + /// + public static readonly BindableProperty MaxLengthProperty = + BindableProperty.Create( + nameof(MaxLength), + typeof(int), + typeof(SkiaEntry), + 0); + + /// + /// Bindable property for IsReadOnly. + /// + public static readonly BindableProperty IsReadOnlyProperty = + BindableProperty.Create( + nameof(IsReadOnly), + typeof(bool), + typeof(SkiaEntry), + false, + propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); + + /// + /// Bindable property for HorizontalTextAlignment. + /// + public static readonly BindableProperty HorizontalTextAlignmentProperty = + BindableProperty.Create( + nameof(HorizontalTextAlignment), + typeof(TextAlignment), + typeof(SkiaEntry), + TextAlignment.Start, + propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); + + /// + /// Bindable property for VerticalTextAlignment. + /// + public static readonly BindableProperty VerticalTextAlignmentProperty = + BindableProperty.Create( + nameof(VerticalTextAlignment), + typeof(TextAlignment), + typeof(SkiaEntry), + TextAlignment.Center, + propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); + + /// + /// Bindable property for ShowClearButton. + /// + public static readonly BindableProperty ShowClearButtonProperty = + BindableProperty.Create( + nameof(ShowClearButton), + typeof(bool), + typeof(SkiaEntry), + false, + propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); + + /// + /// Bindable property for CharacterSpacing. + /// + public static readonly BindableProperty CharacterSpacingProperty = + BindableProperty.Create( + nameof(CharacterSpacing), + typeof(float), + typeof(SkiaEntry), + 0f, + propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); + + #endregion + + #region Properties + + /// + /// Gets or sets the text content. + /// public string Text { - get => _text; - set - { - if (_text != value) - { - var oldText = _text; - _text = value ?? ""; - _cursorPosition = Math.Min(_cursorPosition, _text.Length); - TextChanged?.Invoke(this, new TextChangedEventArgs(oldText, _text)); - Invalidate(); - } - } + get => (string)GetValue(TextProperty); + set => SetValue(TextProperty, value); } - public string Placeholder { get; set; } = ""; - public SKColor PlaceholderColor { get; set; } = new SKColor(0x9E, 0x9E, 0x9E); - public SKColor TextColor { get; set; } = SKColors.Black; - public new SKColor BackgroundColor { get; set; } = SKColors.White; - public SKColor BorderColor { get; set; } = new SKColor(0xBD, 0xBD, 0xBD); - public SKColor FocusedBorderColor { get; set; } = new SKColor(0x21, 0x96, 0xF3); - public SKColor SelectionColor { get; set; } = new SKColor(0x21, 0x96, 0xF3, 0x80); - public SKColor CursorColor { get; set; } = new SKColor(0x21, 0x96, 0xF3); - public string FontFamily { get; set; } = "Sans"; - public float FontSize { get; set; } = 14; - public bool IsBold { get; set; } - public bool IsItalic { get; set; } - public float CharacterSpacing { get; set; } - public float CornerRadius { get; set; } = 4; - public float BorderWidth { get; set; } = 1; - public SKRect Padding { get; set; } = new SKRect(12, 8, 12, 8); - public bool IsPassword { get; set; } - public char PasswordChar { get; set; } = '●'; - public int MaxLength { get; set; } = 0; // 0 = unlimited - public bool IsReadOnly { get; set; } - public TextAlignment HorizontalTextAlignment { get; set; } = TextAlignment.Start; - public TextAlignment VerticalTextAlignment { get; set; } = TextAlignment.Center; - public bool ShowClearButton { get; set; } + /// + /// Gets or sets the placeholder text. + /// + public string Placeholder + { + get => (string)GetValue(PlaceholderProperty); + set => SetValue(PlaceholderProperty, value); + } + /// + /// Gets or sets the placeholder color. + /// + public SKColor PlaceholderColor + { + get => (SKColor)GetValue(PlaceholderColorProperty); + set => SetValue(PlaceholderColorProperty, value); + } + + /// + /// Gets or sets the text color. + /// + public SKColor TextColor + { + get => (SKColor)GetValue(TextColorProperty); + set => SetValue(TextColorProperty, value); + } + + /// + /// Gets or sets the entry background color. + /// + public SKColor EntryBackgroundColor + { + get => (SKColor)GetValue(EntryBackgroundColorProperty); + set => SetValue(EntryBackgroundColorProperty, value); + } + + /// + /// Gets or sets the border color. + /// + public SKColor BorderColor + { + get => (SKColor)GetValue(BorderColorProperty); + set => SetValue(BorderColorProperty, value); + } + + /// + /// Gets or sets the focused border color. + /// + public SKColor FocusedBorderColor + { + get => (SKColor)GetValue(FocusedBorderColorProperty); + set => SetValue(FocusedBorderColorProperty, value); + } + + /// + /// Gets or sets the selection color. + /// + public SKColor SelectionColor + { + get => (SKColor)GetValue(SelectionColorProperty); + set => SetValue(SelectionColorProperty, value); + } + + /// + /// Gets or sets the cursor color. + /// + public SKColor CursorColor + { + get => (SKColor)GetValue(CursorColorProperty); + set => SetValue(CursorColorProperty, value); + } + + /// + /// Gets or sets the font family. + /// + public string FontFamily + { + get => (string)GetValue(FontFamilyProperty); + set => SetValue(FontFamilyProperty, value); + } + + /// + /// Gets or sets the font size. + /// + public float FontSize + { + get => (float)GetValue(FontSizeProperty); + set => SetValue(FontSizeProperty, value); + } + + /// + /// Gets or sets whether the text is bold. + /// + public bool IsBold + { + get => (bool)GetValue(IsBoldProperty); + set => SetValue(IsBoldProperty, value); + } + + /// + /// Gets or sets whether the text is italic. + /// + public bool IsItalic + { + get => (bool)GetValue(IsItalicProperty); + set => SetValue(IsItalicProperty, value); + } + + /// + /// Gets or sets the corner radius. + /// + public float CornerRadius + { + get => (float)GetValue(CornerRadiusProperty); + set => SetValue(CornerRadiusProperty, value); + } + + /// + /// Gets or sets the border width. + /// + public float BorderWidth + { + get => (float)GetValue(BorderWidthProperty); + set => SetValue(BorderWidthProperty, value); + } + + /// + /// Gets or sets the padding. + /// + public SKRect Padding + { + get => (SKRect)GetValue(PaddingProperty); + set => SetValue(PaddingProperty, value); + } + + /// + /// Gets or sets whether this is a password field. + /// + public bool IsPassword + { + get => (bool)GetValue(IsPasswordProperty); + set => SetValue(IsPasswordProperty, value); + } + + /// + /// Gets or sets the password masking character. + /// + public char PasswordChar + { + get => (char)GetValue(PasswordCharProperty); + set => SetValue(PasswordCharProperty, value); + } + + /// + /// Gets or sets the maximum text length. 0 = unlimited. + /// + public int MaxLength + { + get => (int)GetValue(MaxLengthProperty); + set => SetValue(MaxLengthProperty, value); + } + + /// + /// Gets or sets whether the entry is read-only. + /// + public bool IsReadOnly + { + get => (bool)GetValue(IsReadOnlyProperty); + set => SetValue(IsReadOnlyProperty, value); + } + + /// + /// Gets or sets the horizontal text alignment. + /// + public TextAlignment HorizontalTextAlignment + { + get => (TextAlignment)GetValue(HorizontalTextAlignmentProperty); + set => SetValue(HorizontalTextAlignmentProperty, value); + } + + /// + /// Gets or sets the vertical text alignment. + /// + public TextAlignment VerticalTextAlignment + { + get => (TextAlignment)GetValue(VerticalTextAlignmentProperty); + set => SetValue(VerticalTextAlignmentProperty, value); + } + + /// + /// Gets or sets whether to show the clear button. + /// + public bool ShowClearButton + { + get => (bool)GetValue(ShowClearButtonProperty); + set => SetValue(ShowClearButtonProperty, value); + } + + /// + /// Gets or sets the character spacing. + /// + public float CharacterSpacing + { + get => (float)GetValue(CharacterSpacingProperty); + set => SetValue(CharacterSpacingProperty, value); + } + + /// + /// Gets or sets the cursor position. + /// public int CursorPosition { get => _cursorPosition; set { - _cursorPosition = Math.Clamp(value, 0, _text.Length); + _cursorPosition = Math.Clamp(value, 0, Text.Length); ResetCursorBlink(); Invalidate(); } } + /// + /// Gets or sets the selection length. + /// public int SelectionLength { get => _selectionLength; @@ -80,7 +524,27 @@ public class SkiaEntry : SkiaView } } + #endregion + + private int _cursorPosition; + private int _selectionStart; + private int _selectionLength; + private float _scrollOffset; + private DateTime _cursorBlinkTime = DateTime.UtcNow; + private bool _cursorVisible = true; + private bool _isSelecting; // For mouse-based text selection + private DateTime _lastClickTime = DateTime.MinValue; + private float _lastClickX; + private const double DoubleClickThresholdMs = 400; + + /// + /// Event raised when text changes. + /// public event EventHandler? TextChanged; + + /// + /// Event raised when Enter is pressed. + /// public event EventHandler? Completed; public SkiaEntry() @@ -88,12 +552,21 @@ public class SkiaEntry : SkiaView IsFocusable = true; } + private void OnTextPropertyChanged(string oldText, string newText) + { + _cursorPosition = Math.Min(_cursorPosition, (newText ?? "").Length); + _scrollOffset = 0; // Reset scroll when text changes externally + _selectionLength = 0; + TextChanged?.Invoke(this, new TextChangedEventArgs(oldText, newText ?? "")); + Invalidate(); + } + protected override void OnDraw(SKCanvas canvas, SKRect bounds) { // Draw background using var bgPaint = new SKPaint { - Color = BackgroundColor, + Color = EntryBackgroundColor, IsAntialias = true, Style = SKPaintStyle.Fill }; @@ -124,7 +597,7 @@ public class SkiaEntry : SkiaView // Reserve space for clear button if shown var clearButtonSize = 20f; var clearButtonMargin = 8f; - if (ShowClearButton && !string.IsNullOrEmpty(_text) && IsFocused) + if (ShowClearButton && !string.IsNullOrEmpty(Text) && IsFocused) { contentBounds.Right -= clearButtonSize + clearButtonMargin; } @@ -139,12 +612,6 @@ public class SkiaEntry : SkiaView using var font = new SKFont(typeface, FontSize); using var paint = new SKPaint(font) { IsAntialias = true }; - - // Apply character spacing if set - if (CharacterSpacing > 0) - { - // Character spacing applied via SKPaint - } var displayText = GetDisplayText(); var hasText = !string.IsNullOrEmpty(displayText); @@ -167,8 +634,8 @@ public class SkiaEntry : SkiaView _scrollOffset = cursorX; } - // Draw selection - if (IsFocused && _selectionLength > 0) + // Draw selection (check != 0 to handle both forward and backward selection) + if (IsFocused && _selectionLength != 0) { DrawSelection(canvas, paint, displayText, contentBounds); } @@ -215,7 +682,7 @@ public class SkiaEntry : SkiaView canvas.Restore(); // Draw clear button if applicable - if (ShowClearButton && !string.IsNullOrEmpty(_text) && IsFocused) + if (ShowClearButton && !string.IsNullOrEmpty(Text) && IsFocused) { DrawClearButton(canvas, bounds, clearButtonSize, clearButtonMargin); } @@ -263,11 +730,11 @@ public class SkiaEntry : SkiaView private string GetDisplayText() { - if (IsPassword && !string.IsNullOrEmpty(_text)) + if (IsPassword && !string.IsNullOrEmpty(Text)) { - return new string(PasswordChar, _text.Length); + return new string(PasswordChar, Text.Length); } - return _text; + return Text; } private void DrawSelection(SKCanvas canvas, SKPaint paint, string displayText, SKRect bounds) @@ -311,6 +778,9 @@ public class SkiaEntry : SkiaView _cursorVisible = true; } + /// + /// Updates cursor blink animation. + /// public void UpdateCursorBlink() { if (!IsFocused) return; @@ -329,29 +799,33 @@ public class SkiaEntry : SkiaView { if (!IsEnabled || IsReadOnly) return; + // Ignore control characters (Ctrl+key combinations send ASCII control codes) + if (!string.IsNullOrEmpty(e.Text) && e.Text.Length == 1 && e.Text[0] < 32) + return; + // Delete selection if any - if (_selectionLength > 0) + if (_selectionLength != 0) { DeleteSelection(); } // Check max length - if (MaxLength > 0 && _text.Length >= MaxLength) + if (MaxLength > 0 && Text.Length >= MaxLength) return; // Insert text at cursor var insertText = e.Text; if (MaxLength > 0) { - var remaining = MaxLength - _text.Length; + var remaining = MaxLength - Text.Length; insertText = insertText.Substring(0, Math.Min(insertText.Length, remaining)); } - var oldText = _text; - _text = _text.Insert(_cursorPosition, insertText); - _cursorPosition += insertText.Length; + var newText = Text.Insert(_cursorPosition, insertText); + var oldPos = _cursorPosition; + Text = newText; + _cursorPosition = oldPos + insertText.Length; - TextChanged?.Invoke(this, new TextChangedEventArgs(oldText, _text)); ResetCursorBlink(); Invalidate(); } @@ -371,10 +845,10 @@ public class SkiaEntry : SkiaView } else if (_cursorPosition > 0) { - var oldText = _text; - _text = _text.Remove(_cursorPosition - 1, 1); - _cursorPosition--; - TextChanged?.Invoke(this, new TextChangedEventArgs(oldText, _text)); + var newText = Text.Remove(_cursorPosition - 1, 1); + var newPos = _cursorPosition - 1; + Text = newText; + _cursorPosition = newPos; } ResetCursorBlink(); Invalidate(); @@ -389,11 +863,9 @@ public class SkiaEntry : SkiaView { DeleteSelection(); } - else if (_cursorPosition < _text.Length) + else if (_cursorPosition < Text.Length) { - var oldText = _text; - _text = _text.Remove(_cursorPosition, 1); - TextChanged?.Invoke(this, new TextChangedEventArgs(oldText, _text)); + Text = Text.Remove(_cursorPosition, 1); } ResetCursorBlink(); Invalidate(); @@ -420,7 +892,7 @@ public class SkiaEntry : SkiaView break; case Key.Right: - if (_cursorPosition < _text.Length) + if (_cursorPosition < Text.Length) { if (e.Modifiers.HasFlag(KeyModifiers.Shift)) { @@ -455,12 +927,12 @@ public class SkiaEntry : SkiaView case Key.End: if (e.Modifiers.HasFlag(KeyModifiers.Shift)) { - ExtendSelectionTo(_text.Length); + ExtendSelectionTo(Text.Length); } else { ClearSelection(); - _cursorPosition = _text.Length; + _cursorPosition = Text.Length; } ResetCursorBlink(); Invalidate(); @@ -508,44 +980,115 @@ public class SkiaEntry : SkiaView public override void OnPointerPressed(PointerEventArgs e) { + Console.WriteLine($"[SkiaEntry] OnPointerPressed - Text='{Text}', Placeholder='{Placeholder}', IsEnabled={IsEnabled}, IsFocused={IsFocused}"); + Console.WriteLine($"[SkiaEntry] Bounds={Bounds}, ScreenBounds={ScreenBounds}, e.X={e.X}, e.Y={e.Y}"); + if (!IsEnabled) return; // Check if clicked on clear button - if (ShowClearButton && !string.IsNullOrEmpty(_text) && IsFocused) + if (ShowClearButton && !string.IsNullOrEmpty(Text) && IsFocused) { var clearButtonSize = 20f; var clearButtonMargin = 8f; var clearCenterX = Bounds.Right - clearButtonMargin - clearButtonSize / 2; var clearCenterY = Bounds.MidY; - + var dx = e.X - clearCenterX; var dy = e.Y - clearCenterY; if (dx * dx + dy * dy < (clearButtonSize / 2) * (clearButtonSize / 2)) { // Clear button clicked - var oldText = _text; - _text = ""; + Text = ""; _cursorPosition = 0; _selectionLength = 0; - TextChanged?.Invoke(this, new TextChangedEventArgs(oldText, _text)); Invalidate(); return; } } - // Calculate cursor position from click - var clickX = e.X - Bounds.Left - Padding.Left + _scrollOffset; + // Calculate cursor position from click using screen coordinates + var screenBounds = ScreenBounds; + var clickX = e.X - screenBounds.Left - Padding.Left + _scrollOffset; _cursorPosition = GetCharacterIndexAtX(clickX); - _selectionStart = _cursorPosition; - _selectionLength = 0; + + // Check for double-click (select word) + var now = DateTime.UtcNow; + var timeSinceLastClick = (now - _lastClickTime).TotalMilliseconds; + var distanceFromLastClick = Math.Abs(e.X - _lastClickX); + + if (timeSinceLastClick < DoubleClickThresholdMs && distanceFromLastClick < 10) + { + // Double-click: select the word at cursor + SelectWordAtCursor(); + _lastClickTime = DateTime.MinValue; // Reset to prevent triple-click issues + _isSelecting = false; + } + else + { + // Single click: start selection + _selectionStart = _cursorPosition; + _selectionLength = 0; + _isSelecting = true; + _lastClickTime = now; + _lastClickX = e.X; + } ResetCursorBlink(); Invalidate(); } + private void SelectWordAtCursor() + { + if (string.IsNullOrEmpty(Text)) return; + + // Find word boundaries + int start = _cursorPosition; + int end = _cursorPosition; + + // Move start backwards to beginning of word + while (start > 0 && IsWordChar(Text[start - 1])) + start--; + + // Move end forwards to end of word + while (end < Text.Length && IsWordChar(Text[end])) + end++; + + _selectionStart = start; + _cursorPosition = end; + _selectionLength = end - start; + } + + private static bool IsWordChar(char c) + { + return char.IsLetterOrDigit(c) || c == '_'; + } + + public override void OnPointerMoved(PointerEventArgs e) + { + if (!IsEnabled || !_isSelecting) return; + + // Extend selection to current mouse position + var screenBounds = ScreenBounds; + var clickX = e.X - screenBounds.Left - Padding.Left + _scrollOffset; + var newPosition = GetCharacterIndexAtX(clickX); + + if (newPosition != _cursorPosition) + { + _cursorPosition = newPosition; + _selectionLength = _cursorPosition - _selectionStart; + ResetCursorBlink(); + Invalidate(); + } + } + + public override void OnPointerReleased(PointerEventArgs e) + { + _isSelecting = false; + } + private int GetCharacterIndexAtX(float x) { - if (string.IsNullOrEmpty(_text)) return 0; + if (string.IsNullOrEmpty(Text)) return 0; var fontStyle = GetFontStyle(); var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(FontFamily, fontStyle) @@ -582,12 +1125,9 @@ public class SkiaEntry : SkiaView var start = Math.Min(_selectionStart, _selectionStart + _selectionLength); var length = Math.Abs(_selectionLength); - var oldText = _text; - _text = _text.Remove(start, length); + Text = Text.Remove(start, length); _cursorPosition = start; _selectionLength = 0; - - TextChanged?.Invoke(this, new TextChangedEventArgs(oldText, _text)); } private void ClearSelection() @@ -617,29 +1157,36 @@ public class SkiaEntry : SkiaView _selectionLength = _cursorPosition - _selectionStart; } + /// + /// Selects all text. + /// public void SelectAll() { _selectionStart = 0; - _cursorPosition = _text.Length; - _selectionLength = _text.Length; + _cursorPosition = Text.Length; + _selectionLength = Text.Length; Invalidate(); } private void CopyToClipboard() { + // Password fields should not allow copying + if (IsPassword) return; if (_selectionLength == 0) return; var start = Math.Min(_selectionStart, _selectionStart + _selectionLength); var length = Math.Abs(_selectionLength); - var selectedText = _text.Substring(start, length); + var selectedText = Text.Substring(start, length); - // TODO: Implement actual clipboard using X11 - // For now, store in a static field - ClipboardText = selectedText; + // Use system clipboard via xclip/xsel + SystemClipboard.SetText(selectedText); } private void CutToClipboard() { + // Password fields should not allow cutting + if (IsPassword) return; + CopyToClipboard(); DeleteSelection(); Invalidate(); @@ -647,11 +1194,11 @@ public class SkiaEntry : SkiaView private void PasteFromClipboard() { - // TODO: Get from actual X11 clipboard - var text = ClipboardText; + // Get from system clipboard + var text = SystemClipboard.GetText(); if (string.IsNullOrEmpty(text)) return; - if (_selectionLength > 0) + if (_selectionLength != 0) { DeleteSelection(); } @@ -659,20 +1206,28 @@ public class SkiaEntry : SkiaView // Check max length if (MaxLength > 0) { - var remaining = MaxLength - _text.Length; + var remaining = MaxLength - Text.Length; text = text.Substring(0, Math.Min(text.Length, remaining)); } - var oldText = _text; - _text = _text.Insert(_cursorPosition, text); - _cursorPosition += text.Length; - - TextChanged?.Invoke(this, new TextChangedEventArgs(oldText, _text)); + var newText = Text.Insert(_cursorPosition, text); + var newPos = _cursorPosition + text.Length; + Text = newText; + _cursorPosition = newPos; Invalidate(); } - // Temporary clipboard storage - will be replaced with X11 clipboard - private static string ClipboardText { get; set; } = ""; + public override void OnFocusGained() + { + base.OnFocusGained(); + SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.Focused); + } + + public override void OnFocusLost() + { + base.OnFocusLost(); + SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.Normal); + } protected override SKSize MeasureOverride(SKSize availableSize) { @@ -681,17 +1236,15 @@ public class SkiaEntry : SkiaView ?? SKTypeface.Default; using var font = new SKFont(typeface, FontSize); - using var paint = new SKPaint(font); - var textBounds = new SKRect(); - var measureText = !string.IsNullOrEmpty(_text) ? _text : Placeholder; - if (string.IsNullOrEmpty(measureText)) measureText = "Tg"; // Standard height measurement - - paint.MeasureText(measureText, ref textBounds); + // Use font metrics for consistent height regardless of text content + // This prevents size changes when placeholder disappears or text changes + var metrics = font.Metrics; + var textHeight = metrics.Descent - metrics.Ascent + metrics.Leading; return new SKSize( 200, // Default width, will be overridden by layout - textBounds.Height + Padding.Top + Padding.Bottom + BorderWidth * 2); + textHeight + Padding.Top + Padding.Bottom + BorderWidth * 2); } } diff --git a/Views/SkiaItemsView.cs b/Views/SkiaItemsView.cs index b0f13bb..f07667a 100644 --- a/Views/SkiaItemsView.cs +++ b/Views/SkiaItemsView.cs @@ -22,8 +22,13 @@ public class SkiaItemsView : SkiaView private int _firstVisibleIndex; private int _lastVisibleIndex; private bool _isDragging; + private bool _isDraggingScrollbar; private float _dragStartY; private float _dragStartOffset; + private float _scrollbarDragStartY; + private float _scrollbarDragStartScrollOffset; + private float _scrollbarDragAvailableTrack; + private float _scrollbarDragMaxScroll; private float _velocity; private DateTime _lastDragTime; @@ -81,9 +86,21 @@ public class SkiaItemsView : SkiaView public object? EmptyView { get; set; } public string? EmptyViewText { get; set; } = "No items"; - // Item rendering delegate + // Item rendering delegate (legacy) public Func? ItemRenderer { get; set; } + // Item view creator - creates SkiaView from data item using DataTemplate + public Func? ItemViewCreator { get; set; } + + // Cache of created item views for virtualization + protected readonly Dictionary _itemViewCache = new(); + + // Cache of individual item heights for variable height items + protected readonly Dictionary _itemHeights = new(); + + // Track last measured width to clear cache when width changes + private float _lastMeasuredWidth = 0; + // Selection support (overridden in SkiaCollectionView) public virtual int SelectedIndex { get; set; } = -1; @@ -95,9 +112,12 @@ public class SkiaItemsView : SkiaView IsFocusable = true; } - private void RefreshItems() + protected virtual void RefreshItems() { + Console.WriteLine($"[SkiaItemsView] RefreshItems called, clearing {_items.Count} items and {_itemViewCache.Count} cached views"); _items.Clear(); + _itemViewCache.Clear(); // Clear cached views when items change + _itemHeights.Clear(); // Clear cached heights if (_itemsSource != null) { foreach (var item in _itemsSource) @@ -105,6 +125,7 @@ public class SkiaItemsView : SkiaView _items.Add(item); } } + Console.WriteLine($"[SkiaItemsView] RefreshItems done, now have {_items.Count} items"); _scrollOffset = 0; } @@ -114,11 +135,53 @@ public class SkiaItemsView : SkiaView Invalidate(); } - protected float TotalContentHeight => _items.Count * (_itemHeight + _itemSpacing) - _itemSpacing; - protected float MaxScrollOffset => Math.Max(0, TotalContentHeight - Bounds.Height); + /// + /// Gets the height for a specific item, using cached height or default. + /// + protected float GetItemHeight(int index) + { + return _itemHeights.TryGetValue(index, out var height) ? height : _itemHeight; + } + + /// + /// Gets the Y offset for a specific item (cumulative height of all previous items). + /// + protected float GetItemOffset(int index) + { + float offset = 0; + for (int i = 0; i < index && i < _items.Count; i++) + { + offset += GetItemHeight(i) + _itemSpacing; + } + return offset; + } + + /// + /// Calculates total content height based on individual item heights. + /// + protected float TotalContentHeight + { + get + { + if (_items.Count == 0) return 0; + + float total = 0; + for (int i = 0; i < _items.Count; i++) + { + total += GetItemHeight(i); + if (i < _items.Count - 1) total += _itemSpacing; + } + return total; + } + } + + // Use ScreenBounds.Height for visible viewport + protected float MaxScrollOffset => Math.Max(0, TotalContentHeight - ScreenBounds.Height); protected override void OnDraw(SKCanvas canvas, SKRect bounds) { + Console.WriteLine($"[SkiaItemsView] OnDraw - bounds={bounds}, items={_items.Count}, ItemViewCreator={(ItemViewCreator != null ? "set" : "null")}"); + // Draw background if (BackgroundColor != SKColors.Transparent) { @@ -137,30 +200,51 @@ public class SkiaItemsView : SkiaView return; } - // Calculate visible range - _firstVisibleIndex = Math.Max(0, (int)(_scrollOffset / (_itemHeight + _itemSpacing))); - _lastVisibleIndex = Math.Min(_items.Count - 1, - (int)((_scrollOffset + bounds.Height) / (_itemHeight + _itemSpacing)) + 1); + // Find first visible index by walking through items + _firstVisibleIndex = 0; + float cumulativeOffset = 0; + for (int i = 0; i < _items.Count; i++) + { + var itemH = GetItemHeight(i); + if (cumulativeOffset + itemH > _scrollOffset) + { + _firstVisibleIndex = i; + break; + } + cumulativeOffset += itemH + _itemSpacing; + } // Clip to bounds canvas.Save(); canvas.ClipRect(bounds); - // Draw visible items + // Draw visible items using variable heights using var paint = new SKPaint { IsAntialias = true }; - for (int i = _firstVisibleIndex; i <= _lastVisibleIndex; i++) + float currentY = bounds.Top + GetItemOffset(_firstVisibleIndex) - _scrollOffset; + for (int i = _firstVisibleIndex; i < _items.Count; i++) { - var itemY = bounds.Top + (i * (_itemHeight + _itemSpacing)) - _scrollOffset; - var itemRect = new SKRect(bounds.Left, itemY, bounds.Right - (_showVerticalScrollBar ? _scrollBarWidth : 0), itemY + _itemHeight); + var itemH = GetItemHeight(i); + var itemRect = new SKRect(bounds.Left, currentY, bounds.Right - (_showVerticalScrollBar ? _scrollBarWidth : 0), currentY + itemH); - if (itemRect.Bottom < bounds.Top || itemRect.Top > bounds.Bottom) - continue; + // Stop if we've passed the visible area + if (itemRect.Top > bounds.Bottom) + { + _lastVisibleIndex = i - 1; + break; + } - DrawItem(canvas, _items[i], i, itemRect, paint); + _lastVisibleIndex = i; + + if (itemRect.Bottom >= bounds.Top) + { + DrawItem(canvas, _items[i], i, itemRect, paint); + } + + currentY += itemH + _itemSpacing; } canvas.Restore(); @@ -177,11 +261,56 @@ public class SkiaItemsView : SkiaView // Draw selection highlight if (index == SelectedIndex) { - paint.Color = new SKColor(0x21, 0x96, 0xF3, 0x40); // Light blue + paint.Color = new SKColor(0x21, 0x96, 0xF3, 0x59); // Light blue with 35% opacity paint.Style = SKPaintStyle.Fill; canvas.DrawRect(bounds, paint); } + // Try to use ItemViewCreator for templated rendering + if (ItemViewCreator != null) + { + Console.WriteLine($"[SkiaItemsView] DrawItem {index} - ItemViewCreator exists, item: {item}"); + // Get or create cached view for this index + if (!_itemViewCache.TryGetValue(index, out var itemView) || itemView == null) + { + itemView = ItemViewCreator(item); + if (itemView != null) + { + itemView.Parent = this; + _itemViewCache[index] = itemView; + } + } + + if (itemView != null) + { + // Measure with large height to get natural size + var availableSize = new SKSize(bounds.Width, float.MaxValue); + var measuredSize = itemView.Measure(availableSize); + + // Store individual item height (with minimum of default height) + var measuredHeight = Math.Max(measuredSize.Height, _itemHeight); + if (!_itemHeights.TryGetValue(index, out var cachedHeight) || Math.Abs(cachedHeight - measuredHeight) > 1) + { + _itemHeights[index] = measuredHeight; + // Request redraw if height changed significantly + if (Math.Abs(cachedHeight - measuredHeight) > 5) + { + Invalidate(); + } + } + + // Arrange with the actual measured height + var actualBounds = new SKRect(bounds.Left, bounds.Top, bounds.Right, bounds.Top + measuredHeight); + itemView.Arrange(actualBounds); + itemView.Draw(canvas); + return; + } + } + else + { + Console.WriteLine($"[SkiaItemsView] DrawItem {index} - ItemViewCreator is NULL, falling back to ToString"); + } + // Draw separator paint.Color = new SKColor(0xE0, 0xE0, 0xE0); paint.Style = SKPaintStyle.Stroke; @@ -281,8 +410,27 @@ public class SkiaItemsView : SkiaView public override void OnPointerPressed(PointerEventArgs e) { + Console.WriteLine($"[SkiaItemsView] OnPointerPressed - x={e.X}, y={e.Y}, Bounds={Bounds}, ScreenBounds={ScreenBounds}, ItemCount={_items.Count}"); if (!IsEnabled) return; + // Check if clicking on scrollbar thumb + if (_showVerticalScrollBar && TotalContentHeight > Bounds.Height) + { + var thumbBounds = GetScrollbarThumbBounds(); + if (thumbBounds.Contains(e.X, e.Y)) + { + _isDraggingScrollbar = true; + _scrollbarDragStartY = e.Y; + _scrollbarDragStartScrollOffset = _scrollOffset; + // Cache values to prevent stutter + var thumbHeight = Math.Max(20, Bounds.Height * (Bounds.Height / TotalContentHeight)); + _scrollbarDragAvailableTrack = Bounds.Height - thumbHeight; + _scrollbarDragMaxScroll = MaxScrollOffset; + return; + } + } + + // Regular content drag _isDragging = true; _dragStartY = e.Y; _dragStartOffset = _scrollOffset; @@ -290,8 +438,39 @@ public class SkiaItemsView : SkiaView _velocity = 0; } + /// + /// Gets the bounds of the scrollbar thumb in screen coordinates. + /// + private SKRect GetScrollbarThumbBounds() + { + // Use ScreenBounds for hit testing (input events use screen coordinates) + var screenBounds = ScreenBounds; + var viewportRatio = screenBounds.Height / TotalContentHeight; + var thumbHeight = Math.Max(20, screenBounds.Height * viewportRatio); + var scrollRatio = MaxScrollOffset > 0 ? _scrollOffset / MaxScrollOffset : 0; + var thumbY = screenBounds.Top + (screenBounds.Height - thumbHeight) * scrollRatio; + + return new SKRect( + screenBounds.Right - _scrollBarWidth, + thumbY, + screenBounds.Right, + thumbY + thumbHeight); + } + public override void OnPointerMoved(PointerEventArgs e) { + // Handle scrollbar dragging - use cached values to prevent stutter + if (_isDraggingScrollbar) + { + if (_scrollbarDragAvailableTrack > 0) + { + var deltaY = e.Y - _scrollbarDragStartY; + var scrollDelta = (deltaY / _scrollbarDragAvailableTrack) * _scrollbarDragMaxScroll; + SetScrollOffset(_scrollbarDragStartScrollOffset + scrollDelta); + } + return; + } + if (!_isDragging) return; var delta = _dragStartY - e.Y; @@ -311,6 +490,13 @@ public class SkiaItemsView : SkiaView public override void OnPointerReleased(PointerEventArgs e) { + // Handle scrollbar drag release + if (_isDraggingScrollbar) + { + _isDraggingScrollbar = false; + return; + } + if (_isDragging) { _isDragging = false; @@ -319,9 +505,25 @@ public class SkiaItemsView : SkiaView var totalDrag = Math.Abs(e.Y - _dragStartY); if (totalDrag < 5) { - // This was a tap - find which item was tapped - var tapY = e.Y + _scrollOffset - Bounds.Top; - var tappedIndex = (int)(tapY / (_itemHeight + _itemSpacing)); + // This was a tap - find which item was tapped using variable heights + var screenBounds = ScreenBounds; + var localY = e.Y - screenBounds.Top + _scrollOffset; + + // Find tapped index by walking through item heights + int tappedIndex = -1; + float cumulativeY = 0; + for (int i = 0; i < _items.Count; i++) + { + var itemH = GetItemHeight(i); + if (localY >= cumulativeY && localY < cumulativeY + itemH) + { + tappedIndex = i; + break; + } + cumulativeY += itemH + _itemSpacing; + } + + Console.WriteLine($"[SkiaItemsView] Tap at Y={e.Y}, screenBounds.Top={screenBounds.Top}, scrollOffset={_scrollOffset}, localY={localY}, index={tappedIndex}"); if (tappedIndex >= 0 && tappedIndex < _items.Count) { @@ -331,6 +533,24 @@ public class SkiaItemsView : SkiaView } } + /// + /// Gets the total Y scroll offset from all parent ScrollViews. + /// + private float GetTotalParentScrollY() + { + float total = 0; + var parent = Parent; + while (parent != null) + { + if (parent is SkiaScrollView scrollView) + { + total += scrollView.ScrollY; + } + parent = parent.Parent; + } + return total; + } + protected virtual void OnItemTapped(int index, object item) { SelectedIndex = index; @@ -361,7 +581,7 @@ public class SkiaItemsView : SkiaView { if (index < 0 || index >= _items.Count) return; - var targetOffset = index * (_itemHeight + _itemSpacing); + var targetOffset = GetItemOffset(index); SetScrollOffset(targetOffset); } @@ -436,8 +656,8 @@ public class SkiaItemsView : SkiaView private void EnsureIndexVisible(int index) { - var itemTop = index * (_itemHeight + _itemSpacing); - var itemBottom = itemTop + _itemHeight; + var itemTop = GetItemOffset(index); + var itemBottom = itemTop + GetItemHeight(index); if (itemTop < _scrollOffset) { @@ -452,12 +672,43 @@ public class SkiaItemsView : SkiaView protected int ItemCount => _items.Count; protected object? GetItemAt(int index) => index >= 0 && index < _items.Count ? _items[index] : null; + /// + /// Override HitTest to handle scrollbar clicks properly. + /// HitTest receives content-space coordinates (already transformed by parent ScrollView). + /// + public override SkiaView? HitTest(float x, float y) + { + // HitTest uses Bounds (content space) - coordinates are transformed by parent + if (!IsVisible || !Bounds.Contains(new SKPoint(x, y))) + return null; + + // Check scrollbar area FIRST before content + // This ensures scrollbar clicks are handled by this view + if (_showVerticalScrollBar && TotalContentHeight > Bounds.Height) + { + var trackArea = new SKRect(Bounds.Right - _scrollBarWidth, Bounds.Top, Bounds.Right, Bounds.Bottom); + if (trackArea.Contains(x, y)) + return this; + } + + return this; + } + protected override SKSize MeasureOverride(SKSize availableSize) { + var width = availableSize.Width < float.MaxValue ? availableSize.Width : 200; + var height = availableSize.Height < float.MaxValue ? availableSize.Height : 300; + + // Clear item caches when width changes significantly (items need re-measurement for text wrapping) + if (Math.Abs(width - _lastMeasuredWidth) > 5) + { + _itemHeights.Clear(); + _itemViewCache.Clear(); + _lastMeasuredWidth = width; + } + // Items view takes all available space - return new SKSize( - availableSize.Width < float.MaxValue ? availableSize.Width : 200, - availableSize.Height < float.MaxValue ? availableSize.Height : 300); + return new SKSize(width, height); } protected override void Dispose(bool disposing) diff --git a/Views/SkiaLabel.cs b/Views/SkiaLabel.cs index 965c90f..9945af2 100644 --- a/Views/SkiaLabel.cs +++ b/Views/SkiaLabel.cs @@ -7,24 +7,319 @@ using Microsoft.Maui.Platform.Linux.Rendering; namespace Microsoft.Maui.Platform; /// -/// Skia-rendered label control for displaying text. +/// Skia-rendered label control for displaying text with full XAML styling support. /// public class SkiaLabel : SkiaView { - public string Text { get; set; } = ""; - public SKColor TextColor { get; set; } = SKColors.Black; - public string FontFamily { get; set; } = "Sans"; - public float FontSize { get; set; } = 14; - public bool IsBold { get; set; } - public bool IsItalic { get; set; } - public bool IsUnderline { get; set; } - public bool IsStrikethrough { get; set; } - public TextAlignment HorizontalTextAlignment { get; set; } = TextAlignment.Start; - public TextAlignment VerticalTextAlignment { get; set; } = TextAlignment.Center; - public LineBreakMode LineBreakMode { get; set; } = LineBreakMode.TailTruncation; - public int MaxLines { get; set; } = 0; // 0 = unlimited - public float LineHeight { get; set; } = 1.2f; - public float CharacterSpacing { get; set; } + #region BindableProperties + + /// + /// Bindable property for Text. + /// + public static readonly BindableProperty TextProperty = + BindableProperty.Create( + nameof(Text), + typeof(string), + typeof(SkiaLabel), + "", + propertyChanged: (b, o, n) => ((SkiaLabel)b).OnTextChanged()); + + /// + /// Bindable property for TextColor. + /// + public static readonly BindableProperty TextColorProperty = + BindableProperty.Create( + nameof(TextColor), + typeof(SKColor), + typeof(SkiaLabel), + SKColors.Black, + propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate()); + + /// + /// Bindable property for FontFamily. + /// + public static readonly BindableProperty FontFamilyProperty = + BindableProperty.Create( + nameof(FontFamily), + typeof(string), + typeof(SkiaLabel), + "Sans", + propertyChanged: (b, o, n) => ((SkiaLabel)b).OnFontChanged()); + + /// + /// Bindable property for FontSize. + /// + public static readonly BindableProperty FontSizeProperty = + BindableProperty.Create( + nameof(FontSize), + typeof(float), + typeof(SkiaLabel), + 14f, + propertyChanged: (b, o, n) => ((SkiaLabel)b).OnFontChanged()); + + /// + /// Bindable property for IsBold. + /// + public static readonly BindableProperty IsBoldProperty = + BindableProperty.Create( + nameof(IsBold), + typeof(bool), + typeof(SkiaLabel), + false, + propertyChanged: (b, o, n) => ((SkiaLabel)b).OnFontChanged()); + + /// + /// Bindable property for IsItalic. + /// + public static readonly BindableProperty IsItalicProperty = + BindableProperty.Create( + nameof(IsItalic), + typeof(bool), + typeof(SkiaLabel), + false, + propertyChanged: (b, o, n) => ((SkiaLabel)b).OnFontChanged()); + + /// + /// Bindable property for IsUnderline. + /// + public static readonly BindableProperty IsUnderlineProperty = + BindableProperty.Create( + nameof(IsUnderline), + typeof(bool), + typeof(SkiaLabel), + false, + propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate()); + + /// + /// Bindable property for IsStrikethrough. + /// + public static readonly BindableProperty IsStrikethroughProperty = + BindableProperty.Create( + nameof(IsStrikethrough), + typeof(bool), + typeof(SkiaLabel), + false, + propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate()); + + /// + /// Bindable property for HorizontalTextAlignment. + /// + public static readonly BindableProperty HorizontalTextAlignmentProperty = + BindableProperty.Create( + nameof(HorizontalTextAlignment), + typeof(TextAlignment), + typeof(SkiaLabel), + TextAlignment.Start, + propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate()); + + /// + /// Bindable property for VerticalTextAlignment. + /// + public static readonly BindableProperty VerticalTextAlignmentProperty = + BindableProperty.Create( + nameof(VerticalTextAlignment), + typeof(TextAlignment), + typeof(SkiaLabel), + TextAlignment.Center, + propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate()); + + /// + /// Bindable property for LineBreakMode. + /// + public static readonly BindableProperty LineBreakModeProperty = + BindableProperty.Create( + nameof(LineBreakMode), + typeof(LineBreakMode), + typeof(SkiaLabel), + LineBreakMode.TailTruncation, + propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate()); + + /// + /// Bindable property for MaxLines. + /// + public static readonly BindableProperty MaxLinesProperty = + BindableProperty.Create( + nameof(MaxLines), + typeof(int), + typeof(SkiaLabel), + 0, + propertyChanged: (b, o, n) => ((SkiaLabel)b).OnTextChanged()); + + /// + /// Bindable property for LineHeight. + /// + public static readonly BindableProperty LineHeightProperty = + BindableProperty.Create( + nameof(LineHeight), + typeof(float), + typeof(SkiaLabel), + 1.2f, + propertyChanged: (b, o, n) => ((SkiaLabel)b).OnTextChanged()); + + /// + /// Bindable property for CharacterSpacing. + /// + public static readonly BindableProperty CharacterSpacingProperty = + BindableProperty.Create( + nameof(CharacterSpacing), + typeof(float), + typeof(SkiaLabel), + 0f, + propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate()); + + /// + /// Bindable property for Padding. + /// + public static readonly BindableProperty PaddingProperty = + BindableProperty.Create( + nameof(Padding), + typeof(SKRect), + typeof(SkiaLabel), + SKRect.Empty, + propertyChanged: (b, o, n) => ((SkiaLabel)b).OnTextChanged()); + + #endregion + + #region Properties + + /// + /// Gets or sets the text content. + /// + public string Text + { + get => (string)GetValue(TextProperty); + set => SetValue(TextProperty, value); + } + + /// + /// Gets or sets the text color. + /// + public SKColor TextColor + { + get => (SKColor)GetValue(TextColorProperty); + set => SetValue(TextColorProperty, value); + } + + /// + /// Gets or sets the font family. + /// + public string FontFamily + { + get => (string)GetValue(FontFamilyProperty); + set => SetValue(FontFamilyProperty, value); + } + + /// + /// Gets or sets the font size. + /// + public float FontSize + { + get => (float)GetValue(FontSizeProperty); + set => SetValue(FontSizeProperty, value); + } + + /// + /// Gets or sets whether the text is bold. + /// + public bool IsBold + { + get => (bool)GetValue(IsBoldProperty); + set => SetValue(IsBoldProperty, value); + } + + /// + /// Gets or sets whether the text is italic. + /// + public bool IsItalic + { + get => (bool)GetValue(IsItalicProperty); + set => SetValue(IsItalicProperty, value); + } + + /// + /// Gets or sets whether the text has underline. + /// + public bool IsUnderline + { + get => (bool)GetValue(IsUnderlineProperty); + set => SetValue(IsUnderlineProperty, value); + } + + /// + /// Gets or sets whether the text has strikethrough. + /// + public bool IsStrikethrough + { + get => (bool)GetValue(IsStrikethroughProperty); + set => SetValue(IsStrikethroughProperty, value); + } + + /// + /// Gets or sets the horizontal text alignment. + /// + public TextAlignment HorizontalTextAlignment + { + get => (TextAlignment)GetValue(HorizontalTextAlignmentProperty); + set => SetValue(HorizontalTextAlignmentProperty, value); + } + + /// + /// Gets or sets the vertical text alignment. + /// + public TextAlignment VerticalTextAlignment + { + get => (TextAlignment)GetValue(VerticalTextAlignmentProperty); + set => SetValue(VerticalTextAlignmentProperty, value); + } + + /// + /// Gets or sets the line break mode. + /// + public LineBreakMode LineBreakMode + { + get => (LineBreakMode)GetValue(LineBreakModeProperty); + set => SetValue(LineBreakModeProperty, value); + } + + /// + /// Gets or sets the maximum number of lines. 0 = unlimited. + /// + public int MaxLines + { + get => (int)GetValue(MaxLinesProperty); + set => SetValue(MaxLinesProperty, value); + } + + /// + /// Gets or sets the line height multiplier. + /// + public float LineHeight + { + get => (float)GetValue(LineHeightProperty); + set => SetValue(LineHeightProperty, value); + } + + /// + /// Gets or sets the character spacing. + /// + public float CharacterSpacing + { + get => (float)GetValue(CharacterSpacingProperty); + set => SetValue(CharacterSpacingProperty, value); + } + + /// + /// Gets or sets the padding. + /// + public SKRect Padding + { + get => (SKRect)GetValue(PaddingProperty); + set => SetValue(PaddingProperty, value); + } + + /// + /// Gets or sets the horizontal alignment (compatibility property). + /// public SkiaTextAlignment HorizontalAlignment { get => HorizontalTextAlignment switch @@ -42,6 +337,10 @@ public class SkiaLabel : SkiaView _ => TextAlignment.Start }; } + + /// + /// Gets or sets the vertical alignment (compatibility property). + /// public SkiaVerticalAlignment VerticalAlignment { get => VerticalTextAlignment switch @@ -59,7 +358,45 @@ public class SkiaLabel : SkiaView _ => TextAlignment.Start }; } - public SKRect Padding { get; set; } = new SKRect(0, 0, 0, 0); + + #endregion + + private static SKTypeface? _cachedTypeface; + + private void OnTextChanged() + { + InvalidateMeasure(); + Invalidate(); + } + + private void OnFontChanged() + { + InvalidateMeasure(); + Invalidate(); + } + + private static SKTypeface GetLinuxTypeface() + { + if (_cachedTypeface != null) return _cachedTypeface; + + // Try common Linux font paths + string[] fontPaths = { + "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", + "/usr/share/fonts/truetype/ubuntu/Ubuntu-R.ttf", + "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", + "/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf" + }; + + foreach (var path in fontPaths) + { + if (System.IO.File.Exists(path)) + { + _cachedTypeface = SKTypeface.FromFile(path); + if (_cachedTypeface != null) return _cachedTypeface; + } + } + return SKTypeface.Default; + } protected override void OnDraw(SKCanvas canvas, SKRect bounds) { @@ -71,8 +408,11 @@ public class SkiaLabel : SkiaView SKFontStyleWidth.Normal, IsItalic ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright); - var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(FontFamily, fontStyle) - ?? SKTypeface.Default; + var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(FontFamily, fontStyle); + if (typeface == null || typeface == SKTypeface.Default) + { + typeface = GetLinuxTypeface(); + } using var font = new SKFont(typeface, FontSize); using var paint = new SKPaint(font) @@ -89,13 +429,16 @@ public class SkiaLabel : SkiaView bounds.Bottom - Padding.Bottom); // Handle single line vs multiline - if (MaxLines == 1 || !Text.Contains('\n')) + // Use DrawSingleLine for normal labels (MaxLines <= 1 or unlimited) without newlines + // Use DrawMultiLineWithWrapping only when MaxLines > 1 (word wrap needed) or text has newlines + bool needsMultiLine = MaxLines > 1 || Text.Contains('\n'); + if (needsMultiLine) { - DrawSingleLine(canvas, paint, font, contentBounds); + DrawMultiLineWithWrapping(canvas, paint, font, contentBounds); } else { - DrawMultiLine(canvas, paint, font, contentBounds); + DrawSingleLine(canvas, paint, font, contentBounds); } } @@ -160,10 +503,140 @@ public class SkiaLabel : SkiaView } } + private void DrawMultiLineWithWrapping(SKCanvas canvas, SKPaint paint, SKFont font, SKRect bounds) + { + // Handle inverted or zero-height/width bounds + var effectiveBounds = bounds; + + // Fix invalid height + if (bounds.Height <= 0) + { + var effectiveLH = LineHeight <= 0 ? 1.2f : LineHeight; + var estimatedHeight = MaxLines > 0 ? MaxLines * FontSize * effectiveLH : FontSize * effectiveLH * 10; + effectiveBounds = new SKRect(bounds.Left, bounds.Top, bounds.Right, bounds.Top + estimatedHeight); + } + + // Fix invalid width - use a reasonable default if width is invalid or extremely large + float effectiveWidth = effectiveBounds.Width; + if (effectiveWidth <= 0) + { + // Use a default width based on canvas + effectiveWidth = 400; // Reasonable default + } + + // Note: Previously had width capping logic here that reduced effective width + // to 60% for multiline labels. Removed - the layout system should now provide + // correct widths, and artificially capping causes text to wrap too early. + + // First, word-wrap the text to fit within bounds + var wrappedLines = WrapText(paint, Text, effectiveWidth); + + // LineHeight of -1 or <= 0 means "use default" - use 1.2 as default multiplier + var effectiveLineHeight = LineHeight <= 0 ? 1.2f : LineHeight; + var lineSpacing = FontSize * effectiveLineHeight; + var maxLinesToDraw = MaxLines > 0 ? Math.Min(MaxLines, wrappedLines.Count) : wrappedLines.Count; + + // Calculate total height + var totalHeight = maxLinesToDraw * lineSpacing; + + // Calculate starting Y based on vertical alignment + float startY = VerticalTextAlignment switch + { + TextAlignment.Start => effectiveBounds.Top + FontSize, + TextAlignment.Center => effectiveBounds.MidY - totalHeight / 2 + FontSize, + TextAlignment.End => effectiveBounds.Bottom - totalHeight + FontSize, + _ => effectiveBounds.Top + FontSize + }; + + for (int i = 0; i < maxLinesToDraw; i++) + { + var line = wrappedLines[i]; + + // Add ellipsis if this is the last line and there are more lines + bool isLastLine = i == maxLinesToDraw - 1; + bool hasMoreContent = maxLinesToDraw < wrappedLines.Count; + if (isLastLine && hasMoreContent && LineBreakMode == LineBreakMode.TailTruncation) + { + line = TruncateTextWithEllipsis(paint, line, effectiveWidth); + } + + var textBounds = new SKRect(); + paint.MeasureText(line, ref textBounds); + + float x = HorizontalTextAlignment switch + { + TextAlignment.Start => effectiveBounds.Left, + TextAlignment.Center => effectiveBounds.MidX - textBounds.Width / 2, + TextAlignment.End => effectiveBounds.Right - textBounds.Width, + _ => effectiveBounds.Left + }; + + float y = startY + i * lineSpacing; + + // Don't break early for inverted bounds - just draw + if (effectiveBounds.Height > 0 && y > effectiveBounds.Bottom) + break; + + canvas.DrawText(line, x, y, paint); + } + } + + private List WrapText(SKPaint paint, string text, float maxWidth) + { + var result = new List(); + + // Split by newlines first + var paragraphs = text.Split('\n'); + + foreach (var paragraph in paragraphs) + { + if (string.IsNullOrEmpty(paragraph)) + { + result.Add(""); + continue; + } + + // Check if paragraph fits in one line + if (paint.MeasureText(paragraph) <= maxWidth) + { + result.Add(paragraph); + continue; + } + + // Word wrap this paragraph + var words = paragraph.Split(' '); + var currentLine = ""; + + foreach (var word in words) + { + var testLine = string.IsNullOrEmpty(currentLine) ? word : currentLine + " " + word; + var lineWidth = paint.MeasureText(testLine); + + if (lineWidth > maxWidth && !string.IsNullOrEmpty(currentLine)) + { + result.Add(currentLine); + currentLine = word; + } + else + { + currentLine = testLine; + } + } + + if (!string.IsNullOrEmpty(currentLine)) + { + result.Add(currentLine); + } + } + + return result; + } + private void DrawMultiLine(SKCanvas canvas, SKPaint paint, SKFont font, SKRect bounds) { var lines = Text.Split('\n'); - var lineSpacing = FontSize * LineHeight; + var effectiveLineHeight = LineHeight <= 0 ? 1.2f : LineHeight; + var lineSpacing = FontSize * effectiveLineHeight; var maxLinesToDraw = MaxLines > 0 ? Math.Min(MaxLines, lines.Length) : lines.Length; // Calculate total height @@ -208,6 +681,42 @@ public class SkiaLabel : SkiaView } } + /// + /// Truncates text and ALWAYS adds ellipsis (used when there's more content to indicate). + /// + private string TruncateTextWithEllipsis(SKPaint paint, string text, float maxWidth) + { + const string ellipsis = "..."; + var ellipsisWidth = paint.MeasureText(ellipsis); + var textWidth = paint.MeasureText(text); + + // If text + ellipsis fits, just append ellipsis + if (textWidth + ellipsisWidth <= maxWidth) + return text + ellipsis; + + // Otherwise, truncate to make room for ellipsis + var availableWidth = maxWidth - ellipsisWidth; + if (availableWidth <= 0) + return ellipsis; + + // Binary search for the right length + int low = 0; + int high = text.Length; + + while (low < high) + { + int mid = (low + high + 1) / 2; + var substring = text.Substring(0, mid); + + if (paint.MeasureText(substring) <= availableWidth) + low = mid; + else + high = mid - 1; + } + + return text.Substring(0, low) + ellipsis; + } + private string TruncateText(SKPaint paint, string text, float maxWidth) { const string ellipsis = "..."; @@ -252,33 +761,51 @@ public class SkiaLabel : SkiaView SKFontStyleWidth.Normal, IsItalic ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright); - var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(FontFamily, fontStyle) - ?? SKTypeface.Default; + // Use same typeface logic as OnDraw to ensure consistent measurement + var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(FontFamily, fontStyle); + if (typeface == null || typeface == SKTypeface.Default) + { + typeface = GetLinuxTypeface(); + } using var font = new SKFont(typeface, FontSize); using var paint = new SKPaint(font); - if (MaxLines == 1 || !Text.Contains('\n')) + // Use same logic as OnDraw: multiline only when MaxLines > 1 or text has newlines + bool needsMultiLine = MaxLines > 1 || Text.Contains('\n'); + if (!needsMultiLine) { var textBounds = new SKRect(); paint.MeasureText(Text, ref textBounds); + // Add small buffer for font rendering tolerance + const float widthBuffer = 4f; + return new SKSize( - textBounds.Width + Padding.Left + Padding.Right, + textBounds.Width + Padding.Left + Padding.Right + widthBuffer, textBounds.Height + Padding.Top + Padding.Bottom); } else { - var lines = Text.Split('\n'); - var maxLinesToMeasure = MaxLines > 0 ? Math.Min(MaxLines, lines.Length) : lines.Length; + // Use available width for word wrapping measurement + var wrapWidth = availableSize.Width - Padding.Left - Padding.Right; + if (wrapWidth <= 0) + { + wrapWidth = float.MaxValue; // No wrapping if no width constraint + } + + // Wrap text to get actual line count + var wrappedLines = WrapText(paint, Text, wrapWidth); + var maxLinesToMeasure = MaxLines > 0 ? Math.Min(MaxLines, wrappedLines.Count) : wrappedLines.Count; float maxWidth = 0; - foreach (var line in lines.Take(maxLinesToMeasure)) + foreach (var line in wrappedLines.Take(maxLinesToMeasure)) { maxWidth = Math.Max(maxWidth, paint.MeasureText(line)); } - var totalHeight = maxLinesToMeasure * FontSize * LineHeight; + var effectiveLineHeight = LineHeight <= 0 ? 1.2f : LineHeight; + var totalHeight = maxLinesToMeasure * FontSize * effectiveLineHeight; return new SKSize( maxWidth + Padding.Left + Padding.Right, diff --git a/Views/SkiaLayoutView.cs b/Views/SkiaLayoutView.cs index 5800228..f4c17e8 100644 --- a/Views/SkiaLayoutView.cs +++ b/Views/SkiaLayoutView.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using SkiaSharp; +using Microsoft.Maui; namespace Microsoft.Maui.Platform; @@ -32,6 +33,20 @@ public abstract class SkiaLayoutView : SkiaView /// public bool ClipToBounds { get; set; } = false; + /// + /// Called when binding context changes. Propagates to layout children. + /// + protected override void OnBindingContextChanged() + { + base.OnBindingContextChanged(); + + // Propagate binding context to layout children + foreach (var child in _children) + { + SetInheritedBindingContext(child, BindingContext); + } + } + /// /// Adds a child view. /// @@ -44,6 +59,13 @@ public abstract class SkiaLayoutView : SkiaView _children.Add(child); child.Parent = this; + + // Propagate binding context to new child + if (BindingContext != null) + { + SetInheritedBindingContext(child, BindingContext); + } + InvalidateMeasure(); Invalidate(); } @@ -88,6 +110,13 @@ public abstract class SkiaLayoutView : SkiaView _children.Insert(index, child); child.Parent = this; + + // Propagate binding context to new child + if (BindingContext != null) + { + SetInheritedBindingContext(child, BindingContext); + } + InvalidateMeasure(); Invalidate(); } @@ -128,6 +157,31 @@ public abstract class SkiaLayoutView : SkiaView protected override void OnDraw(SKCanvas canvas, SKRect bounds) { + // Draw background if set (for layouts inside CollectionView items) + if (BackgroundColor != SKColors.Transparent) + { + using var bgPaint = new SKPaint { Color = BackgroundColor, Style = SKPaintStyle.Fill }; + canvas.DrawRect(bounds, bgPaint); + } + + // Log for StackLayout + if (this is SkiaStackLayout) + { + bool hasCV = false; + foreach (var c in _children) + { + if (c is SkiaCollectionView) hasCV = true; + } + if (hasCV) + { + Console.WriteLine($"[SkiaStackLayout+CV] OnDraw - bounds={bounds}, children={_children.Count}"); + foreach (var c in _children) + { + Console.WriteLine($"[SkiaStackLayout+CV] Child: {c.GetType().Name}, IsVisible={c.IsVisible}, Bounds={c.Bounds}"); + } + } + } + // Draw children in order foreach (var child in _children) { @@ -140,8 +194,14 @@ public abstract class SkiaLayoutView : SkiaView public override SkiaView? HitTest(float x, float y) { - if (!IsVisible || !Bounds.Contains(new SKPoint(x, y))) + if (!IsVisible || !IsEnabled || !Bounds.Contains(new SKPoint(x, y))) + { + if (this is SkiaBorder) + { + Console.WriteLine($"[SkiaBorder.HitTest] Miss - x={x}, y={y}, Bounds={Bounds}, IsVisible={IsVisible}, IsEnabled={IsEnabled}"); + } return null; + } // Hit test children in reverse order (top-most first) for (int i = _children.Count - 1; i >= 0; i--) @@ -149,11 +209,73 @@ public abstract class SkiaLayoutView : SkiaView var child = _children[i]; var hit = child.HitTest(x, y); if (hit != null) + { + if (this is SkiaBorder) + { + Console.WriteLine($"[SkiaBorder.HitTest] Hit child - x={x}, y={y}, Bounds={Bounds}, child={hit.GetType().Name}"); + } return hit; + } } + if (this is SkiaBorder) + { + Console.WriteLine($"[SkiaBorder.HitTest] Hit self - x={x}, y={y}, Bounds={Bounds}, children={_children.Count}"); + } return this; } + + /// + /// Forward pointer pressed events to the appropriate child. + /// + public override void OnPointerPressed(PointerEventArgs e) + { + // Find which child was hit and forward the event + var hit = HitTest(e.X, e.Y); + if (hit != null && hit != this) + { + hit.OnPointerPressed(e); + } + } + + /// + /// Forward pointer released events to the appropriate child. + /// + public override void OnPointerReleased(PointerEventArgs e) + { + // Find which child was hit and forward the event + var hit = HitTest(e.X, e.Y); + if (hit != null && hit != this) + { + hit.OnPointerReleased(e); + } + } + + /// + /// Forward pointer moved events to the appropriate child. + /// + public override void OnPointerMoved(PointerEventArgs e) + { + // Find which child was hit and forward the event + var hit = HitTest(e.X, e.Y); + if (hit != null && hit != this) + { + hit.OnPointerMoved(e); + } + } + + /// + /// Forward scroll events to the appropriate child. + /// + public override void OnScroll(ScrollEventArgs e) + { + // Find which child was hit and forward the event + var hit = HitTest(e.X, e.Y); + if (hit != null && hit != this) + { + hit.OnScroll(e); + } + } } /// @@ -168,8 +290,18 @@ public class SkiaStackLayout : SkiaLayoutView protected override SKSize MeasureOverride(SKSize availableSize) { - var contentWidth = availableSize.Width - Padding.Left - Padding.Right; - var contentHeight = availableSize.Height - Padding.Top - Padding.Bottom; + // Handle NaN/Infinity in padding + var paddingLeft = float.IsNaN(Padding.Left) ? 0 : Padding.Left; + var paddingRight = float.IsNaN(Padding.Right) ? 0 : Padding.Right; + var paddingTop = float.IsNaN(Padding.Top) ? 0 : Padding.Top; + var paddingBottom = float.IsNaN(Padding.Bottom) ? 0 : Padding.Bottom; + + var contentWidth = availableSize.Width - paddingLeft - paddingRight; + var contentHeight = availableSize.Height - paddingTop - paddingBottom; + + // Clamp negative sizes to 0 + if (contentWidth < 0 || float.IsNaN(contentWidth)) contentWidth = 0; + if (contentHeight < 0 || float.IsNaN(contentHeight)) contentHeight = 0; float totalWidth = 0; float totalHeight = 0; @@ -184,15 +316,19 @@ public class SkiaStackLayout : SkiaLayoutView var childSize = child.Measure(childAvailable); + // Skip NaN sizes from child measurements + var childWidth = float.IsNaN(childSize.Width) ? 0 : childSize.Width; + var childHeight = float.IsNaN(childSize.Height) ? 0 : childSize.Height; + if (Orientation == StackOrientation.Vertical) { - totalHeight += childSize.Height; - maxWidth = Math.Max(maxWidth, childSize.Width); + totalHeight += childHeight; + maxWidth = Math.Max(maxWidth, childWidth); } else { - totalWidth += childSize.Width; - maxHeight = Math.Max(maxHeight, childSize.Height); + totalWidth += childWidth; + maxHeight = Math.Max(maxHeight, childHeight); } } @@ -204,21 +340,26 @@ public class SkiaStackLayout : SkiaLayoutView { totalHeight += totalSpacing; return new SKSize( - maxWidth + Padding.Left + Padding.Right, - totalHeight + Padding.Top + Padding.Bottom); + maxWidth + paddingLeft + paddingRight, + totalHeight + paddingTop + paddingBottom); } else { totalWidth += totalSpacing; return new SKSize( - totalWidth + Padding.Left + Padding.Right, - maxHeight + Padding.Top + Padding.Bottom); + totalWidth + paddingLeft + paddingRight, + maxHeight + paddingTop + paddingBottom); } } protected override SKRect ArrangeOverride(SKRect bounds) { var content = GetContentBounds(bounds); + + // Clamp content dimensions if infinite - use reasonable defaults + var contentWidth = float.IsInfinity(content.Width) || float.IsNaN(content.Width) ? 800f : content.Width; + var contentHeight = float.IsInfinity(content.Height) || float.IsNaN(content.Height) ? 600f : content.Height; + float offset = 0; foreach (var child in Children) @@ -227,27 +368,80 @@ public class SkiaStackLayout : SkiaLayoutView var childDesired = child.DesiredSize; + // Handle NaN and Infinity in desired size + var childWidth = float.IsNaN(childDesired.Width) || float.IsInfinity(childDesired.Width) + ? contentWidth + : childDesired.Width; + var childHeight = float.IsNaN(childDesired.Height) || float.IsInfinity(childDesired.Height) + ? contentHeight + : childDesired.Height; + SKRect childBounds; if (Orientation == StackOrientation.Vertical) { + // For ScrollView children, give them the remaining viewport height + // Clamp to avoid giving them their content size + var remainingHeight = Math.Max(0, contentHeight - offset); + var useHeight = child is SkiaScrollView + ? remainingHeight + : Math.Min(childHeight, remainingHeight > 0 ? remainingHeight : childHeight); + childBounds = new SKRect( content.Left, content.Top + offset, - content.Right, - content.Top + offset + childDesired.Height); - offset += childDesired.Height + Spacing; + content.Left + contentWidth, + content.Top + offset + useHeight); + offset += useHeight + Spacing; } else { + // For ScrollView children, give them the remaining viewport width + var remainingWidth = Math.Max(0, contentWidth - offset); + var useWidth = child is SkiaScrollView + ? remainingWidth + : Math.Min(childWidth, remainingWidth > 0 ? remainingWidth : childWidth); + + // Respect child's VerticalOptions for horizontal layouts + var useHeight = Math.Min(childHeight, contentHeight); + float childTop = content.Top; + float childBottom = content.Top + useHeight; + + var verticalOptions = child.VerticalOptions; + var alignmentValue = (int)verticalOptions.Alignment; + + // LayoutAlignment: Start=0, Center=1, End=2, Fill=3 + if (alignmentValue == 1) // Center + { + childTop = content.Top + (contentHeight - useHeight) / 2; + childBottom = childTop + useHeight; + } + else if (alignmentValue == 2) // End + { + childTop = content.Top + contentHeight - useHeight; + childBottom = content.Top + contentHeight; + } + else if (alignmentValue == 3) // Fill + { + childTop = content.Top; + childBottom = content.Top + contentHeight; + } + childBounds = new SKRect( content.Left + offset, - content.Top, - content.Left + offset + childDesired.Width, - content.Bottom); - offset += childDesired.Width + Spacing; + childTop, + content.Left + offset + useWidth, + childBottom); + offset += useWidth + Spacing; } - child.Arrange(childBounds); + // Apply child's margin + var margin = child.Margin; + var marginedBounds = new SKRect( + childBounds.Left + (float)margin.Left, + childBounds.Top + (float)margin.Top, + childBounds.Right - (float)margin.Right, + childBounds.Bottom - (float)margin.Bottom); + child.Arrange(marginedBounds); } return bounds; } @@ -332,14 +526,73 @@ public class SkiaGrid : SkiaLayoutView var contentWidth = availableSize.Width - Padding.Left - Padding.Right; var contentHeight = availableSize.Height - Padding.Top - Padding.Bottom; - var rowCount = Math.Max(1, _rowDefinitions.Count); - var columnCount = Math.Max(1, _columnDefinitions.Count); + // Handle NaN/Infinity + if (float.IsNaN(contentWidth) || float.IsInfinity(contentWidth)) contentWidth = 800; + if (float.IsNaN(contentHeight) || float.IsInfinity(contentHeight)) contentHeight = float.PositiveInfinity; - // Calculate column widths - _columnWidths = CalculateSizes(_columnDefinitions, contentWidth, ColumnSpacing, columnCount); - _rowHeights = CalculateSizes(_rowDefinitions, contentHeight, RowSpacing, rowCount); + var rowCount = Math.Max(1, _rowDefinitions.Count > 0 ? _rowDefinitions.Count : GetMaxRow() + 1); + var columnCount = Math.Max(1, _columnDefinitions.Count > 0 ? _columnDefinitions.Count : GetMaxColumn() + 1); - // Measure children to adjust auto sizes + // First pass: measure children in Auto columns to get natural widths + var columnNaturalWidths = new float[columnCount]; + var rowNaturalHeights = new float[rowCount]; + + foreach (var child in Children) + { + if (!child.IsVisible) continue; + + var pos = GetPosition(child); + + // For Auto columns, measure with infinite width to get natural size + var def = pos.Column < _columnDefinitions.Count ? _columnDefinitions[pos.Column] : GridLength.Star; + if (def.IsAuto && pos.ColumnSpan == 1) + { + var childSize = child.Measure(new SKSize(float.PositiveInfinity, float.PositiveInfinity)); + var childWidth = float.IsNaN(childSize.Width) ? 0 : childSize.Width; + columnNaturalWidths[pos.Column] = Math.Max(columnNaturalWidths[pos.Column], childWidth); + } + } + + // Calculate column widths - handle Auto, Absolute, and Star + _columnWidths = CalculateSizesWithAuto(_columnDefinitions, contentWidth, ColumnSpacing, columnCount, columnNaturalWidths); + + // Second pass: measure all children with calculated column widths + foreach (var child in Children) + { + if (!child.IsVisible) continue; + + var pos = GetPosition(child); + var cellWidth = GetCellWidth(pos.Column, pos.ColumnSpan); + + // Give infinite height for initial measure + var childSize = child.Measure(new SKSize(cellWidth, float.PositiveInfinity)); + + // Track max height for each row + // Cap infinite/very large heights - child returning infinity means it doesn't have a natural height + var childHeight = childSize.Height; + if (float.IsNaN(childHeight) || float.IsInfinity(childHeight) || childHeight > 100000) + { + // Use a default minimum - will be expanded by Star sizing if finite height is available + childHeight = 44; // Standard row height + } + if (pos.RowSpan == 1) + { + rowNaturalHeights[pos.Row] = Math.Max(rowNaturalHeights[pos.Row], childHeight); + } + } + + // Calculate row heights - use natural heights when available height is infinite or very large + // (Some layouts pass float.MaxValue instead of PositiveInfinity) + if (float.IsInfinity(contentHeight) || contentHeight > 100000) + { + _rowHeights = rowNaturalHeights; + } + else + { + _rowHeights = CalculateSizesWithAuto(_rowDefinitions, contentHeight, RowSpacing, rowCount, rowNaturalHeights); + } + + // Third pass: re-measure children with actual cell sizes foreach (var child in Children) { if (!child.IsVisible) continue; @@ -360,7 +613,27 @@ public class SkiaGrid : SkiaLayoutView totalHeight + Padding.Top + Padding.Bottom); } - private float[] CalculateSizes(List definitions, float available, float spacing, int count) + private int GetMaxRow() + { + int maxRow = 0; + foreach (var pos in _childPositions.Values) + { + maxRow = Math.Max(maxRow, pos.Row + pos.RowSpan - 1); + } + return maxRow; + } + + private int GetMaxColumn() + { + int maxCol = 0; + foreach (var pos in _childPositions.Values) + { + maxCol = Math.Max(maxCol, pos.Column + pos.ColumnSpan - 1); + } + return maxCol; + } + + private float[] CalculateSizesWithAuto(List definitions, float available, float spacing, int count, float[] naturalSizes) { if (count == 0) return new float[] { available }; @@ -381,7 +654,9 @@ public class SkiaGrid : SkiaLayoutView } else if (def.IsAuto) { - sizes[i] = 0; // Will be calculated from children + // Use natural size from measured children + sizes[i] = naturalSizes[i]; + remainingSpace -= sizes[i]; } else if (def.IsStar) { @@ -389,7 +664,7 @@ public class SkiaGrid : SkiaLayoutView } } - // Second pass: star sizes + // Second pass: star sizes (distribute remaining space) if (starTotal > 0 && remainingSpace > 0) { for (int i = 0; i < count; i++) @@ -449,7 +724,52 @@ public class SkiaGrid : SkiaLayoutView protected override SKRect ArrangeOverride(SKRect bounds) { - var content = GetContentBounds(bounds); + try + { + var content = GetContentBounds(bounds); + + // Recalculate row heights for arrange bounds if they differ from measurement + // This ensures Star rows expand to fill available space + var rowCount = _rowHeights.Length > 0 ? _rowHeights.Length : 1; + var columnCount = _columnWidths.Length > 0 ? _columnWidths.Length : 1; + var arrangeRowHeights = _rowHeights; + + // If we have arrange height and rows need recalculating + if (content.Height > 0 && !float.IsInfinity(content.Height)) + { + var measuredRowsTotal = _rowHeights.Sum() + Math.Max(0, rowCount - 1) * RowSpacing; + + // If arrange height is larger than measured, redistribute to Star rows + if (content.Height > measuredRowsTotal + 1) + { + arrangeRowHeights = new float[rowCount]; + var extraHeight = content.Height - measuredRowsTotal; + + // Count Star rows (implicit rows without definitions are Star) + float totalStarWeight = 0; + for (int i = 0; i < rowCount; i++) + { + var def = i < _rowDefinitions.Count ? _rowDefinitions[i] : GridLength.Star; + if (def.IsStar) totalStarWeight += def.Value; + } + + // Distribute extra height to Star rows + for (int i = 0; i < rowCount; i++) + { + var def = i < _rowDefinitions.Count ? _rowDefinitions[i] : GridLength.Star; + arrangeRowHeights[i] = i < _rowHeights.Length ? _rowHeights[i] : 0; + + if (def.IsStar && totalStarWeight > 0) + { + arrangeRowHeights[i] += extraHeight * (def.Value / totalStarWeight); + } + } + } + else + { + arrangeRowHeights = _rowHeights; + } + } foreach (var child in Children) { @@ -458,13 +778,48 @@ public class SkiaGrid : SkiaLayoutView var pos = GetPosition(child); var x = content.Left + GetColumnOffset(pos.Column); - var y = content.Top + GetRowOffset(pos.Row); - var width = GetCellWidth(pos.Column, pos.ColumnSpan); - var height = GetCellHeight(pos.Row, pos.RowSpan); - child.Arrange(new SKRect(x, y, x + width, y + height)); + // Calculate y using arrange row heights + float y = content.Top; + for (int i = 0; i < Math.Min(pos.Row, arrangeRowHeights.Length); i++) + { + y += arrangeRowHeights[i] + RowSpacing; + } + + var width = GetCellWidth(pos.Column, pos.ColumnSpan); + + // Calculate height using arrange row heights + float height = 0; + for (int i = pos.Row; i < Math.Min(pos.Row + pos.RowSpan, arrangeRowHeights.Length); i++) + { + height += arrangeRowHeights[i]; + if (i > pos.Row) height += RowSpacing; + } + + // Clamp infinite dimensions + if (float.IsInfinity(width) || float.IsNaN(width)) + width = content.Width; + if (float.IsInfinity(height) || float.IsNaN(height) || height <= 0) + height = content.Height; + + // Apply child's margin + var margin = child.Margin; + var marginedBounds = new SKRect( + x + (float)margin.Left, + y + (float)margin.Top, + x + width - (float)margin.Right, + y + height - (float)margin.Bottom); + child.Arrange(marginedBounds); } return bounds; + } + catch (Exception ex) + { + Console.WriteLine($"[SkiaGrid] EXCEPTION in ArrangeOverride: {ex.GetType().Name}: {ex.Message}"); + Console.WriteLine($"[SkiaGrid] Bounds: {bounds}, RowHeights: {_rowHeights.Length}, RowDefs: {_rowDefinitions.Count}, Children: {Children.Count}"); + Console.WriteLine($"[SkiaGrid] Stack trace: {ex.StackTrace}"); + throw; + } } } @@ -629,7 +984,14 @@ public class SkiaAbsoluteLayout : SkiaLayoutView else height = childBounds.Height; - child.Arrange(new SKRect(x, y, x + width, y + height)); + // Apply child's margin + var margin = child.Margin; + var marginedBounds = new SKRect( + x + (float)margin.Left, + y + (float)margin.Top, + x + width - (float)margin.Right, + y + height - (float)margin.Bottom); + child.Arrange(marginedBounds); } return bounds; } diff --git a/Views/SkiaNavigationPage.cs b/Views/SkiaNavigationPage.cs index d9ed08c..61c214e 100644 --- a/Views/SkiaNavigationPage.cs +++ b/Views/SkiaNavigationPage.cs @@ -350,6 +350,7 @@ public class SkiaNavigationPage : SkiaView public override void OnPointerPressed(PointerEventArgs e) { + Console.WriteLine($"[SkiaNavigationPage] OnPointerPressed at ({e.X}, {e.Y}), _isAnimating={_isAnimating}"); if (_isAnimating) return; // Check for back button click @@ -357,11 +358,13 @@ public class SkiaNavigationPage : SkiaView { if (e.X < 56 && e.Y < _navigationBarHeight) { + Console.WriteLine($"[SkiaNavigationPage] Back button clicked"); Pop(); return; } } + Console.WriteLine($"[SkiaNavigationPage] Forwarding to _currentPage: {_currentPage?.GetType().Name}"); _currentPage?.OnPointerPressed(e); } @@ -403,6 +406,35 @@ public class SkiaNavigationPage : SkiaView if (_isAnimating) return; _currentPage?.OnScroll(e); } + + public override SkiaView? HitTest(float x, float y) + { + if (!IsVisible) + return null; + + // Back button area - return self so OnPointerPressed handles it + if (_showBackButton && _navigationStack.Count > 0 && x < 56 && y < _navigationBarHeight) + { + return this; + } + + // Check current page + if (_currentPage != null) + { + try + { + var hit = _currentPage.HitTest(x, y); + if (hit != null) + return hit; + } + catch (Exception ex) + { + Console.WriteLine($"[SkiaNavigationPage] HitTest error: {ex.Message}"); + } + } + + return this; + } } /// diff --git a/Views/SkiaPage.cs b/Views/SkiaPage.cs index 0acfdd5..77bf6eb 100644 --- a/Views/SkiaPage.cs +++ b/Views/SkiaPage.cs @@ -153,7 +153,19 @@ public class SkiaPage : SkiaView // Draw content if (_content != null) { - _content.Bounds = contentBounds; + // Apply content's margin to the content bounds + var margin = _content.Margin; + var adjustedBounds = new SKRect( + contentBounds.Left + (float)margin.Left, + contentBounds.Top + (float)margin.Top, + contentBounds.Right - (float)margin.Right, + contentBounds.Bottom - (float)margin.Bottom); + + // Measure and arrange the content before drawing + var availableSize = new SKSize(adjustedBounds.Width, adjustedBounds.Height); + _content.Measure(availableSize); + _content.Arrange(adjustedBounds); + Console.WriteLine($"[SkiaPage] Drawing content: {_content.GetType().Name}, Bounds={_content.Bounds}, IsVisible={_content.IsVisible}"); _content.Draw(canvas); } @@ -233,6 +245,7 @@ public class SkiaPage : SkiaView public void OnAppearing() { + Console.WriteLine($"[SkiaPage] OnAppearing called for: {Title}, HasListeners={Appearing != null}"); Appearing?.Invoke(this, EventArgs.Empty); } @@ -292,13 +305,160 @@ public class SkiaPage : SkiaView { _content?.OnScroll(e); } + + public override SkiaView? HitTest(float x, float y) + { + if (!IsVisible) + return null; + + // Don't check Bounds.Contains for page - it may not be set + // Just forward to content + + // Check content + if (_content != null) + { + var hit = _content.HitTest(x, y); + if (hit != null) + return hit; + } + + return this; + } } /// -/// Simple content page view. +/// Simple content page view with toolbar items support. /// public class SkiaContentPage : SkiaPage { - // SkiaContentPage is essentially the same as SkiaPage - // but represents a ContentPage specifically + private readonly List _toolbarItems = new(); + + /// + /// Gets the toolbar items for this page. + /// + public IList ToolbarItems => _toolbarItems; + + protected override void DrawNavigationBar(SKCanvas canvas, SKRect bounds) + { + // Draw navigation bar background + using var barPaint = new SKPaint + { + Color = TitleBarColor, + Style = SKPaintStyle.Fill + }; + canvas.DrawRect(bounds, barPaint); + + // Draw title + if (!string.IsNullOrEmpty(Title)) + { + using var font = new SKFont(SKTypeface.Default, 20); + using var textPaint = new SKPaint(font) + { + Color = TitleTextColor, + IsAntialias = true + }; + + var textBounds = new SKRect(); + textPaint.MeasureText(Title, ref textBounds); + + var x = bounds.Left + 56; // Leave space for back button + var y = bounds.MidY - textBounds.MidY; + canvas.DrawText(Title, x, y, textPaint); + } + + // Draw toolbar items on the right + DrawToolbarItems(canvas, bounds); + + // Draw shadow + using var shadowPaint = new SKPaint + { + Color = new SKColor(0, 0, 0, 30), + Style = SKPaintStyle.Fill, + MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 2) + }; + canvas.DrawRect(new SKRect(bounds.Left, bounds.Bottom, bounds.Right, bounds.Bottom + 4), shadowPaint); + } + + private void DrawToolbarItems(SKCanvas canvas, SKRect navBarBounds) + { + var primaryItems = _toolbarItems.Where(t => t.Order == SkiaToolbarItemOrder.Primary).ToList(); + Console.WriteLine($"[SkiaContentPage] DrawToolbarItems: {primaryItems.Count} primary items, navBarBounds={navBarBounds}"); + if (primaryItems.Count == 0) return; + + using var font = new SKFont(SKTypeface.Default, 14); + using var textPaint = new SKPaint(font) + { + Color = TitleTextColor, + IsAntialias = true + }; + + float rightEdge = navBarBounds.Right - 16; + + foreach (var item in primaryItems.AsEnumerable().Reverse()) + { + var textBounds = new SKRect(); + textPaint.MeasureText(item.Text, ref textBounds); + + var itemWidth = textBounds.Width + 24; // Padding + var itemLeft = rightEdge - itemWidth; + + // Store hit area for click handling + item.HitBounds = new SKRect(itemLeft, navBarBounds.Top, rightEdge, navBarBounds.Bottom); + Console.WriteLine($"[SkiaContentPage] Toolbar item '{item.Text}' HitBounds set to {item.HitBounds}"); + + // Draw text + var x = itemLeft + 12; + var y = navBarBounds.MidY - textBounds.MidY; + canvas.DrawText(item.Text, x, y, textPaint); + + rightEdge = itemLeft - 8; // Gap between items + } + } + + public override void OnPointerPressed(PointerEventArgs e) + { + Console.WriteLine($"[SkiaContentPage] OnPointerPressed at ({e.X}, {e.Y}), ShowNavigationBar={ShowNavigationBar}, NavigationBarHeight={NavigationBarHeight}"); + Console.WriteLine($"[SkiaContentPage] ToolbarItems count: {_toolbarItems.Count}"); + + // Check toolbar item clicks + if (ShowNavigationBar && e.Y < NavigationBarHeight) + { + Console.WriteLine($"[SkiaContentPage] In navigation bar area, checking toolbar items"); + foreach (var item in _toolbarItems.Where(t => t.Order == SkiaToolbarItemOrder.Primary)) + { + var bounds = item.HitBounds; + var contains = bounds.Contains(e.X, e.Y); + Console.WriteLine($"[SkiaContentPage] Checking item '{item.Text}', HitBounds=({bounds.Left},{bounds.Top},{bounds.Right},{bounds.Bottom}), Click=({e.X},{e.Y}), Contains={contains}, Command={item.Command != null}"); + if (contains) + { + Console.WriteLine($"[SkiaContentPage] Toolbar item clicked: {item.Text}"); + item.Command?.Execute(null); + return; + } + } + Console.WriteLine($"[SkiaContentPage] No toolbar item hit"); + } + + base.OnPointerPressed(e); + } +} + +/// +/// Represents a toolbar item in the navigation bar. +/// +public class SkiaToolbarItem +{ + public string Text { get; set; } = ""; + public SkiaToolbarItemOrder Order { get; set; } = SkiaToolbarItemOrder.Primary; + public System.Windows.Input.ICommand? Command { get; set; } + public SKRect HitBounds { get; set; } +} + +/// +/// Order of toolbar items. +/// +public enum SkiaToolbarItemOrder +{ + Primary, + Secondary } diff --git a/Views/SkiaPicker.cs b/Views/SkiaPicker.cs index 96cd8b2..3348f62 100644 --- a/Views/SkiaPicker.cs +++ b/Views/SkiaPicker.cs @@ -6,67 +6,301 @@ using SkiaSharp; namespace Microsoft.Maui.Platform; /// -/// Skia-rendered picker/dropdown control. +/// Skia-rendered picker/dropdown control with full XAML styling support. /// public class SkiaPicker : SkiaView { - private List _items = new(); - private int _selectedIndex = -1; - private bool _isOpen; - private string _title = ""; - private float _dropdownMaxHeight = 200; - private int _hoveredItemIndex = -1; + #region BindableProperties - // Styling - public SKColor TextColor { get; set; } = SKColors.Black; - public SKColor TitleColor { get; set; } = new SKColor(0x80, 0x80, 0x80); - public SKColor BorderColor { get; set; } = new SKColor(0xBD, 0xBD, 0xBD); - public SKColor DropdownBackgroundColor { get; set; } = SKColors.White; - public SKColor SelectedItemBackgroundColor { get; set; } = new SKColor(0x21, 0x96, 0xF3, 0x30); - public SKColor HoverItemBackgroundColor { get; set; } = new SKColor(0xE0, 0xE0, 0xE0); - public string FontFamily { get; set; } = "Sans"; - public float FontSize { get; set; } = 14; - public float ItemHeight { get; set; } = 40; - public float CornerRadius { get; set; } = 4; + /// + /// Bindable property for SelectedIndex. + /// + public static readonly BindableProperty SelectedIndexProperty = + BindableProperty.Create( + nameof(SelectedIndex), + typeof(int), + typeof(SkiaPicker), + -1, + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaPicker)b).OnSelectedIndexChanged()); - public IList Items => _items; + /// + /// Bindable property for Title. + /// + public static readonly BindableProperty TitleProperty = + BindableProperty.Create( + nameof(Title), + typeof(string), + typeof(SkiaPicker), + "", + propertyChanged: (b, o, n) => ((SkiaPicker)b).Invalidate()); + /// + /// Bindable property for TextColor. + /// + public static readonly BindableProperty TextColorProperty = + BindableProperty.Create( + nameof(TextColor), + typeof(SKColor), + typeof(SkiaPicker), + SKColors.Black, + propertyChanged: (b, o, n) => ((SkiaPicker)b).Invalidate()); + + /// + /// Bindable property for TitleColor. + /// + public static readonly BindableProperty TitleColorProperty = + BindableProperty.Create( + nameof(TitleColor), + typeof(SKColor), + typeof(SkiaPicker), + new SKColor(0x80, 0x80, 0x80), + propertyChanged: (b, o, n) => ((SkiaPicker)b).Invalidate()); + + /// + /// Bindable property for BorderColor. + /// + public static readonly BindableProperty BorderColorProperty = + BindableProperty.Create( + nameof(BorderColor), + typeof(SKColor), + typeof(SkiaPicker), + new SKColor(0xBD, 0xBD, 0xBD), + propertyChanged: (b, o, n) => ((SkiaPicker)b).Invalidate()); + + /// + /// Bindable property for DropdownBackgroundColor. + /// + public static readonly BindableProperty DropdownBackgroundColorProperty = + BindableProperty.Create( + nameof(DropdownBackgroundColor), + typeof(SKColor), + typeof(SkiaPicker), + SKColors.White, + propertyChanged: (b, o, n) => ((SkiaPicker)b).Invalidate()); + + /// + /// Bindable property for SelectedItemBackgroundColor. + /// + public static readonly BindableProperty SelectedItemBackgroundColorProperty = + BindableProperty.Create( + nameof(SelectedItemBackgroundColor), + typeof(SKColor), + typeof(SkiaPicker), + new SKColor(0x21, 0x96, 0xF3, 0x30), + propertyChanged: (b, o, n) => ((SkiaPicker)b).Invalidate()); + + /// + /// Bindable property for HoverItemBackgroundColor. + /// + public static readonly BindableProperty HoverItemBackgroundColorProperty = + BindableProperty.Create( + nameof(HoverItemBackgroundColor), + typeof(SKColor), + typeof(SkiaPicker), + new SKColor(0xE0, 0xE0, 0xE0), + propertyChanged: (b, o, n) => ((SkiaPicker)b).Invalidate()); + + /// + /// Bindable property for FontFamily. + /// + public static readonly BindableProperty FontFamilyProperty = + BindableProperty.Create( + nameof(FontFamily), + typeof(string), + typeof(SkiaPicker), + "Sans", + propertyChanged: (b, o, n) => ((SkiaPicker)b).InvalidateMeasure()); + + /// + /// Bindable property for FontSize. + /// + public static readonly BindableProperty FontSizeProperty = + BindableProperty.Create( + nameof(FontSize), + typeof(float), + typeof(SkiaPicker), + 14f, + propertyChanged: (b, o, n) => ((SkiaPicker)b).InvalidateMeasure()); + + /// + /// Bindable property for ItemHeight. + /// + public static readonly BindableProperty ItemHeightProperty = + BindableProperty.Create( + nameof(ItemHeight), + typeof(float), + typeof(SkiaPicker), + 40f, + propertyChanged: (b, o, n) => ((SkiaPicker)b).Invalidate()); + + /// + /// Bindable property for CornerRadius. + /// + public static readonly BindableProperty CornerRadiusProperty = + BindableProperty.Create( + nameof(CornerRadius), + typeof(float), + typeof(SkiaPicker), + 4f, + propertyChanged: (b, o, n) => ((SkiaPicker)b).Invalidate()); + + #endregion + + #region Properties + + /// + /// Gets or sets the selected index. + /// public int SelectedIndex { - get => _selectedIndex; - set - { - if (_selectedIndex != value) - { - _selectedIndex = value; - SelectedIndexChanged?.Invoke(this, EventArgs.Empty); - Invalidate(); - } - } + get => (int)GetValue(SelectedIndexProperty); + set => SetValue(SelectedIndexProperty, value); } - public string? SelectedItem => _selectedIndex >= 0 && _selectedIndex < _items.Count ? _items[_selectedIndex] : null; - + /// + /// Gets or sets the title/placeholder. + /// public string Title { - get => _title; - set - { - _title = value; - Invalidate(); - } + get => (string)GetValue(TitleProperty); + set => SetValue(TitleProperty, value); } + /// + /// Gets or sets the text color. + /// + public SKColor TextColor + { + get => (SKColor)GetValue(TextColorProperty); + set => SetValue(TextColorProperty, value); + } + + /// + /// Gets or sets the title color. + /// + public SKColor TitleColor + { + get => (SKColor)GetValue(TitleColorProperty); + set => SetValue(TitleColorProperty, value); + } + + /// + /// Gets or sets the border color. + /// + public SKColor BorderColor + { + get => (SKColor)GetValue(BorderColorProperty); + set => SetValue(BorderColorProperty, value); + } + + /// + /// Gets or sets the dropdown background color. + /// + public SKColor DropdownBackgroundColor + { + get => (SKColor)GetValue(DropdownBackgroundColorProperty); + set => SetValue(DropdownBackgroundColorProperty, value); + } + + /// + /// Gets or sets the selected item background color. + /// + public SKColor SelectedItemBackgroundColor + { + get => (SKColor)GetValue(SelectedItemBackgroundColorProperty); + set => SetValue(SelectedItemBackgroundColorProperty, value); + } + + /// + /// Gets or sets the hover item background color. + /// + public SKColor HoverItemBackgroundColor + { + get => (SKColor)GetValue(HoverItemBackgroundColorProperty); + set => SetValue(HoverItemBackgroundColorProperty, value); + } + + /// + /// Gets or sets the font family. + /// + public string FontFamily + { + get => (string)GetValue(FontFamilyProperty); + set => SetValue(FontFamilyProperty, value); + } + + /// + /// Gets or sets the font size. + /// + public float FontSize + { + get => (float)GetValue(FontSizeProperty); + set => SetValue(FontSizeProperty, value); + } + + /// + /// Gets or sets the item height. + /// + public float ItemHeight + { + get => (float)GetValue(ItemHeightProperty); + set => SetValue(ItemHeightProperty, value); + } + + /// + /// Gets or sets the corner radius. + /// + public float CornerRadius + { + get => (float)GetValue(CornerRadiusProperty); + set => SetValue(CornerRadiusProperty, value); + } + + /// + /// Gets the items list. + /// + public IList Items => _items; + + /// + /// Gets the selected item. + /// + public string? SelectedItem => SelectedIndex >= 0 && SelectedIndex < _items.Count ? _items[SelectedIndex] : null; + + /// + /// Gets or sets whether the dropdown is open. + /// public bool IsOpen { get => _isOpen; set { - _isOpen = value; - Invalidate(); + if (_isOpen != value) + { + _isOpen = value; + if (_isOpen) + { + RegisterPopupOverlay(this, DrawDropdownOverlay); + } + else + { + UnregisterPopupOverlay(this); + } + Invalidate(); + } } } + #endregion + + private readonly List _items = new(); + private bool _isOpen; + private float _dropdownMaxHeight = 200; + private int _hoveredItemIndex = -1; + + /// + /// Event raised when selected index changes. + /// public event EventHandler? SelectedIndexChanged; public SkiaPicker() @@ -74,25 +308,36 @@ public class SkiaPicker : SkiaView IsFocusable = true; } + private void OnSelectedIndexChanged() + { + SelectedIndexChanged?.Invoke(this, EventArgs.Empty); + Invalidate(); + } + + /// + /// Sets the items in the picker. + /// public void SetItems(IEnumerable items) { _items.Clear(); _items.AddRange(items); - if (_selectedIndex >= _items.Count) + if (SelectedIndex >= _items.Count) { - _selectedIndex = _items.Count > 0 ? 0 : -1; + SelectedIndex = _items.Count > 0 ? 0 : -1; } Invalidate(); } + private void DrawDropdownOverlay(SKCanvas canvas) + { + if (_items.Count == 0 || !_isOpen) return; + // Use ScreenBounds for overlay drawing to account for scroll offset + DrawDropdown(canvas, ScreenBounds); + } + protected override void OnDraw(SKCanvas canvas, SKRect bounds) { DrawPickerButton(canvas, bounds); - - if (_isOpen) - { - DrawDropdown(canvas, bounds); - } } private void DrawPickerButton(SKCanvas canvas, SKRect bounds) @@ -126,14 +371,14 @@ public class SkiaPicker : SkiaView }; string displayText; - if (_selectedIndex >= 0 && _selectedIndex < _items.Count) + if (SelectedIndex >= 0 && SelectedIndex < _items.Count) { - displayText = _items[_selectedIndex]; + displayText = _items[SelectedIndex]; textPaint.Color = IsEnabled ? TextColor : TextColor.WithAlpha(128); } else { - displayText = _title; + displayText = Title; textPaint.Color = TitleColor; } @@ -166,14 +411,12 @@ public class SkiaPicker : SkiaView using var path = new SKPath(); if (_isOpen) { - // Up arrow path.MoveTo(centerX - arrowSize, centerY + arrowSize / 2); path.LineTo(centerX, centerY - arrowSize / 2); path.LineTo(centerX + arrowSize, centerY + arrowSize / 2); } else { - // Down arrow path.MoveTo(centerX - arrowSize, centerY - arrowSize / 2); path.LineTo(centerX, centerY + arrowSize / 2); path.LineTo(centerX + arrowSize, centerY - arrowSize / 2); @@ -242,7 +485,7 @@ public class SkiaPicker : SkiaView var itemRect = new SKRect(dropdownRect.Left, itemTop, dropdownRect.Right, itemTop + ItemHeight); // Draw item background - if (i == _selectedIndex) + if (i == SelectedIndex) { using var selectedPaint = new SKPaint { @@ -277,10 +520,11 @@ public class SkiaPicker : SkiaView { if (!IsEnabled) return; - if (_isOpen) + if (IsOpen) { - // Check if clicked on dropdown item - var dropdownTop = Bounds.Bottom + 4; + // Use ScreenBounds for popup coordinate calculations (accounts for scroll offset) + var screenBounds = ScreenBounds; + var dropdownTop = screenBounds.Bottom + 4; if (e.Y >= dropdownTop) { var itemIndex = (int)((e.Y - dropdownTop) / ItemHeight); @@ -289,15 +533,11 @@ public class SkiaPicker : SkiaView SelectedIndex = itemIndex; } } - _isOpen = false; + IsOpen = false; } else { - // Check if clicked on picker button - if (e.Y < Bounds.Bottom) - { - _isOpen = true; - } + IsOpen = true; } Invalidate(); @@ -307,7 +547,9 @@ public class SkiaPicker : SkiaView { if (!_isOpen) return; - var dropdownTop = Bounds.Bottom + 4; + // Use ScreenBounds for popup coordinate calculations (accounts for scroll offset) + var screenBounds = ScreenBounds; + var dropdownTop = screenBounds.Bottom + 4; if (e.Y >= dropdownTop) { var newHovered = (int)((e.Y - dropdownTop) / ItemHeight); @@ -341,27 +583,22 @@ public class SkiaPicker : SkiaView { case Key.Enter: case Key.Space: - _isOpen = !_isOpen; + IsOpen = !IsOpen; e.Handled = true; Invalidate(); break; case Key.Escape: - if (_isOpen) + if (IsOpen) { - _isOpen = false; + IsOpen = false; e.Handled = true; Invalidate(); } break; case Key.Up: - if (_isOpen && _selectedIndex > 0) - { - SelectedIndex--; - e.Handled = true; - } - else if (!_isOpen && _selectedIndex > 0) + if (SelectedIndex > 0) { SelectedIndex--; e.Handled = true; @@ -369,12 +606,7 @@ public class SkiaPicker : SkiaView break; case Key.Down: - if (_isOpen && _selectedIndex < _items.Count - 1) - { - SelectedIndex++; - e.Handled = true; - } - else if (!_isOpen && _selectedIndex < _items.Count - 1) + if (SelectedIndex < _items.Count - 1) { SelectedIndex++; e.Handled = true; @@ -383,10 +615,47 @@ public class SkiaPicker : SkiaView } } + public override void OnFocusLost() + { + base.OnFocusLost(); + if (IsOpen) + { + IsOpen = false; + } + } + protected override SKSize MeasureOverride(SKSize availableSize) { return new SKSize( availableSize.Width < float.MaxValue ? Math.Min(availableSize.Width, 200) : 200, 40); } + + /// + /// Override to include dropdown area in hit testing. + /// + protected override bool HitTestPopupArea(float x, float y) + { + // Use ScreenBounds for hit testing (accounts for scroll offset) + var screenBounds = ScreenBounds; + + // Always include the picker button itself + if (screenBounds.Contains(x, y)) + return true; + + // When open, also include the dropdown area + if (_isOpen && _items.Count > 0) + { + var dropdownHeight = Math.Min(_items.Count * ItemHeight, _dropdownMaxHeight); + var dropdownRect = new SKRect( + screenBounds.Left, + screenBounds.Bottom + 4, + screenBounds.Right, + screenBounds.Bottom + 4 + dropdownHeight); + + return dropdownRect.Contains(x, y); + } + + return false; + } } diff --git a/Views/SkiaProgressBar.cs b/Views/SkiaProgressBar.cs index 74ba8a2..ea7f7b6 100644 --- a/Views/SkiaProgressBar.cs +++ b/Views/SkiaProgressBar.cs @@ -6,40 +6,156 @@ using SkiaSharp; namespace Microsoft.Maui.Platform; /// -/// Skia-rendered progress bar control. +/// Skia-rendered progress bar control with full XAML styling support. /// public class SkiaProgressBar : SkiaView { - private double _progress; + #region BindableProperties + /// + /// Bindable property for Progress. + /// + public static readonly BindableProperty ProgressProperty = + BindableProperty.Create( + nameof(Progress), + typeof(double), + typeof(SkiaProgressBar), + 0.0, + BindingMode.TwoWay, + coerceValue: (b, v) => Math.Clamp((double)v, 0, 1), + propertyChanged: (b, o, n) => ((SkiaProgressBar)b).OnProgressChanged()); + + /// + /// Bindable property for TrackColor. + /// + public static readonly BindableProperty TrackColorProperty = + BindableProperty.Create( + nameof(TrackColor), + typeof(SKColor), + typeof(SkiaProgressBar), + new SKColor(0xE0, 0xE0, 0xE0), + propertyChanged: (b, o, n) => ((SkiaProgressBar)b).Invalidate()); + + /// + /// Bindable property for ProgressColor. + /// + public static readonly BindableProperty ProgressColorProperty = + BindableProperty.Create( + nameof(ProgressColor), + typeof(SKColor), + typeof(SkiaProgressBar), + new SKColor(0x21, 0x96, 0xF3), + propertyChanged: (b, o, n) => ((SkiaProgressBar)b).Invalidate()); + + /// + /// Bindable property for DisabledColor. + /// + public static readonly BindableProperty DisabledColorProperty = + BindableProperty.Create( + nameof(DisabledColor), + typeof(SKColor), + typeof(SkiaProgressBar), + new SKColor(0xBD, 0xBD, 0xBD), + propertyChanged: (b, o, n) => ((SkiaProgressBar)b).Invalidate()); + + /// + /// Bindable property for BarHeight. + /// + public static readonly BindableProperty BarHeightProperty = + BindableProperty.Create( + nameof(BarHeight), + typeof(float), + typeof(SkiaProgressBar), + 4f, + propertyChanged: (b, o, n) => ((SkiaProgressBar)b).InvalidateMeasure()); + + /// + /// Bindable property for CornerRadius. + /// + public static readonly BindableProperty CornerRadiusProperty = + BindableProperty.Create( + nameof(CornerRadius), + typeof(float), + typeof(SkiaProgressBar), + 2f, + propertyChanged: (b, o, n) => ((SkiaProgressBar)b).Invalidate()); + + #endregion + + #region Properties + + /// + /// Gets or sets the progress value (0.0 to 1.0). + /// public double Progress { - get => _progress; - set - { - var clamped = Math.Clamp(value, 0, 1); - if (_progress != clamped) - { - _progress = clamped; - ProgressChanged?.Invoke(this, new ProgressChangedEventArgs(_progress)); - Invalidate(); - } - } + get => (double)GetValue(ProgressProperty); + set => SetValue(ProgressProperty, value); } - public SKColor TrackColor { get; set; } = new SKColor(0xE0, 0xE0, 0xE0); - public SKColor ProgressColor { get; set; } = new SKColor(0x21, 0x96, 0xF3); - public SKColor DisabledColor { get; set; } = new SKColor(0xBD, 0xBD, 0xBD); - public float Height { get; set; } = 4; - public float CornerRadius { get; set; } = 2; + /// + /// Gets or sets the track color. + /// + public SKColor TrackColor + { + get => (SKColor)GetValue(TrackColorProperty); + set => SetValue(TrackColorProperty, value); + } + /// + /// Gets or sets the progress color. + /// + public SKColor ProgressColor + { + get => (SKColor)GetValue(ProgressColorProperty); + set => SetValue(ProgressColorProperty, value); + } + + /// + /// Gets or sets the disabled color. + /// + public SKColor DisabledColor + { + get => (SKColor)GetValue(DisabledColorProperty); + set => SetValue(DisabledColorProperty, value); + } + + /// + /// Gets or sets the bar height. + /// + public float BarHeight + { + get => (float)GetValue(BarHeightProperty); + set => SetValue(BarHeightProperty, value); + } + + /// + /// Gets or sets the corner radius. + /// + public float CornerRadius + { + get => (float)GetValue(CornerRadiusProperty); + set => SetValue(CornerRadiusProperty, value); + } + + #endregion + + /// + /// Event raised when progress changes. + /// public event EventHandler? ProgressChanged; + private void OnProgressChanged() + { + ProgressChanged?.Invoke(this, new ProgressChangedEventArgs(Progress)); + Invalidate(); + } + protected override void OnDraw(SKCanvas canvas, SKRect bounds) { var trackY = bounds.MidY; - var trackTop = trackY - Height / 2; - var trackBottom = trackY + Height / 2; + var trackTop = trackY - BarHeight / 2; + var trackBottom = trackY + BarHeight / 2; // Draw track using var trackPaint = new SKPaint @@ -75,10 +191,13 @@ public class SkiaProgressBar : SkiaView protected override SKSize MeasureOverride(SKSize availableSize) { - return new SKSize(200, Height + 8); + return new SKSize(200, BarHeight + 8); } } +/// +/// Event args for progress changed events. +/// public class ProgressChangedEventArgs : EventArgs { public double Progress { get; } diff --git a/Views/SkiaRadioButton.cs b/Views/SkiaRadioButton.cs index 56300e0..60a133d 100644 --- a/Views/SkiaRadioButton.cs +++ b/Views/SkiaRadioButton.cs @@ -6,73 +6,129 @@ using SkiaSharp; namespace Microsoft.Maui.Platform; /// -/// Skia-rendered radio button control. +/// Skia-rendered radio button control with full XAML styling support. /// public class SkiaRadioButton : SkiaView { - private bool _isChecked; - private string _content = ""; - private object? _value; - private string? _groupName; + #region BindableProperties - // Styling - public SKColor RadioColor { get; set; } = new SKColor(0x21, 0x96, 0xF3); - public SKColor UncheckedColor { get; set; } = new SKColor(0x75, 0x75, 0x75); - public SKColor TextColor { get; set; } = SKColors.Black; - public SKColor DisabledColor { get; set; } = new SKColor(0xBD, 0xBD, 0xBD); - public float FontSize { get; set; } = 14; - public float RadioSize { get; set; } = 20; - public float Spacing { get; set; } = 8; + public static readonly BindableProperty IsCheckedProperty = + BindableProperty.Create(nameof(IsChecked), typeof(bool), typeof(SkiaRadioButton), false, BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaRadioButton)b).OnIsCheckedChanged()); - // Static group management - private static readonly Dictionary>> _groups = new(); + public static readonly BindableProperty ContentProperty = + BindableProperty.Create(nameof(Content), typeof(string), typeof(SkiaRadioButton), "", + propertyChanged: (b, o, n) => ((SkiaRadioButton)b).InvalidateMeasure()); + + public static readonly BindableProperty ValueProperty = + BindableProperty.Create(nameof(Value), typeof(object), typeof(SkiaRadioButton), null); + + public static readonly BindableProperty GroupNameProperty = + BindableProperty.Create(nameof(GroupName), typeof(string), typeof(SkiaRadioButton), null, + propertyChanged: (b, o, n) => ((SkiaRadioButton)b).OnGroupNameChanged((string?)o, (string?)n)); + + public static readonly BindableProperty RadioColorProperty = + BindableProperty.Create(nameof(RadioColor), typeof(SKColor), typeof(SkiaRadioButton), new SKColor(0x21, 0x96, 0xF3), + propertyChanged: (b, o, n) => ((SkiaRadioButton)b).Invalidate()); + + public static readonly BindableProperty UncheckedColorProperty = + BindableProperty.Create(nameof(UncheckedColor), typeof(SKColor), typeof(SkiaRadioButton), new SKColor(0x75, 0x75, 0x75), + propertyChanged: (b, o, n) => ((SkiaRadioButton)b).Invalidate()); + + public static readonly BindableProperty TextColorProperty = + BindableProperty.Create(nameof(TextColor), typeof(SKColor), typeof(SkiaRadioButton), SKColors.Black, + propertyChanged: (b, o, n) => ((SkiaRadioButton)b).Invalidate()); + + public static readonly BindableProperty DisabledColorProperty = + BindableProperty.Create(nameof(DisabledColor), typeof(SKColor), typeof(SkiaRadioButton), new SKColor(0xBD, 0xBD, 0xBD), + propertyChanged: (b, o, n) => ((SkiaRadioButton)b).Invalidate()); + + public static readonly BindableProperty FontSizeProperty = + BindableProperty.Create(nameof(FontSize), typeof(float), typeof(SkiaRadioButton), 14f, + propertyChanged: (b, o, n) => ((SkiaRadioButton)b).InvalidateMeasure()); + + public static readonly BindableProperty RadioSizeProperty = + BindableProperty.Create(nameof(RadioSize), typeof(float), typeof(SkiaRadioButton), 20f, + propertyChanged: (b, o, n) => ((SkiaRadioButton)b).InvalidateMeasure()); + + public static readonly BindableProperty SpacingProperty = + BindableProperty.Create(nameof(Spacing), typeof(float), typeof(SkiaRadioButton), 8f, + propertyChanged: (b, o, n) => ((SkiaRadioButton)b).InvalidateMeasure()); + + #endregion + + #region Properties public bool IsChecked { - get => _isChecked; - set - { - if (_isChecked != value) - { - _isChecked = value; - - if (_isChecked && !string.IsNullOrEmpty(_groupName)) - { - UncheckOthersInGroup(); - } - - CheckedChanged?.Invoke(this, EventArgs.Empty); - Invalidate(); - } - } + get => (bool)GetValue(IsCheckedProperty); + set => SetValue(IsCheckedProperty, value); } public string Content { - get => _content; - set { _content = value ?? ""; Invalidate(); } + get => (string)GetValue(ContentProperty); + set => SetValue(ContentProperty, value); } public object? Value { - get => _value; - set { _value = value; } + get => GetValue(ValueProperty); + set => SetValue(ValueProperty, value); } public string? GroupName { - get => _groupName; - set - { - if (_groupName != value) - { - RemoveFromGroup(); - _groupName = value; - AddToGroup(); - } - } + get => (string?)GetValue(GroupNameProperty); + set => SetValue(GroupNameProperty, value); } + public SKColor RadioColor + { + get => (SKColor)GetValue(RadioColorProperty); + set => SetValue(RadioColorProperty, value); + } + + public SKColor UncheckedColor + { + get => (SKColor)GetValue(UncheckedColorProperty); + set => SetValue(UncheckedColorProperty, value); + } + + public SKColor TextColor + { + get => (SKColor)GetValue(TextColorProperty); + set => SetValue(TextColorProperty, value); + } + + public SKColor DisabledColor + { + get => (SKColor)GetValue(DisabledColorProperty); + set => SetValue(DisabledColorProperty, value); + } + + public float FontSize + { + get => (float)GetValue(FontSizeProperty); + set => SetValue(FontSizeProperty, value); + } + + public float RadioSize + { + get => (float)GetValue(RadioSizeProperty); + set => SetValue(RadioSizeProperty, value); + } + + public float Spacing + { + get => (float)GetValue(SpacingProperty); + set => SetValue(SpacingProperty, value); + } + + #endregion + + private static readonly Dictionary>> _groups = new(); + public event EventHandler? CheckedChanged; public SkiaRadioButton() @@ -80,48 +136,59 @@ public class SkiaRadioButton : SkiaView IsFocusable = true; } - private void AddToGroup() + private void OnIsCheckedChanged() { - if (string.IsNullOrEmpty(_groupName)) return; + if (IsChecked && !string.IsNullOrEmpty(GroupName)) + { + UncheckOthersInGroup(); + } + CheckedChanged?.Invoke(this, EventArgs.Empty); + SkiaVisualStateManager.GoToState(this, IsChecked ? SkiaVisualStateManager.CommonStates.Checked : SkiaVisualStateManager.CommonStates.Unchecked); + Invalidate(); + } - if (!_groups.TryGetValue(_groupName, out var group)) + private void OnGroupNameChanged(string? oldValue, string? newValue) + { + RemoveFromGroup(oldValue); + AddToGroup(newValue); + } + + private void AddToGroup(string? groupName) + { + if (string.IsNullOrEmpty(groupName)) return; + + if (!_groups.TryGetValue(groupName, out var group)) { group = new List>(); - _groups[_groupName] = group; + _groups[groupName] = group; } - // Clean up dead references and add this one group.RemoveAll(wr => !wr.TryGetTarget(out _)); group.Add(new WeakReference(this)); } - private void RemoveFromGroup() + private void RemoveFromGroup(string? groupName) { - if (string.IsNullOrEmpty(_groupName)) return; + if (string.IsNullOrEmpty(groupName)) return; - if (_groups.TryGetValue(_groupName, out var group)) + if (_groups.TryGetValue(groupName, out var group)) { group.RemoveAll(wr => !wr.TryGetTarget(out var target) || target == this); - if (group.Count == 0) - { - _groups.Remove(_groupName); - } + if (group.Count == 0) _groups.Remove(groupName); } } private void UncheckOthersInGroup() { - if (string.IsNullOrEmpty(_groupName)) return; + if (string.IsNullOrEmpty(GroupName)) return; - if (_groups.TryGetValue(_groupName, out var group)) + if (_groups.TryGetValue(GroupName, out var group)) { foreach (var weakRef in group) { - if (weakRef.TryGetTarget(out var radioButton) && radioButton != this) + if (weakRef.TryGetTarget(out var radioButton) && radioButton != this && radioButton.IsChecked) { - radioButton._isChecked = false; - radioButton.CheckedChanged?.Invoke(radioButton, EventArgs.Empty); - radioButton.Invalidate(); + radioButton.SetValue(IsCheckedProperty, false); } } } @@ -133,18 +200,16 @@ public class SkiaRadioButton : SkiaView var radioCenterX = bounds.Left + radioRadius; var radioCenterY = bounds.MidY; - // Draw outer circle using var outerPaint = new SKPaint { - Color = IsEnabled ? (_isChecked ? RadioColor : UncheckedColor) : DisabledColor, + Color = IsEnabled ? (IsChecked ? RadioColor : UncheckedColor) : DisabledColor, Style = SKPaintStyle.Stroke, StrokeWidth = 2, IsAntialias = true }; canvas.DrawCircle(radioCenterX, radioCenterY, radioRadius - 1, outerPaint); - // Draw inner circle if checked - if (_isChecked) + if (IsChecked) { using var innerPaint = new SKPaint { @@ -155,7 +220,6 @@ public class SkiaRadioButton : SkiaView canvas.DrawCircle(radioCenterX, radioCenterY, radioRadius - 5, innerPaint); } - // Draw focus ring if (IsFocused) { using var focusPaint = new SKPaint @@ -167,8 +231,7 @@ public class SkiaRadioButton : SkiaView canvas.DrawCircle(radioCenterX, radioCenterY, radioRadius + 4, focusPaint); } - // Draw content text - if (!string.IsNullOrEmpty(_content)) + if (!string.IsNullOrEmpty(Content)) { using var font = new SKFont(SKTypeface.Default, FontSize); using var textPaint = new SKPaint(font) @@ -179,48 +242,43 @@ public class SkiaRadioButton : SkiaView var textX = bounds.Left + RadioSize + Spacing; var textBounds = new SKRect(); - textPaint.MeasureText(_content, ref textBounds); - canvas.DrawText(_content, textX, bounds.MidY - textBounds.MidY, textPaint); + textPaint.MeasureText(Content, ref textBounds); + canvas.DrawText(Content, textX, bounds.MidY - textBounds.MidY, textPaint); } } public override void OnPointerPressed(PointerEventArgs e) { if (!IsEnabled) return; - - if (!_isChecked) - { - IsChecked = true; - } + if (!IsChecked) IsChecked = true; } public override void OnKeyDown(KeyEventArgs e) { if (!IsEnabled) return; - switch (e.Key) + if (e.Key == Key.Space || e.Key == Key.Enter) { - case Key.Space: - case Key.Enter: - if (!_isChecked) - { - IsChecked = true; - } - e.Handled = true; - break; + if (!IsChecked) IsChecked = true; + e.Handled = true; } } + protected override void OnEnabledChanged() + { + base.OnEnabledChanged(); + SkiaVisualStateManager.GoToState(this, IsEnabled ? SkiaVisualStateManager.CommonStates.Normal : SkiaVisualStateManager.CommonStates.Disabled); + } + protected override SKSize MeasureOverride(SKSize availableSize) { var textWidth = 0f; - if (!string.IsNullOrEmpty(_content)) + if (!string.IsNullOrEmpty(Content)) { using var font = new SKFont(SKTypeface.Default, FontSize); using var paint = new SKPaint(font); - textWidth = paint.MeasureText(_content) + Spacing; + textWidth = paint.MeasureText(Content) + Spacing; } - return new SKSize(RadioSize + textWidth, Math.Max(RadioSize, FontSize * 1.5f)); } } diff --git a/Views/SkiaScrollView.cs b/Views/SkiaScrollView.cs index ac63548..3aa9887 100644 --- a/Views/SkiaScrollView.cs +++ b/Views/SkiaScrollView.cs @@ -6,16 +6,132 @@ using SkiaSharp; namespace Microsoft.Maui.Platform; /// -/// Skia-rendered scroll view container. +/// Skia-rendered scroll view container with full XAML styling support. /// public class SkiaScrollView : SkiaView { + #region BindableProperties + + /// + /// Bindable property for Orientation. + /// + public static readonly BindableProperty OrientationProperty = + BindableProperty.Create( + nameof(Orientation), + typeof(ScrollOrientation), + typeof(SkiaScrollView), + ScrollOrientation.Both, + propertyChanged: (b, o, n) => ((SkiaScrollView)b).InvalidateMeasure()); + + /// + /// Bindable property for HorizontalScrollBarVisibility. + /// + public static readonly BindableProperty HorizontalScrollBarVisibilityProperty = + BindableProperty.Create( + nameof(HorizontalScrollBarVisibility), + typeof(ScrollBarVisibility), + typeof(SkiaScrollView), + ScrollBarVisibility.Auto, + propertyChanged: (b, o, n) => ((SkiaScrollView)b).Invalidate()); + + /// + /// Bindable property for VerticalScrollBarVisibility. + /// + public static readonly BindableProperty VerticalScrollBarVisibilityProperty = + BindableProperty.Create( + nameof(VerticalScrollBarVisibility), + typeof(ScrollBarVisibility), + typeof(SkiaScrollView), + ScrollBarVisibility.Auto, + propertyChanged: (b, o, n) => ((SkiaScrollView)b).Invalidate()); + + /// + /// Bindable property for ScrollBarColor. + /// + public static readonly BindableProperty ScrollBarColorProperty = + BindableProperty.Create( + nameof(ScrollBarColor), + typeof(SKColor), + typeof(SkiaScrollView), + new SKColor(0x80, 0x80, 0x80, 0x80), + propertyChanged: (b, o, n) => ((SkiaScrollView)b).Invalidate()); + + /// + /// Bindable property for ScrollBarWidth. + /// + public static readonly BindableProperty ScrollBarWidthProperty = + BindableProperty.Create( + nameof(ScrollBarWidth), + typeof(float), + typeof(SkiaScrollView), + 8f, + propertyChanged: (b, o, n) => ((SkiaScrollView)b).Invalidate()); + + #endregion + + #region Properties + + /// + /// Gets or sets the scroll orientation. + /// + public ScrollOrientation Orientation + { + get => (ScrollOrientation)GetValue(OrientationProperty); + set => SetValue(OrientationProperty, value); + } + + /// + /// Gets or sets whether to show horizontal scrollbar. + /// + public ScrollBarVisibility HorizontalScrollBarVisibility + { + get => (ScrollBarVisibility)GetValue(HorizontalScrollBarVisibilityProperty); + set => SetValue(HorizontalScrollBarVisibilityProperty, value); + } + + /// + /// Gets or sets whether to show vertical scrollbar. + /// + public ScrollBarVisibility VerticalScrollBarVisibility + { + get => (ScrollBarVisibility)GetValue(VerticalScrollBarVisibilityProperty); + set => SetValue(VerticalScrollBarVisibilityProperty, value); + } + + /// + /// Scrollbar color. + /// + public SKColor ScrollBarColor + { + get => (SKColor)GetValue(ScrollBarColorProperty); + set => SetValue(ScrollBarColorProperty, value); + } + + /// + /// Scrollbar width. + /// + public float ScrollBarWidth + { + get => (float)GetValue(ScrollBarWidthProperty); + set => SetValue(ScrollBarWidthProperty, value); + } + + #endregion + private SkiaView? _content; private float _scrollX; private float _scrollY; private float _velocityX; private float _velocityY; private bool _isDragging; + private bool _isDraggingVerticalScrollbar; + private bool _isDraggingHorizontalScrollbar; + private float _scrollbarDragStartY; + private float _scrollbarDragStartScrollY; + private float _scrollbarDragStartX; + private float _scrollbarDragStartScrollX; + private float _scrollbarDragAvailableTrack; // Cache to prevent stutter + private float _scrollbarDragScrollableExtent; // Cache to prevent stutter private float _lastPointerX; private float _lastPointerY; @@ -35,14 +151,36 @@ public class SkiaScrollView : SkiaView _content = value; if (_content != null) + { _content.Parent = this; + // Propagate binding context to new content + if (BindingContext != null) + { + SetInheritedBindingContext(_content, BindingContext); + } + } + InvalidateMeasure(); Invalidate(); } } } + /// + /// Called when binding context changes. Propagates to content. + /// + protected override void OnBindingContextChanged() + { + base.OnBindingContextChanged(); + + // Propagate binding context to content + if (_content != null) + { + SetInheritedBindingContext(_content, BindingContext); + } + } + /// /// Gets or sets the horizontal scroll position. /// @@ -82,43 +220,39 @@ public class SkiaScrollView : SkiaView /// /// Gets the maximum horizontal scroll extent. /// - public float ScrollableWidth => Math.Max(0, ContentSize.Width - Bounds.Width); + public float ScrollableWidth + { + get + { + // Handle infinite or NaN bounds - use a reasonable default viewport + var viewportWidth = float.IsInfinity(Bounds.Width) || float.IsNaN(Bounds.Width) || Bounds.Width <= 0 + ? 800f + : Bounds.Width; + return Math.Max(0, ContentSize.Width - viewportWidth); + } + } /// /// Gets the maximum vertical scroll extent. /// - public float ScrollableHeight => Math.Max(0, ContentSize.Height - Bounds.Height); + public float ScrollableHeight + { + get + { + // Handle infinite, NaN, or unreasonably large bounds - use a reasonable default viewport + var boundsHeight = Bounds.Height; + var viewportHeight = (float.IsInfinity(boundsHeight) || float.IsNaN(boundsHeight) || boundsHeight <= 0 || boundsHeight > 10000) + ? 544f // Default viewport height (600 - 56 for shell header) + : boundsHeight; + return Math.Max(0, ContentSize.Height - viewportHeight); + } + } /// /// Gets the content size. /// public SKSize ContentSize { get; private set; } - /// - /// Gets or sets the scroll orientation. - /// - public ScrollOrientation Orientation { get; set; } = ScrollOrientation.Both; - - /// - /// Gets or sets whether to show horizontal scrollbar. - /// - public ScrollBarVisibility HorizontalScrollBarVisibility { get; set; } = ScrollBarVisibility.Auto; - - /// - /// Gets or sets whether to show vertical scrollbar. - /// - public ScrollBarVisibility VerticalScrollBarVisibility { get; set; } = ScrollBarVisibility.Auto; - - /// - /// Scrollbar color. - /// - public SKColor ScrollBarColor { get; set; } = new SKColor(0x80, 0x80, 0x80, 0x80); - - /// - /// Scrollbar width. - /// - public float ScrollBarWidth { get; set; } = 8; - /// /// Event raised when scroll position changes. /// @@ -133,6 +267,19 @@ public class SkiaScrollView : SkiaView // Draw content with scroll offset if (_content != null) { + // Ensure content is measured and arranged + var availableSize = new SKSize(bounds.Width, float.PositiveInfinity); + _content.Measure(availableSize); + + // Apply content's margin + var margin = _content.Margin; + var contentBounds = new SKRect( + bounds.Left + (float)margin.Left, + bounds.Top + (float)margin.Top, + bounds.Left + Math.Max(bounds.Width, _content.DesiredSize.Width) - (float)margin.Right, + bounds.Top + Math.Max(bounds.Height, _content.DesiredSize.Height) - (float)margin.Bottom); + _content.Arrange(contentBounds); + canvas.Save(); canvas.Translate(-_scrollX, -_scrollY); _content.Draw(canvas); @@ -233,22 +380,89 @@ public class SkiaScrollView : SkiaView public override void OnScroll(ScrollEventArgs e) { + Console.WriteLine($"[SkiaScrollView] OnScroll - DeltaY={e.DeltaY}, ScrollableHeight={ScrollableHeight}, ContentSize={ContentSize}, Bounds={Bounds}"); + // Handle mouse wheel scrolling var deltaMultiplier = 40f; // Scroll speed + bool scrolled = false; - if (Orientation != ScrollOrientation.Horizontal) + if (Orientation != ScrollOrientation.Horizontal && ScrollableHeight > 0) { + var oldScrollY = _scrollY; ScrollY += e.DeltaY * deltaMultiplier; + Console.WriteLine($"[SkiaScrollView] ScrollY changed: {oldScrollY} -> {_scrollY}"); + if (_scrollY != oldScrollY) + scrolled = true; } - if (Orientation != ScrollOrientation.Vertical) + if (Orientation != ScrollOrientation.Vertical && ScrollableWidth > 0) { + var oldScrollX = _scrollX; ScrollX += e.DeltaX * deltaMultiplier; + if (_scrollX != oldScrollX) + scrolled = true; } + + // Mark as handled so parent scroll views don't also scroll + if (scrolled) + e.Handled = true; } public override void OnPointerPressed(PointerEventArgs e) { + // Check if clicking on vertical scrollbar thumb + if (ShouldShowVerticalScrollbar() && ScrollableHeight > 0) + { + var thumbBounds = GetVerticalScrollbarThumbBounds(); + if (thumbBounds.Contains(e.X, e.Y)) + { + _isDraggingVerticalScrollbar = true; + _scrollbarDragStartY = e.Y; + _scrollbarDragStartScrollY = _scrollY; + // Cache values to prevent stutter from floating-point recalculations + var hasHorizontal = ShouldShowHorizontalScrollbar(); + var trackHeight = Bounds.Height - (hasHorizontal ? ScrollBarWidth : 0); + var thumbHeight = Math.Max(20, (Bounds.Height / ContentSize.Height) * trackHeight); + _scrollbarDragAvailableTrack = trackHeight - thumbHeight; + _scrollbarDragScrollableExtent = ScrollableHeight; + return; + } + } + + // Check if clicking on horizontal scrollbar thumb + if (ShouldShowHorizontalScrollbar() && ScrollableWidth > 0) + { + var thumbBounds = GetHorizontalScrollbarThumbBounds(); + if (thumbBounds.Contains(e.X, e.Y)) + { + _isDraggingHorizontalScrollbar = true; + _scrollbarDragStartX = e.X; + _scrollbarDragStartScrollX = _scrollX; + // Cache values to prevent stutter from floating-point recalculations + var hasVertical = ShouldShowVerticalScrollbar(); + var trackWidth = Bounds.Width - (hasVertical ? ScrollBarWidth : 0); + var thumbWidth = Math.Max(20, (Bounds.Width / ContentSize.Width) * trackWidth); + _scrollbarDragAvailableTrack = trackWidth - thumbWidth; + _scrollbarDragScrollableExtent = ScrollableWidth; + return; + } + } + + // Forward click to content first + if (_content != null) + { + // Translate coordinates for scroll offset + var contentE = new PointerEventArgs(e.X + _scrollX, e.Y + _scrollY, e.Button); + var hit = _content.HitTest(contentE.X, contentE.Y); + if (hit != null && hit != _content) + { + // A child view was hit - forward the event to it + hit.OnPointerPressed(contentE); + return; + } + } + + // Regular content dragging _isDragging = true; _lastPointerX = e.X; _lastPointerY = e.Y; @@ -258,19 +472,44 @@ public class SkiaScrollView : SkiaView public override void OnPointerMoved(PointerEventArgs e) { + // Handle vertical scrollbar dragging - use cached values to prevent stutter + if (_isDraggingVerticalScrollbar) + { + if (_scrollbarDragAvailableTrack > 0) + { + var deltaY = e.Y - _scrollbarDragStartY; + var scrollDelta = (deltaY / _scrollbarDragAvailableTrack) * _scrollbarDragScrollableExtent; + ScrollY = _scrollbarDragStartScrollY + scrollDelta; + } + return; + } + + // Handle horizontal scrollbar dragging - use cached values to prevent stutter + if (_isDraggingHorizontalScrollbar) + { + if (_scrollbarDragAvailableTrack > 0) + { + var deltaX = e.X - _scrollbarDragStartX; + var scrollDelta = (deltaX / _scrollbarDragAvailableTrack) * _scrollbarDragScrollableExtent; + ScrollX = _scrollbarDragStartScrollX + scrollDelta; + } + return; + } + + // Handle content dragging if (!_isDragging) return; - var deltaX = _lastPointerX - e.X; - var deltaY = _lastPointerY - e.Y; + var contentDeltaX = _lastPointerX - e.X; + var contentDeltaY = _lastPointerY - e.Y; - _velocityX = deltaX; - _velocityY = deltaY; + _velocityX = contentDeltaX; + _velocityY = contentDeltaY; if (Orientation != ScrollOrientation.Horizontal) - ScrollY += deltaY; + ScrollY += contentDeltaY; if (Orientation != ScrollOrientation.Vertical) - ScrollX += deltaX; + ScrollX += contentDeltaX; _lastPointerX = e.X; _lastPointerY = e.Y; @@ -279,14 +518,62 @@ public class SkiaScrollView : SkiaView public override void OnPointerReleased(PointerEventArgs e) { _isDragging = false; + _isDraggingVerticalScrollbar = false; + _isDraggingHorizontalScrollbar = false; // Momentum scrolling could be added here } + private SKRect GetVerticalScrollbarThumbBounds() + { + var hasHorizontal = ShouldShowHorizontalScrollbar(); + var trackHeight = Bounds.Height - (hasHorizontal ? ScrollBarWidth : 0); + var thumbHeight = Math.Max(20, (Bounds.Height / ContentSize.Height) * trackHeight); + var thumbY = ScrollableHeight > 0 ? (ScrollY / ScrollableHeight) * (trackHeight - thumbHeight) : 0; + + return new SKRect( + Bounds.Right - ScrollBarWidth, + Bounds.Top + thumbY, + Bounds.Right, + Bounds.Top + thumbY + thumbHeight); + } + + private SKRect GetHorizontalScrollbarThumbBounds() + { + var hasVertical = ShouldShowVerticalScrollbar(); + var trackWidth = Bounds.Width - (hasVertical ? ScrollBarWidth : 0); + var thumbWidth = Math.Max(20, (Bounds.Width / ContentSize.Width) * trackWidth); + var thumbX = ScrollableWidth > 0 ? (ScrollX / ScrollableWidth) * (trackWidth - thumbWidth) : 0; + + return new SKRect( + Bounds.Left + thumbX, + Bounds.Bottom - ScrollBarWidth, + Bounds.Left + thumbX + thumbWidth, + Bounds.Bottom); + } + public override SkiaView? HitTest(float x, float y) { - if (!IsVisible || !Bounds.Contains(new SKPoint(x, y))) + if (!IsVisible || !IsEnabled || !Bounds.Contains(new SKPoint(x, y))) return null; + // Check scrollbar areas FIRST before content + // This ensures scrollbar clicks are handled by the ScrollView, not content underneath + if (ShouldShowVerticalScrollbar() && ScrollableHeight > 0) + { + var thumbBounds = GetVerticalScrollbarThumbBounds(); + // Check if click is in the scrollbar track area (not just thumb) + var trackArea = new SKRect(Bounds.Right - ScrollBarWidth, Bounds.Top, Bounds.Right, Bounds.Bottom); + if (trackArea.Contains(x, y)) + return this; + } + + if (ShouldShowHorizontalScrollbar() && ScrollableWidth > 0) + { + var trackArea = new SKRect(Bounds.Left, Bounds.Bottom - ScrollBarWidth, Bounds.Right, Bounds.Bottom); + if (trackArea.Contains(x, y)) + return this; + } + // Hit test content with scroll offset if (_content != null) { @@ -360,35 +647,88 @@ public class SkiaScrollView : SkiaView { if (_content != null) { - // Give content unlimited size in scrollable directions - var contentAvailable = new SKSize( - Orientation == ScrollOrientation.Vertical ? availableSize.Width : float.PositiveInfinity, - Orientation == ScrollOrientation.Horizontal ? availableSize.Height : float.PositiveInfinity); + // For responsive layout: + // - Vertical: give content viewport width, infinite height + // - Horizontal: give content infinite width, viewport height + // - Both: give content viewport width first (for responsive layout), + // but if content exceeds it, horizontal scrollbar appears + // - Neither: give content exact viewport size - ContentSize = _content.Measure(contentAvailable); + float contentWidth, contentHeight; + + switch (Orientation) + { + case ScrollOrientation.Horizontal: + contentWidth = float.PositiveInfinity; + contentHeight = float.IsInfinity(availableSize.Height) ? 400f : availableSize.Height; + break; + case ScrollOrientation.Neither: + contentWidth = float.IsInfinity(availableSize.Width) ? 400f : availableSize.Width; + contentHeight = float.IsInfinity(availableSize.Height) ? 400f : availableSize.Height; + break; + case ScrollOrientation.Both: + // For Both: first measure with viewport width to get responsive layout + // Content can still exceed viewport if it has minimum width constraints + contentWidth = float.IsInfinity(availableSize.Width) ? 800f : availableSize.Width; + contentHeight = float.PositiveInfinity; + break; + case ScrollOrientation.Vertical: + default: + contentWidth = float.IsInfinity(availableSize.Width) ? 800f : availableSize.Width; + contentHeight = float.PositiveInfinity; + break; + } + + ContentSize = _content.Measure(new SKSize(contentWidth, contentHeight)); } else { ContentSize = SKSize.Empty; } - return availableSize; + // Return available size, but clamp infinite dimensions + // IMPORTANT: When available is infinite, return a reasonable viewport size, NOT content size + // A ScrollView should NOT expand to fit its content - it should stay at a fixed viewport + // and scroll the content. Use a default viewport size when parent gives infinity. + const float DefaultViewportWidth = 400f; + const float DefaultViewportHeight = 400f; + + var width = float.IsInfinity(availableSize.Width) || float.IsNaN(availableSize.Width) + ? Math.Min(ContentSize.Width, DefaultViewportWidth) + : availableSize.Width; + var height = float.IsInfinity(availableSize.Height) || float.IsNaN(availableSize.Height) + ? Math.Min(ContentSize.Height, DefaultViewportHeight) + : availableSize.Height; + + return new SKSize(width, height); } protected override SKRect ArrangeOverride(SKRect bounds) { + + // CRITICAL: If bounds has infinite height, use a fixed viewport size + // NOT ContentSize.Height - that would make ScrollableHeight = 0 + const float DefaultViewportHeight = 544f; // 600 - 56 for shell header + var actualBounds = bounds; + if (float.IsInfinity(bounds.Height) || float.IsNaN(bounds.Height)) + { + Console.WriteLine($"[SkiaScrollView] WARNING: Infinite/NaN height, using default viewport={DefaultViewportHeight}"); + actualBounds = new SKRect(bounds.Left, bounds.Top, bounds.Right, bounds.Top + DefaultViewportHeight); + } + if (_content != null) { - // Arrange content at its full size, starting from scroll position + // Apply content's margin and arrange content at its full size + var margin = _content.Margin; var contentBounds = new SKRect( - bounds.Left, - bounds.Top, - bounds.Left + Math.Max(bounds.Width, ContentSize.Width), - bounds.Top + Math.Max(bounds.Height, ContentSize.Height)); + actualBounds.Left + (float)margin.Left, + actualBounds.Top + (float)margin.Top, + actualBounds.Left + Math.Max(actualBounds.Width, ContentSize.Width) - (float)margin.Right, + actualBounds.Top + Math.Max(actualBounds.Height, ContentSize.Height) - (float)margin.Bottom); _content.Arrange(contentBounds); } - return bounds; + return actualBounds; } } diff --git a/Views/SkiaSearchBar.cs b/Views/SkiaSearchBar.cs index 5e0a9e5..64824c9 100644 --- a/Views/SkiaSearchBar.cs +++ b/Views/SkiaSearchBar.cs @@ -55,9 +55,11 @@ public class SkiaSearchBar : SkiaView _entry = new SkiaEntry { Placeholder = "Search...", + EntryBackgroundColor = SKColors.Transparent, BackgroundColor = SKColors.Transparent, BorderColor = SKColors.Transparent, - FocusedBorderColor = SKColors.Transparent + FocusedBorderColor = SKColors.Transparent, + BorderWidth = 0 }; _entry.TextChanged += (s, e) => @@ -193,12 +195,24 @@ public class SkiaSearchBar : SkiaView return; } - // Forward to entry for text input focus + // Forward to entry for text input focus and selection _entry.IsFocused = true; IsFocused = true; + _entry.OnPointerPressed(e); Invalidate(); } + public override void OnPointerMoved(PointerEventArgs e) + { + if (!IsEnabled) return; + _entry.OnPointerMoved(e); + } + + public override void OnPointerReleased(PointerEventArgs e) + { + _entry.OnPointerReleased(e); + } + public override void OnTextInput(TextInputEventArgs e) { _entry.OnTextInput(e); diff --git a/Views/SkiaShell.cs b/Views/SkiaShell.cs index 56cdf8e..27eaabd 100644 --- a/Views/SkiaShell.cs +++ b/Views/SkiaShell.cs @@ -19,6 +19,9 @@ public class SkiaShell : SkiaLayoutView private int _selectedSectionIndex = 0; private int _selectedItemIndex = 0; + // Navigation stack for push/pop navigation + private readonly Stack<(SkiaView Content, string Title)> _navigationStack = new(); + /// /// Gets or sets whether the flyout is presented. /// @@ -93,6 +96,12 @@ public class SkiaShell : SkiaLayoutView /// public bool TabBarIsVisible { get; set; } = false; + /// + /// Gets or sets the padding applied to page content. + /// Default is 16 pixels on all sides. + /// + public float ContentPadding { get; set; } = 16f; + /// /// Current title displayed in the navigation bar. /// @@ -103,6 +112,11 @@ public class SkiaShell : SkiaLayoutView /// public IReadOnlyList Sections => _sections; + /// + /// Gets the currently selected section index. + /// + public int CurrentSectionIndex => _selectedSectionIndex; + /// /// Event raised when FlyoutIsPresented changes. /// @@ -147,6 +161,9 @@ public class SkiaShell : SkiaLayoutView var section = _sections[sectionIndex]; if (itemIndex < 0 || itemIndex >= section.Items.Count) return; + // Clear navigation stack when navigating to a new section + _navigationStack.Clear(); + _selectedSectionIndex = sectionIndex; _selectedItemIndex = itemIndex; @@ -193,6 +210,66 @@ public class SkiaShell : SkiaLayoutView } } + /// + /// Gets whether there are pages on the navigation stack. + /// + public bool CanGoBack => _navigationStack.Count > 0; + + /// + /// Gets the current navigation stack depth. + /// + public int NavigationStackDepth => _navigationStack.Count; + + /// + /// Pushes a new page onto the navigation stack. + /// + public void PushAsync(SkiaView page, string title) + { + // Save current content to stack + if (_currentContent != null) + { + _navigationStack.Push((_currentContent, Title)); + } + + // Set new content + SetCurrentContent(page); + Title = title; + Invalidate(); + } + + /// + /// Pops the current page from the navigation stack. + /// + public bool PopAsync() + { + if (_navigationStack.Count == 0) return false; + + var (previousContent, previousTitle) = _navigationStack.Pop(); + SetCurrentContent(previousContent); + Title = previousTitle; + Invalidate(); + return true; + } + + /// + /// Pops all pages from the navigation stack, returning to the root. + /// + public void PopToRootAsync() + { + if (_navigationStack.Count == 0) return; + + // Get the root content + (SkiaView Content, string Title) root = default; + while (_navigationStack.Count > 0) + { + root = _navigationStack.Pop(); + } + + SetCurrentContent(root.Content); + Title = root.Title; + Invalidate(); + } + private void SetCurrentContent(SkiaView? content) { if (_currentContent != null) @@ -226,16 +303,19 @@ public class SkiaShell : SkiaLayoutView protected override SKRect ArrangeOverride(SKRect bounds) { - // Arrange current content + Console.WriteLine($"[SkiaShell] ArrangeOverride - bounds={bounds}"); + + // Arrange current content with padding if (_currentContent != null) { - float contentTop = bounds.Top + (NavBarIsVisible ? NavBarHeight : 0); - float contentBottom = bounds.Bottom - (TabBarIsVisible ? TabBarHeight : 0); + float contentTop = bounds.Top + (NavBarIsVisible ? NavBarHeight : 0) + ContentPadding; + float contentBottom = bounds.Bottom - (TabBarIsVisible ? TabBarHeight : 0) - ContentPadding; var contentBounds = new SKRect( - bounds.Left, + bounds.Left + ContentPadding, contentTop, - bounds.Right, + bounds.Right - ContentPadding, contentBottom); + Console.WriteLine($"[SkiaShell] Arranging content with bounds={contentBounds}, padding={ContentPadding}"); _currentContent.Arrange(contentBounds); } @@ -288,20 +368,41 @@ public class SkiaShell : SkiaLayoutView }; canvas.DrawRect(navBarBounds, bgPaint); - // Draw hamburger menu icon - if (FlyoutBehavior == ShellFlyoutBehavior.Flyout) + // Draw nav icon (back arrow if can go back, else hamburger menu if flyout enabled) + using var iconPaint = new SKPaint { - using var iconPaint = new SKPaint + Color = NavBarTextColor, + Style = SKPaintStyle.Stroke, + StrokeWidth = 2, + StrokeCap = SKStrokeCap.Round, + IsAntialias = true + }; + + float iconLeft = navBarBounds.Left + 16; + float iconCenter = navBarBounds.MidY; + + if (CanGoBack) + { + // Draw iOS-style back chevron "<" + using var chevronPaint = new SKPaint { Color = NavBarTextColor, Style = SKPaintStyle.Stroke, - StrokeWidth = 2, + StrokeWidth = 2.5f, + StrokeCap = SKStrokeCap.Round, + StrokeJoin = SKStrokeJoin.Round, IsAntialias = true }; - float iconLeft = navBarBounds.Left + 16; - float iconCenter = navBarBounds.MidY; - + // Clean chevron pointing left + float chevronX = iconLeft + 6; + float chevronSize = 10; + canvas.DrawLine(chevronX + chevronSize, iconCenter - chevronSize, chevronX, iconCenter, chevronPaint); + canvas.DrawLine(chevronX, iconCenter, chevronX + chevronSize, iconCenter + chevronSize, chevronPaint); + } + else if (FlyoutBehavior == ShellFlyoutBehavior.Flyout) + { + // Draw hamburger menu icon canvas.DrawLine(iconLeft, iconCenter - 8, iconLeft + 18, iconCenter - 8, iconPaint); canvas.DrawLine(iconLeft, iconCenter, iconLeft + 18, iconCenter, iconPaint); canvas.DrawLine(iconLeft, iconCenter + 8, iconLeft + 18, iconCenter + 8, iconPaint); @@ -316,7 +417,7 @@ public class SkiaShell : SkiaLayoutView FakeBoldText = true }; - float titleX = FlyoutBehavior == ShellFlyoutBehavior.Flyout ? navBarBounds.Left + 56 : navBarBounds.Left + 16; + float titleX = (CanGoBack || FlyoutBehavior == ShellFlyoutBehavior.Flyout) ? navBarBounds.Left + 56 : navBarBounds.Left + 16; float titleY = navBarBounds.MidY + 6; canvas.DrawText(Title, titleX, titleY, titlePaint); } @@ -427,7 +528,8 @@ public class SkiaShell : SkiaLayoutView Color = new SKColor(33, 150, 243, 30), Style = SKPaintStyle.Fill }; - canvas.DrawRect(flyoutBounds.Left, itemY, flyoutBounds.Right, itemY + itemHeight, selectionPaint); + var selectionRect = new SKRect(flyoutBounds.Left, itemY, flyoutBounds.Right, itemY + itemHeight); + canvas.DrawRect(selectionRect, selectionPaint); } itemTextPaint.Color = isSelected ? NavBarBackgroundColor : new SKColor(33, 33, 33); @@ -518,12 +620,23 @@ public class SkiaShell : SkiaLayoutView } } - // Check nav bar hamburger tap - if (NavBarIsVisible && e.Y < Bounds.Top + NavBarHeight && e.X < 56 && FlyoutBehavior == ShellFlyoutBehavior.Flyout) + // Check nav bar icon tap (back button or hamburger menu) + if (NavBarIsVisible && e.Y < Bounds.Top + NavBarHeight && e.X < 56) { - FlyoutIsPresented = !FlyoutIsPresented; - e.Handled = true; - return; + if (CanGoBack) + { + // Back button pressed + PopAsync(); + e.Handled = true; + return; + } + else if (FlyoutBehavior == ShellFlyoutBehavior.Flyout) + { + // Hamburger menu pressed + FlyoutIsPresented = !FlyoutIsPresented; + e.Handled = true; + return; + } } // Check tab bar tap diff --git a/Views/SkiaSlider.cs b/Views/SkiaSlider.cs index 53faa2c..9b30e74 100644 --- a/Views/SkiaSlider.cs +++ b/Views/SkiaSlider.cs @@ -6,40 +6,214 @@ using SkiaSharp; namespace Microsoft.Maui.Platform; /// -/// Skia-rendered slider control. +/// Skia-rendered slider control with full XAML styling support. /// public class SkiaSlider : SkiaView { - private bool _isDragging; - private double _value; + #region BindableProperties - public double Minimum { get; set; } = 0; - public double Maximum { get; set; } = 100; + /// + /// Bindable property for Minimum. + /// + public static readonly BindableProperty MinimumProperty = + BindableProperty.Create( + nameof(Minimum), + typeof(double), + typeof(SkiaSlider), + 0.0, + propertyChanged: (b, o, n) => ((SkiaSlider)b).OnRangeChanged()); - public double Value + /// + /// Bindable property for Maximum. + /// + public static readonly BindableProperty MaximumProperty = + BindableProperty.Create( + nameof(Maximum), + typeof(double), + typeof(SkiaSlider), + 100.0, + propertyChanged: (b, o, n) => ((SkiaSlider)b).OnRangeChanged()); + + /// + /// Bindable property for Value. + /// + public static readonly BindableProperty ValueProperty = + BindableProperty.Create( + nameof(Value), + typeof(double), + typeof(SkiaSlider), + 0.0, + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaSlider)b).OnValuePropertyChanged((double)o, (double)n)); + + /// + /// Bindable property for TrackColor. + /// + public static readonly BindableProperty TrackColorProperty = + BindableProperty.Create( + nameof(TrackColor), + typeof(SKColor), + typeof(SkiaSlider), + new SKColor(0xE0, 0xE0, 0xE0), + propertyChanged: (b, o, n) => ((SkiaSlider)b).Invalidate()); + + /// + /// Bindable property for ActiveTrackColor. + /// + public static readonly BindableProperty ActiveTrackColorProperty = + BindableProperty.Create( + nameof(ActiveTrackColor), + typeof(SKColor), + typeof(SkiaSlider), + new SKColor(0x21, 0x96, 0xF3), + propertyChanged: (b, o, n) => ((SkiaSlider)b).Invalidate()); + + /// + /// Bindable property for ThumbColor. + /// + public static readonly BindableProperty ThumbColorProperty = + BindableProperty.Create( + nameof(ThumbColor), + typeof(SKColor), + typeof(SkiaSlider), + new SKColor(0x21, 0x96, 0xF3), + propertyChanged: (b, o, n) => ((SkiaSlider)b).Invalidate()); + + /// + /// Bindable property for DisabledColor. + /// + public static readonly BindableProperty DisabledColorProperty = + BindableProperty.Create( + nameof(DisabledColor), + typeof(SKColor), + typeof(SkiaSlider), + new SKColor(0xBD, 0xBD, 0xBD), + propertyChanged: (b, o, n) => ((SkiaSlider)b).Invalidate()); + + /// + /// Bindable property for TrackHeight. + /// + public static readonly BindableProperty TrackHeightProperty = + BindableProperty.Create( + nameof(TrackHeight), + typeof(float), + typeof(SkiaSlider), + 4f, + propertyChanged: (b, o, n) => ((SkiaSlider)b).Invalidate()); + + /// + /// Bindable property for ThumbRadius. + /// + public static readonly BindableProperty ThumbRadiusProperty = + BindableProperty.Create( + nameof(ThumbRadius), + typeof(float), + typeof(SkiaSlider), + 10f, + propertyChanged: (b, o, n) => ((SkiaSlider)b).InvalidateMeasure()); + + #endregion + + #region Properties + + /// + /// Gets or sets the minimum value. + /// + public double Minimum { - get => _value; - set - { - var clamped = Math.Clamp(value, Minimum, Maximum); - if (_value != clamped) - { - _value = clamped; - ValueChanged?.Invoke(this, new SliderValueChangedEventArgs(_value)); - Invalidate(); - } - } + get => (double)GetValue(MinimumProperty); + set => SetValue(MinimumProperty, value); } - public SKColor TrackColor { get; set; } = new SKColor(0xE0, 0xE0, 0xE0); - public SKColor ActiveTrackColor { get; set; } = new SKColor(0x21, 0x96, 0xF3); - public SKColor ThumbColor { get; set; } = new SKColor(0x21, 0x96, 0xF3); - public SKColor DisabledColor { get; set; } = new SKColor(0xBD, 0xBD, 0xBD); - public float TrackHeight { get; set; } = 4; - public float ThumbRadius { get; set; } = 10; + /// + /// Gets or sets the maximum value. + /// + public double Maximum + { + get => (double)GetValue(MaximumProperty); + set => SetValue(MaximumProperty, value); + } + /// + /// Gets or sets the current value. + /// + public double Value + { + get => (double)GetValue(ValueProperty); + set => SetValue(ValueProperty, Math.Clamp(value, Minimum, Maximum)); + } + + /// + /// Gets or sets the track color. + /// + public SKColor TrackColor + { + get => (SKColor)GetValue(TrackColorProperty); + set => SetValue(TrackColorProperty, value); + } + + /// + /// Gets or sets the active track color. + /// + public SKColor ActiveTrackColor + { + get => (SKColor)GetValue(ActiveTrackColorProperty); + set => SetValue(ActiveTrackColorProperty, value); + } + + /// + /// Gets or sets the thumb color. + /// + public SKColor ThumbColor + { + get => (SKColor)GetValue(ThumbColorProperty); + set => SetValue(ThumbColorProperty, value); + } + + /// + /// Gets or sets the disabled color. + /// + public SKColor DisabledColor + { + get => (SKColor)GetValue(DisabledColorProperty); + set => SetValue(DisabledColorProperty, value); + } + + /// + /// Gets or sets the track height. + /// + public float TrackHeight + { + get => (float)GetValue(TrackHeightProperty); + set => SetValue(TrackHeightProperty, value); + } + + /// + /// Gets or sets the thumb radius. + /// + public float ThumbRadius + { + get => (float)GetValue(ThumbRadiusProperty); + set => SetValue(ThumbRadiusProperty, value); + } + + #endregion + + private bool _isDragging; + + /// + /// Event raised when the value changes. + /// public event EventHandler? ValueChanged; + + /// + /// Event raised when drag starts. + /// public event EventHandler? DragStarted; + + /// + /// Event raised when drag completes. + /// public event EventHandler? DragCompleted; public SkiaSlider() @@ -47,6 +221,23 @@ public class SkiaSlider : SkiaView IsFocusable = true; } + private void OnRangeChanged() + { + // Clamp value to new range + var clamped = Math.Clamp(Value, Minimum, Maximum); + if (Value != clamped) + { + Value = clamped; + } + Invalidate(); + } + + private void OnValuePropertyChanged(double oldValue, double newValue) + { + ValueChanged?.Invoke(this, new SliderValueChangedEventArgs(newValue)); + Invalidate(); + } + protected override void OnDraw(SKCanvas canvas, SKRect bounds) { var trackY = bounds.MidY; @@ -54,7 +245,7 @@ public class SkiaSlider : SkiaView var trackRight = bounds.Right - ThumbRadius; var trackWidth = trackRight - trackLeft; - var percentage = (Value - Minimum) / (Maximum - Minimum); + var percentage = Maximum > Minimum ? (Value - Minimum) / (Maximum - Minimum) : 0; var thumbX = trackLeft + (float)(percentage * trackWidth); // Draw inactive track @@ -127,6 +318,7 @@ public class SkiaSlider : SkiaView _isDragging = true; UpdateValueFromPosition(e.X); DragStarted?.Invoke(this, EventArgs.Empty); + SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.Pressed); } public override void OnPointerMoved(PointerEventArgs e) @@ -141,6 +333,7 @@ public class SkiaSlider : SkiaView { _isDragging = false; DragCompleted?.Invoke(this, EventArgs.Empty); + SkiaVisualStateManager.GoToState(this, IsEnabled ? SkiaVisualStateManager.CommonStates.Normal : SkiaVisualStateManager.CommonStates.Disabled); } } @@ -183,12 +376,21 @@ public class SkiaSlider : SkiaView } } + protected override void OnEnabledChanged() + { + base.OnEnabledChanged(); + SkiaVisualStateManager.GoToState(this, IsEnabled ? SkiaVisualStateManager.CommonStates.Normal : SkiaVisualStateManager.CommonStates.Disabled); + } + protected override SKSize MeasureOverride(SKSize availableSize) { return new SKSize(200, ThumbRadius * 2 + 16); } } +/// +/// Event args for slider value changed events. +/// public class SliderValueChangedEventArgs : EventArgs { public double NewValue { get; } diff --git a/Views/SkiaStepper.cs b/Views/SkiaStepper.cs index 5991565..7d77f4a 100644 --- a/Views/SkiaStepper.cs +++ b/Views/SkiaStepper.cs @@ -10,66 +10,136 @@ namespace Microsoft.Maui.Platform; /// public class SkiaStepper : SkiaView { - private double _value; - private double _minimum; - private double _maximum = 100; - private double _increment = 1; - private bool _isMinusPressed; - private bool _isPlusPressed; + #region BindableProperties - // Styling - public SKColor ButtonBackgroundColor { get; set; } = new SKColor(0xE0, 0xE0, 0xE0); - public SKColor ButtonPressedColor { get; set; } = new SKColor(0xBD, 0xBD, 0xBD); - public SKColor ButtonDisabledColor { get; set; } = new SKColor(0xF5, 0xF5, 0xF5); - public SKColor BorderColor { get; set; } = new SKColor(0xBD, 0xBD, 0xBD); - public SKColor SymbolColor { get; set; } = SKColors.Black; - public SKColor SymbolDisabledColor { get; set; } = new SKColor(0xBD, 0xBD, 0xBD); - public float CornerRadius { get; set; } = 4; - public float ButtonWidth { get; set; } = 40; + public static readonly BindableProperty ValueProperty = + BindableProperty.Create(nameof(Value), typeof(double), typeof(SkiaStepper), 0.0, BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaStepper)b).OnValuePropertyChanged((double)o, (double)n)); + + public static readonly BindableProperty MinimumProperty = + BindableProperty.Create(nameof(Minimum), typeof(double), typeof(SkiaStepper), 0.0, + propertyChanged: (b, o, n) => ((SkiaStepper)b).OnRangeChanged()); + + public static readonly BindableProperty MaximumProperty = + BindableProperty.Create(nameof(Maximum), typeof(double), typeof(SkiaStepper), 100.0, + propertyChanged: (b, o, n) => ((SkiaStepper)b).OnRangeChanged()); + + public static readonly BindableProperty IncrementProperty = + BindableProperty.Create(nameof(Increment), typeof(double), typeof(SkiaStepper), 1.0); + + public static readonly BindableProperty ButtonBackgroundColorProperty = + BindableProperty.Create(nameof(ButtonBackgroundColor), typeof(SKColor), typeof(SkiaStepper), new SKColor(0xE0, 0xE0, 0xE0), + propertyChanged: (b, o, n) => ((SkiaStepper)b).Invalidate()); + + public static readonly BindableProperty ButtonPressedColorProperty = + BindableProperty.Create(nameof(ButtonPressedColor), typeof(SKColor), typeof(SkiaStepper), new SKColor(0xBD, 0xBD, 0xBD), + propertyChanged: (b, o, n) => ((SkiaStepper)b).Invalidate()); + + public static readonly BindableProperty ButtonDisabledColorProperty = + BindableProperty.Create(nameof(ButtonDisabledColor), typeof(SKColor), typeof(SkiaStepper), new SKColor(0xF5, 0xF5, 0xF5), + propertyChanged: (b, o, n) => ((SkiaStepper)b).Invalidate()); + + public static readonly BindableProperty BorderColorProperty = + BindableProperty.Create(nameof(BorderColor), typeof(SKColor), typeof(SkiaStepper), new SKColor(0xBD, 0xBD, 0xBD), + propertyChanged: (b, o, n) => ((SkiaStepper)b).Invalidate()); + + public static readonly BindableProperty SymbolColorProperty = + BindableProperty.Create(nameof(SymbolColor), typeof(SKColor), typeof(SkiaStepper), SKColors.Black, + propertyChanged: (b, o, n) => ((SkiaStepper)b).Invalidate()); + + public static readonly BindableProperty SymbolDisabledColorProperty = + BindableProperty.Create(nameof(SymbolDisabledColor), typeof(SKColor), typeof(SkiaStepper), new SKColor(0xBD, 0xBD, 0xBD), + propertyChanged: (b, o, n) => ((SkiaStepper)b).Invalidate()); + + public static readonly BindableProperty CornerRadiusProperty = + BindableProperty.Create(nameof(CornerRadius), typeof(float), typeof(SkiaStepper), 4f, + propertyChanged: (b, o, n) => ((SkiaStepper)b).Invalidate()); + + public static readonly BindableProperty ButtonWidthProperty = + BindableProperty.Create(nameof(ButtonWidth), typeof(float), typeof(SkiaStepper), 40f, + propertyChanged: (b, o, n) => ((SkiaStepper)b).InvalidateMeasure()); + + #endregion + + #region Properties public double Value { - get => _value; - set - { - var clamped = Math.Clamp(value, _minimum, _maximum); - if (_value != clamped) - { - _value = clamped; - ValueChanged?.Invoke(this, EventArgs.Empty); - Invalidate(); - } - } + get => (double)GetValue(ValueProperty); + set => SetValue(ValueProperty, Math.Clamp(value, Minimum, Maximum)); } public double Minimum { - get => _minimum; - set - { - _minimum = value; - if (_value < _minimum) Value = _minimum; - Invalidate(); - } + get => (double)GetValue(MinimumProperty); + set => SetValue(MinimumProperty, value); } public double Maximum { - get => _maximum; - set - { - _maximum = value; - if (_value > _maximum) Value = _maximum; - Invalidate(); - } + get => (double)GetValue(MaximumProperty); + set => SetValue(MaximumProperty, value); } public double Increment { - get => _increment; - set { _increment = Math.Max(0.001, value); Invalidate(); } + get => (double)GetValue(IncrementProperty); + set => SetValue(IncrementProperty, Math.Max(0.001, value)); } + public SKColor ButtonBackgroundColor + { + get => (SKColor)GetValue(ButtonBackgroundColorProperty); + set => SetValue(ButtonBackgroundColorProperty, value); + } + + public SKColor ButtonPressedColor + { + get => (SKColor)GetValue(ButtonPressedColorProperty); + set => SetValue(ButtonPressedColorProperty, value); + } + + public SKColor ButtonDisabledColor + { + get => (SKColor)GetValue(ButtonDisabledColorProperty); + set => SetValue(ButtonDisabledColorProperty, value); + } + + public SKColor BorderColor + { + get => (SKColor)GetValue(BorderColorProperty); + set => SetValue(BorderColorProperty, value); + } + + public SKColor SymbolColor + { + get => (SKColor)GetValue(SymbolColorProperty); + set => SetValue(SymbolColorProperty, value); + } + + public SKColor SymbolDisabledColor + { + get => (SKColor)GetValue(SymbolDisabledColorProperty); + set => SetValue(SymbolDisabledColorProperty, value); + } + + public float CornerRadius + { + get => (float)GetValue(CornerRadiusProperty); + set => SetValue(CornerRadiusProperty, value); + } + + public float ButtonWidth + { + get => (float)GetValue(ButtonWidthProperty); + set => SetValue(ButtonWidthProperty, value); + } + + #endregion + + private bool _isMinusPressed; + private bool _isPlusPressed; + public event EventHandler? ValueChanged; public SkiaStepper() @@ -77,19 +147,30 @@ public class SkiaStepper : SkiaView IsFocusable = true; } + private void OnValuePropertyChanged(double oldValue, double newValue) + { + ValueChanged?.Invoke(this, EventArgs.Empty); + Invalidate(); + } + + private void OnRangeChanged() + { + var clamped = Math.Clamp(Value, Minimum, Maximum); + if (Value != clamped) + { + Value = clamped; + } + Invalidate(); + } + protected override void OnDraw(SKCanvas canvas, SKRect bounds) { - var buttonHeight = bounds.Height; var minusRect = new SKRect(bounds.Left, bounds.Top, bounds.Left + ButtonWidth, bounds.Bottom); var plusRect = new SKRect(bounds.Right - ButtonWidth, bounds.Top, bounds.Right, bounds.Bottom); - // Draw minus button DrawButton(canvas, minusRect, "-", _isMinusPressed, !CanDecrement()); - - // Draw plus button DrawButton(canvas, plusRect, "+", _isPlusPressed, !CanIncrement()); - // Draw border using var borderPaint = new SKPaint { Color = BorderColor, @@ -98,29 +179,23 @@ public class SkiaStepper : SkiaView IsAntialias = true }; - // Overall border with rounded corners var totalRect = new SKRect(bounds.Left, bounds.Top, bounds.Right, bounds.Bottom); canvas.DrawRoundRect(new SKRoundRect(totalRect, CornerRadius), borderPaint); - // Center divider var centerX = bounds.MidX; canvas.DrawLine(centerX, bounds.Top, centerX, bounds.Bottom, borderPaint); } private void DrawButton(SKCanvas canvas, SKRect rect, string symbol, bool isPressed, bool isDisabled) { - // Draw background using var bgPaint = new SKPaint { Color = isDisabled ? ButtonDisabledColor : (isPressed ? ButtonPressedColor : ButtonBackgroundColor), Style = SKPaintStyle.Fill, IsAntialias = true }; - - // Draw button background (clipped by overall border) canvas.DrawRect(rect, bgPaint); - // Draw symbol using var font = new SKFont(SKTypeface.Default, 20); using var textPaint = new SKPaint(font) { @@ -133,23 +208,22 @@ public class SkiaStepper : SkiaView canvas.DrawText(symbol, rect.MidX - textBounds.MidX, rect.MidY - textBounds.MidY, textPaint); } - private bool CanIncrement() => IsEnabled && _value < _maximum; - private bool CanDecrement() => IsEnabled && _value > _minimum; + private bool CanIncrement() => IsEnabled && Value < Maximum; + private bool CanDecrement() => IsEnabled && Value > Minimum; public override void OnPointerPressed(PointerEventArgs e) { if (!IsEnabled) return; - var x = e.X; - if (x < ButtonWidth) + if (e.X < ButtonWidth) { _isMinusPressed = true; - if (CanDecrement()) Value -= _increment; + if (CanDecrement()) Value -= Increment; } - else if (x > Bounds.Width - ButtonWidth) + else if (e.X > Bounds.Width - ButtonWidth) { _isPlusPressed = true; - if (CanIncrement()) Value += _increment; + if (CanIncrement()) Value += Increment; } Invalidate(); } @@ -169,12 +243,12 @@ public class SkiaStepper : SkiaView { case Key.Up: case Key.Right: - if (CanIncrement()) Value += _increment; + if (CanIncrement()) Value += Increment; e.Handled = true; break; case Key.Down: case Key.Left: - if (CanDecrement()) Value -= _increment; + if (CanDecrement()) Value -= Increment; e.Handled = true; break; } diff --git a/Views/SkiaSwitch.cs b/Views/SkiaSwitch.cs index da14285..0120307 100644 --- a/Views/SkiaSwitch.cs +++ b/Views/SkiaSwitch.cs @@ -6,37 +6,204 @@ using SkiaSharp; namespace Microsoft.Maui.Platform; /// -/// Skia-rendered toggle switch control. +/// Skia-rendered toggle switch control with full XAML styling support. /// public class SkiaSwitch : SkiaView { - private bool _isOn; - private float _animationProgress; // 0 = off, 1 = on + #region BindableProperties + /// + /// Bindable property for IsOn. + /// + public static readonly BindableProperty IsOnProperty = + BindableProperty.Create( + nameof(IsOn), + typeof(bool), + typeof(SkiaSwitch), + false, + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaSwitch)b).OnIsOnChanged()); + + /// + /// Bindable property for OnTrackColor. + /// + public static readonly BindableProperty OnTrackColorProperty = + BindableProperty.Create( + nameof(OnTrackColor), + typeof(SKColor), + typeof(SkiaSwitch), + new SKColor(0x21, 0x96, 0xF3), + propertyChanged: (b, o, n) => ((SkiaSwitch)b).Invalidate()); + + /// + /// Bindable property for OffTrackColor. + /// + public static readonly BindableProperty OffTrackColorProperty = + BindableProperty.Create( + nameof(OffTrackColor), + typeof(SKColor), + typeof(SkiaSwitch), + new SKColor(0x9E, 0x9E, 0x9E), + propertyChanged: (b, o, n) => ((SkiaSwitch)b).Invalidate()); + + /// + /// Bindable property for ThumbColor. + /// + public static readonly BindableProperty ThumbColorProperty = + BindableProperty.Create( + nameof(ThumbColor), + typeof(SKColor), + typeof(SkiaSwitch), + SKColors.White, + propertyChanged: (b, o, n) => ((SkiaSwitch)b).Invalidate()); + + /// + /// Bindable property for DisabledColor. + /// + public static readonly BindableProperty DisabledColorProperty = + BindableProperty.Create( + nameof(DisabledColor), + typeof(SKColor), + typeof(SkiaSwitch), + new SKColor(0xBD, 0xBD, 0xBD), + propertyChanged: (b, o, n) => ((SkiaSwitch)b).Invalidate()); + + /// + /// Bindable property for TrackWidth. + /// + public static readonly BindableProperty TrackWidthProperty = + BindableProperty.Create( + nameof(TrackWidth), + typeof(float), + typeof(SkiaSwitch), + 52f, + propertyChanged: (b, o, n) => ((SkiaSwitch)b).InvalidateMeasure()); + + /// + /// Bindable property for TrackHeight. + /// + public static readonly BindableProperty TrackHeightProperty = + BindableProperty.Create( + nameof(TrackHeight), + typeof(float), + typeof(SkiaSwitch), + 32f, + propertyChanged: (b, o, n) => ((SkiaSwitch)b).InvalidateMeasure()); + + /// + /// Bindable property for ThumbRadius. + /// + public static readonly BindableProperty ThumbRadiusProperty = + BindableProperty.Create( + nameof(ThumbRadius), + typeof(float), + typeof(SkiaSwitch), + 12f, + propertyChanged: (b, o, n) => ((SkiaSwitch)b).Invalidate()); + + /// + /// Bindable property for ThumbPadding. + /// + public static readonly BindableProperty ThumbPaddingProperty = + BindableProperty.Create( + nameof(ThumbPadding), + typeof(float), + typeof(SkiaSwitch), + 4f, + propertyChanged: (b, o, n) => ((SkiaSwitch)b).Invalidate()); + + #endregion + + #region Properties + + /// + /// Gets or sets whether the switch is on. + /// public bool IsOn { - get => _isOn; - set - { - if (_isOn != value) - { - _isOn = value; - _animationProgress = value ? 1f : 0f; - Toggled?.Invoke(this, new ToggledEventArgs(value)); - Invalidate(); - } - } + get => (bool)GetValue(IsOnProperty); + set => SetValue(IsOnProperty, value); } - public SKColor OnTrackColor { get; set; } = new SKColor(0x21, 0x96, 0xF3); - public SKColor OffTrackColor { get; set; } = new SKColor(0x9E, 0x9E, 0x9E); - public SKColor ThumbColor { get; set; } = SKColors.White; - public SKColor DisabledColor { get; set; } = new SKColor(0xBD, 0xBD, 0xBD); - public float TrackWidth { get; set; } = 52; - public float TrackHeight { get; set; } = 32; - public float ThumbRadius { get; set; } = 12; - public float ThumbPadding { get; set; } = 4; + /// + /// Gets or sets the on track color. + /// + public SKColor OnTrackColor + { + get => (SKColor)GetValue(OnTrackColorProperty); + set => SetValue(OnTrackColorProperty, value); + } + /// + /// Gets or sets the off track color. + /// + public SKColor OffTrackColor + { + get => (SKColor)GetValue(OffTrackColorProperty); + set => SetValue(OffTrackColorProperty, value); + } + + /// + /// Gets or sets the thumb color. + /// + public SKColor ThumbColor + { + get => (SKColor)GetValue(ThumbColorProperty); + set => SetValue(ThumbColorProperty, value); + } + + /// + /// Gets or sets the disabled color. + /// + public SKColor DisabledColor + { + get => (SKColor)GetValue(DisabledColorProperty); + set => SetValue(DisabledColorProperty, value); + } + + /// + /// Gets or sets the track width. + /// + public float TrackWidth + { + get => (float)GetValue(TrackWidthProperty); + set => SetValue(TrackWidthProperty, value); + } + + /// + /// Gets or sets the track height. + /// + public float TrackHeight + { + get => (float)GetValue(TrackHeightProperty); + set => SetValue(TrackHeightProperty, value); + } + + /// + /// Gets or sets the thumb radius. + /// + public float ThumbRadius + { + get => (float)GetValue(ThumbRadiusProperty); + set => SetValue(ThumbRadiusProperty, value); + } + + /// + /// Gets or sets the thumb padding. + /// + public float ThumbPadding + { + get => (float)GetValue(ThumbPaddingProperty); + set => SetValue(ThumbPaddingProperty, value); + } + + #endregion + + private float _animationProgress; // 0 = off, 1 = on + + /// + /// Event raised when the switch is toggled. + /// public event EventHandler? Toggled; public SkiaSwitch() @@ -44,6 +211,14 @@ public class SkiaSwitch : SkiaView IsFocusable = true; } + private void OnIsOnChanged() + { + _animationProgress = IsOn ? 1f : 0f; + Toggled?.Invoke(this, new ToggledEventArgs(IsOn)); + SkiaVisualStateManager.GoToState(this, IsOn ? SkiaVisualStateManager.CommonStates.On : SkiaVisualStateManager.CommonStates.Off); + Invalidate(); + } + protected override void OnDraw(SKCanvas canvas, SKRect bounds) { var centerY = bounds.MidY; @@ -142,12 +317,21 @@ public class SkiaSwitch : SkiaView } } + protected override void OnEnabledChanged() + { + base.OnEnabledChanged(); + SkiaVisualStateManager.GoToState(this, IsEnabled ? SkiaVisualStateManager.CommonStates.Normal : SkiaVisualStateManager.CommonStates.Disabled); + } + protected override SKSize MeasureOverride(SKSize availableSize) { return new SKSize(TrackWidth + 8, TrackHeight + 8); } } +/// +/// Event args for toggled events. +/// public class ToggledEventArgs : EventArgs { public bool Value { get; } diff --git a/Views/SkiaTemplatedView.cs b/Views/SkiaTemplatedView.cs new file mode 100644 index 0000000..630a958 --- /dev/null +++ b/Views/SkiaTemplatedView.cs @@ -0,0 +1,367 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Maui.Controls; +using SkiaSharp; + +namespace Microsoft.Maui.Platform; + +/// +/// Base class for Skia controls that support ControlTemplates. +/// Provides infrastructure for completely redefining control appearance via XAML. +/// +public abstract class SkiaTemplatedView : SkiaView +{ + private SkiaView? _templateRoot; + private bool _templateApplied; + + #region BindableProperties + + public static readonly BindableProperty ControlTemplateProperty = + BindableProperty.Create(nameof(ControlTemplate), typeof(ControlTemplate), typeof(SkiaTemplatedView), null, + propertyChanged: OnControlTemplateChanged); + + #endregion + + #region Properties + + /// + /// Gets or sets the control template that defines the visual appearance. + /// + public ControlTemplate? ControlTemplate + { + get => (ControlTemplate?)GetValue(ControlTemplateProperty); + set => SetValue(ControlTemplateProperty, value); + } + + /// + /// Gets the root element created from the ControlTemplate. + /// + protected SkiaView? TemplateRoot => _templateRoot; + + /// + /// Gets a value indicating whether a template has been applied. + /// + protected bool IsTemplateApplied => _templateApplied; + + #endregion + + private static void OnControlTemplateChanged(BindableObject bindable, object oldValue, object newValue) + { + if (bindable is SkiaTemplatedView view) + { + view.OnControlTemplateChanged((ControlTemplate?)oldValue, (ControlTemplate?)newValue); + } + } + + /// + /// Called when the ControlTemplate changes. + /// + protected virtual void OnControlTemplateChanged(ControlTemplate? oldTemplate, ControlTemplate? newTemplate) + { + _templateApplied = false; + _templateRoot = null; + + if (newTemplate != null) + { + ApplyTemplate(); + } + + InvalidateMeasure(); + } + + /// + /// Applies the current ControlTemplate if one is set. + /// + protected virtual void ApplyTemplate() + { + if (ControlTemplate == null || _templateApplied) + return; + + try + { + // Create content from template + var content = ControlTemplate.CreateContent(); + + // If the content is a MAUI Element, try to convert it to a SkiaView + if (content is Element element) + { + _templateRoot = ConvertElementToSkiaView(element); + } + else if (content is SkiaView skiaView) + { + _templateRoot = skiaView; + } + + if (_templateRoot != null) + { + _templateRoot.Parent = this; + OnTemplateApplied(); + } + + _templateApplied = true; + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error applying template: {ex.Message}"); + } + } + + /// + /// Called after a template has been successfully applied. + /// Override to perform template-specific initialization. + /// + protected virtual void OnTemplateApplied() + { + // Find and bind ContentPresenter if present + var presenter = FindTemplateChild("PART_ContentPresenter"); + if (presenter != null) + { + OnContentPresenterFound(presenter); + } + } + + /// + /// Called when a ContentPresenter is found in the template. + /// Override to set up the content binding. + /// + protected virtual void OnContentPresenterFound(SkiaContentPresenter presenter) + { + // Derived classes should override to bind their content + } + + /// + /// Finds a named element in the template tree. + /// + protected T? FindTemplateChild(string name) where T : SkiaView + { + if (_templateRoot == null) + return null; + + return FindChild(_templateRoot, name); + } + + private static T? FindChild(SkiaView root, string name) where T : SkiaView + { + if (root is T typed && root.Name == name) + return typed; + + if (root is SkiaLayoutView layout) + { + foreach (var child in layout.Children) + { + var found = FindChild(child, name); + if (found != null) + return found; + } + } + else if (root is SkiaContentPresenter presenter && presenter.Content != null) + { + return FindChild(presenter.Content, name); + } + + return null; + } + + /// + /// Converts a MAUI Element to a SkiaView. + /// Override to provide custom conversion logic. + /// + protected virtual SkiaView? ConvertElementToSkiaView(Element element) + { + // This is a simplified conversion - in a full implementation, + // you would use the handler system to create proper platform views + + return element switch + { + // Handle common layout types + Microsoft.Maui.Controls.StackLayout sl => CreateSkiaStackLayout(sl), + Microsoft.Maui.Controls.Grid grid => CreateSkiaGrid(grid), + Microsoft.Maui.Controls.Border border => CreateSkiaBorder(border), + Microsoft.Maui.Controls.Label label => CreateSkiaLabel(label), + Microsoft.Maui.Controls.ContentPresenter cp => new SkiaContentPresenter(), + _ => new SkiaLabel { Text = $"[{element.GetType().Name}]", TextColor = SKColors.Gray } + }; + } + + private SkiaStackLayout CreateSkiaStackLayout(Microsoft.Maui.Controls.StackLayout sl) + { + var layout = new SkiaStackLayout + { + Orientation = sl.Orientation == Microsoft.Maui.Controls.StackOrientation.Vertical + ? StackOrientation.Vertical + : StackOrientation.Horizontal, + Spacing = (float)sl.Spacing + }; + + foreach (var child in sl.Children) + { + if (child is Element element) + { + var skiaChild = ConvertElementToSkiaView(element); + if (skiaChild != null) + layout.AddChild(skiaChild); + } + } + + return layout; + } + + private SkiaGrid CreateSkiaGrid(Microsoft.Maui.Controls.Grid grid) + { + var layout = new SkiaGrid(); + + // Set row definitions + foreach (var rowDef in grid.RowDefinitions) + { + var gridLength = rowDef.Height.IsAuto ? GridLength.Auto : + rowDef.Height.IsStar ? new GridLength((float)rowDef.Height.Value, GridUnitType.Star) : + new GridLength((float)rowDef.Height.Value, GridUnitType.Absolute); + layout.RowDefinitions.Add(gridLength); + } + + // Set column definitions + foreach (var colDef in grid.ColumnDefinitions) + { + var gridLength = colDef.Width.IsAuto ? GridLength.Auto : + colDef.Width.IsStar ? new GridLength((float)colDef.Width.Value, GridUnitType.Star) : + new GridLength((float)colDef.Width.Value, GridUnitType.Absolute); + layout.ColumnDefinitions.Add(gridLength); + } + + // Add children + foreach (var child in grid.Children) + { + if (child is Element element) + { + var skiaChild = ConvertElementToSkiaView(element); + if (skiaChild != null) + { + var row = Microsoft.Maui.Controls.Grid.GetRow((BindableObject)child); + var col = Microsoft.Maui.Controls.Grid.GetColumn((BindableObject)child); + var rowSpan = Microsoft.Maui.Controls.Grid.GetRowSpan((BindableObject)child); + var colSpan = Microsoft.Maui.Controls.Grid.GetColumnSpan((BindableObject)child); + + layout.AddChild(skiaChild, row, col, rowSpan, colSpan); + } + } + } + + return layout; + } + + private SkiaBorder CreateSkiaBorder(Microsoft.Maui.Controls.Border border) + { + float cornerRadius = 0; + if (border.StrokeShape is Microsoft.Maui.Controls.Shapes.RoundRectangle rr) + { + cornerRadius = (float)rr.CornerRadius.TopLeft; + } + + var skiaBorder = new SkiaBorder + { + CornerRadius = cornerRadius, + StrokeThickness = (float)border.StrokeThickness + }; + + if (border.Stroke is SolidColorBrush strokeBrush) + { + skiaBorder.Stroke = strokeBrush.Color.ToSKColor(); + } + + if (border.Background is SolidColorBrush bgBrush) + { + skiaBorder.BackgroundColor = bgBrush.Color.ToSKColor(); + } + + if (border.Content is Element content) + { + var skiaContent = ConvertElementToSkiaView(content); + if (skiaContent != null) + skiaBorder.AddChild(skiaContent); + } + + return skiaBorder; + } + + private SkiaLabel CreateSkiaLabel(Microsoft.Maui.Controls.Label label) + { + var skiaLabel = new SkiaLabel + { + Text = label.Text ?? "", + FontSize = (float)label.FontSize + }; + + if (label.TextColor != null) + { + skiaLabel.TextColor = label.TextColor.ToSKColor(); + } + + return skiaLabel; + } + + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + if (_templateRoot != null && _templateApplied) + { + // Render the template + _templateRoot.Draw(canvas); + } + else + { + // Render default appearance + DrawDefaultAppearance(canvas, bounds); + } + } + + /// + /// Draws the default appearance when no template is applied. + /// Override in derived classes to provide default rendering. + /// + protected abstract void DrawDefaultAppearance(SKCanvas canvas, SKRect bounds); + + protected override SKSize MeasureOverride(SKSize availableSize) + { + if (_templateRoot != null && _templateApplied) + { + return _templateRoot.Measure(availableSize); + } + + return MeasureDefaultAppearance(availableSize); + } + + /// + /// Measures the default appearance when no template is applied. + /// Override in derived classes. + /// + protected virtual SKSize MeasureDefaultAppearance(SKSize availableSize) + { + return new SKSize(100, 40); + } + + public new void Arrange(SKRect bounds) + { + base.Arrange(bounds); + + if (_templateRoot != null && _templateApplied) + { + _templateRoot.Arrange(bounds); + } + } + + public override SkiaView? HitTest(float x, float y) + { + if (!IsVisible || !Bounds.Contains(x, y)) + return null; + + if (_templateRoot != null && _templateApplied) + { + var hit = _templateRoot.HitTest(x, y); + if (hit != null) + return hit; + } + + return this; + } +} + diff --git a/Views/SkiaTimePicker.cs b/Views/SkiaTimePicker.cs index b23e872..3534a60 100644 --- a/Views/SkiaTimePicker.cs +++ b/Views/SkiaTimePicker.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using SkiaSharp; +using Microsoft.Maui.Platform.Linux; namespace Microsoft.Maui.Platform; @@ -10,77 +11,202 @@ namespace Microsoft.Maui.Platform; /// public class SkiaTimePicker : SkiaView { - private TimeSpan _time = DateTime.Now.TimeOfDay; - private bool _isOpen; - private string _format = "t"; - private int _selectedHour; - private int _selectedMinute; - private bool _isSelectingHours = true; + #region BindableProperties - // Styling - public SKColor TextColor { get; set; } = SKColors.Black; - public SKColor BorderColor { get; set; } = new SKColor(0xBD, 0xBD, 0xBD); - public SKColor ClockBackgroundColor { get; set; } = SKColors.White; - public SKColor ClockFaceColor { get; set; } = new SKColor(0xF5, 0xF5, 0xF5); - public SKColor SelectedColor { get; set; } = new SKColor(0x21, 0x96, 0xF3); - public SKColor HeaderColor { get; set; } = new SKColor(0x21, 0x96, 0xF3); - public float FontSize { get; set; } = 14; - public float CornerRadius { get; set; } = 4; + public static readonly BindableProperty TimeProperty = + BindableProperty.Create(nameof(Time), typeof(TimeSpan), typeof(SkiaTimePicker), DateTime.Now.TimeOfDay, BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaTimePicker)b).OnTimePropertyChanged()); - private const float ClockSize = 280; - private const float ClockRadius = 100; - private const float HeaderHeight = 80; + public static readonly BindableProperty FormatProperty = + BindableProperty.Create(nameof(Format), typeof(string), typeof(SkiaTimePicker), "t", + propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate()); + + public static readonly BindableProperty TextColorProperty = + BindableProperty.Create(nameof(TextColor), typeof(SKColor), typeof(SkiaTimePicker), SKColors.Black, + propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate()); + + public static readonly BindableProperty BorderColorProperty = + BindableProperty.Create(nameof(BorderColor), typeof(SKColor), typeof(SkiaTimePicker), new SKColor(0xBD, 0xBD, 0xBD), + propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate()); + + public static readonly BindableProperty ClockBackgroundColorProperty = + BindableProperty.Create(nameof(ClockBackgroundColor), typeof(SKColor), typeof(SkiaTimePicker), SKColors.White, + propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate()); + + public static readonly BindableProperty ClockFaceColorProperty = + BindableProperty.Create(nameof(ClockFaceColor), typeof(SKColor), typeof(SkiaTimePicker), new SKColor(0xF5, 0xF5, 0xF5), + propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate()); + + public static readonly BindableProperty SelectedColorProperty = + BindableProperty.Create(nameof(SelectedColor), typeof(SKColor), typeof(SkiaTimePicker), new SKColor(0x21, 0x96, 0xF3), + propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate()); + + public static readonly BindableProperty HeaderColorProperty = + BindableProperty.Create(nameof(HeaderColor), typeof(SKColor), typeof(SkiaTimePicker), new SKColor(0x21, 0x96, 0xF3), + propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate()); + + public static readonly BindableProperty FontSizeProperty = + BindableProperty.Create(nameof(FontSize), typeof(float), typeof(SkiaTimePicker), 14f, + propertyChanged: (b, o, n) => ((SkiaTimePicker)b).InvalidateMeasure()); + + public static readonly BindableProperty CornerRadiusProperty = + BindableProperty.Create(nameof(CornerRadius), typeof(float), typeof(SkiaTimePicker), 4f, + propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate()); + + #endregion + + #region Properties public TimeSpan Time { - get => _time; - set - { - if (_time != value) - { - _time = value; - _selectedHour = _time.Hours; - _selectedMinute = _time.Minutes; - TimeSelected?.Invoke(this, EventArgs.Empty); - Invalidate(); - } - } + get => (TimeSpan)GetValue(TimeProperty); + set => SetValue(TimeProperty, value); } public string Format { - get => _format; - set { _format = value; Invalidate(); } + get => (string)GetValue(FormatProperty); + set => SetValue(FormatProperty, value); + } + + public SKColor TextColor + { + get => (SKColor)GetValue(TextColorProperty); + set => SetValue(TextColorProperty, value); + } + + public SKColor BorderColor + { + get => (SKColor)GetValue(BorderColorProperty); + set => SetValue(BorderColorProperty, value); + } + + public SKColor ClockBackgroundColor + { + get => (SKColor)GetValue(ClockBackgroundColorProperty); + set => SetValue(ClockBackgroundColorProperty, value); + } + + public SKColor ClockFaceColor + { + get => (SKColor)GetValue(ClockFaceColorProperty); + set => SetValue(ClockFaceColorProperty, value); + } + + public SKColor SelectedColor + { + get => (SKColor)GetValue(SelectedColorProperty); + set => SetValue(SelectedColorProperty, value); + } + + public SKColor HeaderColor + { + get => (SKColor)GetValue(HeaderColorProperty); + set => SetValue(HeaderColorProperty, value); + } + + public float FontSize + { + get => (float)GetValue(FontSizeProperty); + set => SetValue(FontSizeProperty, value); + } + + public float CornerRadius + { + get => (float)GetValue(CornerRadiusProperty); + set => SetValue(CornerRadiusProperty, value); } public bool IsOpen { get => _isOpen; - set { _isOpen = value; Invalidate(); } + set + { + if (_isOpen != value) + { + _isOpen = value; + if (_isOpen) + RegisterPopupOverlay(this, DrawClockOverlay); + else + UnregisterPopupOverlay(this); + Invalidate(); + } + } } + #endregion + + private bool _isOpen; + private int _selectedHour; + private int _selectedMinute; + private bool _isSelectingHours = true; + + private const float ClockSize = 280; + private const float ClockRadius = 100; + private const float HeaderHeight = 80; + private const float PopupHeight = ClockSize + HeaderHeight; + public event EventHandler? TimeSelected; + /// + /// Gets the clock popup rectangle with edge detection applied. + /// + private SKRect GetPopupRect(SKRect pickerBounds) + { + // Get window dimensions for edge detection + var windowWidth = LinuxApplication.Current?.MainWindow?.Width ?? 800; + var windowHeight = LinuxApplication.Current?.MainWindow?.Height ?? 600; + + // Calculate default position (below the picker) + var popupLeft = pickerBounds.Left; + var popupTop = pickerBounds.Bottom + 4; + + // Edge detection: adjust horizontal position if popup would go off-screen + if (popupLeft + ClockSize > windowWidth) + { + popupLeft = windowWidth - ClockSize - 4; + } + if (popupLeft < 0) popupLeft = 4; + + // Edge detection: show above if popup would go off-screen vertically + if (popupTop + PopupHeight > windowHeight) + { + popupTop = pickerBounds.Top - PopupHeight - 4; + } + if (popupTop < 0) popupTop = 4; + + return new SKRect(popupLeft, popupTop, popupLeft + ClockSize, popupTop + PopupHeight); + } + public SkiaTimePicker() { IsFocusable = true; - _selectedHour = _time.Hours; - _selectedMinute = _time.Minutes; + _selectedHour = DateTime.Now.Hour; + _selectedMinute = DateTime.Now.Minute; + } + + private void OnTimePropertyChanged() + { + _selectedHour = Time.Hours; + _selectedMinute = Time.Minutes; + TimeSelected?.Invoke(this, EventArgs.Empty); + Invalidate(); + } + + private void DrawClockOverlay(SKCanvas canvas) + { + if (!_isOpen) return; + // Use ScreenBounds for popup drawing (accounts for scroll offset) + DrawClockPopup(canvas, ScreenBounds); } protected override void OnDraw(SKCanvas canvas, SKRect bounds) { DrawPickerButton(canvas, bounds); - - if (_isOpen) - { - DrawClockPopup(canvas, bounds); - } } private void DrawPickerButton(SKCanvas canvas, SKRect bounds) { - // Draw background using var bgPaint = new SKPaint { Color = IsEnabled ? BackgroundColor : new SKColor(0xF5, 0xF5, 0xF5), @@ -89,7 +215,6 @@ public class SkiaTimePicker : SkiaView }; canvas.DrawRoundRect(new SKRoundRect(bounds, CornerRadius), bgPaint); - // Draw border using var borderPaint = new SKPaint { Color = IsFocused ? SelectedColor : BorderColor, @@ -99,23 +224,17 @@ public class SkiaTimePicker : SkiaView }; canvas.DrawRoundRect(new SKRoundRect(bounds, CornerRadius), borderPaint); - // Draw time text using var font = new SKFont(SKTypeface.Default, FontSize); using var textPaint = new SKPaint(font) { Color = IsEnabled ? TextColor : TextColor.WithAlpha(128), IsAntialias = true }; - - var timeText = DateTime.Today.Add(_time).ToString(_format); + var timeText = DateTime.Today.Add(Time).ToString(Format); var textBounds = new SKRect(); textPaint.MeasureText(timeText, ref textBounds); + canvas.DrawText(timeText, bounds.Left + 12, bounds.MidY - textBounds.MidY, textPaint); - var textX = bounds.Left + 12; - var textY = bounds.MidY - textBounds.MidY; - canvas.DrawText(timeText, textX, textY, textPaint); - - // Draw clock icon DrawClockIcon(canvas, new SKRect(bounds.Right - 36, bounds.MidY - 10, bounds.Right - 12, bounds.MidY + 10)); } @@ -128,108 +247,52 @@ public class SkiaTimePicker : SkiaView StrokeWidth = 1.5f, IsAntialias = true }; - - var centerX = bounds.MidX; - var centerY = bounds.MidY; var radius = Math.Min(bounds.Width, bounds.Height) / 2 - 2; - - // Clock circle - canvas.DrawCircle(centerX, centerY, radius, paint); - - // Hour hand - canvas.DrawLine(centerX, centerY, centerX, centerY - radius * 0.5f, paint); - - // Minute hand - canvas.DrawLine(centerX, centerY, centerX + radius * 0.4f, centerY, paint); - - // Center dot + canvas.DrawCircle(bounds.MidX, bounds.MidY, radius, paint); + canvas.DrawLine(bounds.MidX, bounds.MidY, bounds.MidX, bounds.MidY - radius * 0.5f, paint); + canvas.DrawLine(bounds.MidX, bounds.MidY, bounds.MidX + radius * 0.4f, bounds.MidY, paint); paint.Style = SKPaintStyle.Fill; - canvas.DrawCircle(centerX, centerY, 1.5f, paint); + canvas.DrawCircle(bounds.MidX, bounds.MidY, 1.5f, paint); } private void DrawClockPopup(SKCanvas canvas, SKRect bounds) { - var popupRect = new SKRect( - bounds.Left, - bounds.Bottom + 4, - bounds.Left + ClockSize, - bounds.Bottom + 4 + HeaderHeight + ClockSize); + var popupRect = GetPopupRect(bounds); - // Draw shadow - using var shadowPaint = new SKPaint - { - Color = new SKColor(0, 0, 0, 40), - MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 4), - Style = SKPaintStyle.Fill - }; + using var shadowPaint = new SKPaint { Color = new SKColor(0, 0, 0, 40), MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 4), Style = SKPaintStyle.Fill }; canvas.DrawRoundRect(new SKRoundRect(new SKRect(popupRect.Left + 2, popupRect.Top + 2, popupRect.Right + 2, popupRect.Bottom + 2), CornerRadius), shadowPaint); - // Draw background - using var bgPaint = new SKPaint - { - Color = ClockBackgroundColor, - Style = SKPaintStyle.Fill, - IsAntialias = true - }; + using var bgPaint = new SKPaint { Color = ClockBackgroundColor, Style = SKPaintStyle.Fill, IsAntialias = true }; canvas.DrawRoundRect(new SKRoundRect(popupRect, CornerRadius), bgPaint); - // Draw border - using var borderPaint = new SKPaint - { - Color = BorderColor, - Style = SKPaintStyle.Stroke, - StrokeWidth = 1, - IsAntialias = true - }; + using var borderPaint = new SKPaint { Color = BorderColor, Style = SKPaintStyle.Stroke, StrokeWidth = 1, IsAntialias = true }; canvas.DrawRoundRect(new SKRoundRect(popupRect, CornerRadius), borderPaint); - // Draw header with time display DrawTimeHeader(canvas, new SKRect(popupRect.Left, popupRect.Top, popupRect.Right, popupRect.Top + HeaderHeight)); - - // Draw clock face DrawClockFace(canvas, new SKRect(popupRect.Left, popupRect.Top + HeaderHeight, popupRect.Right, popupRect.Bottom)); } private void DrawTimeHeader(SKCanvas canvas, SKRect bounds) { - // Draw header background - using var headerPaint = new SKPaint - { - Color = HeaderColor, - Style = SKPaintStyle.Fill - }; - + using var headerPaint = new SKPaint { Color = HeaderColor, Style = SKPaintStyle.Fill }; canvas.Save(); canvas.ClipRoundRect(new SKRoundRect(new SKRect(bounds.Left, bounds.Top, bounds.Right, bounds.Top + CornerRadius * 2), CornerRadius)); canvas.DrawRect(bounds, headerPaint); canvas.Restore(); canvas.DrawRect(new SKRect(bounds.Left, bounds.Top + CornerRadius, bounds.Right, bounds.Bottom), headerPaint); - // Draw time display using var font = new SKFont(SKTypeface.Default, 32); - using var selectedPaint = new SKPaint(font) - { - Color = SKColors.White, - IsAntialias = true - }; - using var unselectedPaint = new SKPaint(font) - { - Color = new SKColor(255, 255, 255, 150), - IsAntialias = true - }; + using var selectedPaint = new SKPaint(font) { Color = SKColors.White, IsAntialias = true }; + using var unselectedPaint = new SKPaint(font) { Color = new SKColor(255, 255, 255, 150), IsAntialias = true }; var hourText = _selectedHour.ToString("D2"); var minuteText = _selectedMinute.ToString("D2"); - var colonText = ":"; - var hourPaint = _isSelectingHours ? selectedPaint : unselectedPaint; var minutePaint = _isSelectingHours ? unselectedPaint : selectedPaint; - var hourBounds = new SKRect(); - var colonBounds = new SKRect(); - var minuteBounds = new SKRect(); + var hourBounds = new SKRect(); var colonBounds = new SKRect(); var minuteBounds = new SKRect(); hourPaint.MeasureText(hourText, ref hourBounds); - selectedPaint.MeasureText(colonText, ref colonBounds); + selectedPaint.MeasureText(":", ref colonBounds); minutePaint.MeasureText(minuteText, ref minuteBounds); var totalWidth = hourBounds.Width + colonBounds.Width + minuteBounds.Width + 8; @@ -237,7 +300,7 @@ public class SkiaTimePicker : SkiaView var centerY = bounds.MidY - hourBounds.MidY; canvas.DrawText(hourText, startX, centerY, hourPaint); - canvas.DrawText(colonText, startX + hourBounds.Width + 4, centerY, selectedPaint); + canvas.DrawText(":", startX + hourBounds.Width + 4, centerY, selectedPaint); canvas.DrawText(minuteText, startX + hourBounds.Width + colonBounds.Width + 8, centerY, minutePaint); } @@ -246,94 +309,53 @@ public class SkiaTimePicker : SkiaView var centerX = bounds.MidX; var centerY = bounds.MidY; - // Draw clock face background - using var facePaint = new SKPaint - { - Color = ClockFaceColor, - Style = SKPaintStyle.Fill, - IsAntialias = true - }; + using var facePaint = new SKPaint { Color = ClockFaceColor, Style = SKPaintStyle.Fill, IsAntialias = true }; canvas.DrawCircle(centerX, centerY, ClockRadius + 20, facePaint); - // Draw numbers using var font = new SKFont(SKTypeface.Default, 14); - using var textPaint = new SKPaint(font) - { - Color = TextColor, - IsAntialias = true - }; + using var textPaint = new SKPaint(font) { Color = TextColor, IsAntialias = true }; if (_isSelectingHours) { - // Draw hour numbers (1-12) for (int i = 1; i <= 12; i++) { var angle = (i * 30 - 90) * Math.PI / 180; var x = centerX + (float)(ClockRadius * Math.Cos(angle)); var y = centerY + (float)(ClockRadius * Math.Sin(angle)); - - var numText = i.ToString(); - var textBounds = new SKRect(); - textPaint.MeasureText(numText, ref textBounds); - var isSelected = (_selectedHour % 12 == i % 12); if (isSelected) { - using var selectedBgPaint = new SKPaint - { - Color = SelectedColor, - Style = SKPaintStyle.Fill, - IsAntialias = true - }; - canvas.DrawCircle(x, y, 18, selectedBgPaint); + using var selBgPaint = new SKPaint { Color = SelectedColor, Style = SKPaintStyle.Fill, IsAntialias = true }; + canvas.DrawCircle(x, y, 18, selBgPaint); textPaint.Color = SKColors.White; } - else - { - textPaint.Color = TextColor; - } - - canvas.DrawText(numText, x - textBounds.MidX, y - textBounds.MidY, textPaint); + else textPaint.Color = TextColor; + var textBounds = new SKRect(); + textPaint.MeasureText(i.ToString(), ref textBounds); + canvas.DrawText(i.ToString(), x - textBounds.MidX, y - textBounds.MidY, textPaint); } - - // Draw center point and hand DrawClockHand(canvas, centerX, centerY, (_selectedHour % 12) * 30 - 90, ClockRadius - 18); } else { - // Draw minute numbers (0, 5, 10, ... 55) for (int i = 0; i < 12; i++) { var minute = i * 5; var angle = (minute * 6 - 90) * Math.PI / 180; var x = centerX + (float)(ClockRadius * Math.Cos(angle)); var y = centerY + (float)(ClockRadius * Math.Sin(angle)); - - var numText = minute.ToString("D2"); - var textBounds = new SKRect(); - textPaint.MeasureText(numText, ref textBounds); - var isSelected = (_selectedMinute / 5 == i); if (isSelected) { - using var selectedBgPaint = new SKPaint - { - Color = SelectedColor, - Style = SKPaintStyle.Fill, - IsAntialias = true - }; - canvas.DrawCircle(x, y, 18, selectedBgPaint); + using var selBgPaint = new SKPaint { Color = SelectedColor, Style = SKPaintStyle.Fill, IsAntialias = true }; + canvas.DrawCircle(x, y, 18, selBgPaint); textPaint.Color = SKColors.White; } - else - { - textPaint.Color = TextColor; - } - - canvas.DrawText(numText, x - textBounds.MidX, y - textBounds.MidY, textPaint); + else textPaint.Color = TextColor; + var textBounds = new SKRect(); + textPaint.MeasureText(minute.ToString("D2"), ref textBounds); + canvas.DrawText(minute.ToString("D2"), x - textBounds.MidX, y - textBounds.MidY, textPaint); } - - // Draw center point and hand DrawClockHand(canvas, centerX, centerY, _selectedMinute * 6 - 90, ClockRadius - 18); } } @@ -341,19 +363,8 @@ public class SkiaTimePicker : SkiaView private void DrawClockHand(SKCanvas canvas, float centerX, float centerY, float angleDegrees, float length) { var angle = angleDegrees * Math.PI / 180; - var endX = centerX + (float)(length * Math.Cos(angle)); - var endY = centerY + (float)(length * Math.Sin(angle)); - - using var handPaint = new SKPaint - { - Color = SelectedColor, - Style = SKPaintStyle.Stroke, - StrokeWidth = 2, - IsAntialias = true - }; - canvas.DrawLine(centerX, centerY, endX, endY, handPaint); - - // Center dot + using var handPaint = new SKPaint { Color = SelectedColor, Style = SKPaintStyle.Stroke, StrokeWidth = 2, IsAntialias = true }; + canvas.DrawLine(centerX, centerY, centerX + (float)(length * Math.Cos(angle)), centerY + (float)(length * Math.Sin(angle)), handPaint); handPaint.Style = SKPaintStyle.Fill; canvas.DrawCircle(centerX, centerY, 6, handPaint); } @@ -362,31 +373,24 @@ public class SkiaTimePicker : SkiaView { if (!IsEnabled) return; - if (_isOpen) + if (IsOpen) { - var popupTop = Bounds.Bottom + 4; - var popupLeft = Bounds.Left; + // Use ScreenBounds for popup coordinate calculations (accounts for scroll offset) + var screenBounds = ScreenBounds; + var popupRect = GetPopupRect(screenBounds); - // Check header click (toggle hours/minutes) - if (e.Y >= popupTop && e.Y < popupTop + HeaderHeight) + // Check if click is in header area + var headerRect = new SKRect(popupRect.Left, popupRect.Top, popupRect.Right, popupRect.Top + HeaderHeight); + if (headerRect.Contains(e.X, e.Y)) { - var centerX = popupLeft + ClockSize / 2; - if (e.X < centerX) - { - _isSelectingHours = true; - } - else - { - _isSelectingHours = false; - } + _isSelectingHours = e.X < popupRect.Left + ClockSize / 2; Invalidate(); return; } - // Check clock face click - var clockCenterX = popupLeft + ClockSize / 2; - var clockCenterY = popupTop + HeaderHeight + ClockSize / 2; - + // Check if click is in clock face area + var clockCenterX = popupRect.Left + ClockSize / 2; + var clockCenterY = popupRect.Top + HeaderHeight + ClockSize / 2; var dx = e.X - clockCenterX; var dy = e.Y - clockCenterY; var distance = Math.Sqrt(dx * dx + dy * dy); @@ -400,114 +404,86 @@ public class SkiaTimePicker : SkiaView { _selectedHour = ((int)Math.Round(angle / 30) % 12); if (_selectedHour == 0) _selectedHour = 12; - // Preserve AM/PM - if (_time.Hours >= 12 && _selectedHour != 12) - _selectedHour += 12; - else if (_time.Hours < 12 && _selectedHour == 12) - _selectedHour = 0; - - _isSelectingHours = false; // Move to minutes + if (Time.Hours >= 12 && _selectedHour != 12) _selectedHour += 12; + else if (Time.Hours < 12 && _selectedHour == 12) _selectedHour = 0; + _isSelectingHours = false; } else { _selectedMinute = ((int)Math.Round(angle / 6) % 60); - // Apply the time Time = new TimeSpan(_selectedHour, _selectedMinute, 0); - _isOpen = false; + IsOpen = false; } Invalidate(); return; } - // Click outside popup - close - if (e.Y < popupTop) + // Click is outside clock - check if it's on the picker itself to toggle + if (screenBounds.Contains(e.X, e.Y)) { - _isOpen = false; + IsOpen = false; } } else { - _isOpen = true; + IsOpen = true; _isSelectingHours = true; } - Invalidate(); } + public override void OnFocusLost() + { + base.OnFocusLost(); + // Close popup when focus is lost (clicking outside) + if (IsOpen) + { + IsOpen = false; + } + } + public override void OnKeyDown(KeyEventArgs e) { if (!IsEnabled) return; switch (e.Key) { - case Key.Enter: - case Key.Space: - if (_isOpen) - { - if (_isSelectingHours) - { - _isSelectingHours = false; - } - else - { - Time = new TimeSpan(_selectedHour, _selectedMinute, 0); - _isOpen = false; - } - } - else - { - _isOpen = true; - _isSelectingHours = true; - } - e.Handled = true; - break; - - case Key.Escape: - if (_isOpen) - { - _isOpen = false; - e.Handled = true; - } - break; - - case Key.Up: - if (_isSelectingHours) - { - _selectedHour = (_selectedHour + 1) % 24; - } - else - { - _selectedMinute = (_selectedMinute + 1) % 60; - } - e.Handled = true; - break; - - case Key.Down: - if (_isSelectingHours) - { - _selectedHour = (_selectedHour - 1 + 24) % 24; - } - else - { - _selectedMinute = (_selectedMinute - 1 + 60) % 60; - } - e.Handled = true; - break; - - case Key.Left: - case Key.Right: - _isSelectingHours = !_isSelectingHours; - e.Handled = true; - break; + case Key.Enter: case Key.Space: + if (IsOpen) { if (_isSelectingHours) _isSelectingHours = false; else { Time = new TimeSpan(_selectedHour, _selectedMinute, 0); IsOpen = false; } } + else { IsOpen = true; _isSelectingHours = true; } + e.Handled = true; break; + case Key.Escape: if (IsOpen) { IsOpen = false; e.Handled = true; } break; + case Key.Up: if (_isSelectingHours) _selectedHour = (_selectedHour + 1) % 24; else _selectedMinute = (_selectedMinute + 1) % 60; e.Handled = true; break; + case Key.Down: if (_isSelectingHours) _selectedHour = (_selectedHour - 1 + 24) % 24; else _selectedMinute = (_selectedMinute - 1 + 60) % 60; e.Handled = true; break; + case Key.Left: case Key.Right: _isSelectingHours = !_isSelectingHours; e.Handled = true; break; } - Invalidate(); } protected override SKSize MeasureOverride(SKSize availableSize) { - return new SKSize( - availableSize.Width < float.MaxValue ? Math.Min(availableSize.Width, 200) : 200, - 40); + return new SKSize(availableSize.Width < float.MaxValue ? Math.Min(availableSize.Width, 200) : 200, 40); + } + + /// + /// Override to include clock popup area in hit testing. + /// + protected override bool HitTestPopupArea(float x, float y) + { + // Use ScreenBounds for hit testing (accounts for scroll offset) + var screenBounds = ScreenBounds; + + // Always include the picker button itself + if (screenBounds.Contains(x, y)) + return true; + + // When open, also include the clock popup area (with edge detection) + if (_isOpen) + { + var popupRect = GetPopupRect(screenBounds); + return popupRect.Contains(x, y); + } + + return false; } } diff --git a/Views/SkiaView.cs b/Views/SkiaView.cs index da90956..bfb4e03 100644 --- a/Views/SkiaView.cs +++ b/Views/SkiaView.cs @@ -7,8 +7,9 @@ namespace Microsoft.Maui.Platform; /// /// Base class for all Skia-rendered views on Linux. +/// Inherits from BindableObject to enable XAML styling, data binding, and Visual State Manager. /// -public abstract class SkiaView : IDisposable +public abstract class SkiaView : BindableObject, IDisposable { // Popup overlay system for dropdowns, calendars, etc. private static readonly List<(SkiaView Owner, Action Draw)> _popupOverlays = new(); @@ -32,7 +33,7 @@ public abstract class SkiaView : IDisposable { canvas.Restore(); } - + foreach (var (_, draw) in _popupOverlays) { canvas.Save(); @@ -41,6 +42,189 @@ public abstract class SkiaView : IDisposable } } + /// + /// Gets the popup owner that should receive pointer events at the given coordinates. + /// This allows popups to receive events even outside their normal bounds. + /// + public static SkiaView? GetPopupOwnerAt(float x, float y) + { + // Check in reverse order (topmost popup first) + for (int i = _popupOverlays.Count - 1; i >= 0; i--) + { + var owner = _popupOverlays[i].Owner; + if (owner.HitTestPopupArea(x, y)) + { + return owner; + } + } + return null; + } + + /// + /// Checks if there are any active popup overlays. + /// + public static bool HasActivePopup => _popupOverlays.Count > 0; + + /// + /// Override this to define the popup area for hit testing. + /// + protected virtual bool HitTestPopupArea(float x, float y) + { + // Default: no popup area beyond normal bounds + return Bounds.Contains(x, y); + } + + #region BindableProperties + + /// + /// Bindable property for IsVisible. + /// + public static readonly BindableProperty IsVisibleProperty = + BindableProperty.Create( + nameof(IsVisible), + typeof(bool), + typeof(SkiaView), + true, + propertyChanged: (b, o, n) => ((SkiaView)b).OnVisibilityChanged()); + + /// + /// Bindable property for IsEnabled. + /// + public static readonly BindableProperty IsEnabledProperty = + BindableProperty.Create( + nameof(IsEnabled), + typeof(bool), + typeof(SkiaView), + true, + propertyChanged: (b, o, n) => ((SkiaView)b).OnEnabledChanged()); + + /// + /// Bindable property for Opacity. + /// + public static readonly BindableProperty OpacityProperty = + BindableProperty.Create( + nameof(Opacity), + typeof(float), + typeof(SkiaView), + 1.0f, + coerceValue: (b, v) => Math.Clamp((float)v, 0f, 1f), + propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate()); + + /// + /// Bindable property for BackgroundColor. + /// + public static readonly BindableProperty BackgroundColorProperty = + BindableProperty.Create( + nameof(BackgroundColor), + typeof(SKColor), + typeof(SkiaView), + SKColors.Transparent, + propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate()); + + /// + /// Bindable property for WidthRequest. + /// + public static readonly BindableProperty WidthRequestProperty = + BindableProperty.Create( + nameof(WidthRequest), + typeof(double), + typeof(SkiaView), + -1.0, + propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure()); + + /// + /// Bindable property for HeightRequest. + /// + public static readonly BindableProperty HeightRequestProperty = + BindableProperty.Create( + nameof(HeightRequest), + typeof(double), + typeof(SkiaView), + -1.0, + propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure()); + + /// + /// Bindable property for MinimumWidthRequest. + /// + public static readonly BindableProperty MinimumWidthRequestProperty = + BindableProperty.Create( + nameof(MinimumWidthRequest), + typeof(double), + typeof(SkiaView), + 0.0, + propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure()); + + /// + /// Bindable property for MinimumHeightRequest. + /// + public static readonly BindableProperty MinimumHeightRequestProperty = + BindableProperty.Create( + nameof(MinimumHeightRequest), + typeof(double), + typeof(SkiaView), + 0.0, + propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure()); + + /// + /// Bindable property for IsFocusable. + /// + public static readonly BindableProperty IsFocusableProperty = + BindableProperty.Create( + nameof(IsFocusable), + typeof(bool), + typeof(SkiaView), + false); + + /// + /// Bindable property for Margin. + /// + public static readonly BindableProperty MarginProperty = + BindableProperty.Create( + nameof(Margin), + typeof(Thickness), + typeof(SkiaView), + default(Thickness), + propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure()); + + /// + /// Bindable property for HorizontalOptions. + /// + public static readonly BindableProperty HorizontalOptionsProperty = + BindableProperty.Create( + nameof(HorizontalOptions), + typeof(LayoutOptions), + typeof(SkiaView), + LayoutOptions.Fill, + propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure()); + + /// + /// Bindable property for VerticalOptions. + /// + public static readonly BindableProperty VerticalOptionsProperty = + BindableProperty.Create( + nameof(VerticalOptions), + typeof(LayoutOptions), + typeof(SkiaView), + LayoutOptions.Fill, + propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure()); + + /// + /// Bindable property for Name (used for template child lookup). + /// + public static readonly BindableProperty NameProperty = + BindableProperty.Create( + nameof(Name), + typeof(string), + typeof(SkiaView), + string.Empty); + + #endregion + + private bool _disposed; + private SKRect _bounds; + private SkiaView? _parent; + private readonly List _children = new(); + /// /// Gets the absolute bounds of this view in screen coordinates. /// @@ -64,15 +248,6 @@ public abstract class SkiaView : IDisposable return bounds; } - private bool _disposed; - private SKRect _bounds; - private bool _isVisible = true; - private bool _isEnabled = true; - private float _opacity = 1.0f; - private SKColor _backgroundColor = SKColors.Transparent; - private SkiaView? _parent; - private readonly List _children = new(); - /// /// Gets or sets the bounds of this view in parent coordinates. /// @@ -94,15 +269,8 @@ public abstract class SkiaView : IDisposable /// public bool IsVisible { - get => _isVisible; - set - { - if (_isVisible != value) - { - _isVisible = value; - Invalidate(); - } - } + get => (bool)GetValue(IsVisibleProperty); + set => SetValue(IsVisibleProperty, value); } /// @@ -110,15 +278,8 @@ public abstract class SkiaView : IDisposable /// public bool IsEnabled { - get => _isEnabled; - set - { - if (_isEnabled != value) - { - _isEnabled = value; - Invalidate(); - } - } + get => (bool)GetValue(IsEnabledProperty); + set => SetValue(IsEnabledProperty, value); } /// @@ -126,21 +287,14 @@ public abstract class SkiaView : IDisposable /// public float Opacity { - get => _opacity; - set - { - var clamped = Math.Clamp(value, 0f, 1f); - if (_opacity != clamped) - { - _opacity = clamped; - Invalidate(); - } - } + get => (float)GetValue(OpacityProperty); + set => SetValue(OpacityProperty, value); } /// /// Gets or sets the background color. /// + private SKColor _backgroundColor = SKColors.Transparent; public SKColor BackgroundColor { get => _backgroundColor; @@ -149,6 +303,7 @@ public abstract class SkiaView : IDisposable if (_backgroundColor != value) { _backgroundColor = value; + SetValue(BackgroundColorProperty, value); // Keep BindableProperty in sync for bindings Invalidate(); } } @@ -157,17 +312,101 @@ public abstract class SkiaView : IDisposable /// /// Gets or sets the requested width. /// - public double RequestedWidth { get; set; } = -1; + public double WidthRequest + { + get => (double)GetValue(WidthRequestProperty); + set => SetValue(WidthRequestProperty, value); + } /// /// Gets or sets the requested height. /// - public double RequestedHeight { get; set; } = -1; + public double HeightRequest + { + get => (double)GetValue(HeightRequestProperty); + set => SetValue(HeightRequestProperty, value); + } + + /// + /// Gets or sets the minimum width request. + /// + public double MinimumWidthRequest + { + get => (double)GetValue(MinimumWidthRequestProperty); + set => SetValue(MinimumWidthRequestProperty, value); + } + + /// + /// Gets or sets the minimum height request. + /// + public double MinimumHeightRequest + { + get => (double)GetValue(MinimumHeightRequestProperty); + set => SetValue(MinimumHeightRequestProperty, value); + } + + /// + /// Gets or sets the requested width (backwards compatibility alias). + /// + public double RequestedWidth + { + get => WidthRequest; + set => WidthRequest = value; + } + + /// + /// Gets or sets the requested height (backwards compatibility alias). + /// + public double RequestedHeight + { + get => HeightRequest; + set => HeightRequest = value; + } /// /// Gets or sets whether this view can receive keyboard focus. /// - public bool IsFocusable { get; set; } + public bool IsFocusable + { + get => (bool)GetValue(IsFocusableProperty); + set => SetValue(IsFocusableProperty, value); + } + + /// + /// Gets or sets the margin around this view. + /// + public Thickness Margin + { + get => (Thickness)GetValue(MarginProperty); + set => SetValue(MarginProperty, value); + } + + /// + /// Gets or sets the horizontal layout options. + /// + public LayoutOptions HorizontalOptions + { + get => (LayoutOptions)GetValue(HorizontalOptionsProperty); + set => SetValue(HorizontalOptionsProperty, value); + } + + /// + /// Gets or sets the vertical layout options. + /// + public LayoutOptions VerticalOptions + { + get => (LayoutOptions)GetValue(VerticalOptionsProperty); + set => SetValue(VerticalOptionsProperty, value); + } + + /// + /// Gets or sets the name of this view (used for template child lookup). + /// + public string Name + { + get => (string)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } /// /// Gets or sets whether this view currently has keyboard focus. @@ -183,6 +422,34 @@ public abstract class SkiaView : IDisposable internal set => _parent = value; } + /// + /// Gets the bounds of this view in screen coordinates (accounting for scroll offsets). + /// + public SKRect ScreenBounds + { + get + { + var bounds = Bounds; + var parent = _parent; + + // Walk up the tree and adjust for scroll offsets + while (parent != null) + { + if (parent is SkiaScrollView scrollView) + { + bounds = new SKRect( + bounds.Left - scrollView.ScrollX, + bounds.Top - scrollView.ScrollY, + bounds.Right - scrollView.ScrollX, + bounds.Bottom - scrollView.ScrollY); + } + parent = parent.Parent; + } + + return bounds; + } + } + /// /// Gets the desired size calculated during measure. /// @@ -198,6 +465,36 @@ public abstract class SkiaView : IDisposable /// public event EventHandler? Invalidated; + /// + /// Called when visibility changes. + /// + protected virtual void OnVisibilityChanged() + { + Invalidate(); + } + + /// + /// Called when enabled state changes. + /// + protected virtual void OnEnabledChanged() + { + Invalidate(); + } + + /// + /// Called when binding context changes. Propagates to children. + /// + protected override void OnBindingContextChanged() + { + base.OnBindingContextChanged(); + + // Propagate binding context to children + foreach (var child in _children) + { + SetInheritedBindingContext(child, BindingContext); + } + } + /// /// Adds a child view. /// @@ -208,6 +505,13 @@ public abstract class SkiaView : IDisposable child._parent = this; _children.Add(child); + + // Propagate binding context to new child + if (BindingContext != null) + { + SetInheritedBindingContext(child, BindingContext); + } + Invalidate(); } @@ -234,6 +538,13 @@ public abstract class SkiaView : IDisposable child._parent = this; _children.Insert(index, child); + + // Propagate binding context to new child + if (BindingContext != null) + { + SetInheritedBindingContext(child, BindingContext); + } + Invalidate(); } @@ -275,7 +586,9 @@ public abstract class SkiaView : IDisposable public void Draw(SKCanvas canvas) { if (!IsVisible || Opacity <= 0) + { return; + } canvas.Save(); @@ -338,8 +651,8 @@ public abstract class SkiaView : IDisposable /// protected virtual SKSize MeasureOverride(SKSize availableSize) { - var width = RequestedWidth >= 0 ? (float)RequestedWidth : 0; - var height = RequestedHeight >= 0 ? (float)RequestedHeight : 0; + var width = WidthRequest >= 0 ? (float)WidthRequest : 0; + var height = HeightRequest >= 0 ? (float)HeightRequest : 0; return new SKSize(width, height); } @@ -369,6 +682,7 @@ public abstract class SkiaView : IDisposable /// /// Performs hit testing to find the view at the given coordinates. + /// Coordinates are in absolute window space, matching how Bounds are stored. /// public virtual SkiaView? HitTest(float x, float y) { @@ -379,11 +693,10 @@ public abstract class SkiaView : IDisposable return null; // Check children in reverse order (top-most first) - var localX = x - Bounds.Left; - var localY = y - Bounds.Top; + // Coordinates stay in absolute space since children have absolute Bounds for (int i = _children.Count - 1; i >= 0; i--) { - var hit = _children[i].HitTest(localX, localY); + var hit = _children[i].HitTest(x, y); if (hit != null) return hit; } diff --git a/Views/SkiaVisualStateManager.cs b/Views/SkiaVisualStateManager.cs new file mode 100644 index 0000000..6472d8d --- /dev/null +++ b/Views/SkiaVisualStateManager.cs @@ -0,0 +1,216 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Maui.Platform; + +/// +/// Visual State Manager for Skia-rendered controls. +/// Provides state-based styling through XAML VisualStateGroups. +/// +public static class SkiaVisualStateManager +{ + /// + /// Common visual state names. + /// + public static class CommonStates + { + public const string Normal = "Normal"; + public const string Disabled = "Disabled"; + public const string Focused = "Focused"; + public const string PointerOver = "PointerOver"; + public const string Pressed = "Pressed"; + public const string Selected = "Selected"; + public const string Checked = "Checked"; + public const string Unchecked = "Unchecked"; + public const string On = "On"; + public const string Off = "Off"; + } + + /// + /// Attached property for VisualStateGroups. + /// + public static readonly BindableProperty VisualStateGroupsProperty = + BindableProperty.CreateAttached( + "VisualStateGroups", + typeof(SkiaVisualStateGroupList), + typeof(SkiaVisualStateManager), + null, + propertyChanged: OnVisualStateGroupsChanged); + + /// + /// Gets the visual state groups for the specified view. + /// + public static SkiaVisualStateGroupList? GetVisualStateGroups(SkiaView view) + { + return (SkiaVisualStateGroupList?)view.GetValue(VisualStateGroupsProperty); + } + + /// + /// Sets the visual state groups for the specified view. + /// + public static void SetVisualStateGroups(SkiaView view, SkiaVisualStateGroupList? value) + { + view.SetValue(VisualStateGroupsProperty, value); + } + + private static void OnVisualStateGroupsChanged(BindableObject bindable, object? oldValue, object? newValue) + { + if (bindable is SkiaView view && newValue is SkiaVisualStateGroupList groups) + { + // Initialize to default state + GoToState(view, CommonStates.Normal); + } + } + + /// + /// Transitions the view to the specified visual state. + /// + /// The view to transition. + /// The name of the state to transition to. + /// True if the state was found and applied, false otherwise. + public static bool GoToState(SkiaView view, string stateName) + { + var groups = GetVisualStateGroups(view); + if (groups == null || groups.Count == 0) + return false; + + bool stateFound = false; + + foreach (var group in groups) + { + // Find the state in this group + SkiaVisualState? targetState = null; + foreach (var state in group.States) + { + if (state.Name == stateName) + { + targetState = state; + break; + } + } + + if (targetState != null) + { + // Unapply current state if different + if (group.CurrentState != null && group.CurrentState != targetState) + { + UnapplyState(view, group.CurrentState); + } + + // Apply new state + ApplyState(view, targetState); + group.CurrentState = targetState; + stateFound = true; + } + } + + return stateFound; + } + + private static void ApplyState(SkiaView view, SkiaVisualState state) + { + foreach (var setter in state.Setters) + { + setter.Apply(view); + } + } + + private static void UnapplyState(SkiaView view, SkiaVisualState state) + { + foreach (var setter in state.Setters) + { + setter.Unapply(view); + } + } +} + +/// +/// A list of visual state groups. +/// +public class SkiaVisualStateGroupList : List +{ +} + +/// +/// A group of mutually exclusive visual states. +/// +public class SkiaVisualStateGroup +{ + /// + /// Gets or sets the name of this group. + /// + public string Name { get; set; } = ""; + + /// + /// Gets the collection of states in this group. + /// + public List States { get; } = new(); + + /// + /// Gets or sets the currently active state. + /// + public SkiaVisualState? CurrentState { get; set; } +} + +/// +/// Represents a single visual state with its setters. +/// +public class SkiaVisualState +{ + /// + /// Gets or sets the name of this state. + /// + public string Name { get; set; } = ""; + + /// + /// Gets the collection of setters for this state. + /// + public List Setters { get; } = new(); +} + +/// +/// Sets a property value when a visual state is active. +/// +public class SkiaVisualStateSetter +{ + /// + /// Gets or sets the property to set. + /// + public BindableProperty? Property { get; set; } + + /// + /// Gets or sets the value to set. + /// + public object? Value { get; set; } + + // Store original value for unapply + private object? _originalValue; + private bool _hasOriginalValue; + + /// + /// Applies this setter to the target view. + /// + public void Apply(SkiaView view) + { + if (Property == null) return; + + // Store original value if not already stored + if (!_hasOriginalValue) + { + _originalValue = view.GetValue(Property); + _hasOriginalValue = true; + } + + view.SetValue(Property, Value); + } + + /// + /// Unapplies this setter, restoring the original value. + /// + public void Unapply(SkiaView view) + { + if (Property == null || !_hasOriginalValue) return; + + view.SetValue(Property, _originalValue); + } +} diff --git a/Views/SkiaWebView.cs b/Views/SkiaWebView.cs new file mode 100644 index 0000000..371c9c1 --- /dev/null +++ b/Views/SkiaWebView.cs @@ -0,0 +1,695 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using SkiaSharp; + +namespace Microsoft.Maui.Platform; + +/// +/// WebView implementation using WebKitGTK for Linux. +/// Renders web content in a native GTK window and composites to Skia. +/// +public class SkiaWebView : SkiaView +{ + #region Native Interop - GTK + + private const string LibGtk4 = "libgtk-4.so.1"; + private const string LibGtk3 = "libgtk-3.so.0"; + private const string LibWebKit2Gtk4 = "libwebkitgtk-6.0.so.4"; + private const string LibWebKit2Gtk3 = "libwebkit2gtk-4.1.so.0"; + private const string LibGObject = "libgobject-2.0.so.0"; + private const string LibGLib = "libglib-2.0.so.0"; + + private static bool _useGtk4; + private static bool _gtkInitialized; + private static string _webkitLib = LibWebKit2Gtk3; + + // GTK functions + [DllImport(LibGtk4, EntryPoint = "gtk_init")] + private static extern void gtk4_init(); + + [DllImport(LibGtk3, EntryPoint = "gtk_init_check")] + private static extern bool gtk3_init_check(ref int argc, ref IntPtr argv); + + [DllImport(LibGtk4, EntryPoint = "gtk_window_new")] + private static extern IntPtr gtk4_window_new(); + + [DllImport(LibGtk3, EntryPoint = "gtk_window_new")] + private static extern IntPtr gtk3_window_new(int type); + + [DllImport(LibGtk4, EntryPoint = "gtk_window_set_default_size")] + private static extern void gtk4_window_set_default_size(IntPtr window, int width, int height); + + [DllImport(LibGtk3, EntryPoint = "gtk_window_set_default_size")] + private static extern void gtk3_window_set_default_size(IntPtr window, int width, int height); + + [DllImport(LibGtk4, EntryPoint = "gtk_window_set_child")] + private static extern void gtk4_window_set_child(IntPtr window, IntPtr child); + + [DllImport(LibGtk3, EntryPoint = "gtk_container_add")] + private static extern void gtk3_container_add(IntPtr container, IntPtr widget); + + [DllImport(LibGtk4, EntryPoint = "gtk_widget_show")] + private static extern void gtk4_widget_show(IntPtr widget); + + [DllImport(LibGtk3, EntryPoint = "gtk_widget_show_all")] + private static extern void gtk3_widget_show_all(IntPtr widget); + + [DllImport(LibGtk4, EntryPoint = "gtk_widget_hide")] + private static extern void gtk4_widget_hide(IntPtr widget); + + [DllImport(LibGtk3, EntryPoint = "gtk_widget_hide")] + private static extern void gtk3_widget_hide(IntPtr widget); + + [DllImport(LibGtk4, EntryPoint = "gtk_widget_get_width")] + private static extern int gtk4_widget_get_width(IntPtr widget); + + [DllImport(LibGtk4, EntryPoint = "gtk_widget_get_height")] + private static extern int gtk4_widget_get_height(IntPtr widget); + + // GObject + [DllImport(LibGObject, EntryPoint = "g_object_unref")] + private static extern void g_object_unref(IntPtr obj); + + [DllImport(LibGObject, EntryPoint = "g_signal_connect_data")] + private static extern ulong g_signal_connect_data(IntPtr instance, + [MarshalAs(UnmanagedType.LPStr)] string signal, + IntPtr handler, IntPtr data, IntPtr destroyData, int flags); + + // GLib main loop (for event processing) + [DllImport(LibGLib, EntryPoint = "g_main_context_iteration")] + private static extern bool g_main_context_iteration(IntPtr context, bool mayBlock); + + #endregion + + #region WebKit Functions + + // We'll load these dynamically based on available version + private delegate IntPtr WebKitWebViewNewDelegate(); + private delegate void WebKitWebViewLoadUriDelegate(IntPtr webView, [MarshalAs(UnmanagedType.LPStr)] string uri); + private delegate void WebKitWebViewLoadHtmlDelegate(IntPtr webView, [MarshalAs(UnmanagedType.LPStr)] string html, [MarshalAs(UnmanagedType.LPStr)] string? baseUri); + private delegate IntPtr WebKitWebViewGetUriDelegate(IntPtr webView); + private delegate IntPtr WebKitWebViewGetTitleDelegate(IntPtr webView); + private delegate void WebKitWebViewGoBackDelegate(IntPtr webView); + private delegate void WebKitWebViewGoForwardDelegate(IntPtr webView); + private delegate bool WebKitWebViewCanGoBackDelegate(IntPtr webView); + private delegate bool WebKitWebViewCanGoForwardDelegate(IntPtr webView); + private delegate void WebKitWebViewReloadDelegate(IntPtr webView); + private delegate void WebKitWebViewStopLoadingDelegate(IntPtr webView); + private delegate double WebKitWebViewGetEstimatedLoadProgressDelegate(IntPtr webView); + private delegate IntPtr WebKitWebViewGetSettingsDelegate(IntPtr webView); + private delegate void WebKitSettingsSetEnableJavascriptDelegate(IntPtr settings, bool enabled); + + private static WebKitWebViewNewDelegate? _webkitWebViewNew; + private static WebKitWebViewLoadUriDelegate? _webkitLoadUri; + private static WebKitWebViewLoadHtmlDelegate? _webkitLoadHtml; + private static WebKitWebViewGetUriDelegate? _webkitGetUri; + private static WebKitWebViewGetTitleDelegate? _webkitGetTitle; + private static WebKitWebViewGoBackDelegate? _webkitGoBack; + private static WebKitWebViewGoForwardDelegate? _webkitGoForward; + private static WebKitWebViewCanGoBackDelegate? _webkitCanGoBack; + private static WebKitWebViewCanGoForwardDelegate? _webkitCanGoForward; + private static WebKitWebViewReloadDelegate? _webkitReload; + private static WebKitWebViewStopLoadingDelegate? _webkitStopLoading; + private static WebKitWebViewGetEstimatedLoadProgressDelegate? _webkitGetProgress; + private static WebKitWebViewGetSettingsDelegate? _webkitGetSettings; + private static WebKitSettingsSetEnableJavascriptDelegate? _webkitSetJavascript; + + [DllImport("libdl.so.2")] + private static extern IntPtr dlopen([MarshalAs(UnmanagedType.LPStr)] string? filename, int flags); + + [DllImport("libdl.so.2")] + private static extern IntPtr dlsym(IntPtr handle, [MarshalAs(UnmanagedType.LPStr)] string symbol); + + [DllImport("libdl.so.2")] + private static extern IntPtr dlerror(); + + private const int RTLD_NOW = 2; + private const int RTLD_GLOBAL = 0x100; + + private static IntPtr _webkitHandle; + + #endregion + + #region Fields + + private IntPtr _gtkWindow; + private IntPtr _webView; + private string _source = ""; + private string _html = ""; + private bool _isInitialized; + private bool _javascriptEnabled = true; + private double _loadProgress; + + #endregion + + #region Properties + + /// + /// Gets or sets the URL to navigate to. + /// + public string Source + { + get => _source; + set + { + if (_source != value) + { + _source = value; + if (_isInitialized && !string.IsNullOrEmpty(value)) + { + LoadUrl(value); + } + Invalidate(); + } + } + } + + /// + /// Gets or sets the HTML content to display. + /// + public string Html + { + get => _html; + set + { + if (_html != value) + { + _html = value; + if (_isInitialized && !string.IsNullOrEmpty(value)) + { + LoadHtml(value); + } + Invalidate(); + } + } + } + + /// + /// Gets whether the WebView can navigate back. + /// + public bool CanGoBack => _webView != IntPtr.Zero && _webkitCanGoBack?.Invoke(_webView) == true; + + /// + /// Gets whether the WebView can navigate forward. + /// + public bool CanGoForward => _webView != IntPtr.Zero && _webkitCanGoForward?.Invoke(_webView) == true; + + /// + /// Gets the current URL. + /// + public string? CurrentUrl + { + get + { + if (_webView == IntPtr.Zero || _webkitGetUri == null) return null; + var ptr = _webkitGetUri(_webView); + return ptr != IntPtr.Zero ? Marshal.PtrToStringAnsi(ptr) : null; + } + } + + /// + /// Gets the current page title. + /// + public string? Title + { + get + { + if (_webView == IntPtr.Zero || _webkitGetTitle == null) return null; + var ptr = _webkitGetTitle(_webView); + return ptr != IntPtr.Zero ? Marshal.PtrToStringAnsi(ptr) : null; + } + } + + /// + /// Gets or sets whether JavaScript is enabled. + /// + public bool JavaScriptEnabled + { + get => _javascriptEnabled; + set + { + _javascriptEnabled = value; + UpdateJavaScriptSetting(); + } + } + + /// + /// Gets the load progress (0.0 to 1.0). + /// + public double LoadProgress => _loadProgress; + + /// + /// Gets whether WebKit is available on this system. + /// + public static bool IsSupported => InitializeWebKit(); + + #endregion + + #region Events + + public event EventHandler? Navigating; + public event EventHandler? Navigated; + public event EventHandler? TitleChanged; + public event EventHandler? LoadProgressChanged; + + #endregion + + #region Constructor + + public SkiaWebView() + { + RequestedWidth = 400; + RequestedHeight = 300; + BackgroundColor = SKColors.White; + } + + #endregion + + #region Initialization + + private static bool InitializeWebKit() + { + if (_webkitHandle != IntPtr.Zero) return true; + + // Try WebKitGTK 6.0 (GTK4) first + _webkitHandle = dlopen(LibWebKit2Gtk4, RTLD_NOW | RTLD_GLOBAL); + if (_webkitHandle != IntPtr.Zero) + { + _useGtk4 = true; + _webkitLib = LibWebKit2Gtk4; + } + else + { + // Fall back to WebKitGTK 4.1 (GTK3) + _webkitHandle = dlopen(LibWebKit2Gtk3, RTLD_NOW | RTLD_GLOBAL); + if (_webkitHandle != IntPtr.Zero) + { + _useGtk4 = false; + _webkitLib = LibWebKit2Gtk3; + } + else + { + // Try older WebKitGTK 4.0 + _webkitHandle = dlopen("libwebkit2gtk-4.0.so.37", RTLD_NOW | RTLD_GLOBAL); + if (_webkitHandle != IntPtr.Zero) + { + _useGtk4 = false; + _webkitLib = "libwebkit2gtk-4.0.so.37"; + } + } + } + + if (_webkitHandle == IntPtr.Zero) + { + Console.WriteLine("[WebView] WebKitGTK not found. Install with: sudo apt install libwebkit2gtk-4.1-0"); + return false; + } + + // Load function pointers + _webkitWebViewNew = LoadFunction("webkit_web_view_new"); + _webkitLoadUri = LoadFunction("webkit_web_view_load_uri"); + _webkitLoadHtml = LoadFunction("webkit_web_view_load_html"); + _webkitGetUri = LoadFunction("webkit_web_view_get_uri"); + _webkitGetTitle = LoadFunction("webkit_web_view_get_title"); + _webkitGoBack = LoadFunction("webkit_web_view_go_back"); + _webkitGoForward = LoadFunction("webkit_web_view_go_forward"); + _webkitCanGoBack = LoadFunction("webkit_web_view_can_go_back"); + _webkitCanGoForward = LoadFunction("webkit_web_view_can_go_forward"); + _webkitReload = LoadFunction("webkit_web_view_reload"); + _webkitStopLoading = LoadFunction("webkit_web_view_stop_loading"); + _webkitGetProgress = LoadFunction("webkit_web_view_get_estimated_load_progress"); + _webkitGetSettings = LoadFunction("webkit_web_view_get_settings"); + _webkitSetJavascript = LoadFunction("webkit_settings_set_enable_javascript"); + + Console.WriteLine($"[WebView] Using {_webkitLib}"); + return _webkitWebViewNew != null; + } + + private static T? LoadFunction(string name) where T : Delegate + { + var ptr = dlsym(_webkitHandle, name); + if (ptr == IntPtr.Zero) return null; + return Marshal.GetDelegateForFunctionPointer(ptr); + } + + private void Initialize() + { + if (_isInitialized) return; + if (!InitializeWebKit()) return; + + try + { + // Initialize GTK if needed + if (!_gtkInitialized) + { + if (_useGtk4) + { + gtk4_init(); + } + else + { + int argc = 0; + IntPtr argv = IntPtr.Zero; + gtk3_init_check(ref argc, ref argv); + } + _gtkInitialized = true; + } + + // Create WebKit view + _webView = _webkitWebViewNew!(); + if (_webView == IntPtr.Zero) + { + Console.WriteLine("[WebView] Failed to create WebKit view"); + return; + } + + // Create GTK window to host the WebView + if (_useGtk4) + { + _gtkWindow = gtk4_window_new(); + gtk4_window_set_default_size(_gtkWindow, (int)RequestedWidth, (int)RequestedHeight); + gtk4_window_set_child(_gtkWindow, _webView); + } + else + { + _gtkWindow = gtk3_window_new(0); // GTK_WINDOW_TOPLEVEL + gtk3_window_set_default_size(_gtkWindow, (int)RequestedWidth, (int)RequestedHeight); + gtk3_container_add(_gtkWindow, _webView); + } + + UpdateJavaScriptSetting(); + _isInitialized = true; + + // Load initial content + if (!string.IsNullOrEmpty(_source)) + { + LoadUrl(_source); + } + else if (!string.IsNullOrEmpty(_html)) + { + LoadHtml(_html); + } + + Console.WriteLine("[WebView] Initialized successfully"); + } + catch (Exception ex) + { + Console.WriteLine($"[WebView] Initialization failed: {ex.Message}"); + } + } + + #endregion + + #region Navigation + + public void LoadUrl(string url) + { + if (!_isInitialized) Initialize(); + if (_webView == IntPtr.Zero || _webkitLoadUri == null) return; + + Navigating?.Invoke(this, new WebNavigatingEventArgs(url)); + _webkitLoadUri(_webView, url); + } + + public void LoadHtml(string html, string? baseUrl = null) + { + if (!_isInitialized) Initialize(); + if (_webView == IntPtr.Zero || _webkitLoadHtml == null) return; + + _webkitLoadHtml(_webView, html, baseUrl); + } + + public void GoBack() + { + if (_webView != IntPtr.Zero && CanGoBack) + { + _webkitGoBack?.Invoke(_webView); + } + } + + public void GoForward() + { + if (_webView != IntPtr.Zero && CanGoForward) + { + _webkitGoForward?.Invoke(_webView); + } + } + + public void Reload() + { + if (_webView != IntPtr.Zero) + { + _webkitReload?.Invoke(_webView); + } + } + + public void Stop() + { + if (_webView != IntPtr.Zero) + { + _webkitStopLoading?.Invoke(_webView); + } + } + + private void UpdateJavaScriptSetting() + { + if (_webView == IntPtr.Zero || _webkitGetSettings == null || _webkitSetJavascript == null) return; + + var settings = _webkitGetSettings(_webView); + if (settings != IntPtr.Zero) + { + _webkitSetJavascript(settings, _javascriptEnabled); + } + } + + #endregion + + #region Event Processing + + /// + /// Process pending GTK events. Call this from your main loop. + /// + public void ProcessEvents() + { + if (!_isInitialized) return; + + // Process GTK events + g_main_context_iteration(IntPtr.Zero, false); + + // Update progress + if (_webView != IntPtr.Zero && _webkitGetProgress != null) + { + var progress = _webkitGetProgress(_webView); + if (Math.Abs(progress - _loadProgress) > 0.01) + { + _loadProgress = progress; + LoadProgressChanged?.Invoke(this, progress); + } + } + } + + /// + /// Show the native WebView window (for testing/debugging). + /// + public void ShowNativeWindow() + { + if (!_isInitialized) Initialize(); + if (_gtkWindow == IntPtr.Zero) return; + + if (_useGtk4) + { + gtk4_widget_show(_gtkWindow); + } + else + { + gtk3_widget_show_all(_gtkWindow); + } + } + + /// + /// Hide the native WebView window. + /// + public void HideNativeWindow() + { + if (_gtkWindow == IntPtr.Zero) return; + + if (_useGtk4) + { + gtk4_widget_hide(_gtkWindow); + } + else + { + gtk3_widget_hide(_gtkWindow); + } + } + + #endregion + + #region Rendering + + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + base.OnDraw(canvas, bounds); + + // Draw placeholder/loading state + using var bgPaint = new SKPaint { Color = BackgroundColor, Style = SKPaintStyle.Fill }; + canvas.DrawRect(bounds, bgPaint); + + // Draw border + using var borderPaint = new SKPaint + { + Color = new SKColor(200, 200, 200), + Style = SKPaintStyle.Stroke, + StrokeWidth = 1 + }; + canvas.DrawRect(bounds, borderPaint); + + // Draw web icon and status + var centerX = bounds.MidX; + var centerY = bounds.MidY; + + // Globe icon + using var iconPaint = new SKPaint + { + Color = new SKColor(100, 100, 100), + Style = SKPaintStyle.Stroke, + StrokeWidth = 2, + IsAntialias = true + }; + canvas.DrawCircle(centerX, centerY - 20, 25, iconPaint); + canvas.DrawLine(centerX - 25, centerY - 20, centerX + 25, centerY - 20, iconPaint); + canvas.DrawArc(new SKRect(centerX - 15, centerY - 45, centerX + 15, centerY + 5), 0, 180, false, iconPaint); + + // Status text + using var textPaint = new SKPaint + { + Color = new SKColor(80, 80, 80), + IsAntialias = true, + TextSize = 14 + }; + + string statusText; + if (!IsSupported) + { + statusText = "WebKitGTK not installed"; + } + else if (_isInitialized) + { + statusText = string.IsNullOrEmpty(_source) ? "No URL loaded" : $"Loading: {_source}"; + if (_loadProgress > 0 && _loadProgress < 1) + { + statusText = $"Loading: {(int)(_loadProgress * 100)}%"; + } + } + else + { + statusText = "WebView (click to open)"; + } + + var textWidth = textPaint.MeasureText(statusText); + canvas.DrawText(statusText, centerX - textWidth / 2, centerY + 30, textPaint); + + // Draw install hint if not supported + if (!IsSupported) + { + using var hintPaint = new SKPaint + { + Color = new SKColor(120, 120, 120), + IsAntialias = true, + TextSize = 11 + }; + var hint = "Install: sudo apt install libwebkit2gtk-4.1-0"; + var hintWidth = hintPaint.MeasureText(hint); + canvas.DrawText(hint, centerX - hintWidth / 2, centerY + 50, hintPaint); + } + + // Progress bar + if (_loadProgress > 0 && _loadProgress < 1) + { + var progressRect = new SKRect(bounds.Left + 20, bounds.Bottom - 30, bounds.Right - 20, bounds.Bottom - 20); + using var progressBgPaint = new SKPaint { Color = new SKColor(230, 230, 230), Style = SKPaintStyle.Fill }; + canvas.DrawRoundRect(new SKRoundRect(progressRect, 5), progressBgPaint); + + var filledWidth = progressRect.Width * (float)_loadProgress; + var filledRect = new SKRect(progressRect.Left, progressRect.Top, progressRect.Left + filledWidth, progressRect.Bottom); + using var progressPaint = new SKPaint { Color = new SKColor(33, 150, 243), Style = SKPaintStyle.Fill }; + canvas.DrawRoundRect(new SKRoundRect(filledRect, 5), progressPaint); + } + } + + public override void OnPointerPressed(PointerEventArgs e) + { + base.OnPointerPressed(e); + + if (!_isInitialized && IsSupported) + { + Initialize(); + ShowNativeWindow(); + } + else if (_isInitialized) + { + ShowNativeWindow(); + } + } + + #endregion + + #region Cleanup + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (_gtkWindow != IntPtr.Zero) + { + if (_useGtk4) + { + gtk4_widget_hide(_gtkWindow); + } + else + { + gtk3_widget_hide(_gtkWindow); + } + g_object_unref(_gtkWindow); + _gtkWindow = IntPtr.Zero; + } + _webView = IntPtr.Zero; + _isInitialized = false; + } + + base.Dispose(disposing); + } + + #endregion +} + +#region Event Args + +public class WebNavigatingEventArgs : EventArgs +{ + public string Url { get; } + public bool Cancel { get; set; } + + public WebNavigatingEventArgs(string url) + { + Url = url; + } +} + +public class WebNavigatedEventArgs : EventArgs +{ + public string Url { get; } + public bool Success { get; } + public string? Error { get; } + + public WebNavigatedEventArgs(string url, bool success, string? error = null) + { + Url = url; + Success = success; + Error = error; + } +} + +#endregion diff --git a/Window/WaylandWindow.cs b/Window/WaylandWindow.cs new file mode 100644 index 0000000..7cdf811 --- /dev/null +++ b/Window/WaylandWindow.cs @@ -0,0 +1,1334 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using Microsoft.Maui.Platform.Linux.Input; + +namespace Microsoft.Maui.Platform.Linux.Window; + +/// +/// Native Wayland window implementation using xdg-shell protocol. +/// Provides full Wayland support without XWayland dependency. +/// +public class WaylandWindow : IDisposable +{ + #region Native Interop - libwayland-client + + private const string LibWaylandClient = "libwayland-client.so.0"; + + // Core display functions (actually exported) + [DllImport(LibWaylandClient)] + private static extern IntPtr wl_display_connect(string? name); + + [DllImport(LibWaylandClient)] + private static extern void wl_display_disconnect(IntPtr display); + + [DllImport(LibWaylandClient)] + private static extern int wl_display_dispatch(IntPtr display); + + [DllImport(LibWaylandClient)] + private static extern int wl_display_dispatch_pending(IntPtr display); + + [DllImport(LibWaylandClient)] + private static extern int wl_display_roundtrip(IntPtr display); + + [DllImport(LibWaylandClient)] + private static extern int wl_display_flush(IntPtr display); + + [DllImport(LibWaylandClient)] + private static extern int wl_display_get_fd(IntPtr display); + + // Low-level proxy API (actually exported - used to implement protocol wrappers) + [DllImport(LibWaylandClient)] + private static extern IntPtr wl_proxy_marshal_constructor( + IntPtr proxy, uint opcode, IntPtr iface, IntPtr arg); + + [DllImport(LibWaylandClient)] + private static extern IntPtr wl_proxy_marshal_constructor_versioned( + IntPtr proxy, uint opcode, IntPtr iface, uint version, IntPtr arg); + + [DllImport(LibWaylandClient)] + private static extern void wl_proxy_marshal(IntPtr proxy, uint opcode); + + [DllImport(LibWaylandClient)] + private static extern void wl_proxy_marshal(IntPtr proxy, uint opcode, IntPtr arg1); + + [DllImport(LibWaylandClient)] + private static extern void wl_proxy_marshal(IntPtr proxy, uint opcode, int arg1, int arg2); + + [DllImport(LibWaylandClient)] + private static extern void wl_proxy_marshal(IntPtr proxy, uint opcode, IntPtr arg1, int arg2, int arg3); + + [DllImport(LibWaylandClient)] + private static extern void wl_proxy_marshal(IntPtr proxy, uint opcode, int arg1, int arg2, int arg3, int arg4); + + [DllImport(LibWaylandClient)] + private static extern void wl_proxy_marshal(IntPtr proxy, uint opcode, uint arg1); + + [DllImport(LibWaylandClient)] + private static extern void wl_proxy_marshal(IntPtr proxy, uint opcode, + [MarshalAs(UnmanagedType.LPStr)] string arg1); + + [DllImport(LibWaylandClient)] + private static extern IntPtr wl_proxy_marshal_array_constructor( + IntPtr proxy, uint opcode, IntPtr args, IntPtr iface); + + [DllImport(LibWaylandClient)] + private static extern IntPtr wl_proxy_marshal_array_constructor_versioned( + IntPtr proxy, uint opcode, IntPtr args, IntPtr iface, uint version); + + [DllImport(LibWaylandClient)] + private static extern int wl_proxy_add_listener(IntPtr proxy, IntPtr impl, IntPtr data); + + [DllImport(LibWaylandClient)] + private static extern void wl_proxy_destroy(IntPtr proxy); + + [DllImport(LibWaylandClient)] + private static extern uint wl_proxy_get_version(IntPtr proxy); + + // Interface globals (exported as data symbols) + [DllImport(LibWaylandClient)] + private static extern IntPtr wl_registry_interface_ptr(); + + // We need to load these at runtime since they're data symbols + private static IntPtr _wl_registry_interface; + private static IntPtr _wl_compositor_interface; + private static IntPtr _wl_shm_interface; + private static IntPtr _wl_shm_pool_interface; + private static IntPtr _wl_buffer_interface; + private static IntPtr _wl_surface_interface; + private static IntPtr _wl_seat_interface; + private static IntPtr _wl_pointer_interface; + private static IntPtr _wl_keyboard_interface; + + // dlsym for loading interface symbols + [DllImport("libdl.so.2", EntryPoint = "dlopen")] + private static extern IntPtr dlopen(string? filename, int flags); + + [DllImport("libdl.so.2", EntryPoint = "dlsym")] + private static extern IntPtr dlsym(IntPtr handle, string symbol); + + [DllImport("libdl.so.2", EntryPoint = "dlclose")] + private static extern int dlclose(IntPtr handle); + + private const int RTLD_NOW = 2; + private const int RTLD_GLOBAL = 0x100; + + #endregion + + #region Wayland Protocol Opcodes + + // wl_display opcodes + private const uint WL_DISPLAY_GET_REGISTRY = 1; + + // wl_registry opcodes + private const uint WL_REGISTRY_BIND = 0; + + // wl_compositor opcodes + private const uint WL_COMPOSITOR_CREATE_SURFACE = 0; + + // wl_surface opcodes + private const uint WL_SURFACE_DESTROY = 0; + private const uint WL_SURFACE_ATTACH = 1; + private const uint WL_SURFACE_DAMAGE = 2; + private const uint WL_SURFACE_COMMIT = 6; + private const uint WL_SURFACE_DAMAGE_BUFFER = 9; + + // wl_shm opcodes + private const uint WL_SHM_CREATE_POOL = 0; + + // wl_shm_pool opcodes + private const uint WL_SHM_POOL_CREATE_BUFFER = 0; + private const uint WL_SHM_POOL_DESTROY = 1; + + // wl_buffer opcodes + private const uint WL_BUFFER_DESTROY = 0; + + // wl_seat opcodes + private const uint WL_SEAT_GET_POINTER = 0; + private const uint WL_SEAT_GET_KEYBOARD = 1; + + // xdg_wm_base opcodes + private const uint XDG_WM_BASE_GET_XDG_SURFACE = 2; + private const uint XDG_WM_BASE_PONG = 3; + + // xdg_surface opcodes + private const uint XDG_SURFACE_DESTROY = 0; + private const uint XDG_SURFACE_GET_TOPLEVEL = 1; + private const uint XDG_SURFACE_ACK_CONFIGURE = 4; + + // xdg_toplevel opcodes + private const uint XDG_TOPLEVEL_DESTROY = 0; + private const uint XDG_TOPLEVEL_SET_TITLE = 2; + private const uint XDG_TOPLEVEL_SET_APP_ID = 3; + + #endregion + + #region Protocol Wrapper Methods + + private static void LoadInterfaceSymbols() + { + if (_wl_registry_interface != IntPtr.Zero) return; + + var handle = dlopen("libwayland-client.so.0", RTLD_NOW | RTLD_GLOBAL); + if (handle == IntPtr.Zero) + throw new InvalidOperationException("Failed to load libwayland-client.so.0"); + + _wl_registry_interface = dlsym(handle, "wl_registry_interface"); + _wl_compositor_interface = dlsym(handle, "wl_compositor_interface"); + _wl_shm_interface = dlsym(handle, "wl_shm_interface"); + _wl_shm_pool_interface = dlsym(handle, "wl_shm_pool_interface"); + _wl_buffer_interface = dlsym(handle, "wl_buffer_interface"); + _wl_surface_interface = dlsym(handle, "wl_surface_interface"); + _wl_seat_interface = dlsym(handle, "wl_seat_interface"); + _wl_pointer_interface = dlsym(handle, "wl_pointer_interface"); + _wl_keyboard_interface = dlsym(handle, "wl_keyboard_interface"); + + // Don't close - we need the symbols to remain valid + } + + // wl_display_get_registry wrapper + private static IntPtr wl_display_get_registry(IntPtr display) + { + return wl_proxy_marshal_constructor(display, WL_DISPLAY_GET_REGISTRY, + _wl_registry_interface, IntPtr.Zero); + } + + // wl_registry_add_listener wrapper + private static int wl_registry_add_listener(IntPtr registry, IntPtr listener, IntPtr data) + { + return wl_proxy_add_listener(registry, listener, data); + } + + // wl_registry_bind wrapper - uses special marshaling + [DllImport(LibWaylandClient, EntryPoint = "wl_proxy_marshal_flags")] + private static extern IntPtr wl_proxy_marshal_flags( + IntPtr proxy, uint opcode, IntPtr iface, uint version, uint flags, + uint name, IntPtr ifaceName, uint ifaceVersion); + + private static IntPtr wl_registry_bind(IntPtr registry, uint name, IntPtr iface, uint version) + { + // For registry bind, we need to use marshal_flags with the interface + return wl_proxy_marshal_flags(registry, WL_REGISTRY_BIND, iface, version, 0, + name, iface, version); + } + + // wl_compositor_create_surface wrapper + private static IntPtr wl_compositor_create_surface(IntPtr compositor) + { + return wl_proxy_marshal_constructor(compositor, WL_COMPOSITOR_CREATE_SURFACE, + _wl_surface_interface, IntPtr.Zero); + } + + // wl_surface methods + private static void wl_surface_attach(IntPtr surface, IntPtr buffer, int x, int y) + { + wl_proxy_marshal(surface, WL_SURFACE_ATTACH, buffer, x, y); + } + + private static void wl_surface_damage(IntPtr surface, int x, int y, int width, int height) + { + wl_proxy_marshal(surface, WL_SURFACE_DAMAGE, x, y, width, height); + } + + private static void wl_surface_damage_buffer(IntPtr surface, int x, int y, int width, int height) + { + wl_proxy_marshal(surface, WL_SURFACE_DAMAGE_BUFFER, x, y, width, height); + } + + private static void wl_surface_commit(IntPtr surface) + { + wl_proxy_marshal(surface, WL_SURFACE_COMMIT); + } + + private static void wl_surface_destroy(IntPtr surface) + { + wl_proxy_marshal(surface, WL_SURFACE_DESTROY); + wl_proxy_destroy(surface); + } + + // wl_shm_create_pool wrapper + [DllImport(LibWaylandClient, EntryPoint = "wl_proxy_marshal_flags")] + private static extern IntPtr wl_proxy_marshal_flags_fd( + IntPtr proxy, uint opcode, IntPtr iface, uint version, uint flags, + IntPtr newId, int fd, int size); + + private static IntPtr wl_shm_create_pool(IntPtr shm, int fd, int size) + { + return wl_proxy_marshal_flags_fd(shm, WL_SHM_CREATE_POOL, + _wl_shm_pool_interface, wl_proxy_get_version(shm), 0, + IntPtr.Zero, fd, size); + } + + // wl_shm_pool methods + [DllImport(LibWaylandClient, EntryPoint = "wl_proxy_marshal_flags")] + private static extern IntPtr wl_proxy_marshal_flags_buffer( + IntPtr proxy, uint opcode, IntPtr iface, uint version, uint flags, + IntPtr newId, int offset, int width, int height, int stride, uint format); + + private static IntPtr wl_shm_pool_create_buffer(IntPtr pool, int offset, int width, int height, int stride, uint format) + { + return wl_proxy_marshal_flags_buffer(pool, WL_SHM_POOL_CREATE_BUFFER, + _wl_buffer_interface, wl_proxy_get_version(pool), 0, + IntPtr.Zero, offset, width, height, stride, format); + } + + private static void wl_shm_pool_destroy(IntPtr pool) + { + wl_proxy_marshal(pool, WL_SHM_POOL_DESTROY); + wl_proxy_destroy(pool); + } + + // wl_buffer methods + private static void wl_buffer_destroy(IntPtr buffer) + { + wl_proxy_marshal(buffer, WL_BUFFER_DESTROY); + wl_proxy_destroy(buffer); + } + + private static int wl_buffer_add_listener(IntPtr buffer, IntPtr listener, IntPtr data) + { + return wl_proxy_add_listener(buffer, listener, data); + } + + // wl_seat methods + private static int wl_seat_add_listener(IntPtr seat, IntPtr listener, IntPtr data) + { + return wl_proxy_add_listener(seat, listener, data); + } + + private static IntPtr wl_seat_get_pointer(IntPtr seat) + { + return wl_proxy_marshal_constructor(seat, WL_SEAT_GET_POINTER, + _wl_pointer_interface, IntPtr.Zero); + } + + private static IntPtr wl_seat_get_keyboard(IntPtr seat) + { + return wl_proxy_marshal_constructor(seat, WL_SEAT_GET_KEYBOARD, + _wl_keyboard_interface, IntPtr.Zero); + } + + private static int wl_pointer_add_listener(IntPtr pointer, IntPtr listener, IntPtr data) + { + return wl_proxy_add_listener(pointer, listener, data); + } + + private static int wl_keyboard_add_listener(IntPtr keyboard, IntPtr listener, IntPtr data) + { + return wl_proxy_add_listener(keyboard, listener, data); + } + + #endregion + + #region xdg-shell Protocol Wrappers + + private static IntPtr _xdg_wm_base_interface; + private static IntPtr _xdg_surface_interface; + private static IntPtr _xdg_toplevel_interface; + + // We need to create and pin interface structures for xdg-shell + private static GCHandle _xdgWmBaseInterfaceHandle; + private static GCHandle _xdgSurfaceInterfaceHandle; + private static GCHandle _xdgToplevelInterfaceHandle; + private static IntPtr _xdgWmBaseName; + private static IntPtr _xdgSurfaceName; + private static IntPtr _xdgToplevelName; + + private static void LoadXdgShellInterfaces() + { + if (_xdg_wm_base_interface != IntPtr.Zero) return; + + // xdg-shell interfaces are NOT in libwayland-client + // We need to create minimal interface structs ourselves + // The key fields are: name (string ptr), version, method_count, methods, event_count, events + + // Allocate interface names + _xdgWmBaseName = Marshal.StringToHGlobalAnsi("xdg_wm_base"); + _xdgSurfaceName = Marshal.StringToHGlobalAnsi("xdg_surface"); + _xdgToplevelName = Marshal.StringToHGlobalAnsi("xdg_toplevel"); + + // Create interface structures + var wmBaseInterface = new WlInterface + { + Name = _xdgWmBaseName, + Version = 6, + MethodCount = 4, // destroy, create_positioner, get_xdg_surface, pong + Methods = IntPtr.Zero, + EventCount = 1, // ping + Events = IntPtr.Zero + }; + _xdgWmBaseInterfaceHandle = GCHandle.Alloc(wmBaseInterface, GCHandleType.Pinned); + _xdg_wm_base_interface = _xdgWmBaseInterfaceHandle.AddrOfPinnedObject(); + + var surfaceInterface = new WlInterface + { + Name = _xdgSurfaceName, + Version = 6, + MethodCount = 5, // destroy, get_toplevel, get_popup, set_window_geometry, ack_configure + Methods = IntPtr.Zero, + EventCount = 1, // configure + Events = IntPtr.Zero + }; + _xdgSurfaceInterfaceHandle = GCHandle.Alloc(surfaceInterface, GCHandleType.Pinned); + _xdg_surface_interface = _xdgSurfaceInterfaceHandle.AddrOfPinnedObject(); + + var toplevelInterface = new WlInterface + { + Name = _xdgToplevelName, + Version = 6, + MethodCount = 14, // destroy, set_parent, set_title, set_app_id, etc. + Methods = IntPtr.Zero, + EventCount = 4, // configure, close, configure_bounds, wm_capabilities + Events = IntPtr.Zero + }; + _xdgToplevelInterfaceHandle = GCHandle.Alloc(toplevelInterface, GCHandleType.Pinned); + _xdg_toplevel_interface = _xdgToplevelInterfaceHandle.AddrOfPinnedObject(); + } + + private static IntPtr xdg_wm_base_get_xdg_surface(IntPtr wmBase, IntPtr surface) + { + return wl_proxy_marshal_constructor(wmBase, XDG_WM_BASE_GET_XDG_SURFACE, + _xdg_surface_interface, surface); + } + + private static void xdg_wm_base_pong(IntPtr wmBase, uint serial) + { + wl_proxy_marshal(wmBase, XDG_WM_BASE_PONG, serial); + } + + private static int xdg_wm_base_add_listener(IntPtr wmBase, IntPtr listener, IntPtr data) + { + return wl_proxy_add_listener(wmBase, listener, data); + } + + private static IntPtr xdg_surface_get_toplevel(IntPtr xdgSurface) + { + return wl_proxy_marshal_constructor(xdgSurface, XDG_SURFACE_GET_TOPLEVEL, + _xdg_toplevel_interface, IntPtr.Zero); + } + + private static void xdg_surface_ack_configure(IntPtr xdgSurface, uint serial) + { + wl_proxy_marshal(xdgSurface, XDG_SURFACE_ACK_CONFIGURE, serial); + } + + private static int xdg_surface_add_listener(IntPtr xdgSurface, IntPtr listener, IntPtr data) + { + return wl_proxy_add_listener(xdgSurface, listener, data); + } + + private static void xdg_surface_destroy(IntPtr xdgSurface) + { + wl_proxy_marshal(xdgSurface, XDG_SURFACE_DESTROY); + wl_proxy_destroy(xdgSurface); + } + + private static void xdg_toplevel_set_title(IntPtr toplevel, string title) + { + wl_proxy_marshal(toplevel, XDG_TOPLEVEL_SET_TITLE, title); + } + + private static void xdg_toplevel_set_app_id(IntPtr toplevel, string appId) + { + wl_proxy_marshal(toplevel, XDG_TOPLEVEL_SET_APP_ID, appId); + } + + private static int xdg_toplevel_add_listener(IntPtr toplevel, IntPtr listener, IntPtr data) + { + return wl_proxy_add_listener(toplevel, listener, data); + } + + private static void xdg_toplevel_destroy(IntPtr toplevel) + { + wl_proxy_marshal(toplevel, XDG_TOPLEVEL_DESTROY); + wl_proxy_destroy(toplevel); + } + + #endregion + + #region Native Interop - libc + + [DllImport("libc", EntryPoint = "shm_open")] + private static extern int shm_open([MarshalAs(UnmanagedType.LPStr)] string name, int oflag, int mode); + + [DllImport("libc", EntryPoint = "shm_unlink")] + private static extern int shm_unlink([MarshalAs(UnmanagedType.LPStr)] string name); + + [DllImport("libc", EntryPoint = "ftruncate")] + private static extern int ftruncate(int fd, long length); + + [DllImport("libc", EntryPoint = "mmap")] + private static extern IntPtr mmap(IntPtr addr, nuint length, int prot, int flags, int fd, long offset); + + [DllImport("libc", EntryPoint = "munmap")] + private static extern int munmap(IntPtr addr, nuint length); + + [DllImport("libc", EntryPoint = "close")] + private static extern int close(int fd); + + [DllImport("libc", EntryPoint = "memfd_create")] + private static extern int memfd_create([MarshalAs(UnmanagedType.LPStr)] string name, uint flags); + + private const int O_RDWR = 2; + private const int O_CREAT = 64; + private const int O_EXCL = 128; + private const int PROT_READ = 1; + private const int PROT_WRITE = 2; + private const int MAP_SHARED = 1; + private const uint MFD_CLOEXEC = 1; + + #endregion + + #region Wayland Structures + + [StructLayout(LayoutKind.Sequential)] + private struct WlInterface + { + public IntPtr Name; + public int Version; + public int MethodCount; + public IntPtr Methods; + public int EventCount; + public IntPtr Events; + } + + [StructLayout(LayoutKind.Sequential)] + private struct WlRegistryListener + { + public IntPtr Global; + public IntPtr GlobalRemove; + } + + [StructLayout(LayoutKind.Sequential)] + private struct WlSurfaceListener + { + public IntPtr Enter; + public IntPtr Leave; + } + + [StructLayout(LayoutKind.Sequential)] + private struct WlBufferListener + { + public IntPtr Release; + } + + [StructLayout(LayoutKind.Sequential)] + private struct WlSeatListener + { + public IntPtr Capabilities; + public IntPtr Name; + } + + [StructLayout(LayoutKind.Sequential)] + private struct WlPointerListener + { + public IntPtr Enter; + public IntPtr Leave; + public IntPtr Motion; + public IntPtr Button; + public IntPtr Axis; + public IntPtr Frame; + public IntPtr AxisSource; + public IntPtr AxisStop; + public IntPtr AxisDiscrete; + } + + [StructLayout(LayoutKind.Sequential)] + private struct WlKeyboardListener + { + public IntPtr Keymap; + public IntPtr Enter; + public IntPtr Leave; + public IntPtr Key; + public IntPtr Modifiers; + public IntPtr RepeatInfo; + } + + [StructLayout(LayoutKind.Sequential)] + private struct XdgWmBaseListener + { + public IntPtr Ping; + } + + [StructLayout(LayoutKind.Sequential)] + private struct XdgSurfaceListener + { + public IntPtr Configure; + } + + [StructLayout(LayoutKind.Sequential)] + private struct XdgToplevelListener + { + public IntPtr Configure; + public IntPtr Close; + } + + private const uint WL_SHM_FORMAT_ARGB8888 = 0; + private const uint WL_SHM_FORMAT_XRGB8888 = 1; + + // Seat capabilities + private const uint WL_SEAT_CAPABILITY_POINTER = 1; + private const uint WL_SEAT_CAPABILITY_KEYBOARD = 2; + + // Pointer button states + private const uint WL_POINTER_BUTTON_STATE_RELEASED = 0; + private const uint WL_POINTER_BUTTON_STATE_PRESSED = 1; + + // Linux input button codes + private const uint BTN_LEFT = 0x110; + private const uint BTN_RIGHT = 0x111; + private const uint BTN_MIDDLE = 0x112; + + // Key states + private const uint WL_KEYBOARD_KEY_STATE_RELEASED = 0; + private const uint WL_KEYBOARD_KEY_STATE_PRESSED = 1; + + #endregion + + #region Fields + + private IntPtr _display; + private IntPtr _registry; + private IntPtr _compositor; + private IntPtr _shm; + private IntPtr _seat; + private IntPtr _xdgWmBase; + private IntPtr _surface; + private IntPtr _xdgSurface; + private IntPtr _xdgToplevel; + private IntPtr _pointer; + private IntPtr _keyboard; + private IntPtr _shmPool; + private IntPtr _buffer; + private IntPtr _pixelData; + private int _shmFd = -1; + private int _bufferSize; + private int _stride; + + private int _width; + private int _height; + private int _pendingWidth; + private int _pendingHeight; + private string _title; + private bool _isRunning; + private bool _disposed; + private bool _configured; + private uint _lastConfigureSerial; + + // Input state + private float _pointerX; + private float _pointerY; + private uint _pointerSerial; + private uint _modifiers; + + // Delegates to prevent GC + private WlRegistryListener _registryListener; + private WlSeatListener _seatListener; + private WlPointerListener _pointerListener; + private WlKeyboardListener _keyboardListener; + private XdgWmBaseListener _wmBaseListener; + private XdgSurfaceListener _xdgSurfaceListener; + private XdgToplevelListener _toplevelListener; + private WlBufferListener _bufferListener; + + // GCHandles for listener structs to prevent GC + private GCHandle _registryListenerHandle; + private GCHandle _seatListenerHandle; + private GCHandle _pointerListenerHandle; + private GCHandle _keyboardListenerHandle; + private GCHandle _wmBaseListenerHandle; + private GCHandle _xdgSurfaceListenerHandle; + private GCHandle _toplevelListenerHandle; + private GCHandle _bufferListenerHandle; + + private static bool _interfacesInitialized; + + // GCHandles to prevent delegate collection + private GCHandle _thisHandle; + + #endregion + + #region Properties + + public IntPtr Display => _display; + public IntPtr Surface => _surface; + public int Width => _width; + public int Height => _height; + public bool IsRunning => _isRunning; + public IntPtr PixelData => _pixelData; + public int Stride => _stride; + + #endregion + + #region Events + + public event EventHandler? KeyDown; + public event EventHandler? KeyUp; + public event EventHandler? TextInput; + public event EventHandler? PointerMoved; + public event EventHandler? PointerPressed; + public event EventHandler? PointerReleased; + public event EventHandler? Scroll; + public event EventHandler? Exposed; + public event EventHandler<(int Width, int Height)>? Resized; + public event EventHandler? CloseRequested; + public event EventHandler? FocusGained; + public event EventHandler? FocusLost; + + #endregion + + #region Constructor + + public WaylandWindow(string title, int width, int height) + { + _title = title; + _width = width; + _height = height; + _pendingWidth = width; + _pendingHeight = height; + + InitializeInterfaces(); + Initialize(); + } + + #endregion + + #region Initialization + + private static void InitializeInterfaces() + { + if (_interfacesInitialized) return; + + // Load interface symbols from libwayland-client using dlsym + LoadInterfaceSymbols(); + LoadXdgShellInterfaces(); + + _interfacesInitialized = true; + } + + private void Initialize() + { + // Keep this object alive for callbacks + _thisHandle = GCHandle.Alloc(this); + + // Connect to Wayland display + _display = wl_display_connect(null); + if (_display == IntPtr.Zero) + { + throw new InvalidOperationException( + "Failed to connect to Wayland display. " + + "Ensure WAYLAND_DISPLAY is set and a compositor is running."); + } + + // Get registry + _registry = wl_display_get_registry(_display); + if (_registry == IntPtr.Zero) + { + throw new InvalidOperationException("Failed to get Wayland registry"); + } + + // Set up registry listener + _registryListener = new WlRegistryListener + { + Global = Marshal.GetFunctionPointerForDelegate(RegistryGlobal), + GlobalRemove = Marshal.GetFunctionPointerForDelegate(RegistryGlobalRemove) + }; + _registryListenerHandle = GCHandle.Alloc(_registryListener, GCHandleType.Pinned); + wl_registry_add_listener(_registry, _registryListenerHandle.AddrOfPinnedObject(), GCHandle.ToIntPtr(_thisHandle)); + + // Do initial roundtrip to get globals + wl_display_roundtrip(_display); + + // Verify we got required globals + if (_compositor == IntPtr.Zero) + throw new InvalidOperationException("Wayland compositor not found"); + if (_shm == IntPtr.Zero) + throw new InvalidOperationException("Wayland shm not found"); + if (_xdgWmBase == IntPtr.Zero) + throw new InvalidOperationException("xdg_wm_base not found - compositor doesn't support xdg-shell"); + + // Create surface + _surface = wl_compositor_create_surface(_compositor); + if (_surface == IntPtr.Zero) + throw new InvalidOperationException("Failed to create Wayland surface"); + + // Create xdg_surface + _xdgSurface = xdg_wm_base_get_xdg_surface(_xdgWmBase, _surface); + if (_xdgSurface == IntPtr.Zero) + throw new InvalidOperationException("Failed to create xdg_surface"); + + _xdgSurfaceListener = new XdgSurfaceListener + { + Configure = Marshal.GetFunctionPointerForDelegate(XdgSurfaceConfigure) + }; + _xdgSurfaceListenerHandle = GCHandle.Alloc(_xdgSurfaceListener, GCHandleType.Pinned); + xdg_surface_add_listener(_xdgSurface, _xdgSurfaceListenerHandle.AddrOfPinnedObject(), GCHandle.ToIntPtr(_thisHandle)); + + // Create toplevel + _xdgToplevel = xdg_surface_get_toplevel(_xdgSurface); + if (_xdgToplevel == IntPtr.Zero) + throw new InvalidOperationException("Failed to create xdg_toplevel"); + + _toplevelListener = new XdgToplevelListener + { + Configure = Marshal.GetFunctionPointerForDelegate(XdgToplevelConfigure), + Close = Marshal.GetFunctionPointerForDelegate(XdgToplevelClose) + }; + _toplevelListenerHandle = GCHandle.Alloc(_toplevelListener, GCHandleType.Pinned); + xdg_toplevel_add_listener(_xdgToplevel, _toplevelListenerHandle.AddrOfPinnedObject(), GCHandle.ToIntPtr(_thisHandle)); + + // Set title and app_id + xdg_toplevel_set_title(_xdgToplevel, _title); + xdg_toplevel_set_app_id(_xdgToplevel, "com.openmaui.app"); + + // Commit empty surface to get initial configure + wl_surface_commit(_surface); + wl_display_roundtrip(_display); + + // Create shared memory buffer + CreateShmBuffer(); + + Console.WriteLine($"[Wayland] Window created: {_width}x{_height}"); + } + + private void CreateShmBuffer() + { + _stride = _width * 4; + _bufferSize = _stride * _height; + + // Create anonymous file for shared memory + _shmFd = memfd_create("maui-buffer", MFD_CLOEXEC); + if (_shmFd < 0) + { + // Fall back to shm_open + string shmName = $"/maui-{Environment.ProcessId}-{DateTime.Now.Ticks}"; + _shmFd = shm_open(shmName, O_RDWR | O_CREAT | O_EXCL, 0x180); // 0600 + if (_shmFd >= 0) + shm_unlink(shmName); + } + + if (_shmFd < 0) + throw new InvalidOperationException("Failed to create shared memory"); + + if (ftruncate(_shmFd, _bufferSize) < 0) + { + close(_shmFd); + throw new InvalidOperationException("Failed to resize shared memory"); + } + + _pixelData = mmap(IntPtr.Zero, (nuint)_bufferSize, PROT_READ | PROT_WRITE, MAP_SHARED, _shmFd, 0); + if (_pixelData == IntPtr.Zero || _pixelData == new IntPtr(-1)) + { + close(_shmFd); + throw new InvalidOperationException("Failed to mmap shared memory"); + } + + // Create pool and buffer + _shmPool = wl_shm_create_pool(_shm, _shmFd, _bufferSize); + if (_shmPool == IntPtr.Zero) + { + munmap(_pixelData, (nuint)_bufferSize); + close(_shmFd); + throw new InvalidOperationException("Failed to create wl_shm_pool"); + } + + _buffer = wl_shm_pool_create_buffer(_shmPool, 0, _width, _height, _stride, WL_SHM_FORMAT_ARGB8888); + if (_buffer == IntPtr.Zero) + { + wl_shm_pool_destroy(_shmPool); + munmap(_pixelData, (nuint)_bufferSize); + close(_shmFd); + throw new InvalidOperationException("Failed to create wl_buffer"); + } + + // Listen for buffer release + _bufferListener = new WlBufferListener + { + Release = Marshal.GetFunctionPointerForDelegate(BufferRelease) + }; + if (_bufferListenerHandle.IsAllocated) _bufferListenerHandle.Free(); + _bufferListenerHandle = GCHandle.Alloc(_bufferListener, GCHandleType.Pinned); + wl_buffer_add_listener(_buffer, _bufferListenerHandle.AddrOfPinnedObject(), GCHandle.ToIntPtr(_thisHandle)); + } + + private void ResizeBuffer(int newWidth, int newHeight) + { + if (newWidth == _width && newHeight == _height) return; + if (newWidth <= 0 || newHeight <= 0) return; + + // Destroy old buffer + if (_buffer != IntPtr.Zero) + wl_buffer_destroy(_buffer); + if (_shmPool != IntPtr.Zero) + wl_shm_pool_destroy(_shmPool); + if (_pixelData != IntPtr.Zero && _pixelData != new IntPtr(-1)) + munmap(_pixelData, (nuint)_bufferSize); + if (_shmFd >= 0) + close(_shmFd); + + _width = newWidth; + _height = newHeight; + + CreateShmBuffer(); + Resized?.Invoke(this, (_width, _height)); + } + + #endregion + + #region Callback Delegates + + private delegate void RegistryGlobalDelegate(IntPtr data, IntPtr registry, uint name, IntPtr iface, uint version); + private delegate void RegistryGlobalRemoveDelegate(IntPtr data, IntPtr registry, uint name); + private delegate void SeatCapabilitiesDelegate(IntPtr data, IntPtr seat, uint capabilities); + private delegate void SeatNameDelegate(IntPtr data, IntPtr seat, IntPtr name); + private delegate void PointerEnterDelegate(IntPtr data, IntPtr pointer, uint serial, IntPtr surface, int x, int y); + private delegate void PointerLeaveDelegate(IntPtr data, IntPtr pointer, uint serial, IntPtr surface); + private delegate void PointerMotionDelegate(IntPtr data, IntPtr pointer, uint time, int x, int y); + private delegate void PointerButtonDelegate(IntPtr data, IntPtr pointer, uint serial, uint time, uint button, uint state); + private delegate void PointerAxisDelegate(IntPtr data, IntPtr pointer, uint time, uint axis, int value); + private delegate void PointerFrameDelegate(IntPtr data, IntPtr pointer); + private delegate void KeyboardKeymapDelegate(IntPtr data, IntPtr keyboard, uint format, int fd, uint size); + private delegate void KeyboardEnterDelegate(IntPtr data, IntPtr keyboard, uint serial, IntPtr surface, IntPtr keys); + private delegate void KeyboardLeaveDelegate(IntPtr data, IntPtr keyboard, uint serial, IntPtr surface); + private delegate void KeyboardKeyDelegate(IntPtr data, IntPtr keyboard, uint serial, uint time, uint key, uint state); + private delegate void KeyboardModifiersDelegate(IntPtr data, IntPtr keyboard, uint serial, uint modsDepressed, uint modsLatched, uint modsLocked, uint group); + private delegate void XdgWmBasePingDelegate(IntPtr data, IntPtr wmBase, uint serial); + private delegate void XdgSurfaceConfigureDelegate(IntPtr data, IntPtr xdgSurface, uint serial); + private delegate void XdgToplevelConfigureDelegate(IntPtr data, IntPtr toplevel, int width, int height, IntPtr states); + private delegate void XdgToplevelCloseDelegate(IntPtr data, IntPtr toplevel); + private delegate void BufferReleaseDelegate(IntPtr data, IntPtr buffer); + + #endregion + + #region Callback Implementations + + private static void RegistryGlobal(IntPtr data, IntPtr registry, uint name, IntPtr iface, uint version) + { + var handle = GCHandle.FromIntPtr(data); + if (!handle.IsAllocated) return; + var window = (WaylandWindow)handle.Target!; + + var interfaceName = Marshal.PtrToStringAnsi(iface); + Console.WriteLine($"[Wayland] Global: {interfaceName} v{version}"); + + switch (interfaceName) + { + case "wl_compositor": + window._compositor = wl_registry_bind(registry, name, _wl_compositor_interface, Math.Min(version, 4u)); + break; + case "wl_shm": + window._shm = wl_registry_bind(registry, name, _wl_shm_interface, 1); + break; + case "wl_seat": + window._seat = wl_registry_bind(registry, name, _wl_seat_interface, Math.Min(version, 5u)); + window.SetupSeat(); + break; + case "xdg_wm_base": + window._xdgWmBase = wl_registry_bind(registry, name, _xdg_wm_base_interface, Math.Min(version, 2u)); + window.SetupXdgWmBase(); + break; + } + } + + private static void RegistryGlobalRemove(IntPtr data, IntPtr registry, uint name) + { + // Handle global removal if needed + } + + private void SetupSeat() + { + if (_seat == IntPtr.Zero) return; + + _seatListener = new WlSeatListener + { + Capabilities = Marshal.GetFunctionPointerForDelegate(SeatCapabilities), + Name = Marshal.GetFunctionPointerForDelegate(SeatName) + }; + _seatListenerHandle = GCHandle.Alloc(_seatListener, GCHandleType.Pinned); + wl_seat_add_listener(_seat, _seatListenerHandle.AddrOfPinnedObject(), GCHandle.ToIntPtr(_thisHandle)); + } + + private static void SeatCapabilities(IntPtr data, IntPtr seat, uint capabilities) + { + var handle = GCHandle.FromIntPtr(data); + if (!handle.IsAllocated) return; + var window = (WaylandWindow)handle.Target!; + + if ((capabilities & WL_SEAT_CAPABILITY_POINTER) != 0 && window._pointer == IntPtr.Zero) + { + window._pointer = wl_seat_get_pointer(seat); + window.SetupPointer(); + } + + if ((capabilities & WL_SEAT_CAPABILITY_KEYBOARD) != 0 && window._keyboard == IntPtr.Zero) + { + window._keyboard = wl_seat_get_keyboard(seat); + window.SetupKeyboard(); + } + } + + private static void SeatName(IntPtr data, IntPtr seat, IntPtr name) { } + + private void SetupPointer() + { + if (_pointer == IntPtr.Zero) return; + + _pointerListener = new WlPointerListener + { + Enter = Marshal.GetFunctionPointerForDelegate(PointerEnter), + Leave = Marshal.GetFunctionPointerForDelegate(PointerLeave), + Motion = Marshal.GetFunctionPointerForDelegate(PointerMotion), + Button = Marshal.GetFunctionPointerForDelegate(OnPointerButton), + Axis = Marshal.GetFunctionPointerForDelegate(PointerAxis), + Frame = Marshal.GetFunctionPointerForDelegate(PointerFrame), + }; + _pointerListenerHandle = GCHandle.Alloc(_pointerListener, GCHandleType.Pinned); + wl_pointer_add_listener(_pointer, _pointerListenerHandle.AddrOfPinnedObject(), GCHandle.ToIntPtr(_thisHandle)); + } + + private static void PointerEnter(IntPtr data, IntPtr pointer, uint serial, IntPtr surface, int x, int y) + { + var handle = GCHandle.FromIntPtr(data); + if (!handle.IsAllocated) return; + var window = (WaylandWindow)handle.Target!; + window._pointerSerial = serial; + window._pointerX = x / 256.0f; + window._pointerY = y / 256.0f; + } + + private static void PointerLeave(IntPtr data, IntPtr pointer, uint serial, IntPtr surface) { } + + private static void PointerMotion(IntPtr data, IntPtr pointer, uint time, int x, int y) + { + var handle = GCHandle.FromIntPtr(data); + if (!handle.IsAllocated) return; + var window = (WaylandWindow)handle.Target!; + + window._pointerX = x / 256.0f; + window._pointerY = y / 256.0f; + window.PointerMoved?.Invoke(window, new PointerEventArgs((int)window._pointerX, (int)window._pointerY)); + } + + private static void OnPointerButton(IntPtr data, IntPtr pointer, uint serial, uint time, uint button, uint state) + { + var handle = GCHandle.FromIntPtr(data); + if (!handle.IsAllocated) return; + var window = (WaylandWindow)handle.Target!; + + var ptrButton = button switch + { + BTN_LEFT => Microsoft.Maui.Platform.PointerButton.Left, + BTN_RIGHT => Microsoft.Maui.Platform.PointerButton.Right, + BTN_MIDDLE => Microsoft.Maui.Platform.PointerButton.Middle, + _ => Microsoft.Maui.Platform.PointerButton.None + }; + + var args = new PointerEventArgs((int)window._pointerX, (int)window._pointerY, ptrButton); + + if (state == WL_POINTER_BUTTON_STATE_PRESSED) + window.PointerPressed?.Invoke(window, args); + else + window.PointerReleased?.Invoke(window, args); + } + + private static void PointerAxis(IntPtr data, IntPtr pointer, uint time, uint axis, int value) + { + var handle = GCHandle.FromIntPtr(data); + if (!handle.IsAllocated) return; + var window = (WaylandWindow)handle.Target!; + + float delta = value / 256.0f / 10.0f; + if (axis == 0) // Vertical + window.Scroll?.Invoke(window, new ScrollEventArgs((int)window._pointerX, (int)window._pointerY, 0, delta)); + else // Horizontal + window.Scroll?.Invoke(window, new ScrollEventArgs((int)window._pointerX, (int)window._pointerY, delta, 0)); + } + + private static void PointerFrame(IntPtr data, IntPtr pointer) { } + + private void SetupKeyboard() + { + if (_keyboard == IntPtr.Zero) return; + + _keyboardListener = new WlKeyboardListener + { + Keymap = Marshal.GetFunctionPointerForDelegate(KeyboardKeymap), + Enter = Marshal.GetFunctionPointerForDelegate(KeyboardEnter), + Leave = Marshal.GetFunctionPointerForDelegate(KeyboardLeave), + Key = Marshal.GetFunctionPointerForDelegate(KeyboardKey), + Modifiers = Marshal.GetFunctionPointerForDelegate(KeyboardModifiers), + }; + _keyboardListenerHandle = GCHandle.Alloc(_keyboardListener, GCHandleType.Pinned); + wl_keyboard_add_listener(_keyboard, _keyboardListenerHandle.AddrOfPinnedObject(), GCHandle.ToIntPtr(_thisHandle)); + } + + private static void KeyboardKeymap(IntPtr data, IntPtr keyboard, uint format, int fd, uint size) + { + close(fd); + } + + private static void KeyboardEnter(IntPtr data, IntPtr keyboard, uint serial, IntPtr surface, IntPtr keys) + { + var handle = GCHandle.FromIntPtr(data); + if (!handle.IsAllocated) return; + var window = (WaylandWindow)handle.Target!; + window.FocusGained?.Invoke(window, EventArgs.Empty); + } + + private static void KeyboardLeave(IntPtr data, IntPtr keyboard, uint serial, IntPtr surface) + { + var handle = GCHandle.FromIntPtr(data); + if (!handle.IsAllocated) return; + var window = (WaylandWindow)handle.Target!; + window.FocusLost?.Invoke(window, EventArgs.Empty); + } + + private static void KeyboardKey(IntPtr data, IntPtr keyboard, uint serial, uint time, uint keycode, uint state) + { + var handle = GCHandle.FromIntPtr(data); + if (!handle.IsAllocated) return; + var window = (WaylandWindow)handle.Target!; + + // Convert Linux keycode to Key enum (add 8 for X11 compat) + var key = KeyMapping.FromLinuxKeycode(keycode + 8); + var modifiers = (KeyModifiers)window._modifiers; + var args = new KeyEventArgs(key, modifiers); + + if (state == WL_KEYBOARD_KEY_STATE_PRESSED) + { + window.KeyDown?.Invoke(window, args); + + // Generate text input for printable keys + char? ch = KeyMapping.ToChar(key, modifiers); + if (ch.HasValue) + window.TextInput?.Invoke(window, new TextInputEventArgs(ch.Value.ToString())); + } + else + { + window.KeyUp?.Invoke(window, args); + } + } + + private static void KeyboardModifiers(IntPtr data, IntPtr keyboard, uint serial, uint modsDepressed, uint modsLatched, uint modsLocked, uint group) + { + var handle = GCHandle.FromIntPtr(data); + if (!handle.IsAllocated) return; + var window = (WaylandWindow)handle.Target!; + window._modifiers = modsDepressed | modsLatched; + } + + private void SetupXdgWmBase() + { + if (_xdgWmBase == IntPtr.Zero) return; + + _wmBaseListener = new XdgWmBaseListener + { + Ping = Marshal.GetFunctionPointerForDelegate(XdgWmBasePing) + }; + _wmBaseListenerHandle = GCHandle.Alloc(_wmBaseListener, GCHandleType.Pinned); + xdg_wm_base_add_listener(_xdgWmBase, _wmBaseListenerHandle.AddrOfPinnedObject(), GCHandle.ToIntPtr(_thisHandle)); + } + + private static void XdgWmBasePing(IntPtr data, IntPtr wmBase, uint serial) + { + xdg_wm_base_pong(wmBase, serial); + } + + private static void XdgSurfaceConfigure(IntPtr data, IntPtr xdgSurface, uint serial) + { + var handle = GCHandle.FromIntPtr(data); + if (!handle.IsAllocated) return; + var window = (WaylandWindow)handle.Target!; + + xdg_surface_ack_configure(xdgSurface, serial); + window._lastConfigureSerial = serial; + + if (!window._configured) + { + window._configured = true; + if (window._pendingWidth > 0 && window._pendingHeight > 0) + { + window.ResizeBuffer(window._pendingWidth, window._pendingHeight); + } + window.Exposed?.Invoke(window, EventArgs.Empty); + } + } + + private static void XdgToplevelConfigure(IntPtr data, IntPtr toplevel, int width, int height, IntPtr states) + { + var handle = GCHandle.FromIntPtr(data); + if (!handle.IsAllocated) return; + var window = (WaylandWindow)handle.Target!; + + if (width > 0 && height > 0) + { + window._pendingWidth = width; + window._pendingHeight = height; + + if (window._configured) + { + window.ResizeBuffer(width, height); + } + } + } + + private static void XdgToplevelClose(IntPtr data, IntPtr toplevel) + { + var handle = GCHandle.FromIntPtr(data); + if (!handle.IsAllocated) return; + var window = (WaylandWindow)handle.Target!; + + window.CloseRequested?.Invoke(window, EventArgs.Empty); + window._isRunning = false; + } + + private static void BufferRelease(IntPtr data, IntPtr buffer) + { + // Buffer is available for reuse + } + + #endregion + + #region Public Methods + + public void Show() + { + _isRunning = true; + + // Attach buffer and commit + wl_surface_attach(_surface, _buffer, 0, 0); + wl_surface_damage_buffer(_surface, 0, 0, _width, _height); + wl_surface_commit(_surface); + wl_display_flush(_display); + } + + public void Hide() + { + wl_surface_attach(_surface, IntPtr.Zero, 0, 0); + wl_surface_commit(_surface); + wl_display_flush(_display); + } + + public void SetTitle(string title) + { + _title = title; + if (_xdgToplevel != IntPtr.Zero) + xdg_toplevel_set_title(_xdgToplevel, title); + } + + public void Resize(int width, int height) + { + ResizeBuffer(width, height); + } + + public void ProcessEvents() + { + if (!_isRunning || _display == IntPtr.Zero) return; + + wl_display_dispatch_pending(_display); + wl_display_flush(_display); + } + + public void Stop() + { + _isRunning = false; + } + + public void CommitFrame() + { + if (_surface != IntPtr.Zero && _buffer != IntPtr.Zero) + { + wl_surface_attach(_surface, _buffer, 0, 0); + wl_surface_damage_buffer(_surface, 0, 0, _width, _height); + wl_surface_commit(_surface); + wl_display_flush(_display); + } + } + + public int GetFileDescriptor() + { + return wl_display_get_fd(_display); + } + + #endregion + + #region IDisposable + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + _isRunning = false; + + if (_buffer != IntPtr.Zero) + { + wl_buffer_destroy(_buffer); + _buffer = IntPtr.Zero; + } + + if (_shmPool != IntPtr.Zero) + { + wl_shm_pool_destroy(_shmPool); + _shmPool = IntPtr.Zero; + } + + if (_pixelData != IntPtr.Zero && _pixelData != new IntPtr(-1)) + { + munmap(_pixelData, (nuint)_bufferSize); + _pixelData = IntPtr.Zero; + } + + if (_shmFd >= 0) + { + close(_shmFd); + _shmFd = -1; + } + + if (_xdgToplevel != IntPtr.Zero) + { + xdg_toplevel_destroy(_xdgToplevel); + _xdgToplevel = IntPtr.Zero; + } + + if (_xdgSurface != IntPtr.Zero) + { + xdg_surface_destroy(_xdgSurface); + _xdgSurface = IntPtr.Zero; + } + + if (_surface != IntPtr.Zero) + { + wl_surface_destroy(_surface); + _surface = IntPtr.Zero; + } + + if (_display != IntPtr.Zero) + { + wl_display_disconnect(_display); + _display = IntPtr.Zero; + } + + // Free listener GCHandles + if (_registryListenerHandle.IsAllocated) _registryListenerHandle.Free(); + if (_seatListenerHandle.IsAllocated) _seatListenerHandle.Free(); + if (_pointerListenerHandle.IsAllocated) _pointerListenerHandle.Free(); + if (_keyboardListenerHandle.IsAllocated) _keyboardListenerHandle.Free(); + if (_wmBaseListenerHandle.IsAllocated) _wmBaseListenerHandle.Free(); + if (_xdgSurfaceListenerHandle.IsAllocated) _xdgSurfaceListenerHandle.Free(); + if (_toplevelListenerHandle.IsAllocated) _toplevelListenerHandle.Free(); + if (_bufferListenerHandle.IsAllocated) _bufferListenerHandle.Free(); + + if (_thisHandle.IsAllocated) + _thisHandle.Free(); + + GC.SuppressFinalize(this); + } + + ~WaylandWindow() + { + Dispose(); + } + + #endregion +} diff --git a/Window/X11Window.cs b/Window/X11Window.cs index 45641e7..49358fa 100644 --- a/Window/X11Window.cs +++ b/Window/X11Window.cs @@ -288,8 +288,12 @@ public class X11Window : IDisposable KeyDown?.Invoke(this, new KeyEventArgs(key, modifiers)); - // Generate text input for printable characters - if (keysym >= 32 && keysym <= 126) + // Generate text input for printable characters, but NOT when Control or Alt is held + // (those are keyboard shortcuts, not text input) + bool isControlHeld = (keyEvent.State & 0x04) != 0; // ControlMask + bool isAltHeld = (keyEvent.State & 0x08) != 0; // Mod1Mask (Alt) + + if (keysym >= 32 && keysym <= 126 && !isControlHeld && !isAltHeld) { TextInput?.Invoke(this, new TextInputEventArgs(((char)keysym).ToString())); } diff --git a/out.xml b/out.xml new file mode 100644 index 0000000..2a5502c --- /dev/null +++ b/out.xml @@ -0,0 +1,19333 @@ + + + + + + <_AfterSdkPublishDependsOn Condition="'$(_IsAspNetCoreProject)' == 'true'">AfterPublish + <_AfterSdkPublishDependsOn Condition="'$(_IsAspNetCoreProject)' != 'true'">Publish + + + + + true + + true + $(CustomAfterDirectoryBuildProps);$(MSBuildThisFileDirectory)UseArtifactsOutputPath.props + + + $(ProjectExtensionsPathForSpecifiedProject) + + + + + + true + true + true + true + true + + + + <_DirectoryBuildPropsFile Condition="'$(_DirectoryBuildPropsFile)' == ''">Directory.Build.props + <_DirectoryBuildPropsBasePath Condition="'$(_DirectoryBuildPropsBasePath)' == ''">$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), '$(_DirectoryBuildPropsFile)')) + $([System.IO.Path]::Combine('$(_DirectoryBuildPropsBasePath)', '$(_DirectoryBuildPropsFile)')) + + + + + + + + + true + $(MSBuildProjectName) + + + $(ArtifactsPath)\obj\$(ArtifactsProjectName)\ + $(ArtifactsPath)\obj\ + + + + <_ArtifactsPathSetEarly>true + + + + + + obj\ + $(BaseIntermediateOutputPath)\ + <_InitialBaseIntermediateOutputPath>$(BaseIntermediateOutputPath) + $(BaseIntermediateOutputPath) + + $([System.IO.Path]::Combine('$(MSBuildProjectDirectory)', '$(MSBuildProjectExtensionsPath)')) + $(MSBuildProjectExtensionsPath)\ + + false + true + <_InitialMSBuildProjectExtensionsPath Condition=" '$(ImportProjectExtensionProps)' == 'true' ">$(MSBuildProjectExtensionsPath) + + + + True + NuGet + $(MSBuildThisFileDirectory)project.assets.json + /Users/nible/.nuget/packages/ + /Users/nible/.nuget/packages/ + PackageReference + 6.12.2 + + + + + + + + + + + + + <_MauiSkipSdkAutoImport>true + + + + + + Designer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MSBuild:Compile + $(DefaultXamlRuntime) + + + MSBuild:Compile + $(DefaultXamlRuntime) + + + MSBuild:Compile + $(DefaultXamlRuntime) + + + + + + + + + + + + + + + + $(MSBuildExtensionsPath)\v$(MSBuildToolsVersion)\Custom.Before.$(MSBuildThisFile) + $(MSBuildExtensionsPath)\v$(MSBuildToolsVersion)\Custom.After.$(MSBuildThisFile) + + + + + true + + + $(DefaultProjectConfiguration) + $(DefaultProjectPlatform) + + + WJProject + JavaScript + + + + + + + + $([MSBuild]::IsRunningFromVisualStudio()) + $([MSBuild]::GetToolsDirectory32())\..\..\..\Common7\IDE\CommonExtensions\Microsoft\NuGet\NuGet.props + $(MSBuildToolsPath)\NuGet.props + + + + + + true + + + + <_DirectoryPackagesPropsFile Condition="'$(_DirectoryPackagesPropsFile)' == ''">Directory.Packages.props + <_DirectoryPackagesPropsBasePath Condition="'$(_DirectoryPackagesPropsBasePath)' == ''">$([MSBuild]::GetDirectoryNameOfFileAbove('$(MSBuildProjectDirectory)', '$(_DirectoryPackagesPropsFile)')) + $([MSBuild]::NormalizePath('$(_DirectoryPackagesPropsBasePath)', '$(_DirectoryPackagesPropsFile)')) + + + + true + + + + true + true + true + true + true + true + true + true + true + true + true + true + true + + + + + + + true + + + + Debug;Release + AnyCPU + Debug + AnyCPU + + + + + true + + + + Library + 512 + prompt + $(MSBuildProjectName) + $(MSBuildProjectName.Replace(" ", "_")) + true + + + + true + false + + + true + + + + + <_PlatformWithoutConfigurationInference>$(Platform) + + + x64 + + + x86 + + + ARM + + + arm64 + + + + + {CandidateAssemblyFiles} + $(AssemblySearchPaths);{HintPathFromItem} + $(AssemblySearchPaths);{TargetFrameworkDirectory} + $(AssemblySearchPaths);{RawFileName} + + + None + portable + + false + + true + true + + PackageReference + $(AssemblySearchPaths) + false + false + false + false + false + false + false + false + false + false + true + 1.0.3 + false + true + true + + + + + + $(MSBuildThisFileDirectory)GenerateDeps\GenerateDeps.proj + + + + + + $(MSBuildThisFileDirectory)..\..\..\Microsoft.NETCoreSdk.BundledVersions.props + + + + + $([MSBuild]::NormalizePath('$(MSBuildThisFileDirectory)../../')) + $([MSBuild]::EnsureTrailingSlash('$(NetCoreRoot)'))packs + <_NetFrameworkHostedCompilersVersion>4.12.0-3.24570.6 + 9.0 + 9.0 + 9.0.0 + 2.1 + 2.1.0 + 9.0.0-rtm.24528.9 + $(MSBuildThisFileDirectory)RuntimeIdentifierGraph.json + 9.0.101 + 9.0.100 + osx-arm64 + osx-arm64 + <_NETCoreSdkIsPreview>false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_KnownRuntimeIdentiferPlatforms Include="any;aot;freebsd;illumos;solaris;unix;any;aot;freebsd;illumos;solaris;unix;any;aot;freebsd;illumos;solaris;unix;any;aot;freebsd;illumos;solaris;unix;any;aot;freebsd;illumos;solaris;unix;any;aot;freebsd;illumos;solaris;unix;any;aot;freebsd;illumos;solaris;unix;any;aot;freebsd;illumos;solaris;unix;any;aot;freebsd;illumos;solaris;unix;any;aot;freebsd;illumos;solaris;unix;any;aot;freebsd;illumos;solaris;unix;any;aot;freebsd;illumos;solaris;unix;any;aot;freebsd;illumos;solaris;unix;any;aot;freebsd;illumos;solaris;unix;any;aot;freebsd;illumos;solaris;unix" /> + <_ExcludedKnownRuntimeIdentiferPlatforms Include="rhel.6;tizen.4.0.0;tizen.5.0.0;rhel.6;tizen.4.0.0;tizen.5.0.0;rhel.6;tizen.4.0.0;tizen.5.0.0;rhel.6;tizen.4.0.0;tizen.5.0.0;rhel.6;tizen.4.0.0;tizen.5.0.0;rhel.6;tizen.4.0.0;tizen.5.0.0;rhel.6;tizen.4.0.0;tizen.5.0.0;rhel.6;tizen.4.0.0;tizen.5.0.0;rhel.6;tizen.4.0.0;tizen.5.0.0;rhel.6;tizen.4.0.0;tizen.5.0.0;rhel.6;tizen.4.0.0;tizen.5.0.0;rhel.6;tizen.4.0.0;tizen.5.0.0;rhel.6;tizen.4.0.0;tizen.5.0.0;rhel.6;tizen.4.0.0;tizen.5.0.0;rhel.6;tizen.4.0.0;tizen.5.0.0;rhel.6;tizen.4.0.0;tizen.5.0.0" /> + + + + $(MSBuildThisFileDirectory)..\..\..\Microsoft.NETCoreSdk.BundledMSBuildInformation.props + + + + + 17.11.0 + 17.12.12 + <_MSBuildVersionMajorMinor>$([System.Version]::Parse('$(MSBuildVersion)').ToString(2)) + <_IsDisjointMSBuildVersion>$([MSBuild]::VersionLessThan('$(_MSBuildVersionMajorMinor)', '17.12')) + + + + + false + + + <__WindowsAppSdkDefaultImageIncludes>**/*.png;**/*.bmp;**/*.jpg;**/*.dds;**/*.tif;**/*.tga;**/*.gif + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <__DisableWorkloadResolverSentinelPath Condition="'$(MSBuildRuntimeType)' == 'Core'">$(MSBuildBinPath)\DisableWorkloadResolver.sentinel + <__DisableWorkloadResolverSentinelPath Condition="'$(MSBuildRuntimeType)' != 'Core'">$(MSBuildToolsPath32)\SdkResolvers\Microsoft.DotNet.MSBuildSdkResolver\DisableWorkloadResolver.sentinel + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + false + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + true + + + + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + true + + + + + + + + + + + + + + + + + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(Identity)')) + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(Identity)')) + false + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(Identity)')) + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(Identity)')) + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(Identity)')) + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(Identity)')) + true + + + + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(Identity)')) + true + + + + + + + + + + + + + + + + + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + false + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + true + + + + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + true + + + + + + + + + + + + + + + + + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + false + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + true + + + + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + true + + + + + + + + + + + + + + + + + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + false + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + true + + + + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + true + + + + + + + + + + + + + + + + + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(Identity)')) + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(Identity)')) + false + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(Identity)')) + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(Identity)')) + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(Identity)')) + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(Identity)')) + true + + + + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(Identity)')) + true + + + + + + + + + + + + + + + + + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + false + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + true + + + + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + true + + + + + + + + + + + + + + + + + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + false + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + true + + + + true + + + + $([MSBuild]::MakeRelative ('$(MSBuildProjectDirectory)', '%(FullPath)')) + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 10.0 + 17.16 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + <_SourceLinkPropsImported>true + + + + + + $(MSBuildThisFileDirectory)..\tools\net472\Microsoft.Build.Tasks.Git.dll + $(MSBuildThisFileDirectory)..\tools\core\Microsoft.Build.Tasks.Git.dll + + + + + + <_MicrosoftSourceLinkCommonAssemblyFile Condition="'$(MSBuildRuntimeType)' != 'Core'">$(MSBuildThisFileDirectory)..\tools\net472\Microsoft.SourceLink.Common.dll + <_MicrosoftSourceLinkCommonAssemblyFile Condition="'$(MSBuildRuntimeType)' == 'Core'">$(MSBuildThisFileDirectory)..\tools\core\Microsoft.SourceLink.Common.dll + + + + true + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1701;1702 + + $(WarningsAsErrors);NU1605 + + + $(DefineConstants); + $(DefineConstants)TRACE + + + + + + + + + + + + + + + + + + $(TargetsForTfmSpecificContentInPackage);PackTool + + + + + + $(TargetsForTfmSpecificContentInPackage);_PackProjectToolValidation + + + + + + MSBuild:Compile + $(DefaultXamlRuntime) + Designer + + + MSBuild:Compile + $(DefaultXamlRuntime) + Designer + + + + + + + + + + + + + + + + + + <_WpfCommonNetFxReference Include="WindowsBase" /> + <_WpfCommonNetFxReference Include="PresentationCore" /> + <_WpfCommonNetFxReference Include="PresentationFramework" /> + <_WpfCommonNetFxReference Include="System.Xaml" Condition="'$(_TargetFrameworkVersionValue)' != '' And '$(_TargetFrameworkVersionValue)' >= '4.0'"> + 4.0 + + <_WpfCommonNetFxReference Include="UIAutomationClient" Condition="'$(_TargetFrameworkVersionValue)' != '' And '$(_TargetFrameworkVersionValue)' >= '4.0'" /> + <_WpfCommonNetFxReference Include="UIAutomationClientSideProviders" Condition="'$(_TargetFrameworkVersionValue)' != '' And '$(_TargetFrameworkVersionValue)' >= '4.0'" /> + <_WpfCommonNetFxReference Include="UIAutomationProvider" Condition="'$(_TargetFrameworkVersionValue)' != '' And '$(_TargetFrameworkVersionValue)' >= '4.0'" /> + <_WpfCommonNetFxReference Include="UIAutomationTypes" Condition="'$(_TargetFrameworkVersionValue)' != '' And '$(_TargetFrameworkVersionValue)' >= '4.0'" /> + <_WpfCommonNetFxReference Include="System.Windows.Controls.Ribbon" Condition="'$(_TargetFrameworkVersionValue)' != '' And '$(_TargetFrameworkVersionValue)' >= '4.5'" /> + + + <_SDKImplicitReference Include="@(_WpfCommonNetFxReference)" Condition="'$(UseWPF)' == 'true'" /> + <_SDKImplicitReference Include="System.Windows.Forms" Condition="('$(UseWindowsForms)' == 'true') " /> + <_SDKImplicitReference Include="WindowsFormsIntegration" Condition=" ('$(UseWindowsForms)' == 'true') And ('$(UseWPF)' == 'true') " /> + + + + + + <_UnsupportedNETCoreAppTargetFramework Include=".NETCoreApp,Version=v1.0" /> + <_UnsupportedNETCoreAppTargetFramework Include=".NETCoreApp,Version=v1.1" /> + <_UnsupportedNETCoreAppTargetFramework Include=".NETCoreApp,Version=v2.0" /> + <_UnsupportedNETCoreAppTargetFramework Include=".NETCoreApp,Version=v2.1" /> + <_UnsupportedNETCoreAppTargetFramework Include=".NETCoreApp,Version=v2.2" /> + + <_UnsupportedNETStandardTargetFramework Include="@(SupportedNETStandardTargetFramework)" /> + + <_UnsupportedNETFrameworkTargetFramework Include=".NETFramework,Version=v2.0" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + <_TargetFrameworkVersionValue>0.0 + <_WindowsDesktopSdkTargetFrameworkVersionFloor>3.0 + + + + + + net9.0 + enable + enable + OpenMaui.Platform.Linux + OpenMaui.Controls.Linux + true + true + $(NoWarn);CS0108;CS1591;CS0618 + + OpenMaui.Controls.Linux + 1.0.0-preview.1 + MarketAlly LLC, David H. Friedel Jr. + MarketAlly LLC + OpenMaui Linux Controls + Linux desktop support for .NET MAUI applications using SkiaSharp rendering. Supports X11 and Wayland display servers with 35+ controls, platform services, and accessibility support. + Copyright 2025 MarketAlly LLC + MIT + https://github.com/open-maui/maui-linux + https://github.com/open-maui/maui-linux.git + git + maui;linux;desktop;skia;gui;cross-platform;dotnet;x11;wayland;openmaui + Initial preview release with 35+ controls and full platform services. + README.md + false + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + + + + + + + + + + <_IsExecutable Condition="'$(OutputType)' == 'Exe' or '$(OutputType)'=='WinExe'">true + + + $(_IsExecutable) + <_UsingDefaultForHasRuntimeOutput>true + + + + + 1.0.0 + $(VersionPrefix)-$(VersionSuffix) + $(VersionPrefix) + + + $(AssemblyName) + $(Authors) + $(AssemblyName) + $(AssemblyName) + + + + + Debug + AnyCPU + $(Platform) + + + + + + + true + <_PublishProfileDesignerFolder Condition="'$(AppDesignerFolder)' != ''">$(AppDesignerFolder) + <_PublishProfileDesignerFolder Condition="'$(_PublishProfileDesignerFolder)' == ''">Properties + <_PublishProfileRootFolder Condition="'$(_PublishProfileRootFolder)' == ''">$(MSBuildProjectDirectory)\$(_PublishProfileDesignerFolder)\PublishProfiles\ + $([System.IO.Path]::GetFileNameWithoutExtension($(PublishProfile))) + $(_PublishProfileRootFolder)$(PublishProfileName).pubxml + $(PublishProfileFullPath) + + false + + + + + + + + + + + + + $([MSBuild]::GetTargetFrameworkIdentifier('$(TargetFramework)')) + v$([MSBuild]::GetTargetFrameworkVersion('$(TargetFramework)', 2)) + + + <_TargetFrameworkVersionWithoutV>$(TargetFrameworkVersion.TrimStart('vV')) + + + + $([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) + $([MSBuild]::GetTargetPlatformVersion('$(TargetFramework)', 4)) + $([MSBuild]::GetTargetPlatformVersion('$(TargetFramework)', 2)) + + + Windows + + + + <_UnsupportedTargetFrameworkError>true + + + + + + + + + + true + true + + + + + + + + + + + + + + + + + + + + v0.0 + + + _ + + + + + true + + + + + + + + + true + + + + + + + + + + <_EnableDefaultWindowsPlatform>false + false + + + 2.1 + + + + + + + + + + + + + + <_ApplicableTargetPlatformVersion Include="@(SdkSupportedTargetPlatformVersion)" Condition="'@(SdkSupportedTargetPlatformVersion)' != '' and '%(SdkSupportedTargetPlatformVersion.DefineConstantsOnly)' != 'true'" RemoveMetadata="DefineConstantsOnly" /> + <_ValidTargetPlatformVersion Include="@(_ApplicableTargetPlatformVersion)" Condition="'@(_ApplicableTargetPlatformVersion)' != '' and $([MSBuild]::VersionEquals(%(Identity), $(TargetPlatformVersion)))" /> + + + @(_ValidTargetPlatformVersion->Distinct()) + + + + + true + <_ValidTargetPlatformVersions Condition="'@(_ApplicableTargetPlatformVersion)' != ''">@(_ApplicableTargetPlatformVersion, '%0a') + <_ValidTargetPlatformVersions Condition="'@(_ApplicableTargetPlatformVersion)' == ''">None + + + + + + + true + true + + + + + + + + + true + false + true + <_PlatformToAppendToOutputPath Condition="'$(AppendPlatformToOutputPath)' == 'true'">$(PlatformName)\ + + + + + + + + <_DefaultArtifactsPathPropsImported>true + + + + true + true + <_ArtifactsPathLocationType>ExplicitlySpecified + + + + + $(_DirectoryBuildPropsBasePath)\artifacts + true + <_ArtifactsPathLocationType>DirectoryBuildPropsFolder + + + + $(MSBuildProjectDirectory)\artifacts + <_ArtifactsPathLocationType>ProjectFolder + + + + $(MSBuildProjectName) + bin + publish + package + + + $(Configuration.ToLowerInvariant()) + + $(ArtifactsPivots)_$(TargetFramework.ToLowerInvariant()) + + $(ArtifactsPivots)_$(RuntimeIdentifier.ToLowerInvariant()) + + + + $(ArtifactsPath)\$(ArtifactsBinOutputName)\$(ArtifactsProjectName)\ + $(ArtifactsPath)\obj\$(ArtifactsProjectName)\ + $(ArtifactsPath)\$(ArtifactsPublishOutputName)\$(ArtifactsProjectName)\$(ArtifactsPivots)\ + + + + $(ArtifactsPath)\$(ArtifactsBinOutputName)\ + $(ArtifactsPath)\obj\ + $(ArtifactsPath)\$(ArtifactsPublishOutputName)\$(ArtifactsPivots)\ + + + $(BaseOutputPath)$(ArtifactsPivots)\ + $(BaseIntermediateOutputPath)$(ArtifactsPivots)\ + + $(ArtifactsPath)\$(ArtifactsPackageOutputName)\$(Configuration.ToLowerInvariant())\ + + + bin\ + $(BaseOutputPath)\ + $(BaseOutputPath)$(_PlatformToAppendToOutputPath)$(Configuration)\ + $(OutputPath)\ + + + + obj\ + $(BaseIntermediateOutputPath)\ + $(BaseIntermediateOutputPath)$(_PlatformToAppendToOutputPath)$(Configuration)\ + $(IntermediateOutputPath)\ + + + + $(OutputPath) + + + + $(DefaultItemExcludes);$(OutputPath)/** + $(DefaultItemExcludes);$(IntermediateOutputPath)/** + + + $(DefaultItemExcludes);$(ArtifactsPath)/** + + $(DefaultItemExcludes);bin/**;obj/** + + + + $(OutputPath)$(TargetFramework.ToLowerInvariant())\ + + + $(IntermediateOutputPath)$(TargetFramework.ToLowerInvariant())\ + + + + + + + + + + + true + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_RuntimePackInWorkloadVersionCurrent>9.0.11 + <_RuntimePackInWorkloadVersion8>8.0.22 + <_RuntimePackInWorkloadVersion7>7.0.20 + <_RuntimePackInWorkloadVersion6>6.0.36 + true + + false + true + + + true + true + true + + $(WasmNativeWorkload8) + + + + <_BrowserWorkloadNotSupportedForTFM Condition="$([MSBuild]::VersionLessThan($(TargetFrameworkVersion), '6.0'))">true + <_BrowserWorkloadDisabled>$(_BrowserWorkloadNotSupportedForTFM) + <_UsingBlazorOrWasmSdk Condition="'$(UsingMicrosoftNETSdkBlazorWebAssembly)' == 'true' or '$(UsingMicrosoftNETSdkWebAssembly)' == 'true'">true + + + true + $(WasmNativeWorkload9) + $(WasmNativeWorkload8) + $(WasmNativeWorkload7) + $(WasmNativeWorkload) + false + $(WasmNativeWorkloadAvailable) + + + + + + <_WasmPropertiesDifferFromRuntimePackThusNativeBuildNeeded Condition=" '$(WasmEnableSIMD)' == 'false' or '$(WasmEnableExceptionHandling)' == 'false' or '$(InvariantTimezone)' == 'true' or ('$(_UsingBlazorOrWasmSdk)' != 'true' and '$(InvariantGlobalization)' == 'true') or '$(WasmNativeStrip)' == 'false'">true + + + <_WasmNativeWorkloadNeeded Condition=" '$(_WasmPropertiesDifferFromRuntimePackThusNativeBuildNeeded)' == 'true' or '$(RunAOTCompilation)' == 'true' or '$(WasmBuildNative)' == 'true' or '$(WasmGenerateAppBundle)' == 'true' or '$(_UsingBlazorOrWasmSdk)' != 'true' or '$(EmccMaximumHeapSize)' != '' ">true + false + true + $(WasmNativeWorkloadAvailable) + + + + <_IsAndroidLibraryMode Condition="'$(RuntimeIdentifier)' == 'android-arm64' or '$(RuntimeIdentifier)' == 'android-arm' or '$(RuntimeIdentifier)' == 'android-x64' or '$(RuntimeIdentifier)' == 'android-x86'">true + <_IsAppleMobileLibraryMode Condition="'$(RuntimeIdentifier)' == 'ios-arm64' or '$(RuntimeIdentifier)' == 'iossimulator-arm64' or '$(RuntimeIdentifier)' == 'iossimulator-x64' or '$(RuntimeIdentifier)' == 'maccatalyst-arm64' or '$(RuntimeIdentifier)' == 'maccatalyst-x64' or '$(RuntimeIdentifier)' == 'tvos-arm64'">true + <_IsiOSLibraryMode Condition="'$(RuntimeIdentifier)' == 'ios-arm64' or '$(RuntimeIdentifier)' == 'iossimulator-arm64' or '$(RuntimeIdentifier)' == 'iossimulator-x64'">true + <_IsMacCatalystLibraryMode Condition="'$(RuntimeIdentifier)' == 'maccatalyst-arm64' or '$(RuntimeIdentifier)' == 'maccatalyst-x64'">true + <_IstvOSLibraryMode Condition="'$(RuntimeIdentifier)' == 'tvos-arm64'">true + + + true + + + false + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_MonoWorkloadTargetsMobile>true + <_MonoWorkloadRuntimePackPackageVersion>$(_RuntimePackInWorkloadVersionCurrent) + <_KnownWebAssemblySdkPackVersion>$(_RuntimePackInWorkloadVersionCurrent) + + + + true + 1.0 + + + + + + + true + 1.0 + + + + + + + + %(RuntimePackRuntimeIdentifiers);wasi-wasm + $(_MonoWorkloadRuntimePackPackageVersion) + + Microsoft.NETCore.App.Runtime.Mono.multithread.**RID** + + + $(_MonoWorkloadRuntimePackPackageVersion) + + + $(_KnownWebAssemblySdkPackVersion) + $(_KnownWebAssemblySdkPackVersion) + + + + + + + + + + + + + + + + + + + + + + true + + + <_NativeBuildNeeded Condition="'$(RunAOTCompilation)' == 'true'">true + WebAssembly workloads (required for AOT) are only supported for projects targeting net6.0+ + + + true + $(WasmNativeWorkload) + + + 8.0 + 9.0 + + + false + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_MonoWorkloadTargetsMobile>true + <_MonoWorkloadRuntimePackPackageVersion>$(_RuntimePackInWorkloadVersion6) + + + + $(_MonoWorkloadRuntimePackPackageVersion) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_MonoWorkloadTargetsMobile>true + <_MonoWorkloadRuntimePackPackageVersion>$(_RuntimePackInWorkloadVersion7) + + + + $(_MonoWorkloadRuntimePackPackageVersion) + + Microsoft.NETCore.App.Runtime.Mono.multithread.**RID** + Microsoft.NETCore.App.Runtime.Mono.perftrace.**RID** + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_MonoWorkloadTargetsMobile>true + <_MonoWorkloadRuntimePackPackageVersion>$(_RuntimePackInWorkloadVersion8) + + + + + %(RuntimePackRuntimeIdentifiers);wasi-wasm + $(_MonoWorkloadRuntimePackPackageVersion) + + Microsoft.NETCore.App.Runtime.Mono.multithread.**RID** + + + + + + + + + + + + + + + + + + + + + + + <_ResolvedSuggestedWorkload Include="@(SuggestedWorkload)" /> + <_ResolvedSuggestedWorkload Include="@(SuggestedWorkloadFromReference)" /> + + + + + + + + + <_UsingDefaultRuntimeIdentifier>true + win7-x64 + win7-x86 + win-x64 + win-x86 + + + + true + + + + $(PublishSelfContained) + + + + true + + + $(NETCoreSdkPortableRuntimeIdentifier) + + + $(PublishRuntimeIdentifier) + + + <_UsingDefaultPlatformTarget>true + + + + + + + x86 + + + + + x64 + + + + + arm + + + + + arm64 + + + + + AnyCPU + + + + + + + <_SelfContainedWasSpecified Condition="'$(SelfContained)' != ''">true + + + + true + false + <_RuntimeIdentifierUsesAppHost Condition="$(RuntimeIdentifier.StartsWith('ios')) or $(RuntimeIdentifier.StartsWith('tvos')) or $(RuntimeIdentifier.StartsWith('maccatalyst')) or $(RuntimeIdentifier.StartsWith('android')) or $(RuntimeIdentifier.StartsWith('browser'))">false + <_RuntimeIdentifierUsesAppHost Condition="'$(_IsPublishing)' == 'true' and '$(PublishAot)' == 'true'">false + <_RuntimeIdentifierUsesAppHost Condition="'$(_RuntimeIdentifierUsesAppHost)' == ''">true + true + false + + + + $(NETCoreSdkRuntimeIdentifier) + win-x64 + win-x86 + win-arm + win-arm64 + + $(DefaultAppHostRuntimeIdentifier.Replace("arm64", "x64")) + + $(DefaultAppHostRuntimeIdentifier.Replace("arm64", "x64")) + + + + + + + + + + + + + + + + + + + + + + + + + + + false + + + + + + false + + + + + + + + + + + + + + + + + true + + + + $(IntermediateOutputPath)$(RuntimeIdentifier)\ + $(OutputPath)$(RuntimeIdentifier)\ + + + + + + + + + + + + + + + true + true + + + + <_EolNetCoreTargetFrameworkVersions Include="1.0;1.1;2.0;2.1;2.2;3.0;3.1;5.0;7.0" /> + + + <_MinimumNonEolSupportedNetCoreTargetFramework>net6.0 + + + + + + + + + + + <_IsNETCoreOrNETStandard Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'">true + <_IsNETCoreOrNETStandard Condition="'$(TargetFrameworkIdentifier)' == '.NETStandard'">true + + + + true + true + true + + + true + + + + true + + true + + .dll + + false + + + + $(PreserveCompilationContext) + + + + publish + + $(OutputPath)$(RuntimeIdentifier)\$(PublishDirName)\ + $(OutputPath)$(PublishDirName)\ + + + + + + <_NugetFallbackFolder>$(MSBuildThisFileDirectory)..\..\..\..\NuGetFallbackFolder + <_IsNETCore1x Condition=" '$(TargetFrameworkIdentifier)' == '.NETCoreApp' and '$(_TargetFrameworkVersionWithoutV)' < '2.0' ">true + <_WorkloadLibraryPacksFolder Condition="'$(_WorkloadLibraryPacksFolder)' == ''">$([MSBuild]::EnsureTrailingSlash('$(NetCoreRoot)'))library-packs + + + $(RestoreAdditionalProjectSources);$(_NugetFallbackFolder) + $(RestoreAdditionalProjectFallbackFoldersExcludes);$(_NugetFallbackFolder) + $(RestoreAdditionalProjectFallbackFolders);$(_NugetFallbackFolder) + + + $(RestoreAdditionalProjectSources);$(_WorkloadLibraryPacksFolder) + + + + <_SDKImplicitReference Include="System" /> + <_SDKImplicitReference Include="System.Data" /> + <_SDKImplicitReference Include="System.Drawing" /> + <_SDKImplicitReference Include="System.Xml" /> + + + <_SDKImplicitReference Include="System.Core" Condition=" '$(_TargetFrameworkVersionWithoutV)' >= '3.5' " /> + <_SDKImplicitReference Include="System.Runtime.Serialization" Condition=" '$(_TargetFrameworkVersionWithoutV)' >= '3.5' " /> + <_SDKImplicitReference Include="System.Xml.Linq" Condition=" '$(_TargetFrameworkVersionWithoutV)' >= '3.5' " /> + + <_SDKImplicitReference Include="System.Numerics" Condition=" '$(_TargetFrameworkVersionWithoutV)' >= '4.0' " /> + + <_SDKImplicitReference Include="System.IO.Compression.FileSystem" Condition=" '$(_TargetFrameworkVersionWithoutV)' >= '4.5' " /> + <_SDKImplicitReference Update="@(_SDKImplicitReference)" Pack="false" IsImplicitlyDefined="true" /> + + <_SDKImplicitReference Remove="@(Reference)" /> + + + + + + false + + + $(AssetTargetFallback);net461;net462;net47;net471;net472;net48;net481 + + + + <_FrameworkIdentifierForImplicitDefine>$(TargetFrameworkIdentifier.Replace('.', '').ToUpperInvariant()) + <_FrameworkIdentifierForImplicitDefine Condition=" '$(TargetFrameworkIdentifier)' == '.NETCoreApp' and $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), 5.0)) ">NET + $(_FrameworkIdentifierForImplicitDefine) + <_FrameworkIdentifierForImplicitDefine Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework'">NET + <_FrameworkVersionForImplicitDefine>$(TargetFrameworkVersion.TrimStart('vV')) + <_FrameworkVersionForImplicitDefine>$(_FrameworkVersionForImplicitDefine.Replace('.', '_')) + <_FrameworkVersionForImplicitDefine Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework'">$(_FrameworkVersionForImplicitDefine.Replace('_', '')) + $(_FrameworkIdentifierForImplicitDefine)$(_FrameworkVersionForImplicitDefine) + $(TargetFrameworkIdentifier.Replace('.', '').ToUpperInvariant()) + + + + + <_PlatformIdentifierForImplicitDefine>$(TargetPlatformIdentifier.ToUpperInvariant()) + <_PlatformVersionForImplicitDefine>$(TargetPlatformVersion.Replace('.', '_')) + + + <_ImplicitDefineConstant Include="$(_PlatformIdentifierForImplicitDefine)" /> + <_ImplicitDefineConstant Include="$(_PlatformIdentifierForImplicitDefine)$(_PlatformVersionForImplicitDefine)" /> + + + + + + <_SupportedFrameworkVersions Include="@(SupportedNETCoreAppTargetFramework->'%(Identity)'->TrimStart('.NETCoreApp,Version=v'))" Condition=" '$(TargetFrameworkIdentifier)' == '.NETCoreApp' " /> + <_SupportedFrameworkVersions Include="@(SupportedNETFrameworkTargetFramework->'%(Identity)'->TrimStart('.NETFramework,Version=v'))" Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' " /> + <_SupportedFrameworkVersions Include="@(SupportedNETStandardTargetFramework->'%(Identity)'->TrimStart('.NETStandard,Version=v'))" Condition=" '$(TargetFrameworkIdentifier)' == '.NETStandard' " /> + <_CompatibleFrameworkVersions Include="@(_SupportedFrameworkVersions)" Condition=" $([MSBuild]::VersionLessThanOrEquals(%(Identity), $(TargetFrameworkVersion))) " /> + <_FormattedCompatibleFrameworkVersions Include="@(_CompatibleFrameworkVersions)" Condition=" '$(TargetFrameworkIdentifier)' == '.NETCoreApp' or '$(TargetFrameworkIdentifier)' == '.NETStandard' " /> + <_FormattedCompatibleFrameworkVersions Include="@(_CompatibleFrameworkVersions->'%(Identity)'->Replace('.', ''))" Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' " /> + <_ImplicitDefineConstant Include="@(_FormattedCompatibleFrameworkVersions->'$(_FrameworkIdentifierForImplicitDefine)%(Identity)_OR_GREATER'->Replace('.', '_'))" Condition=" '$(TargetFrameworkIdentifier)' != '.NETCoreApp' or $([MSBuild]::VersionGreaterThanOrEquals(%(_FormattedCompatibleFrameworkVersions.Identity), 5.0)) " /> + <_ImplicitDefineConstant Include="@(_FormattedCompatibleFrameworkVersions->'NETCOREAPP%(Identity)_OR_GREATER'->Replace('.', '_'))" Condition=" '$(TargetFrameworkIdentifier)' == '.NETCoreApp' and $([MSBuild]::VersionLessThan(%(_FormattedCompatibleFrameworkVersions.Identity), 5.0)) " /> + + + + + + <_SupportedPlatformCompatibleVersions Include="@(SdkSupportedTargetPlatformVersion)" Condition=" %(Identity) != '' and $([MSBuild]::VersionLessThanOrEquals(%(Identity), $(TargetPlatformVersion))) " /> + <_ImplicitDefineConstant Include="@(_SupportedPlatformCompatibleVersions->Distinct()->'$(TargetPlatformIdentifier.ToUpper())%(Identity)_OR_GREATER'->Replace('.', '_'))" /> + + + + + + <_DefineConstantsWithoutTrace Include="$(DefineConstants)" /> + <_DefineConstantsWithoutTrace Remove="TRACE" /> + + + @(_DefineConstantsWithoutTrace) + + + + + + $(DefineConstants);@(_ImplicitDefineConstant) + $(FinalDefineConstants),@(_ImplicitDefineConstant->'%(Identity)=-1', ',') + + + + + false + true + + + $(AssemblyName).xml + $(IntermediateOutputPath)$(AssemblyName).xml + + + + + + true + true + true + + + + + + + true + + + + $(MSBuildToolsPath)\Microsoft.CSharp.targets + $(MSBuildToolsPath)\Microsoft.VisualBasic.targets + $(MSBuildThisFileDirectory)..\targets\Microsoft.NET.Sdk.FSharpTargetsShim.targets + + $(MSBuildToolsPath)\Microsoft.Common.targets + + + + + + + + $(MSBuildToolsPath)\Microsoft.CSharp.CrossTargeting.targets + + + + + $(MSBuildToolsPath)\Microsoft.CSharp.CurrentVersion.targets + + + + + + + + true + + + + + + true + true + true + true + + + + + $(MSBuildExtensionsPath)\v$(MSBuildToolsVersion)\Custom.Before.Microsoft.CSharp.targets + $(MSBuildExtensionsPath)\v$(MSBuildToolsVersion)\Custom.After.Microsoft.CSharp.targets + + + + .cs + C# + Managed + true + true + true + true + true + {FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Properties + + + + + File + + + BrowseObject + + + + + + + + + + + + + <_Temporary Remove="@(_Temporary)" /> + + + + + + + + + + + + + <_Temporary Remove="@(_Temporary)" /> + + + + + + + + + + + + + + true + + + + + + <_DebugSymbolsIntermediatePathTemporary Include="$(PdbFile)" /> + + <_DebugSymbolsIntermediatePath Include="@(_DebugSymbolsIntermediatePathTemporary->'%(RootDir)%(Directory)%(Filename).pdb')" /> + + + $(CoreCompileDependsOn);_ComputeNonExistentFileProperty;ResolveCodeAnalysisRuleSet + true + + + + + + $(NoWarn);1701;1702 + + + + $(NoWarn);2008 + + + + + + + + + $(AppConfig) + + $(IntermediateOutputPath)$(TargetName).compile.pdb + + + + false + + + + + + + true + + + + + + + + + + $(RoslynTargetsPath)\Microsoft.CSharp.Core.targets + + + + + + + + + + roslyn4.12 + + + + + + + + + + + + + + + + + false + + + + + + + + true + + + + + + + + <_SkipAnalyzers /> + <_ImplicitlySkipAnalyzers /> + + + + <_SkipAnalyzers>true + + + + <_ImplicitlySkipAnalyzers>true + <_SkipAnalyzers>true + run-nullable-analysis=never;$(Features) + + + + + + <_LastBuildWithSkipAnalyzers>$(IntermediateOutputPath)$(MSBuildProjectFile).BuildWithSkipAnalyzers + + + + + + + + + + + + + + <_AllDirectoriesAbove Include="@(Compile->GetPathsOfAllDirectoriesAbove())" Condition="'$(DiscoverEditorConfigFiles)' != 'false' or '$(DiscoverGlobalAnalyzerConfigFiles)' != 'false'" /> + + + + + + + + + + + + $(IntermediateOutputPath)$(MSBuildProjectName).GeneratedMSBuildEditorConfig.editorconfig + true + <_GeneratedEditorConfigHasItems Condition="'@(CompilerVisibleItemMetadata->Count())' != '0'">true + <_GeneratedEditorConfigShouldRun Condition="'$(GenerateMSBuildEditorConfigFile)' == 'true' and ('$(_GeneratedEditorConfigHasItems)' == 'true' or '@(CompilerVisibleProperty->Count())' != '0')">true + + + + + + <_GeneratedEditorConfigProperty Include="@(CompilerVisibleProperty)"> + $(%(CompilerVisibleProperty.Identity)) + + + <_GeneratedEditorConfigMetadata Include="@(%(CompilerVisibleItemMetadata.Identity))" Condition="'$(_GeneratedEditorConfigHasItems)' == 'true'"> + %(Identity) + %(CompilerVisibleItemMetadata.MetadataName) + + + + + + + + + + + true + + + + + <_MappedSourceRoot Remove="@(_MappedSourceRoot)" /> + + + + + + + + + + + + true + + + + + + + <_TopLevelSourceRoot Include="@(SourceRoot)" Condition="'%(SourceRoot.NestedRoot)' == ''"> + $([MSBuild]::ValueOrDefault('%(Identity)', '').Replace(',', ',,').Replace('=', '==')) + $([MSBuild]::ValueOrDefault('%(MappedPath)', '').Replace(',', ',,').Replace('=', '==')) + + + + + @(_TopLevelSourceRoot->'%(EscapedKey)=%(EscapedValue)', ','),$(PathMap) + + + + + + + + + + + false + + $(IntermediateOutputPath)/generated + + + + + + + + + + + + + <_MaxSupportedLangVersion Condition="('$(TargetFrameworkIdentifier)' != '.NETCoreApp' OR '$(_TargetFrameworkVersionWithoutV)' < '3.0') AND ('$(TargetFrameworkIdentifier)' != '.NETStandard' OR '$(_TargetFrameworkVersionWithoutV)' < '2.1')">7.3 + + <_MaxSupportedLangVersion Condition="(('$(TargetFrameworkIdentifier)' == '.NETCoreApp' AND '$(_TargetFrameworkVersionWithoutV)' < '5.0') OR ('$(TargetFrameworkIdentifier)' == '.NETStandard' AND '$(_TargetFrameworkVersionWithoutV)' == '2.1')) AND '$(_MaxSupportedLangVersion)' == ''">8.0 + + <_MaxSupportedLangVersion Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp' AND '$(_TargetFrameworkVersionWithoutV)' == '5.0' AND '$(_MaxSupportedLangVersion)' == ''">9.0 + + <_MaxSupportedLangVersion Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp' AND '$(_TargetFrameworkVersionWithoutV)' == '6.0' AND '$(_MaxSupportedLangVersion)' == ''">10.0 + + <_MaxSupportedLangVersion Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp' AND '$(_TargetFrameworkVersionWithoutV)' == '7.0' AND '$(_MaxSupportedLangVersion)' == ''">11.0 + + <_MaxSupportedLangVersion Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp' AND '$(_TargetFrameworkVersionWithoutV)' == '8.0' AND '$(_MaxSupportedLangVersion)' == ''">12.0 + + <_MaxSupportedLangVersion Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp' AND '$(_TargetFrameworkVersionWithoutV)' == '9.0' AND '$(_MaxSupportedLangVersion)' == ''">13.0 + $(_MaxSupportedLangVersion) + $(_MaxSupportedLangVersion) + + + + + $(NoWarn);1701;1702 + + + + $(NoWarn);2008 + + + + $(AppConfig) + + $(IntermediateOutputPath)$(TargetName).compile.pdb + + + + + + + <_CoreCompileResourceInputs Remove="@(_CoreCompileResourceInputs)" /> + + + + + + -langversion:$(LangVersion) + $(CommandLineArgsForDesignTimeEvaluation) -checksumalgorithm:$(ChecksumAlgorithm) + $(CommandLineArgsForDesignTimeEvaluation) -define:$(DefineConstants) + + + + + + $(MSBuildExtensionsPath)\Microsoft\VisualStudio\Managed\Microsoft.CSharp.DesignTime.targets + + + + + + $(MSBuildToolsPath)\Microsoft.Common.CurrentVersion.targets + + + + + + true + true + true + true + + + + + + + 10.0 + + + $(MSBuildExtensionsPath)\v$(MSBuildToolsVersion)\Custom.Before.Microsoft.Common.targets + $(MSBuildExtensionsPath)\v$(MSBuildToolsVersion)\Custom.After.Microsoft.Common.targets + $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\ReportingServices\Microsoft.ReportingServices.targets + + + + + Managed + + + + .NETFramework + v4.0 + + + + Any CPU,x86,x64,Itanium + Any CPU,x86,x64 + + + + + + + + $(SDK40ToolsPath) + + + + true + + + false + + + + + true + + true + + + $(TargetFrameworkIdentifier),Version=$(TargetFrameworkVersion),Profile=$(TargetFrameworkProfile) + $(TargetFrameworkIdentifier),Version=$(TargetFrameworkVersion) + + $(TargetFrameworkRootPath)$(TargetFrameworkIdentifier)\$(TargetFrameworkVersion) + + $([Microsoft.Build.Utilities.ToolLocationHelper]::GetPathToStandardLibraries($(TargetFrameworkIdentifier), $(TargetFrameworkVersion), $(TargetFrameworkProfile), $(PlatformTarget), $(TargetFrameworkRootPath), $(TargetFrameworkFallbackSearchPaths))) + $(MSBuildFrameworkToolsPath) + + + Windows + 7.0 + $(TargetPlatformSdkRootOverride)\ + $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\Software\Microsoft\Microsoft SDKs\Windows\v$(TargetPlatformVersion)', InstallationFolder, null, RegistryView.Registry32, RegistryView.Default)) + $([Microsoft.Build.Utilities.ToolLocationHelper]::GetPlatformSDKLocation($(TargetPlatformIdentifier), $(TargetPlatformVersion))) + $(TargetPlatformSdkPath)Windows Metadata + $(TargetPlatformSdkPath)References\CommonConfiguration\Neutral + $(TargetPlatformSdkMetadataLocation) + true + $(WinDir)\System32\WinMetadata + $(TargetPlatformIdentifier),Version=$(TargetPlatformVersion) + $([Microsoft.Build.Utilities.ToolLocationHelper]::GetPlatformSDKDisplayName($(TargetPlatformIdentifier), $(TargetPlatformVersion))) + + + + + <_OriginalPlatform>$(Platform) + + <_OriginalConfiguration>$(Configuration) + + <_OutputPathWasMissing Condition="'$(_OriginalPlatform)' != '' and '$(_OriginalConfiguration)' != '' and '$(OutputPath)' == ''">true + + true + + + AnyCPU + $(Platform) + Debug + $(Configuration) + bin\ + $(BaseOutputPath)\ + $(BaseOutputPath)$(Configuration)\ + $(BaseOutputPath)$(PlatformName)\$(Configuration)\ + $(OutputPath)\ + obj\ + $(BaseIntermediateOutputPath)\ + $(BaseIntermediateOutputPath)$(Configuration)\ + $(BaseIntermediateOutputPath)$(PlatformName)\$(Configuration)\ + $(IntermediateOutputPath)\ + + + + $(TargetType) + library + exe + true + + <_DebugSymbolsProduced>false + <_DebugSymbolsProduced Condition="'$(DebugSymbols)'=='true'">true + <_DebugSymbolsProduced Condition="'$(DebugType)'=='none'">false + <_DebugSymbolsProduced Condition="'$(DebugType)'=='pdbonly'">true + <_DebugSymbolsProduced Condition="'$(DebugType)'=='full'">true + <_DebugSymbolsProduced Condition="'$(DebugType)'=='portable'">true + <_DebugSymbolsProduced Condition="'$(DebugType)'=='embedded'">false + <_DebugSymbolsProduced Condition="'$(ProduceOnlyReferenceAssembly)'=='true'">false + + <_DocumentationFileProduced>true + <_DocumentationFileProduced Condition="'$(DocumentationFile)'==''">false + + false + + + + + <_InvalidConfigurationError Condition=" '$(SkipInvalidConfigurations)' != 'true' ">true + <_InvalidConfigurationWarning Condition=" '$(SkipInvalidConfigurations)' == 'true' ">true + + + + .exe + .exe + .exe + .dll + .netmodule + .winmdobj + + + + true + $(OutputPath) + + + $(OutDir)\ + $(MSBuildProjectName) + + + $(OutDir)$(ProjectName)\ + $(MSBuildProjectName) + $(RootNamespace) + $(AssemblyName) + + $(MSBuildProjectFile) + + $(MSBuildProjectExtension) + + $(TargetName).winmd + $(WinMDExpOutputWindowsMetadataFilename) + $(TargetName)$(TargetExt) + + + + + <_DeploymentPublishableProjectDefault Condition="'$(OutputType)'=='winexe' or '$(OutputType)'=='exe' or '$(OutputType)'=='appcontainerexe'">true + $(_DeploymentPublishableProjectDefault) + <_DeploymentTargetApplicationManifestFileName Condition="'$(OutputType)'=='library'">Native.$(AssemblyName).manifest + + <_DeploymentTargetApplicationManifestFileName Condition="'$(OutputType)'=='winexe'">$(TargetFileName).manifest + + <_DeploymentTargetApplicationManifestFileName Condition="'$(OutputType)'=='exe'">$(TargetFileName).manifest + + <_DeploymentTargetApplicationManifestFileName Condition="'$(OutputType)'=='appcontainerexe'">$(TargetFileName).manifest + + $(AssemblyName).application + + $(AssemblyName).xbap + + $(GenerateManifests) + <_DeploymentApplicationManifestIdentity Condition="'$(OutputType)'=='library'">Native.$(AssemblyName) + <_DeploymentApplicationManifestIdentity Condition="'$(OutputType)'=='winexe'">$(AssemblyName).exe + <_DeploymentApplicationManifestIdentity Condition="'$(OutputType)'=='exe'">$(AssemblyName).exe + <_DeploymentApplicationManifestIdentity Condition="'$(OutputType)'=='appcontainerexe'">$(AssemblyName).exe + <_DeploymentDeployManifestIdentity Condition="'$(HostInBrowser)' != 'true'">$(AssemblyName).application + <_DeploymentDeployManifestIdentity Condition="'$(HostInBrowser)' == 'true'">$(AssemblyName).xbap + <_DeploymentFileMappingExtension Condition="'$(MapFileExtensions)'=='true'">.deploy + <_DeploymentFileMappingExtension Condition="'$(MapFileExtensions)'!='true'" /> + <_DeploymentBuiltUpdateInterval Condition="'$(UpdatePeriodically)'=='true'">$(UpdateInterval) + <_DeploymentBuiltUpdateIntervalUnits Condition="'$(UpdatePeriodically)'=='true'">$(UpdateIntervalUnits) + <_DeploymentBuiltUpdateInterval Condition="'$(UpdatePeriodically)'!='true'">0 + <_DeploymentBuiltUpdateIntervalUnits Condition="'$(UpdatePeriodically)'!='true'">Days + <_DeploymentBuiltMinimumRequiredVersion Condition="'$(UpdateRequired)'=='true' and '$(Install)'=='true'">$(MinimumRequiredVersion) + <_DeploymentLauncherBased Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'">true + 100 + + + + * + $(UICulture) + + + + <_OutputPathItem Include="$(OutDir)" /> + <_UnmanagedRegistrationCache Include="$(BaseIntermediateOutputPath)$(MSBuildProjectFile).UnmanagedRegistration.cache" /> + <_ResolveComReferenceCache Include="$(IntermediateOutputPath)$(MSBuildProjectFile).ResolveComReference.cache" /> + + + + + $([MSBuild]::Escape($([System.IO.Path]::GetFullPath(`$([System.IO.Path]::Combine(`$(MSBuildProjectDirectory)`, `$(OutDir)`))`)))) + + $(TargetDir)$(TargetFileName) + $([MSBuild]::NormalizePath($(TargetDir), 'ref', $(TargetFileName))) + $([MSBuild]::NormalizePath($(MSBuildProjectDirectory), $(IntermediateOutputPath), 'ref', $(TargetFileName))) + + $([MSBuild]::EnsureTrailingSlash($(MSBuildProjectDirectory))) + + $(ProjectDir)$(ProjectFileName) + + + + + + + + *Undefined* + *Undefined* + + *Undefined* + + *Undefined* + + *Undefined* + + *Undefined* + + + + true + + true + + + true + false + + + $(MSBuildProjectFile).FileListAbsolute.txt + + false + + true + true + <_ResolveReferenceDependencies Condition="'$(_ResolveReferenceDependencies)' == ''">false + <_GetChildProjectCopyToOutputDirectoryItems Condition="'$(_GetChildProjectCopyToOutputDirectoryItems)' == ''">true + false + false + + + <_GenerateBindingRedirectsIntermediateAppConfig>$(IntermediateOutputPath)$(TargetFileName).config + + + $(MSBuildProjectFile) + + $([MSBuild]::SubstringByAsciiChars($(MSBuildProjectFile), 0, 8)).$([MSBuild]::StableStringHash($(MSBuildProjectFile)).ToString("X8")) + $(MSBuildCopyMarkerName).Up2Date + + + + + + + + + + + + + + <_DebugSymbolsIntermediatePath Include="$(IntermediateOutputPath)$(TargetName).compile.pdb" Condition="'$(OutputType)' == 'winmdobj' and '@(_DebugSymbolsIntermediatePath)' == ''" /> + <_DebugSymbolsIntermediatePath Include="$(IntermediateOutputPath)$(TargetName).pdb" Condition="'$(OutputType)' != 'winmdobj' and '@(_DebugSymbolsIntermediatePath)' == ''" /> + <_DebugSymbolsOutputPath Include="@(_DebugSymbolsIntermediatePath->'$(OutDir)%(Filename)%(Extension)')" /> + + + $(IntermediateOutputPath)$(TargetName).pdb + <_WinMDDebugSymbolsOutputPath>$([System.IO.Path]::Combine('$(OutDir)', $([System.IO.Path]::GetFileName('$(WinMDExpOutputPdb)')))) + + + $(IntermediateOutputPath)$(TargetName).xml + <_WinMDDocFileOutputPath>$([System.IO.Path]::Combine('$(OutDir)', $([System.IO.Path]::GetFileName('$(WinMDOutputDocumentationFile)')))) + + + <_IntermediateWindowsMetadataPath>$(IntermediateOutputPath)$(WinMDExpOutputWindowsMetadataFilename) + <_WindowsMetadataOutputPath>$(OutDir)$(WinMDExpOutputWindowsMetadataFilename) + + + + <_SupportedArchitectures>amd64 arm64 + + + + <_DeploymentManifestEntryPoint Include="@(IntermediateAssembly)"> + $(TargetFileName) + + + + <_DeploymentManifestIconFile Include="$(ApplicationIcon)" Condition="Exists('$(ApplicationIcon)')"> + $(ApplicationIcon) + + + + $(_DeploymentTargetApplicationManifestFileName) + + + <_ApplicationManifestFinal Include="$(OutDir)$(_DeploymentTargetApplicationManifestFileName)"> + $(_DeploymentTargetApplicationManifestFileName) + + + + $(TargetDeployManifestFileName) + + + <_DeploymentIntermediateTrustInfoFile Include="$(IntermediateOutputPath)$(TargetName).TrustInfo.xml" Condition="'$(TargetZone)'!=''" /> + + + + <_DeploymentUrl Condition="'$(_DeploymentUrl)'==''">$(UpdateUrl) + <_DeploymentUrl Condition="'$(_DeploymentUrl)'==''">$(InstallUrl) + <_DeploymentUrl Condition="'$(_DeploymentUrl)'==''">$(PublishUrl) + <_DeploymentUrl Condition="!('$(UpdateUrl)'=='') and '$(Install)'=='false'" /> + <_DeploymentUrl Condition="'$(_DeploymentUrl)'!=''">$(_DeploymentUrl)$(TargetDeployManifestFileName) + + <_DeploymentUrl Condition="'$(UpdateUrl)'=='' and !('$(Install)'=='true' and '$(UpdateEnabled)'=='true')" /> + <_DeploymentUrl Condition="'$(ExcludeDeploymentUrl)'=='true'" /> + + + + <_DeploymentApplicationUrl Condition="'$(IsWebBootstrapper)'=='true'">$(InstallUrl) + <_DeploymentApplicationUrl Condition="'$(IsWebBootstrapper)'=='true' and '$(InstallUrl)'==''">$(PublishUrl) + <_DeploymentComponentsUrl Condition="'$(BootstrapperComponentsLocation)'=='Absolute'">$(BootstrapperComponentsUrl) + + + + $(PublishDir)\ + $(OutputPath)app.publish\ + + + + $(PublishDir) + $(ClickOncePublishDir)\ + + + + + $(PlatformTarget) + + msil + amd64 + ia64 + x86 + arm + + + true + + + + $(Platform) + msil + amd64 + ia64 + x86 + arm + + None + $(PROCESSOR_ARCHITECTURE) + + + + CLR2 + CLR4 + CurrentRuntime + true + false + $(PlatformTarget) + x86 + x64 + CurrentArchitecture + + + + Client + + + + false + + + + + true + true + false + + + + AssemblyFoldersEx + Software\Microsoft\$(TargetFrameworkIdentifier) + Software\Microsoft\Microsoft SDKs\$(TargetPlatformIdentifier) + $([MSBuild]::GetToolsDirectory32())\AssemblyFolders.config + {AssemblyFoldersFromConfig:$(AssemblyFoldersConfigFile),$(TargetFrameworkVersion)}; + + + .winmd; + .dll; + .exe + + + + .pdb; + .xml; + .pri; + .dll.config; + .exe.config + + + Full + + + + {CandidateAssemblyFiles} + $(AssemblySearchPaths);$(ReferencePath) + $(AssemblySearchPaths);{HintPathFromItem} + $(AssemblySearchPaths);{TargetFrameworkDirectory} + $(AssemblySearchPaths);$(AssemblyFoldersConfigFileSearchPath) + $(AssemblySearchPaths);{Registry:$(FrameworkRegistryBase),$(TargetFrameworkVersion),$(AssemblyFoldersSuffix)$(AssemblyFoldersExConditions)} + $(AssemblySearchPaths);{AssemblyFolders} + $(AssemblySearchPaths);{GAC} + $(AssemblySearchPaths);{RawFileName} + $(AssemblySearchPaths);$(OutDir) + + + + false + + + + $(NoWarn) + $(WarningsAsErrors) + $(WarningsNotAsErrors) + + + + $(MSBuildThisFileDirectory)$(LangName)\ + + + + $(MSBuildThisFileDirectory)en-US\ + + + + + Project + + + BrowseObject + + + File + + + Invisible + + + File;BrowseObject + + + File;ProjectSubscriptionService + + + + $(DefineCommonItemSchemas) + + + + + ;BrowseObject + + + ProjectSubscriptionService;BrowseObject + + + + ;BrowseObject + + + ProjectSubscriptionService;BrowseObject + + + + ;BrowseObject + + + ProjectSubscriptionService;BrowseObject + + + + + + + + + Never + + + Never + + + Never + + + Never + + + + + + true + + + + + <_GlobalPropertiesToRemoveFromProjectReferences Condition="'$(PassOutputPathToReferencedProjects)'=='false'">$(_GlobalPropertiesToRemoveFromProjectReferences);OutputPath + + + + + + <_InvalidConfigurationMessageText>The BaseOutputPath/OutputPath property is not set for project '$(MSBuildProjectFile)'. Please check to make sure that you have specified a valid combination of Configuration and Platform for this project. Configuration='$(_OriginalConfiguration)' Platform='$(_OriginalPlatform)'. + <_InvalidConfigurationMessageText Condition="'$(BuildingInsideVisualStudio)' == 'true'">$(_InvalidConfigurationMessageText) This error may also appear if some other project is trying to follow a project-to-project reference to this project, this project has been unloaded or is not included in the solution, and the referencing project does not build using the same or an equivalent Configuration or Platform. + <_InvalidConfigurationMessageText Condition="'$(BuildingInsideVisualStudio)' != 'true'">$(_InvalidConfigurationMessageText) You may be seeing this message because you are trying to build a project without a solution file, and have specified a non-default Configuration or Platform that doesn't exist for this project. + + + + + + + + + + + + x86 + + + + + + + + + + + + + BeforeBuild; + CoreBuild; + AfterBuild + + + + + + + + + + + BuildOnlySettings; + PrepareForBuild; + PreBuildEvent; + ResolveReferences; + PrepareResources; + ResolveKeySource; + Compile; + ExportWindowsMDFile; + UnmanagedUnregistration; + GenerateSerializationAssemblies; + CreateSatelliteAssemblies; + GenerateManifests; + GetTargetPath; + PrepareForRun; + UnmanagedRegistration; + IncrementalClean; + PostBuildEvent + + + + + + + + + <_ProjectDefaultTargets Condition="'$(MSBuildProjectDefaultTargets)' != ''">$(MSBuildProjectDefaultTargets) + <_ProjectDefaultTargets Condition="'$(MSBuildProjectDefaultTargets)' == ''">Build + + BeforeRebuild; + Clean; + $(_ProjectDefaultTargets); + AfterRebuild; + + + BeforeRebuild; + Clean; + Build; + AfterRebuild; + + + + + + + + + + Build + + + + + + + + + + + Build + + + + + + + + + + + Build + + + + + + + + + + + + + + + + + + + + + + + false + + + + true + + + + + + $(PrepareForBuildDependsOn);GetFrameworkPaths;GetReferenceAssemblyPaths;AssignLinkMetadata + + + + + $(TargetFileName).config + + + + + + + + + + + + + @(_TargetFramework40DirectoryItem) + @(_TargetFramework35DirectoryItem) + @(_TargetFramework30DirectoryItem) + @(_TargetFramework20DirectoryItem) + + @(_TargetFramework20DirectoryItem) + @(_TargetFramework40DirectoryItem) + @(_TargetedFrameworkDirectoryItem) + @(_TargetFrameworkSDKDirectoryItem) + + + + + + + + + + + + + + + + + + $(_TargetFrameworkDirectories);$(TargetFrameworkDirectory);$(WinFXAssemblyDirectory) + $(TargetFrameworkDirectory);$(TargetPlatformWinMDLocation) + + + + true + + + $(AssemblySearchPaths.Replace('{AssemblyFolders}', '').Split(';')) + + + + + + + $(TargetFrameworkDirectory);@(DesignTimeFacadeDirectories) + + + + + + + + + + + + + + + + + + + + + <_Temp Remove="@(_Temp)" /> + + + + + + + + + <_Temp Remove="@(_Temp)" /> + + + + + + + + + <_Temp Remove="@(_Temp)" /> + + + + + + + + + <_Temp Remove="@(_Temp)" /> + + + + + + + + + <_Temp Remove="@(_Temp)" /> + + + + + + + + + <_Temp Remove="@(_Temp)" /> + + + + + + + + + + + + + + + + + + $(PlatformTargetAsMSBuildArchitecture) + + + + $(TargetFrameworkAsMSBuildRuntime) + + CurrentRuntime + + + + + + + + + + BeforeResolveReferences; + AssignProjectConfiguration; + ResolveProjectReferences; + FindInvalidProjectReferences; + ResolveNativeReferences; + ResolveAssemblyReferences; + GenerateBindingRedirects; + GenerateBindingRedirectsUpdateAppConfig; + ResolveComReferences; + AfterResolveReferences + + + + + + + + + + + + false + + + + + + + true + true + false + + false + + true + + + + + + + + + + + <_ProjectReferenceWithConfiguration> + true + true + + + true + true + + + + + + + + + + + + + <_MSBuildProjectReference Include="@(ProjectReferenceWithConfiguration)" Condition="'$(BuildingInsideVisualStudio)'!='true' and '@(ProjectReferenceWithConfiguration)'!=''" /> + + + + <_MSBuildProjectReferenceExistent Include="@(_MSBuildProjectReference)" Condition="Exists('%(Identity)')" /> + <_MSBuildProjectReferenceNonexistent Include="@(_MSBuildProjectReference)" Condition="!Exists('%(Identity)')" /> + + + + + true + + + + + + <_MSBuildProjectReferenceExistent Condition="'%(_MSBuildProjectReferenceExistent.SetPlatform)' != ''"> + true + + + + <_ProjectReferencePlatformPossibilities Include="@(_MSBuildProjectReferenceExistent)" Condition="'%(_MSBuildProjectReferenceExistent.SkipGetPlatformProperties)' != 'true'" /> + + + + + <_ProjectReferencePlatformPossibilities Condition="'$(MSBuildProjectExtension)' != '.vcxproj' and '$(MSBuildProjectExtension)' != '.nativeproj' and '%(_ProjectReferencePlatformPossibilities.IsVcxOrNativeProj)' == 'true'"> + + x86=Win32 + + + <_ProjectReferencePlatformPossibilities Condition="('$(MSBuildProjectExtension)' == '.vcxproj' or '$(MSBuildProjectExtension)' == '.nativeproj') and '%(_ProjectReferencePlatformPossibilities.IsVcxOrNativeProj)' != 'true'"> + Win32=x86 + + + + + + + + + + Platform=%(ProjectsWithNearestPlatform.NearestPlatform) + + + + %(ProjectsWithNearestPlatform.UndefineProperties);Platform + + <_MSBuildProjectReferenceExistent Remove="@(_MSBuildProjectReferenceExistent)" Condition="'%(_MSBuildProjectReferenceExistent.SkipGetPlatformProperties)' != 'true'" /> + <_MSBuildProjectReferenceExistent Include="@(ProjectsWithNearestPlatform)" /> + + + + + + + $(NuGetTargetMoniker) + $(TargetFrameworkMoniker) + + + + <_MSBuildProjectReferenceExistent Condition="'%(_MSBuildProjectReferenceExistent.SkipGetTargetFrameworkProperties)' == '' and ('%(Extension)' == '.vcxproj' or '%(Extension)' == '.nativeproj')"> + + true + %(_MSBuildProjectReferenceExistent.UndefineProperties);TargetFramework + + + + + <_MSBuildProjectReferenceExistent Condition="'%(_MSBuildProjectReferenceExistent.SetTargetFramework)' != ''"> + + true + + + + + + + + + + + + + <_ProjectReferenceTargetFrameworkPossibilitiesOriginalItemSpec Include="@(_ProjectReferenceTargetFrameworkPossibilities->'%(OriginalItemSpec)')" /> + <_ProjectReferenceTargetFrameworkPossibilities Remove="@(_ProjectReferenceTargetFrameworkPossibilities)" /> + <_ProjectReferenceTargetFrameworkPossibilities Include="@(_ProjectReferenceTargetFrameworkPossibilitiesOriginalItemSpec)" /> + + + + + + + + + + + + + + + + + + + + + + + + TargetFramework=%(AnnotatedProjects.NearestTargetFramework) + + + + %(AnnotatedProjects.UndefineProperties);TargetFramework + + + + %(AnnotatedProjects.UndefineProperties);RuntimeIdentifier;SelfContained + + + <_MSBuildProjectReferenceExistent Remove="@(_MSBuildProjectReferenceExistent)" Condition="'%(_MSBuildProjectReferenceExistent.SkipGetTargetFrameworkProperties)' != 'true'" /> + <_MSBuildProjectReferenceExistent Include="@(AnnotatedProjects)" /> + + + + + + + + + <_ThisProjectBuildMetadata Include="$(MSBuildProjectFullPath)"> + @(_TargetFrameworkInfo) + @(_TargetFrameworkInfo->'%(TargetFrameworkMonikers)') + @(_TargetFrameworkInfo->'%(TargetPlatformMonikers)') + $(_AdditionalPropertiesFromProject) + true + @(_TargetFrameworkInfo->'%(IsRidAgnostic)') + + true + $(Platform) + $(Platforms) + + @(ProjectConfiguration->'%(Platform)'->Distinct()) + + + + + + <_AdditionalTargetFrameworkInfoPropertyWithValue Include="@(AdditionalTargetFrameworkInfoProperty)"> + $(%(AdditionalTargetFrameworkInfoProperty.Identity)) + + + + <_UseAttributeForTargetFrameworkInfoPropertyNames Condition="'$(_UseAttributeForTargetFrameworkInfoPropertyNames)' == ''">false + + + + + + <_TargetFrameworkInfo Include="$(TargetFramework)"> + $(TargetFramework) + $(TargetFrameworkMoniker) + $(TargetPlatformMoniker) + None + $(_AdditionalTargetFrameworkInfoProperties) + + $(IsRidAgnostic) + true + false + + + + + + + + + AssignProjectConfiguration; + _SplitProjectReferencesByFileExistence; + _GetProjectReferenceTargetFrameworkProperties; + _GetProjectReferencePlatformProperties + + + + + + + + + $(ProjectReferenceBuildTargets) + + + ProjectReference + + + + + + + + + + + + + + + + + + + <_ResolvedProjectReferencePaths Remove="@(_ResolvedProjectReferencePaths)" Condition="'%(_ResolvedProjectReferencePaths.ResolveableAssembly)' == 'false'" /> + + <_ResolvedProjectReferencePaths> + %(_ResolvedProjectReferencePaths.OriginalItemSpec) + + + + + + + + + + <_ProjectReferencesFromRAR Include="@(ReferencePath->WithMetadataValue('ReferenceSourceTarget', 'ProjectReference'))"> + %(ReferencePath.ProjectReferenceOriginalItemSpec) + + + + + + + + + $(GetTargetPathDependsOn) + + + + + + $(TargetPlatformMoniker) + $(TargetPlatformIdentifier) + $(TargetFrameworkIdentifier) + $(TargetFrameworkVersion.TrimStart('vV')) + $(TargetRefPath) + @(CopyUpToDateMarker) + + + + + + + + %(_ApplicationManifestFinal.FullPath) + + + + + + + + + + + + + + + + + + ResolveProjectReferences; + FindInvalidProjectReferences; + GetFrameworkPaths; + GetReferenceAssemblyPaths; + PrepareForBuild; + ResolveSDKReferences; + ExpandSDKReferences; + + + + + <_ReferenceInstalledAssemblyDirectory Include="$(TargetFrameworkDirectory)" /> + <_ReferenceInstalledAssemblySubsets Include="$(TargetFrameworkSubset)" /> + + + + $(IntermediateOutputPath)$(MSBuildProjectFile).AssemblyReference.cache + + + + <_ResolveAssemblyReferencesApplicationConfigFileForExes Include="@(AppConfigWithTargetPath)" Condition="'$(AutoGenerateBindingRedirects)'=='true' or '$(AutoUnifyAssemblyReferences)'=='false'" /> + + + + <_FindDependencies Condition="'$(BuildingProject)' != 'true' and '$(_ResolveReferenceDependencies)' != 'true'">false + true + false + Warning + $(BuildingProject) + $(BuildingProject) + $(BuildingProject) + false + + + + + + true + + + + + false + true + + + + + + + + + + + + + + + + + + + + + + + %(FullPath) + + + %(ReferencePath.Identity) + + + + + + + + + + + + + + + <_NewGenerateBindingRedirectsIntermediateAppConfig Condition="Exists('$(_GenerateBindingRedirectsIntermediateAppConfig)')">true + $(_GenerateBindingRedirectsIntermediateAppConfig) + + + + + $(TargetFileName).config + + + + + + Software\Microsoft\Microsoft SDKs + $(LocalAppData)\Microsoft SDKs;$(MSBuildProgramFiles32)\Microsoft SDKs + + $(MSBuildProgramFiles32)\Microsoft SDKs\Windows Kits\10;$(WindowsKitsRoot) + + true + Windows + 8.1 + + false + WindowsPhoneApp + 8.1 + + + + + + + + + + + + + + + + + GetInstalledSDKLocations + + + + Debug + Retail + Retail + $(ProcessorArchitecture) + Neutral + + + true + + + + + + + + + + + + + + + + GetReferenceTargetPlatformMonikers + + + + + + + + <_ResolvedProjectReferencePaths Remove="@(InvalidProjectReferences)" /> + + + + + + + + + + + + + + ResolveSDKReferences + + + .winmd; + .dll + + + + + + + + + + + + + + + + false + false + false + $(TargetFrameworkSDKToolsDirectory) + true + + + + + + + + + + + + + + + <_ReferencesFromRAR Include="@(ReferencePath->WithMetadataValue('ReferenceSourceTarget', 'ResolveAssemblyReference'))" /> + + + + + {CandidateAssemblyFiles}; + $(ReferencePath); + {HintPathFromItem}; + {TargetFrameworkDirectory}; + {Registry:$(FrameworkRegistryBase),$(TargetFrameworkVersion),$(AssemblyFoldersSuffix)$(AssemblyFoldersExConditions)}; + {RawFileName}; + $(TargetDir) + + + + + + GetFrameworkPaths; + GetReferenceAssemblyPaths; + ResolveReferences + + + + + <_DesignTimeReferenceInstalledAssemblyDirectory Include="$(TargetFrameworkDirectory)" /> + + + $(IntermediateOutputPath)$(MSBuildProjectFile)DesignTimeResolveAssemblyReferences.cache + + + + {CandidateAssemblyFiles}; + $(ReferencePath); + {HintPathFromItem}; + {TargetFrameworkDirectory}; + {Registry:$(FrameworkRegistryBase),$(TargetFrameworkVersion),$(AssemblyFoldersSuffix)$(AssemblyFoldersExConditions)}; + {RawFileName}; + $(OutDir) + + + + false + false + false + false + false + true + false + + + <_DesignTimeReferenceAssemblies Include="$(DesignTimeReference)" /> + + + <_RARResolvedReferencePath Include="@(ReferencePath)" /> + + + + + + + + + + false + + + + $(IntermediateOutputPath) + + + + + $(PlatformTargetAsMSBuildArchitecture) + $(TargetFrameworkSDKToolsDirectory) + false + + + + + + + + + + + + + + + + + + + + + + + + + + + $(PrepareResourcesDependsOn); + PrepareResourceNames; + ResGen; + CompileLicxFiles + + + + + + + AssignTargetPaths; + SplitResourcesByCulture; + CreateManifestResourceNames; + CreateCustomManifestResourceNames + + + + + + + + + + <_Temporary Remove="@(_Temporary)" /> + + + + + + + + + + <_Temporary Remove="@(_Temporary)" /> + + + + + + + + + + + + + + + + + + + + false + + + + + + + <_LicxFile Include="@(EmbeddedResource)" Condition="'%(Extension)'=='.licx'" /> + + + Resx + + + Non-Resx + + + + + + + + + + + + + + + + Resx + + + Non-Resx + + + + + + + + + + + + <_MixedResourceWithNoCulture Remove="@(_MixedResourceWithNoCulture)" /> + <_MixedResourceWithCulture Remove="@(_MixedResourceWithCulture)" /> + + + + + + + + + + ResolveAssemblyReferences;SplitResourcesByCulture;BeforeResGen;CoreResGen;AfterResGen + FindReferenceAssembliesForReferences + true + false + + + + + + + + + + <_Temporary Remove="@(_Temporary)" /> + + + $(PlatformTargetAsMSBuildArchitecture) + $(TargetFrameworkSDKToolsDirectory) + + + + $(TargetFrameworkAsMSBuildRuntime) + + CurrentRuntime + + + + + + + + + + + + + + + + + + + + <_Temporary Remove="@(_Temporary)" /> + + + true + + + true + + + + true + + + true + + + + + + + + + + $(PlatformTargetAsMSBuildArchitecture) + + + + + + + + + + + + + + + + + + + + ResolveReferences; + ResolveKeySource; + SetWin32ManifestProperties; + _SetPreferNativeArm64Win32ManifestProperties; + FindReferenceAssembliesForReferences; + _GenerateCompileInputs; + BeforeCompile; + _TimeStampBeforeCompile; + _GenerateCompileDependencyCache; + CoreCompile; + _TimeStampAfterCompile; + AfterCompile; + + + + + + + + + + <_CoreCompileResourceInputs Include="@(EmbeddedResource->'%(OutputResource)')" Condition="'%(EmbeddedResource.WithCulture)' == 'false' and '%(EmbeddedResource.Type)' == 'Resx'" /> + <_CoreCompileResourceInputs Include="@(EmbeddedResource)" Condition="'%(EmbeddedResource.WithCulture)' == 'false' and '%(EmbeddedResource.Type)' == 'Non-Resx' " /> + + <_CoreCompileResourceInputs Include="@(ManifestResourceWithNoCulture)" Condition="'%(ManifestResourceWithNoCulture.EmittedForCompatibilityOnly)'==''"> + Resx + false + + <_CoreCompileResourceInputs Include="@(ManifestNonResxWithNoCultureOnDisk)" Condition="'%(ManifestNonResxWithNoCultureOnDisk.EmittedForCompatibilityOnly)'==''"> + Non-Resx + false + + + + + + + true + $([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)')) + + + true + + + + + + + + + + + + + + + true + + + + + + + + + + + + + + + + + + <_AssemblyTimestampBeforeCompile>%(IntermediateAssembly.ModifiedTime) + + + + + + $(IntermediateOutputPath)$(MSBuildProjectFile).SuggestedBindingRedirects.cache + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_AssemblyTimestampAfterCompile>%(IntermediateAssembly.ModifiedTime) + + + + + + __NonExistentSubDir__\__NonExistentFile__ + + + + + <_SGenDllName>$(TargetName).XmlSerializers.dll + <_SGenDllCreated>false + <_SGenGenerateSerializationAssembliesConfig>$(GenerateSerializationAssemblies) + <_SGenGenerateSerializationAssembliesConfig Condition="'$(GenerateSerializationAssemblies)' == ''">Auto + <_SGenGenerateSerializationAssembliesConfig Condition="'$(ConfigurationName)'=='Debug' and '$(_SGenGenerateSerializationAssembliesConfig)' == 'Auto'">Off + true + false + true + + + + + $(PlatformTargetAsMSBuildArchitecture) + + + + + + + + + + $(CreateSatelliteAssembliesDependsOn); + _GenerateSatelliteAssemblyInputs; + ComputeIntermediateSatelliteAssemblies; + GenerateSatelliteAssemblies + + + + + + + + + + <_SatelliteAssemblyResourceInputs Include="@(EmbeddedResource->'%(OutputResource)')" Condition="'%(EmbeddedResource.WithCulture)' == 'true' and '%(EmbeddedResource.Type)' == 'Resx'" /> + <_SatelliteAssemblyResourceInputs Include="@(EmbeddedResource)" Condition="'%(EmbeddedResource.WithCulture)' == 'true' and '%(EmbeddedResource.Type)' == 'Non-Resx'" /> + + <_SatelliteAssemblyResourceInputs Include="@(ManifestResourceWithCulture)" Condition="'%(ManifestResourceWithCulture.EmittedForCompatibilityOnly)'==''"> + Resx + true + + <_SatelliteAssemblyResourceInputs Include="@(ManifestNonResxWithCultureOnDisk)" Condition="'%(ManifestNonResxWithCultureOnDisk.EmittedForCompatibilityOnly)'==''"> + Non-Resx + true + + + + + + + <_ALExeToolPath Condition="'$(_ALExeToolPath)' == ''">$(TargetFrameworkSDKToolsDirectory) + + + + + + + + + + CreateManifestResourceNames + + + + + + %(EmbeddedResource.Culture) + %(EmbeddedResource.Culture)\$(TargetName).resources.dll + + + + + + $(Win32Manifest) + + + + + + + <_DeploymentBaseManifest>$(ApplicationManifest) + <_DeploymentBaseManifest Condition="'$(_DeploymentBaseManifest)'==''">@(_DeploymentBaseManifestWithTargetPath) + + true + + + + + $(ApplicationManifest) + $(ApplicationManifest) + + + + + + + $(_FrameworkVersion40Path)\default.win32manifest + + + + + + + + + $(_Win32Manifest) + + + + + + + SetWin32ManifestProperties; + GenerateApplicationManifest; + GenerateDeploymentManifest + + + + + + <_DeploymentPublishFileOfTypeManifestEntryPoint Include="@(PublishFile)" Condition="'%(FileType)'=='ManifestEntryPoint'" /> + + + + + + + + + + + + + + + + + <_DeploymentCopyApplicationManifest>true + + + + + + <_DeploymentManifestTargetFrameworkMoniker>$(TargetFrameworkMoniker) + <_DeploymentManifestTargetFrameworkVersion>$(TargetFrameworkVersion) + + + + + + + + + + + + + + + + + + + <_DeploymentManifestTargetFrameworkVersion Condition="'$(DeploymentManifestTargetFrameworkVersionOverride)' == ''">v4.5 + <_DeploymentManifestTargetFrameworkVersion Condition="'$(DeploymentManifestTargetFrameworkVersionOverride)' != ''">$(DeploymentManifestTargetFrameworkVersionOverride) + <_DeploymentManifestTargetFrameworkMoniker>.NETFramework,Version=$(_DeploymentManifestTargetFrameworkVersion) + + + + + + + + + + + <_DeploymentManifestEntryPoint Remove="@(_DeploymentManifestEntryPoint)" /> + <_DeploymentManifestEntryPoint Include="@(_DeploymentManifestLauncherEntryPoint)" /> + + + + + + + + + + <_DeploymentManifestType>Native + + + + + + + <_DeploymentManifestVersion>@(_IntermediateAssemblyIdentity->'%(Version)') + + + + + + + <_SGenDllsRelatedToCurrentDll Include="@(_ReferenceSerializationAssemblyPaths->'%(FullPath)')" Condition="'%(Extension)' == '.dll'" /> + <_SGenDllsRelatedToCurrentDll Include="@(SerializationAssembly->'%(FullPath)')" Condition="'%(Extension)' == '.dll'" /> + + + <_CopyLocalFalseRefPaths Include="@(ReferencePath)" Condition="'%(CopyLocal)' == 'false'" /> + <_CopyLocalFalseRefPathsWithExclusion Include="@(_CopyLocalFalseRefPaths)" Exclude="@(ReferenceCopyLocalPaths);@(_NETStandardLibraryNETFrameworkLib)" /> + + + <_ClickOnceSatelliteAssemblies Include="@(IntermediateSatelliteAssembliesWithTargetPath);@(ReferenceSatellitePaths)" /> + + + + <_DeploymentReferencePaths Include="@(ReferenceCopyLocalPaths)" Condition="('%(Extension)' == '.dll' Or '%(Extension)' == '.exe' Or '%(Extension)' == '.md') and ('%(ReferenceCopyLocalPaths.CopyToPublishDirectory)' != 'false')"> + true + + <_DeploymentReferencePaths Include="@(_CopyLocalFalseRefPathsWithExclusion)" /> + + + + <_ManifestManagedReferences Include="@(_DeploymentReferencePaths);@(ReferenceDependencyPaths);@(_SGenDllsRelatedToCurrentDll);@(SerializationAssembly);@(ReferenceCOMWrappersToCopyLocal)" Exclude="@(_ClickOnceSatelliteAssemblies);@(_ReferenceScatterPaths);@(_ExcludedAssembliesFromManifestGeneration)" /> + + + + + <_ClickOnceRuntimeCopyLocalItems Include="@(RuntimeTargetsCopyLocalItems)" Condition="'%(RuntimeTargetsCopyLocalItems.CopyLocal)' == 'true'" /> + <_ClickOnceRuntimeCopyLocalItems Include="@(NativeCopyLocalItems)" Condition="'%(NativeCopyLocalItems.CopyLocal)' == 'true'" /> + <_ClickOnceRuntimeCopyLocalItems Remove="@(_DeploymentReferencePaths)" /> + + <_ClickOnceTransitiveContentItemsTemp Include="@(_TransitiveItemsToCopyToOutputDirectory->WithoutMetadataValue('CopyToPublishDirectory', 'Never')->'%(TargetPath)')" Condition="'$(PublishProtocol)' == 'ClickOnce'"> + %(Identity) + + <_ClickOnceTransitiveContentItems Include="@(_ClickOnceTransitiveContentItemsTemp->'%(SavedIdentity)')" Condition="'%(Identity)'=='@(PublishFile)' Or '%(Extension)'=='.exe' Or '%(Extension)'=='.dll'" /> + + <_ClickOnceContentItems Include="@(ContentWithTargetPath->WithoutMetadataValue('CopyToPublishDirectory', 'Never'))" /> + <_ClickOnceContentItems Include="@(_ClickOnceTransitiveContentItems)" /> + + + <_ClickOnceNoneItemsTemp Include="@(_NoneWithTargetPath->WithoutMetadataValue('CopyToPublishDirectory', 'Never')->'%(TargetPath)')" Condition="'$(PublishProtocol)'=='Clickonce' And ('%(_NoneWithTargetPath.CopyToOutputDirectory)'=='Always' or '%(_NoneWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest')"> + %(Identity) + + <_ClickOnceNoneItems Include="@(_ClickOnceNoneItemsTemp->'%(SavedIdentity)')" Condition="'%(Identity)'=='@(PublishFile)' Or '%(Extension)'=='.exe' Or '%(Extension)'=='.dll'" /> + <_ClickOnceFiles Include="@(_ClickOnceContentItems);@(_DeploymentManifestIconFile);@(AppConfigWithTargetPath);@(NetCoreRuntimeJsonFilesForClickOnce);@(_ClickOnceRuntimeCopyLocalItems);@(_ClickOnceNoneItems)" /> + + <_ClickOnceNoneItemsTemp Remove="@(_ClickOnceNoneItemsTemp)" /> + <_ClickOnceNoneItems Remove="@(_ClickOnceNoneItems)" /> + <_ClickOnceTransitiveContentItemsTemp Remove="@(_ClickOnceTransitiveContentItemsTemp)" /> + <_ClickOnceTransitiveContentItems Remove="@(_ClickOnceTransitiveContentItems)" /> + <_ClickOnceContentItems Remove="@(_ClickOnceContentItems)" /> + <_ClickOnceRuntimeCopyLocalItems Remove="@(_ClickOnceRuntimeCopyLocalItems)" /> + + + + <_ClickOnceFiles Include="$(PublishedSingleFilePath);@(_DeploymentManifestIconFile)" /> + <_ClickOnceFiles Include="@(_FilesExcludedFromBundle)" /> + + <_FileAssociationIcons Include="%(FileAssociation.DefaultIcon)" /> + <_ClickOnceFiles Include="@(ContentWithTargetPath)" Condition="'%(Identity)'=='@(_FileAssociationIcons)'" /> + + + + + + <_ManifestManagedReferences Remove="@(_ReadyToRunCompileList)" /> + <_ClickOnceFiles Remove="@(_ReadyToRunCompileList)" /> + <_ClickOnceFiles Include="@(_ReadyToRunFilesToPublish)" /> + <_ClickOnceTargetFile Include="@(_ReadyToRunFilesToPublish)" Condition="'%(Filename)%(Extension)' == '$(TargetFileName)'" /> + + + + + + + + + + + + + + + + + + + <_DeploymentManifestDependencies Include="@(_DeploymentManifestDependenciesUnfiltered)" Condition="!('%(_DeploymentManifestDependenciesUnfiltered.CopyLocal)' == 'false' And '%(_DeploymentManifestDependenciesUnfiltered.DependencyType)' != 'Install')" /> + + + <_DeploymentManifestType>ClickOnce + + + + <_DeploymentPlatformTarget Condition="'$(_DeploymentLauncherBased)' != 'true'">$(PlatformTarget) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + false + + + + + CopyFilesToOutputDirectory + + + + + + + false + false + + + + + false + false + false + + + true + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + false + false + + + + + + + + + + + + + + + + + <_TargetsThatPrepareProjectReferences>_SplitProjectReferencesByFileExistence + + true + <_TargetsThatPrepareProjectReferences Condition=" '$(MSBuildCopyContentTransitively)' == 'true' "> + AssignProjectConfiguration; + _SplitProjectReferencesByFileExistence + + + AssignTargetPaths; + $(_TargetsThatPrepareProjectReferences); + _GetProjectReferenceTargetFrameworkProperties; + _PopulateCommonStateForGetCopyToOutputDirectoryItems + + + <_RecursiveTargetForContentCopying>GetCopyToOutputDirectoryItems + + <_RecursiveTargetForContentCopying Condition=" '$(MSBuildCopyContentTransitively)' == 'false' ">_GetCopyToOutputDirectoryItemsFromThisProject + + + + + <_GCTODIKeepDuplicates>false + <_GCTODIKeepMetadata>CopyToOutputDirectory;TargetPath + + + + + + + + + + <_CopyToOutputDirectoryTransitiveItems KeepDuplicates=" '$(_GCTODIKeepDuplicates)' != 'false' " KeepMetadata="$(_GCTODIKeepMetadata)" Include="@(_AllChildProjectItemsWithTargetPath->'%(FullPath)')" Condition="'%(_AllChildProjectItemsWithTargetPath.CopyToOutputDirectory)'=='Always'" /> + <_CopyToOutputDirectoryTransitiveItems KeepDuplicates=" '$(_GCTODIKeepDuplicates)' != 'false' " KeepMetadata="$(_GCTODIKeepMetadata)" Include="@(_AllChildProjectItemsWithTargetPath->'%(FullPath)')" Condition="'%(_AllChildProjectItemsWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest'" /> + + + + <_AllChildProjectItemsWithTargetPath Remove="@(_AllChildProjectItemsWithTargetPath)" /> + + + + <_CopyToOutputDirectoryTransitiveItems KeepMetadata="$(_GCTODIKeepMetadata)" Include="@(ContentWithTargetPath->'%(FullPath)')" Condition="'%(ContentWithTargetPath.CopyToOutputDirectory)'=='Always' AND '%(ContentWithTargetPath.MSBuildSourceProjectFile)'!=''" /> + <_CopyToOutputDirectoryTransitiveItems KeepMetadata="$(_GCTODIKeepMetadata)" Include="@(ContentWithTargetPath->'%(FullPath)')" Condition="'%(ContentWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest' AND '%(ContentWithTargetPath.MSBuildSourceProjectFile)'!=''" /> + + + <_CopyToOutputDirectoryTransitiveItems KeepMetadata="$(_GCTODIKeepMetadata)" Include="@(EmbeddedResource->'%(FullPath)')" Condition="'%(EmbeddedResource.CopyToOutputDirectory)'=='Always' AND '%(EmbeddedResource.MSBuildSourceProjectFile)'!=''" /> + <_CopyToOutputDirectoryTransitiveItems KeepMetadata="$(_GCTODIKeepMetadata)" Include="@(EmbeddedResource->'%(FullPath)')" Condition="'%(EmbeddedResource.CopyToOutputDirectory)'=='PreserveNewest' AND '%(EmbeddedResource.MSBuildSourceProjectFile)'!=''" /> + + + <_CompileItemsToCopy Include="@(Compile->'%(FullPath)')" Condition="('%(Compile.CopyToOutputDirectory)'=='Always' or '%(Compile.CopyToOutputDirectory)'=='PreserveNewest') AND '%(Compile.MSBuildSourceProjectFile)'!=''" /> + + + + + + <_CopyToOutputDirectoryTransitiveItems KeepMetadata="$(_GCTODIKeepMetadata)" Include="@(_CompileItemsToCopyWithTargetPath)" Condition="'%(_CompileItemsToCopyWithTargetPath.CopyToOutputDirectory)'=='Always'" /> + <_CopyToOutputDirectoryTransitiveItems KeepMetadata="$(_GCTODIKeepMetadata)" Include="@(_CompileItemsToCopyWithTargetPath)" Condition="'%(_CompileItemsToCopyWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest'" /> + + + <_CopyToOutputDirectoryTransitiveItems KeepMetadata="$(_GCTODIKeepMetadata)" Include="@(_NoneWithTargetPath->'%(FullPath)')" Condition="'%(_NoneWithTargetPath.CopyToOutputDirectory)'=='Always' AND '%(_NoneWithTargetPath.MSBuildSourceProjectFile)'!=''" /> + <_CopyToOutputDirectoryTransitiveItems KeepMetadata="$(_GCTODIKeepMetadata)" Include="@(_NoneWithTargetPath->'%(FullPath)')" Condition="'%(_NoneWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest' AND '%(_NoneWithTargetPath.MSBuildSourceProjectFile)'!=''" /> + + + + + <_ThisProjectItemsToCopyToOutputDirectory KeepMetadata="$(_GCTODIKeepMetadata)" Include="@(ContentWithTargetPath->'%(FullPath)')" Condition="'%(ContentWithTargetPath.CopyToOutputDirectory)'=='Always' AND '%(ContentWithTargetPath.MSBuildSourceProjectFile)'==''" /> + <_ThisProjectItemsToCopyToOutputDirectory KeepMetadata="$(_GCTODIKeepMetadata)" Include="@(ContentWithTargetPath->'%(FullPath)')" Condition="'%(ContentWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest' AND '%(ContentWithTargetPath.MSBuildSourceProjectFile)'==''" /> + + + <_ThisProjectItemsToCopyToOutputDirectory KeepMetadata="$(_GCTODIKeepMetadata)" Include="@(EmbeddedResource->'%(FullPath)')" Condition="'%(EmbeddedResource.CopyToOutputDirectory)'=='Always' AND '%(EmbeddedResource.MSBuildSourceProjectFile)'==''" /> + <_ThisProjectItemsToCopyToOutputDirectory KeepMetadata="$(_GCTODIKeepMetadata)" Include="@(EmbeddedResource->'%(FullPath)')" Condition="'%(EmbeddedResource.CopyToOutputDirectory)'=='PreserveNewest' AND '%(EmbeddedResource.MSBuildSourceProjectFile)'==''" /> + + + <_CompileItemsToCopy Include="@(Compile->'%(FullPath)')" Condition="('%(Compile.CopyToOutputDirectory)'=='Always' or '%(Compile.CopyToOutputDirectory)'=='PreserveNewest') AND '%(Compile.MSBuildSourceProjectFile)'==''" /> + + + + + + <_ThisProjectItemsToCopyToOutputDirectory KeepMetadata="$(_GCTODIKeepMetadata)" Include="@(_CompileItemsToCopyWithTargetPath)" Condition="'%(_CompileItemsToCopyWithTargetPath.CopyToOutputDirectory)'=='Always'" /> + <_ThisProjectItemsToCopyToOutputDirectory KeepMetadata="$(_GCTODIKeepMetadata)" Include="@(_CompileItemsToCopyWithTargetPath)" Condition="'%(_CompileItemsToCopyWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest'" /> + + + <_ThisProjectItemsToCopyToOutputDirectory KeepMetadata="$(_GCTODIKeepMetadata)" Include="@(_NoneWithTargetPath->'%(FullPath)')" Condition="'%(_NoneWithTargetPath.CopyToOutputDirectory)'=='Always' AND '%(_NoneWithTargetPath.MSBuildSourceProjectFile)'==''" /> + <_ThisProjectItemsToCopyToOutputDirectory KeepMetadata="$(_GCTODIKeepMetadata)" Include="@(_NoneWithTargetPath->'%(FullPath)')" Condition="'%(_NoneWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest' AND '%(_NoneWithTargetPath.MSBuildSourceProjectFile)'==''" /> + + + + + + + + + + + + + <_TransitiveItemsToCopyToOutputDirectory Remove="@(_ThisProjectItemsToCopyToOutputDirectory)" MatchOnMetadata="TargetPath" MatchOnMetadataOptions="PathLike" /> + + + <_TransitiveItemsToCopyToOutputDirectoryAlways KeepDuplicates=" '$(_GCTODIKeepDuplicates)' != 'false' " KeepMetadata="$(_GCTODIKeepMetadata)" Include="@(_TransitiveItemsToCopyToOutputDirectory->'%(FullPath)')" Condition="'%(_TransitiveItemsToCopyToOutputDirectory.CopyToOutputDirectory)'=='Always'" /> + <_TransitiveItemsToCopyToOutputDirectoryPreserveNewest KeepDuplicates=" '$(_GCTODIKeepDuplicates)' != 'false' " KeepMetadata="$(_GCTODIKeepMetadata)" Include="@(_TransitiveItemsToCopyToOutputDirectory->'%(FullPath)')" Condition="'%(_TransitiveItemsToCopyToOutputDirectory.CopyToOutputDirectory)'=='PreserveNewest'" /> + <_ThisProjectItemsToCopyToOutputDirectoryAlways KeepDuplicates=" '$(_GCTODIKeepDuplicates)' != 'false' " KeepMetadata="$(_GCTODIKeepMetadata)" Include="@(_ThisProjectItemsToCopyToOutputDirectory->'%(FullPath)')" Condition="'%(_ThisProjectItemsToCopyToOutputDirectory.CopyToOutputDirectory)'=='Always'" /> + <_ThisProjectItemsToCopyToOutputDirectoryPreserveNewest KeepDuplicates=" '$(_GCTODIKeepDuplicates)' != 'false' " KeepMetadata="$(_GCTODIKeepMetadata)" Include="@(_ThisProjectItemsToCopyToOutputDirectory->'%(FullPath)')" Condition="'%(_ThisProjectItemsToCopyToOutputDirectory.CopyToOutputDirectory)'=='PreserveNewest'" /> + + <_SourceItemsToCopyToOutputDirectoryAlways Include="@(_TransitiveItemsToCopyToOutputDirectoryAlways);@(_ThisProjectItemsToCopyToOutputDirectoryAlways)" /> + <_SourceItemsToCopyToOutputDirectory Include="@(_TransitiveItemsToCopyToOutputDirectoryPreserveNewest);@(_ThisProjectItemsToCopyToOutputDirectoryPreserveNewest)" /> + + + <_TransitiveItemsToCopyToOutputDirectoryAlways Remove="@(_TransitiveItemsToCopyToOutputDirectoryAlways)" /> + <_TransitiveItemsToCopyToOutputDirectoryPreserveNewest Remove="@(_TransitiveItemsToCopyToOutputDirectoryPreserveNewest)" /> + <_ThisProjectItemsToCopyToOutputDirectoryAlways Remove="@(_ThisProjectItemsToCopyToOutputDirectoryAlways)" /> + <_ThisProjectItemsToCopyToOutputDirectoryPreserveNewest Remove="@(_ThisProjectItemsToCopyToOutputDirectoryPreserveNewest)" /> + <_ThisProjectItemsToCopyToOutputDirectory Remove="@(_ThisProjectItemsToCopyToOutputDirectory)" /> + + + + + + + %(CopyToOutputDirectory) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_DocumentationFileProduced Condition="!Exists('@(DocFileItem)')">false + + + + + + + <_DebugSymbolsProduced Condition="!Exists('@(_DebugSymbolsIntermediatePath)')">false + + + + + + + + + + <_SGenDllCreated Condition="Exists('$(IntermediateOutputPath)$(_SGenDllName)')">true + + + + + + + + + + + + + $(PlatformTargetAsMSBuildArchitecture) + + + + $(TargetFrameworkAsMSBuildRuntime) + + CurrentRuntime + + + + + + + + + + + + <_CleanOrphanFileWrites Include="@(_CleanPriorFileWrites)" Exclude="@(_CleanCurrentFileWrites)" /> + + + + + + + + + + + + + + + + <_CleanRemainingFileWritesAfterIncrementalClean Include="@(_CleanPriorFileWrites);@(_CleanCurrentFileWrites)" Exclude="@(_CleanOrphanFilesDeleted)" /> + + + + + + + + + + + + + + + + + + + + + <_CleanPriorFileWrites Include="@(_CleanUnfilteredPriorFileWrites)" Exclude="@(_ResolveAssemblyReferenceResolvedFilesAbsolute)" /> + + + + + + + + + + + + + + + + <_CleanCurrentFileWritesWithNoReferences Include="@(_CleanCurrentFileWritesInOutput);@(_CleanCurrentFileWritesInIntermediate)" Exclude="@(_ResolveAssemblyReferenceResolvedFilesAbsolute)" /> + + + + + + + + + + + BeforeClean; + UnmanagedUnregistration; + CoreClean; + CleanReferencedProjects; + CleanPublishFolder; + AfterClean + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_CleanRemainingFileWritesAfterClean Include="@(_CleanPriorFileWrites)" Exclude="@(_CleanPriorFileWritesDeleted)" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CleanPublishFolder; + $(_RecursiveTargetForContentCopying); + _DeploymentGenerateTrustInfo; + $(DeploymentComputeClickOnceManifestInfoDependsOn) + + + + + + SetGenerateManifests; + Build; + PublishOnly + + + _DeploymentUnpublishable + + + + + + + + + + + + + true + + + + + + SetGenerateManifests; + PublishBuild; + BeforePublish; + GenerateManifests; + CopyFilesToOutputDirectory; + _CopyFilesToPublishFolder; + _DeploymentGenerateBootstrapper; + ResolveKeySource; + _DeploymentSignClickOnceDeployment; + AfterPublish + + + + + + + + + + + BuildOnlySettings; + PrepareForBuild; + ResolveReferences; + PrepareResources; + ResolveKeySource; + GenerateSerializationAssemblies; + CreateSatelliteAssemblies; + + + + + + + + + + + <_DeploymentApplicationFolderName>Application Files\$(AssemblyName)_$(_DeploymentApplicationVersionFragment) + <_DeploymentApplicationDir>$(ClickOncePublishDir)$(_DeploymentApplicationFolderName)\ + + + + false + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + $(TargetPath) + $(TargetFileName) + true + + + + + + true + $(TargetPath) + $(TargetFileName) + + + + + PrepareForBuild + true + + + + <_BuiltProjectOutputGroupOutputIntermediate Include="@(BuiltProjectOutputGroupKeyOutput)" /> + + + + <_BuiltProjectOutputGroupOutputIntermediate Include="$(AppConfig)" Condition="'$(AddAppConfigToBuildOutputs)'=='true'"> + $(TargetDir)$(TargetFileName).config + $(TargetFileName).config + + $(AppConfig) + + + + <_IsolatedComReference Include="@(COMReference)" Condition=" '%(COMReference.Isolated)' == 'true' " /> + <_IsolatedComReference Include="@(COMFileReference)" Condition=" '%(COMFileReference.Isolated)' == 'true' " /> + + + + <_BuiltProjectOutputGroupOutputIntermediate Include="$(OutDir)$(_DeploymentTargetApplicationManifestFileName)" Condition="('@(NativeReference)'!='' or '@(_IsolatedComReference)'!='') And Exists('$(OutDir)$(_DeploymentTargetApplicationManifestFileName)')"> + $(_DeploymentTargetApplicationManifestFileName) + + $(OutDir)$(_DeploymentTargetApplicationManifestFileName) + + + + + + + %(_BuiltProjectOutputGroupOutputIntermediate.FullPath) + + + + + + + + + + @(_DebugSymbolsOutputPath->'%(FullPath)') + @(_DebugSymbolsIntermediatePath->'%(Filename)%(Extension)') + + + + + + + @(WinMDExpFinalOutputPdbItem->'%(FullPath)') + @(WinMDExpOutputPdbItem->'%(Filename)%(Extension)') + + + + + + + + + + @(FinalDocFile->'%(FullPath)') + true + @(DocFileItem->'%(Filename)%(Extension)') + + + + + + + @(WinMDExpFinalOutputDocItem->'%(FullPath)') + @(WinMDOutputDocumentationFileItem->'%(Filename)%(Extension)') + + + + + + $(SatelliteDllsProjectOutputGroupDependsOn);PrepareForBuild;PrepareResourceNames + + + + + %(EmbeddedResource.Culture)\$(TargetName).resources.dll + %(EmbeddedResource.Culture) + + + + + + $(TargetDir)%(SatelliteDllsProjectOutputGroupOutputIntermediate.TargetPath) + + %(SatelliteDllsProjectOutputGroupOutputIntermediate.Identity) + + + + + + PrepareForBuild;AssignTargetPaths + + + + + + + + + + + + $(MSBuildProjectFullPath) + $(ProjectFileName) + + + + + + + + PrepareForBuild;AssignTargetPaths + + + + + + + + + + + + + + @(_OutputPathItem->'%(FullPath)$(_SGenDllName)') + $(_SGenDllName) + + + + + + + + + + + + + + + + + + + ResolveSDKReferences;ExpandSDKReferences + + + + + + + + + + + + + $(CommonOutputGroupsDependsOn); + BuildOnlySettings; + PrepareForBuild; + AssignTargetPaths; + ResolveReferences + + + + + + + + $(BuiltProjectOutputGroupDependenciesDependsOn); + $(CommonOutputGroupsDependsOn) + + + + + + + + + + + $(DebugSymbolsProjectOutputGroupDependenciesDependsOn); + $(CommonOutputGroupsDependsOn) + + + + + + + + + + + + $(SatelliteDllsProjectOutputGroupDependenciesDependsOn); + $(CommonOutputGroupsDependsOn) + + + + + + + + + + + + $(DocumentationProjectOutputGroupDependenciesDependsOn); + $(CommonOutputGroupsDependsOn) + + + + + + + + + + + + $(SGenFilesOutputGroupDependenciesDependsOn); + $(CommonOutputGroupsDependsOn) + + + + + + + + + + + + $(ReferenceCopyLocalPathsOutputGroupDependsOn); + $(CommonOutputGroupsDependsOn) + + + + + + %(ReferenceCopyLocalPaths.DestinationSubDirectory)%(ReferenceCopyLocalPaths.Filename)%(ReferenceCopyLocalPaths.Extension) + + + + + + + $(DesignerRuntimeImplementationProjectOutputGroupDependsOn); + $(CommonOutputGroupsDependsOn) + + + + + + + + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeAnalysis\Microsoft.CodeAnalysis.targets + + + + + + true + + + + + + + + + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TeamTest\Microsoft.TeamTest.targets + + + + false + + + + + + + + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\AppxPackage\Microsoft.AppXPackage.Targets + + true + + + + + + + + $([MSBuild]::IsRunningFromVisualStudio()) + $([MSBuild]::GetToolsDirectory32())\..\..\..\Common7\IDE\CommonExtensions\Microsoft\NuGet\NuGet.targets + $(MSBuildToolsPath)\NuGet.targets + + + + + + true + + NuGet.Build.Tasks.dll + + false + + true + true + + false + + WarnAndContinue + + $(BuildInParallel) + true + + <_RestoreSolutionFileUsed Condition=" '$(_RestoreSolutionFileUsed)' == '' AND '$(SolutionDir)' != '' AND $(MSBuildProjectFullPath.EndsWith('.metaproj')) == 'true' ">true + + $(MSBuildInteractive) + + true + + true + + <_CentralPackageVersionsEnabled Condition="'$(ManagePackageVersionsCentrally)' == 'true' AND '$(CentralPackageVersionsFileImported)' == 'true'">true + + + + + true + + low + + direct + + + + <_GenerateRestoreGraphProjectEntryInputProperties>ExcludeRestorePackageImports=true + + <_GenerateRestoreGraphProjectEntryInputProperties Condition=" '$(RestoreUseCustomAfterTargets)' == 'true' "> + $(_GenerateRestoreGraphProjectEntryInputProperties); + NuGetRestoreTargets=$(MSBuildThisFileFullPath); + RestoreUseCustomAfterTargets=$(RestoreUseCustomAfterTargets); + CustomAfterMicrosoftCommonCrossTargetingTargets=$(MSBuildThisFileFullPath); + CustomAfterMicrosoftCommonTargets=$(MSBuildThisFileFullPath); + + + <_GenerateRestoreGraphProjectEntryInputProperties Condition=" '$(_RestoreSolutionFileUsed)' == 'true' "> + $(_GenerateRestoreGraphProjectEntryInputProperties); + _RestoreSolutionFileUsed=true; + SolutionDir=$(SolutionDir); + SolutionName=$(SolutionName); + SolutionFileName=$(SolutionFileName); + SolutionPath=$(SolutionPath); + SolutionExt=$(SolutionExt); + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(ContinueOnError) + false + + + + + + + + + + + + + + $(ContinueOnError) + false + + + + + + + + + + + + + + $(ContinueOnError) + false + + + + + + + + + + + + + <_FrameworkReferenceForRestore Include="@(FrameworkReference)" Condition="'%(FrameworkReference.IsTransitiveFrameworkReference)' != 'true'" /> + + + + + + + $(ContinueOnError) + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + exclusionlist + + + + <_FilteredRestoreGraphProjectInputItemsTmp Include="@(RestoreGraphProjectInputItems)" Condition=" '%(RestoreGraphProjectInputItems.Extension)' == '.csproj' Or '%(RestoreGraphProjectInputItems.Extension)' == '.vbproj' Or '%(RestoreGraphProjectInputItems.Extension)' == '.fsproj' Or '%(RestoreGraphProjectInputItems.Extension)' == '.nuproj' Or '%(RestoreGraphProjectInputItems.Extension)' == '.proj' Or '%(RestoreGraphProjectInputItems.Extension)' == '.msbuildproj' Or '%(RestoreGraphProjectInputItems.Extension)' == '.vcxproj' " /> + + + + <_FilteredRestoreGraphProjectInputItemsTmp Include="@(RestoreGraphProjectInputItems)" Condition=" '%(RestoreGraphProjectInputItems.Extension)' != '.metaproj' AND '%(RestoreGraphProjectInputItems.Extension)' != '.shproj' AND '%(RestoreGraphProjectInputItems.Extension)' != '.vcxitems' AND '%(RestoreGraphProjectInputItems.Extension)' != '.vdproj' AND '%(RestoreGraphProjectInputItems.Extension)' != '' " /> + + + + <_FilteredRestoreGraphProjectInputItemsTmp Include="@(RestoreGraphProjectInputItems)" /> + + + + + + + + + + + + + + + + + + + + + + + + <_GenerateRestoreGraphProjectEntryInput Include="@(FilteredRestoreGraphProjectInputItems)" Condition=" '$(RestoreRecursive)' != 'true' " /> + <_GenerateRestoreGraphProjectEntryInput Include="@(_RestoreProjectPathItems)" Condition=" '$(RestoreRecursive)' == 'true' " /> + + + + + + + + + + + + + + + + + + + + <_RestoreGraphEntry Include="$([System.Guid]::NewGuid())" Condition=" '$(RestoreProjectStyle)' != 'Unknown' "> + RestoreSpec + $(MSBuildProjectFullPath) + + + + + + + netcoreapp1.0 + + + + + + + + + + + + + + + + + + + <_HasPackageReferenceItems Condition="'@(PackageReference)' != ''">true + + + <_HasPackageReferenceItems Condition="@(PackageReference->Count()) > 0">true + + + + + + + <_HasPackageReferenceItems /> + + + + + + true + + + + + + <_RestoreProjectFramework /> + <_TargetFrameworkToBeUsed Condition=" '$(_TargetFrameworkOverride)' == '' ">$(TargetFrameworks) + + + + + + + <_RestoreTargetFrameworksOutputFiltered Include="$(_RestoreProjectFramework.Split(';'))" /> + + + + + + <_RestoreTargetFrameworkItems Include="$(TargetFrameworks.Split(';'))" /> + + + <_RestoreTargetFrameworkItems Include="$(_TargetFrameworkOverride)" /> + + + + + + $(SolutionDir) + + + + + + + + + + + + + + + + + + + + + + + <_RestoreSettingsPerFramework Include="$([System.Guid]::NewGuid())"> + $(RestoreAdditionalProjectSources) + $(RestoreAdditionalProjectFallbackFolders) + $(RestoreAdditionalProjectFallbackFoldersExcludes) + + + + + + + + $(MSBuildProjectExtensionsPath) + + + + + + + <_RestoreProjectName>$(MSBuildProjectName) + <_RestoreProjectName Condition=" '$(PackageReferenceCompatibleProjectStyle)' == 'true' AND '$(AssemblyName)' != '' ">$(AssemblyName) + <_RestoreProjectName Condition=" '$(PackageReferenceCompatibleProjectStyle)' == 'true' AND '$(PackageId)' != '' ">$(PackageId) + + + + <_RestoreProjectVersion>1.0.0 + <_RestoreProjectVersion Condition=" '$(Version)' != '' ">$(Version) + <_RestoreProjectVersion Condition=" '$(PackageVersion)' != '' ">$(PackageVersion) + + + + <_RestoreCrossTargeting>true + + + + <_RestoreSkipContentFileWrite Condition=" '$(TargetFrameworks)' == '' AND '$(TargetFramework)' == '' ">true + + + + <_RestoreGraphEntry Include="$([System.Guid]::NewGuid())"> + ProjectSpec + $(_RestoreProjectVersion) + $(MSBuildProjectFullPath) + $(MSBuildProjectFullPath) + $(_RestoreProjectName) + $(_OutputSources) + $(_OutputFallbackFolders) + $(_OutputPackagesPath) + $(RestoreProjectStyle) + $(RestoreOutputAbsolutePath) + $(RuntimeIdentifiers);$(RuntimeIdentifier) + $(RuntimeSupports) + $(_RestoreCrossTargeting) + $(RestoreLegacyPackagesDirectory) + $(ValidateRuntimeIdentifierCompatibility) + $(_RestoreSkipContentFileWrite) + $(_OutputConfigFilePaths) + $(TreatWarningsAsErrors) + $(WarningsAsErrors) + $(WarningsNotAsErrors) + $(NoWarn) + $(RestorePackagesWithLockFile) + $(NuGetLockFilePath) + $(RestoreLockedMode) + <_CentralPackageVersionsEnabled>$(_CentralPackageVersionsEnabled) + $(CentralPackageFloatingVersionsEnabled) + $(CentralPackageVersionOverrideEnabled) + $(CentralPackageTransitivePinningEnabled) + $(NuGetAudit) + $(NuGetAuditLevel) + $(NuGetAuditMode) + $(SdkAnalysisLevel) + $(UsingMicrosoftNETSdk) + $(RestoreUseLegacyDependencyResolver) + + + + + <_RestoreGraphEntry Include="$([System.Guid]::NewGuid())"> + ProjectSpec + $(MSBuildProjectFullPath) + $(MSBuildProjectFullPath) + $(_RestoreProjectName) + $(_OutputSources) + $(RestoreOutputAbsolutePath) + $(_OutputFallbackFolders) + $(_OutputPackagesPath) + $(_CurrentProjectJsonPath) + $(RestoreProjectStyle) + $(_OutputConfigFilePaths) + + + + + <_RestoreGraphEntry Include="$([System.Guid]::NewGuid())"> + ProjectSpec + $(MSBuildProjectFullPath) + $(MSBuildProjectFullPath) + $(_RestoreProjectName) + $(RestoreProjectStyle) + $(MSBuildProjectDirectory)\packages.$(MSBuildProjectName).config + $(MSBuildProjectDirectory)\packages.config + $(RestorePackagesWithLockFile) + $(NuGetLockFilePath) + $(RestoreLockedMode) + $(_OutputSources) + $(SolutionDir) + $(_OutputRepositoryPath) + $(_OutputConfigFilePaths) + $(_OutputPackagesPath) + @(_RestoreTargetFrameworksOutputFiltered) + $(NuGetAudit) + $(NuGetAuditLevel) + + + + + <_RestoreGraphEntry Include="$([System.Guid]::NewGuid())"> + ProjectSpec + $(MSBuildProjectFullPath) + $(MSBuildProjectFullPath) + $(_RestoreProjectName) + $(RestoreProjectStyle) + @(_RestoreTargetFrameworksOutputFiltered) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_RestoreGraphEntry Include="$([System.Guid]::NewGuid())"> + TargetFrameworkInformation + $(MSBuildProjectFullPath) + $(PackageTargetFallback) + $(AssetTargetFallback) + $(TargetFramework) + $(TargetFrameworkIdentifier) + $(TargetFrameworkVersion) + $(TargetFrameworkMoniker) + $(TargetFrameworkProfile) + $(TargetPlatformMoniker) + $(TargetPlatformIdentifier) + $(TargetPlatformVersion) + $(TargetPlatformMinVersion) + $(CLRSupport) + $(RuntimeIdentifierGraphPath) + $(WindowsTargetPlatformMinVersion) + + + + + + + + + + + + + <_RestoreProjectPathItems Include="$(_RestoreGraphAbsoluteProjectPaths)" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_GenerateRestoreProjectPathWalkOutputs Include="$(MSBuildProjectFullPath)" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_RestorePackagesPathOverride>$(RestorePackagesPath) + + + + + + <_RestorePackagesPathOverride>$(RestoreRepositoryPath) + + + + + + <_RestoreSourcesOverride>$(RestoreSources) + + + + + + <_RestoreFallbackFoldersOverride>$(RestoreFallbackFolders) + + + + + + + + + + + + + <_TargetFrameworkOverride Condition=" '$(TargetFrameworks)' == '' ">$(TargetFramework) + + + + + + <_ValidProjectsForRestore Include="$(MSBuildProjectFullPath)" /> + + + + + + + + + + $(MSBuildExtensionsPath)\Microsoft\Microsoft.NET.Build.Extensions\Microsoft.NET.Build.Extensions.targets + + + + + <_TargetFrameworkVersionWithoutV>$(TargetFrameworkVersion.TrimStart('vV')) + $(MSBuildThisFileDirectory)\tools\net9.0\Microsoft.NET.Build.Extensions.Tasks.dll + $(MSBuildThisFileDirectory)\tools\net472\Microsoft.NET.Build.Extensions.Tasks.dll + + true + + + + + + + + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + $(MSBuildExtensionsPath)\Microsoft.TestPlatform.targets + + + + + + Microsoft.TestPlatform.Build.dll + $([System.IO.Path]::Combine($(MSBuildThisFileDirectory),"vstest.console.dll")) + False + False + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + false + true + + + + + + true + + + + + $(AfterMicrosoftNETSdkTargets);$(MSBuildThisFileDirectory)Microsoft.Maui.Resizetizer.After.targets + + + + + + <_Microsoft_Extensions_OptionsAnalyzer Include="@(Analyzer)" Condition="'%(Analyzer.NuGetPackageId)' == 'Microsoft.Extensions.Options'" /> + + + + + + + + + + + + + + + + + + + <_Microsoft_Extensions_Logging_AbstractionsAnalyzer Include="@(Analyzer)" Condition="'%(Analyzer.NuGetPackageId)' == 'Microsoft.Extensions.Logging.Abstractions'" /> + + + + + + + + + + + + + + + + + + true + + + + + true + + + + $(AfterMicrosoftNETSdkTargets);$(MSBuildThisFileDirectory)Microsoft.Maui.Core.After.targets + + + + + true + + + + + <_IsHotRestartDefined>$([System.Text.RegularExpressions.Regex]::IsMatch('$(DefineConstants.Trim())', '(^|;)HOTRESTART($|;)')) + HOTRESTART;$(DefineConstants) + True + + + + + + + + + + + + Platforms\ + $([MSBuild]::EnsureTrailingSlash('$(PlatformsProjectFolder)')) + + <_KeepLaunchProfiles>true + + false + $(PlatformsProjectFolder)Android\ + $([MSBuild]::EnsureTrailingSlash('$(AndroidProjectFolder)')) + + false + $(PlatformsProjectFolder)iOS\ + $([MSBuild]::EnsureTrailingSlash('$(iOSProjectFolder)')) + + false + $(PlatformsProjectFolder)MacCatalyst\ + $([MSBuild]::EnsureTrailingSlash('$(MacCatalystProjectFolder)')) + + false + $(PlatformsProjectFolder)Windows\ + $([MSBuild]::EnsureTrailingSlash('$(WindowsProjectFolder)')) + + false + $(PlatformsProjectFolder)Tizen\ + $([MSBuild]::EnsureTrailingSlash('$(TizenProjectFolder)')) + + + + + + + + + + $(AndroidProjectFolder)AndroidManifest.xml + $(AndroidProjectFolder)Resources + $(AndroidProjectFolder)Assets + + + $(iOSProjectFolder)Resources + $(iOSProjectFolder)Entitlements.plist + <_SingleProjectiOSExcludes>$(iOSProjectFolder)/**/.*/** + + + $(MacCatalystProjectFolder)Resources + $(MacCatalystProjectFolder)Entitlements.plist + <_SingleProjectMacCatalystExcludes>$(MacCatalystProjectFolder)/**/.*/** + + + $(WindowsProjectFolder)app.manifest + $(WindowsProjectFolder)Package.appxmanifest + False + False + <_SingleProjectWindowsExcludes>$(WindowsProjectFolder)/**/.*/** + + + $(TizenProjectFolder)tizen-manifest.xml + $(TizenProjectFolder)res + $(TizenProjectFolder)shared + + + + + + $(AfterMicrosoftNETSdkTargets);$(MSBuildThisFileDirectory)Microsoft.Maui.Controls.Build.Tasks.After.targets + + + + + + true + + + + <_DirectoryBuildTargetsFile Condition="'$(_DirectoryBuildTargetsFile)' == ''">Directory.Build.targets + <_DirectoryBuildTargetsBasePath Condition="'$(_DirectoryBuildTargetsBasePath)' == ''">$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), '$(_DirectoryBuildTargetsFile)')) + $([System.IO.Path]::Combine('$(_DirectoryBuildTargetsBasePath)', '$(_DirectoryBuildTargetsFile)')) + + + + + + + + + +// <autogenerated /> +using System%3b +using System.Reflection%3b +[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute("$(TargetFrameworkMoniker)", FrameworkDisplayName = "$(TargetFrameworkMonikerDisplayName)")] + + + + + true + + true + true + + $([System.Globalization.CultureInfo]::CurrentUICulture.Name) + + + + + <_ExplicitReference Include="$(FrameworkPathOverride)\mscorlib.dll" /> + + + + + + + + + + TargetFramework + TargetFrameworks + + + true + + + + + + + + + <_MainReferenceTargetForBuild Condition="'$(BuildProjectReferences)' == '' or '$(BuildProjectReferences)' == 'true'">.projectReferenceTargetsOrDefaultTargets + <_MainReferenceTargetForBuild Condition="'$(_MainReferenceTargetForBuild)' == ''">GetTargetPath + $(_MainReferenceTargetForBuild);GetNativeManifest;$(_RecursiveTargetForContentCopying);$(ProjectReferenceTargetsForBuild) + + <_MainReferenceTargetForPublish Condition="'$(NoBuild)' == 'true'">GetTargetPath + <_MainReferenceTargetForPublish Condition="'$(NoBuild)' != 'true'">$(_MainReferenceTargetForBuild) + GetTargetFrameworks;$(_MainReferenceTargetForPublish);GetNativeManifest;GetCopyToPublishDirectoryItems;$(ProjectReferenceTargetsForPublish) + + $(ProjectReferenceTargetsForBuild);$(ProjectReferenceTargetsForPublish) + $(ProjectReferenceTargetsForRebuild);$(ProjectReferenceTargetsForPublish) + GetCopyToPublishDirectoryItems;$(ProjectReferenceTargetsForGetCopyToPublishDirectoryItems) + + + .default;$(ProjectReferenceTargetsForBuild) + + + Clean;$(ProjectReferenceTargetsForClean) + $(ProjectReferenceTargetsForClean);$(ProjectReferenceTargetsForBuild);$(ProjectReferenceTargetsForRebuild) + + + + + + + + + + + + + + + + + + + + + + + + + + $(MSBuildThisFileDirectory)..\tools\ + net9.0 + net472 + $(MicrosoftNETBuildTasksDirectoryRoot)$(MicrosoftNETBuildTasksTFM)\ + $(MicrosoftNETBuildTasksDirectory)Microsoft.NET.Build.Tasks.dll + + Microsoft.NETCore.App;NETStandard.Library + + + + <_IsExecutable Condition="'$(OutputType)' == 'Exe' or '$(OutputType)'=='WinExe'">true + $(_IsExecutable) + + + + netcoreapp2.2 + + + false + DotnetTool + $(RuntimeIdentifiers);$(PackAsToolShimRuntimeIdentifiers) + + + Preview + + + + + + + + true + + + + + + + + $(MSBuildProjectExtensionsPath)/project.assets.json + $([MSBuild]::NormalizePath($(MSBuildProjectDirectory), $(ProjectAssetsFile))) + + $(IntermediateOutputPath)$(MSBuildProjectName).assets.cache + $([MSBuild]::NormalizePath($(MSBuildProjectDirectory), $(ProjectAssetsCacheFile))) + + false + + false + + true + $(IntermediateOutputPath)NuGet\ + true + $(TargetPlatformIdentifier),Version=v$([System.Version]::Parse('$(TargetPlatformMinVersion)').ToString(3)) + $(TargetFrameworkMoniker) + true + + false + + true + + + + <_NugetTargetMonikerAndRID Condition="'$(RuntimeIdentifier)' == ''">$(NuGetTargetMoniker) + <_NugetTargetMonikerAndRID Condition="'$(RuntimeIdentifier)' != ''">$(NuGetTargetMoniker)/$(RuntimeIdentifier) + + + + + + + + + $(ResolveAssemblyReferencesDependsOn); + ResolvePackageDependenciesForBuild; + _HandlePackageFileConflicts; + + + ResolvePackageDependenciesForBuild; + _HandlePackageFileConflicts; + $(PrepareResourcesDependsOn) + + + + + + $(RootNamespace) + + + $(AssemblyName) + + + $(MSBuildProjectDirectory) + + + $(TargetFileName) + + + $(MSBuildProjectFile) + + + + + + true + + + + + + ResolveLockFileReferences; + ResolveLockFileAnalyzers; + ResolveLockFileCopyLocalFiles; + ResolveRuntimePackAssets; + RunProduceContentAssets; + IncludeTransitiveProjectReferences + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_RoslynApiVersion>$([System.Version]::Parse(%(_CodeAnalysisIdentity.Version)).Major).$([System.Version]::Parse(%(_CodeAnalysisIdentity.Version)).Minor) + roslyn$(_RoslynApiVersion) + + + + + + false + + + true + + + true + + + + true + + + <_PackAsToolShimRuntimeIdentifiers Condition="@(_PackAsToolShimRuntimeIdentifiers) ==''" Include="$(PackAsToolShimRuntimeIdentifiers)" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_NativeRestoredAppHostNETCore Include="@(NativeCopyLocalItems)" Condition="'%(NativeCopyLocalItems.FileName)%(NativeCopyLocalItems.Extension)' == '$(_DotNetAppHostExecutableName)'" /> + + + <_ApphostsForShimRuntimeIdentifiers Include="@(_ApphostsForShimRuntimeIdentifiersResolvePackageAssets)" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_ResolvedCopyLocalBuildAssets Include="@(RuntimeCopyLocalItems)" Condition="'%(RuntimeCopyLocalItems.CopyLocal)' == 'true'" /> + <_ResolvedCopyLocalBuildAssets Include="@(ResourceCopyLocalItems)" Condition="'%(ResourceCopyLocalItems.CopyLocal)' == 'true'" /> + + <_ResolvedCopyLocalBuildAssets Include="@(NativeCopyLocalItems)" Exclude="@(_NativeRestoredAppHostNETCore)" Condition="'%(NativeCopyLocalItems.CopyLocal)' == 'true'" /> + <_ResolvedCopyLocalBuildAssets Include="@(RuntimeTargetsCopyLocalItems)" Condition="'%(RuntimeTargetsCopyLocalItems.CopyLocal)' == 'true'" /> + + + + + + + + + + + + + + true + true + true + true + + + + + $(DefaultItemExcludes);$(BaseOutputPath)/** + + $(DefaultItemExcludes);$(BaseIntermediateOutputPath)/** + + $(DefaultItemExcludes);**/*.user + $(DefaultItemExcludes);**/*.*proj + $(DefaultItemExcludes);**/*.sln + $(DefaultItemExcludes);**/*.vssscc + $(DefaultItemExcludes);**/.DS_Store + + $(DefaultExcludesInProjectFolder);$(DefaultItemExcludesInProjectFolder);**/.*/** + + + + + 1.6.1 + + 2.0.3 + + + + + + true + false + <_TargetLatestRuntimePatchIsDefault>true + + + true + + + + + all + true + + + all + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(_TargetFrameworkVersionWithoutV) + + + + + + + + https://aka.ms/sdkimplicitrefs + + + + + + + + + + + <_PackageReferenceToAdd Remove="@(_PackageReferenceToAdd)" /> + + + + false + + + + + + https://aka.ms/sdkimplicititems + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_WindowsDesktopTransitiveFrameworkReference Include="@(TransitiveFrameworkReference)" Condition="'%(Identity)' == 'Microsoft.WindowsDesktop.App' Or '%(Identity)' == 'Microsoft.WindowsDesktop.App.WPF' Or '%(Identity)' == 'Microsoft.WindowsDesktop.App.WindowsForms'" /> + + + + + + + + + + + + + $([MSBuild]::EnsureTrailingSlash(%(LinkBase))) + + %(LinkBase)%(RecursiveDir)%(Filename)%(Extension) + + + $([MSBuild]::EnsureTrailingSlash(%(LinkBase))) + %(LinkBase)%(RecursiveDir)%(Filename)%(Extension) + + + $([MSBuild]::EnsureTrailingSlash(%(LinkBase))) + %(LinkBase)%(RecursiveDir)%(Filename)%(Extension) + + + $([MSBuild]::EnsureTrailingSlash(%(LinkBase))) + %(LinkBase)%(RecursiveDir)%(Filename)%(Extension) + + + $([MSBuild]::EnsureTrailingSlash(%(LinkBase))) + %(LinkBase)%(RecursiveDir)%(Filename)%(Extension) + + + $([MSBuild]::EnsureTrailingSlash(%(LinkBase))) + %(LinkBase)%(RecursiveDir)%(Filename)%(Extension) + + + $([MSBuild]::EnsureTrailingSlash(%(LinkBase))) + %(LinkBase)%(RecursiveDir)%(Filename)%(Extension) + + + + + + + + + + + + + + + + + true + + + + + + + + + + + $(ResolveAssemblyReferencesDependsOn); + ResolveTargetingPackAssets; + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + true + + + <_NuGetRestoreSupported Condition="('$(Language)' == 'C++' and '$(_EnablePackageReferencesInVCProjects)' != 'true')">false + + + <_PackAsToolShimRuntimeIdentifiers Condition="@(_PackAsToolShimRuntimeIdentifiers) ==''" Include="$(PackAsToolShimRuntimeIdentifiers)" /> + + + + + + + + + + + + + + + $(RuntimeIdentifier) + $(DefaultAppHostRuntimeIdentifier) + + + + + + + + + + + true + true + false + + + + [%(_PackageToDownload.Version)] + + + + + + + + <_ImplicitPackageReference Remove="@(PackageReference)" /> + + + + + + + + + + + + + + + + + + %(ResolvedTargetingPack.PackageDirectory) + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_ApphostsForShimRuntimeIdentifiers Include="%(_ApphostsForShimRuntimeIdentifiersGetPackageDirectory.PackageDirectory)\%(_ApphostsForShimRuntimeIdentifiersGetPackageDirectory.PathInPackage)"> + %(_ApphostsForShimRuntimeIdentifiersGetPackageDirectory.RuntimeIdentifier) + + + + + %(ResolvedAppHostPack.PackageDirectory)\%(ResolvedAppHostPack.PathInPackage) + + + + @(ResolvedAppHostPack->'%(Path)') + + + + %(ResolvedSingleFileHostPack.PackageDirectory)\%(ResolvedSingleFileHostPack.PathInPackage) + + + + @(ResolvedSingleFileHostPack->'%(Path)') + + + + %(ResolvedComHostPack.PackageDirectory)\%(ResolvedComHostPack.PathInPackage) + + + + @(ResolvedComHostPack->'%(Path)') + + + + %(ResolvedIjwHostPack.PackageDirectory)\%(ResolvedIjwHostPack.PathInPackage) + + + + @(ResolvedIjwHostPack->'%(Path)') + + + + + + + + + + + + + + + true + false + + + + + + + + + + + + $([MSBuild]::Unescape($(PackageConflictPreferredPackages))) + + + + + + + + + + + + + + + + + + + + + + + + + + <_ExistingReferenceAssembliesPackageReference Include="@(PackageReference)" Condition="'%(PackageReference.Identity)' == 'Microsoft.NETFramework.ReferenceAssemblies'" /> + + + + + + + + + + + + + + + + + + <_Parameter1>$(UserSecretsId.Trim()) + + + + + + + + + + $(_IsNETCoreOrNETStandard) + + + true + false + true + $(MSBuildProjectDirectory)/runtimeconfig.template.json + true + true + <_GenerateRuntimeConfigurationPropertyInputsCache Condition="'$(_GenerateRuntimeConfigurationPropertyInputsCache)' == ''">$(IntermediateOutputPath)$(MSBuildProjectName).genruntimeconfig.cache + <_GenerateRuntimeConfigurationPropertyInputsCache>$([MSBuild]::NormalizePath($(MSBuildProjectDirectory), $(_GenerateRuntimeConfigurationPropertyInputsCache))) + <_GeneratePublishDependencyFilePropertyInputsCache Condition="'$(_GeneratePublishDependencyFilePropertyInputsCache)' == ''">$(IntermediateOutputPath)$(MSBuildProjectName).genpublishdeps.cache + <_GeneratePublishDependencyFilePropertyInputsCache>$([MSBuild]::NormalizePath($(MSBuildProjectDirectory), $(_GeneratePublishDependencyFilePropertyInputsCache))) + <_GenerateSingleFileBundlePropertyInputsCache Condition="'$(_GenerateSingleFileBundlePropertyInputsCache)' == ''">$(IntermediateOutputPath)$(MSBuildProjectName).genbundle.cache + <_GenerateSingleFileBundlePropertyInputsCache>$([MSBuild]::NormalizePath($(MSBuildProjectDirectory), $(_GenerateSingleFileBundlePropertyInputsCache))) + + + + <_UseRidGraphWasSpecified Condition="'$(UseRidGraph)' != ''">true + + + false + true + + + $(BundledRuntimeIdentifierGraphFile) + + $([System.IO.Path]::GetDirectoryName($(BundledRuntimeIdentifierGraphFile)))/PortableRuntimeIdentifierGraph.json + + + + + + + + + + + true + + + false + + + $(AssemblyName).deps.json + $(TargetDir)$(ProjectDepsFileName) + $(AssemblyName).runtimeconfig.json + $(TargetDir)$(ProjectRuntimeConfigFileName) + $(TargetDir)$(AssemblyName).runtimeconfig.dev.json + true + + + + + + true + true + + + + CurrentArchitecture + CurrentRuntime + + + <_NativeLibraryPrefix Condition="'$(_NativeLibraryPrefix)' == '' and !$(RuntimeIdentifier.StartsWith('win'))">lib + <_NativeLibraryExtension Condition="'$(_NativeLibraryExtension)' == '' and $(RuntimeIdentifier.StartsWith('win'))">.dll + <_NativeLibraryExtension Condition="'$(_NativeLibraryExtension)' == '' and $(RuntimeIdentifier.StartsWith('osx'))">.dylib + <_NativeLibraryExtension Condition="'$(_NativeLibraryExtension)' == ''">.so + <_NativeExecutableExtension Condition="'$(_NativeExecutableExtension)' == '' and ($(RuntimeIdentifier.StartsWith('win')) or $(DefaultAppHostRuntimeIdentifier.StartsWith('win')))">.exe + <_ComHostLibraryExtension Condition="'$(_ComHostLibraryExtension)' == '' and ($(RuntimeIdentifier.StartsWith('win')) or $(DefaultAppHostRuntimeIdentifier.StartsWith('win')))">.dll + <_IjwHostLibraryExtension Condition="'$(_IjwHostLibraryExtension)' == '' and ($(RuntimeIdentifier.StartsWith('win')) or $(DefaultAppHostRuntimeIdentifier.StartsWith('win')))">.dll + <_DotNetHostExecutableName>dotnet$(_NativeExecutableExtension) + <_DotNetAppHostExecutableNameWithoutExtension>apphost + <_DotNetAppHostExecutableName>$(_DotNetAppHostExecutableNameWithoutExtension)$(_NativeExecutableExtension) + <_DotNetSingleFileHostExecutableNameWithoutExtension>singlefilehost + <_DotNetComHostLibraryNameWithoutExtension>comhost + <_DotNetComHostLibraryName>$(_DotNetComHostLibraryNameWithoutExtension)$(_ComHostLibraryExtension) + <_DotNetIjwHostLibraryNameWithoutExtension>Ijwhost + <_DotNetIjwHostLibraryName>$(_DotNetIjwHostLibraryNameWithoutExtension)$(_IjwHostLibraryExtension) + <_DotNetHostPolicyLibraryName>$(_NativeLibraryPrefix)hostpolicy$(_NativeLibraryExtension) + <_DotNetHostFxrLibraryName>$(_NativeLibraryPrefix)hostfxr$(_NativeLibraryExtension) + + + + + + <_ExcludeFromPublishPackageReference Include="@(PackageReference)" Condition="('%(PackageReference.Publish)' == 'false')" /> + + + + + + Microsoft.NETCore.App + + + + + <_DefaultUserProfileRuntimeStorePath>$(HOME) + <_DefaultUserProfileRuntimeStorePath Condition="$([MSBuild]::IsOSPlatform(`Windows`))">$(USERPROFILE) + <_DefaultUserProfileRuntimeStorePath>$([System.IO.Path]::Combine($(_DefaultUserProfileRuntimeStorePath), '.dotnet', 'store')) + $(_DefaultUserProfileRuntimeStorePath) + + + true + + + true + + + true + + + + false + true + + + + true + + + true + + + $(AvailablePlatforms),ARM32 + + + $(AvailablePlatforms),ARM64 + + + $(AvailablePlatforms),ARM64 + + + + false + + + + true + + + + + <_ProjectTypeRequiresBinaryFormatter Condition="'$(UseWindowsForms)' == 'true' AND $([MSBuild]::VersionLessThanOrEquals($(TargetFrameworkVersion), '8.0'))">true + <_ProjectTypeRequiresBinaryFormatter Condition="'$(UseWPF)' == 'true' AND $([MSBuild]::VersionLessThanOrEquals($(TargetFrameworkVersion), '8.0'))">true + + <_BinaryFormatterObsoleteAsError>true + + true + false + + + + _CheckForBuildWithNoBuild; + $(CoreBuildDependsOn); + GenerateBuildDependencyFile; + GenerateBuildRuntimeConfigurationFiles + + + + + _SdkBeforeClean; + $(CoreCleanDependsOn) + + + + + _SdkBeforeRebuild; + $(RebuildDependsOn) + + + + + true + + + $(NuGetPackageRoot)\microsoft.net.sdk.compilers.toolset\$(NETCoreSdkVersion) + <_NeedToDownloadMicrosoftNetSdkCompilersToolsetPackage>true + <_MicrosoftNetSdkCompilersToolsetPackageRootEmpty Condition="'$(NuGetPackageRoot)' == ''">true + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1.0.0 + + + + + + + + + + + + + <_ValidRuntimeIdentifierPlatformsForAssets Include="@(_KnownRuntimeIdentiferPlatforms)" /> + + <_ValidRuntimeIdentifierPlatformsForAssets Include="@(_KnownRuntimeIdentifierPlatformsForTargetFramework)" Exclude="@(_ExcludedKnownRuntimeIdentiferPlatforms)" /> + + + + + + + + + + + + <_GenerateRuntimeConfigurationPropertyInputsCacheToHash Include="@(AdditionalProbingPath->'%(Identity)')" /> + <_GenerateRuntimeConfigurationPropertyInputsCacheToHash Include="$(EnableDynamicLoading)" /> + <_GenerateRuntimeConfigurationPropertyInputsCacheToHash Include="$(RollForward)" /> + <_GenerateRuntimeConfigurationPropertyInputsCacheToHash Include="@(RuntimeHostConfigurationOption->'%(Identity)%(Value)')" /> + <_GenerateRuntimeConfigurationPropertyInputsCacheToHash Include="$(RuntimeIdentifier)" /> + <_GenerateRuntimeConfigurationPropertyInputsCacheToHash Include="$(SelfContained)" /> + <_GenerateRuntimeConfigurationPropertyInputsCacheToHash Include="$(TargetFramework)" /> + <_GenerateRuntimeConfigurationPropertyInputsCacheToHash Include="$(UserRuntimeConfig)" /> + <_GenerateRuntimeConfigurationPropertyInputsCacheToHash Include="$(_WriteIncludedFrameworks)" /> + + + + + + + + + + + + + <_IsRollForwardSupported Condition="'$(_TargetFrameworkVersionWithoutV)' >= '3.0'">true + LatestMinor + + + + + <_WriteIncludedFrameworks Condition="'$(SelfContained)' == 'true' and '$(_TargetFrameworkVersionWithoutV)' >= '3.1'">true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_CleaningWithoutRebuilding>true + false + + + + + <_CleaningWithoutRebuilding>false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(CompileDependsOn); + _CreateAppHost; + _CreateComHost; + _GetIjwHostPaths; + + + + + + <_UseWindowsGraphicalUserInterface Condition="($(RuntimeIdentifier.StartsWith('win')) or $(DefaultAppHostRuntimeIdentifier.StartsWith('win'))) and '$(OutputType)'=='WinExe'">true + <_EnableMacOSCodeSign Condition="'$(_EnableMacOSCodeSign)' == '' and $([MSBuild]::IsOSPlatform(`OSX`)) and Exists('/usr/bin/codesign') and ($(RuntimeIdentifier.StartsWith('osx')) or $(AppHostRuntimeIdentifier.StartsWith('osx')))">true + <_UseSingleFileHostForPublish Condition="'$(PublishSingleFile)' == 'true' and '$(SelfContained)' == 'true' and '$(SingleFileHostSourcePath)' != '' and '$(TargetFrameworkIdentifier)' == '.NETCoreApp' and $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), 5.0))">true + <_DisableCetCompat Condition="'$(CetCompat)' == 'false'">true + + AppRelative + <_UpdateAppHostForPublish Condition="'$(_UseSingleFileHostForPublish)' != 'true' and ('$(AppHostRelativeDotNet)' != '' or '$(AppHostDotNetSearch)' != '')">true + + + + + + + + + + + + + @(_NativeRestoredAppHostNETCore) + + + $([System.IO.Path]::GetFullPath('$(IntermediateOutputPath)apphost$(_NativeExecutableExtension)')) + $([System.IO.Path]::GetFullPath('$(IntermediateOutputPath)apphost_publish$(_NativeExecutableExtension)')) + $([System.IO.Path]::GetFullPath('$(IntermediateOutputPath)singlefilehost$(_NativeExecutableExtension)')) + + + + + + + + + + + + + + + + + + + + + + + + + $(AssemblyName).comhost$(_ComHostLibraryExtension) + $([System.IO.Path]::GetFullPath('$(IntermediateOutputPath)$(ComHostFileName)')) + + + + + + + + + + + + <_CopyAndRenameDotnetHost Condition="'$(_CopyAndRenameDotnetHost)' == ''">true + + + + $(AssemblyName)$(_NativeExecutableExtension) + PreserveNewest + PreserveNewest + + + + + + PreserveNewest + Never + + + + + $(AssemblyName)$(_NativeExecutableExtension) + PreserveNewest + + Always + + + + + $(AssemblyName).$(_DotNetComHostLibraryName) + PreserveNewest + PreserveNewest + + + %(FileName)%(Extension) + PreserveNewest + PreserveNewest + + + + + $(_DotNetIjwHostLibraryName) + PreserveNewest + PreserveNewest + + + + + + + <_FrameworkReferenceAssemblies Include="@(ReferencePath)" Condition="(%(ReferencePath.FrameworkFile) == 'true' or %(ReferencePath.ResolvedFrom) == 'ImplicitlyExpandDesignTimeFacades') and ('%(ReferencePath.NuGetSourceType)' == '' or '%(ReferencePath.NuGetIsFrameworkReference)' == 'true')" /> + + <_ReferenceOnlyAssemblies Include="@(ReferencePath)" Exclude="@(_FrameworkReferenceAssemblies)" Condition="%(ReferencePath.CopyLocal) != 'true' and %(ReferencePath.NuGetSourceType) == ''" /> + <_ReferenceAssemblies Include="@(_FrameworkReferenceAssemblies)" /> + <_ReferenceAssemblies Include="@(_ReferenceOnlyAssemblies)" /> + + + + + + + + true + + + true + + + + + + + + + $(StartWorkingDirectory) + + + + + $(StartProgram) + $(StartArguments) + + + + + + dotnet + <_NetCoreRunArguments>exec "$(TargetPath)" + $(_NetCoreRunArguments) $(StartArguments) + $(_NetCoreRunArguments) + + + $(TargetDir)$(AssemblyName)$(_NativeExecutableExtension) + $(StartArguments) + + + + + $(TargetPath) + $(StartArguments) + + + mono + "$(TargetPath)" $(StartArguments) + + + + + + $([System.IO.Path]::GetFullPath($([System.IO.Path]::Combine('$(MSBuildProjectDirectory)', '$(RunWorkingDirectory)')))) + + + + + + + + + + + + + + + $(CreateSatelliteAssembliesDependsOn); + CoreGenerateSatelliteAssemblies + + + + + + + <_AssemblyInfoFile>$(IntermediateOutputPath)%(_SatelliteAssemblyResourceInputs.Culture)\$(TargetName).resources.cs + <_OutputAssembly>$(IntermediateOutputPath)%(_SatelliteAssemblyResourceInputs.Culture)\$(TargetName).resources.dll + + + + <_Parameter1>%(_SatelliteAssemblyResourceInputs.Culture) + + + + + + + true + + + <_SatelliteAssemblyReferences Remove="@(_SatelliteAssemblyReferences)" /> + <_SatelliteAssemblyReferences Include="@(ReferencePath)" Condition="'%(Filename)' == 'mscorlib' or '%(Filename)' == 'netstandard' or '%(Filename)' == 'System.Runtime' " /> + + + + + + + + + + + + + + + + + + + + + + + + + $(TargetFrameworkIdentifier) + $(_TargetFrameworkVersionWithoutV) + + + + + + + + + + + + + + + + + + + + false + + + + false + + + + false + + + + <_UseAttributeForTargetFrameworkInfoPropertyNames Condition="$([MSBuild]::VersionGreaterThanOrEquals($(MSBuildVersion), '17.0'))">true + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_SourceLinkSdkSubDir>build + <_SourceLinkSdkSubDir Condition="'$(IsCrossTargetingBuild)' == 'true'">buildMultiTargeting + + true + + + + + + + + local + + + + + + + + + + + + git + + + + + + + + + + + + + + + + + + + + + + + + + + $(RepositoryUrl) + $(ScmRepositoryUrl) + + + + %(SourceRoot.ScmRepositoryUrl) + + + + + + + + <_SourceLinkFilePath>$(IntermediateOutputPath)$(MSBuildProjectName).sourcelink.json + + + + + + + + + <_GenerateSourceLinkFileBeforeTargets>Link + <_GenerateSourceLinkFileDependsOnTargets>ComputeLinkSwitches + + + <_GenerateSourceLinkFileBeforeTargets>CoreCompile + <_GenerateSourceLinkFileDependsOnTargets /> + + + + + + + + + + + + + + + + %(Link.AdditionalOptions) /sourcelink:"$(SourceLink)" + + + + + + + + + <_SourceLinkGitHubAssemblyFile Condition="'$(MSBuildRuntimeType)' != 'Core'">$(MSBuildThisFileDirectory)..\tools\net472\Microsoft.SourceLink.GitHub.dll + <_SourceLinkGitHubAssemblyFile Condition="'$(MSBuildRuntimeType)' == 'Core'">$(MSBuildThisFileDirectory)..\tools\core\Microsoft.SourceLink.GitHub.dll + + + + + $(SourceLinkUrlInitializerTargets);_InitializeGitHubSourceLinkUrl + $(SourceControlManagerUrlTranslationTargets);TranslateGitHubUrlsInSourceControlInformation + + + + + + + + + + + + + + + <_TranslatedSourceRoot Remove="@(_TranslatedSourceRoot)" /> + + + + + + + + + + + + + + + <_SourceLinkGitLabAssemblyFile Condition="'$(MSBuildRuntimeType)' != 'Core'">$(MSBuildThisFileDirectory)..\tools\net472\Microsoft.SourceLink.GitLab.dll + <_SourceLinkGitLabAssemblyFile Condition="'$(MSBuildRuntimeType)' == 'Core'">$(MSBuildThisFileDirectory)..\tools\core\Microsoft.SourceLink.GitLab.dll + + + + + $(SourceLinkUrlInitializerTargets);_InitializeGitLabSourceLinkUrl + $(SourceControlManagerUrlTranslationTargets);TranslateGitLabUrlsInSourceControlInformation + + + + + + + + + + + + + + + <_TranslatedSourceRoot Remove="@(_TranslatedSourceRoot)" /> + + + + + + + + + + + + + + + <_SourceLinkAzureReposGitAssemblyFile Condition="'$(MSBuildRuntimeType)' != 'Core'">$(MSBuildThisFileDirectory)..\tools\net472\Microsoft.SourceLink.AzureRepos.Git.dll + <_SourceLinkAzureReposGitAssemblyFile Condition="'$(MSBuildRuntimeType)' == 'Core'">$(MSBuildThisFileDirectory)..\tools\core\Microsoft.SourceLink.AzureRepos.Git.dll + + + + + $(SourceLinkUrlInitializerTargets);_InitializeAzureReposGitSourceLinkUrl + $(SourceControlManagerUrlTranslationTargets);TranslateAzureReposGitUrlsInSourceControlInformation + + + + + + + + + + + + + + + <_TranslatedSourceRoot Remove="@(_TranslatedSourceRoot)" /> + + + + + + + + + + + + + + + <_SourceLinkBitbucketAssemblyFile Condition="'$(MSBuildRuntimeType)' != 'Core'">$(MSBuildThisFileDirectory)..\tools\net472\Microsoft.SourceLink.Bitbucket.Git.dll + <_SourceLinkBitbucketAssemblyFile Condition="'$(MSBuildRuntimeType)' == 'Core'">$(MSBuildThisFileDirectory)..\tools\core\Microsoft.SourceLink.Bitbucket.Git.dll + + + + + $(SourceLinkUrlInitializerTargets);_InitializeBitbucketGitSourceLinkUrl + $(SourceControlManagerUrlTranslationTargets);TranslateBitbucketGitUrlsInSourceControlInformation + + + + + + + + + + + + + + + <_TranslatedSourceRoot Remove="@(_TranslatedSourceRoot)" /> + + + + + + + + + + + + + + + + + + .NET Standard $(_TargetFrameworkVersionWithoutV) + .NET Core $(_TargetFrameworkVersionWithoutV) + .NET $(_TargetFrameworkVersionWithoutV) + <_TargetFrameworkDirectories /> + + + + true + + + + + + + $(CommonOutputGroupsDependsOn); + + + + + $(DesignerRuntimeImplementationProjectOutputGroupDependsOn); + _GenerateDesignerDepsFile; + _GenerateDesignerRuntimeConfigFile; + GetCopyToOutputDirectoryItems; + _GatherDesignerShadowCopyFiles; + + <_DesignerDepsFileName>$(AssemblyName).designer.deps.json + <_DesignerRuntimeConfigFileName>$(AssemblyName).designer.runtimeconfig.json + <_DesignerDepsFilePath>$(IntermediateOutputPath)$(_DesignerDepsFileName) + <_DesignerRuntimeConfigFilePath>$(IntermediateOutputPath)$(_DesignerRuntimeConfigFileName) + + + + + + + + + + + + + + <_DesignerHostConfigurationOption Include="Microsoft.NETCore.DotNetHostPolicy.SetAppPaths" Value="true" /> + + + + + + + + + + + <_DesignerShadowCopy Include="@(ReferenceCopyLocalPaths)" /> + + <_DesignerShadowCopy Remove="@(_ResolvedCopyLocalBuildAssets)" Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'" /> + + <_DesignerShadowCopy Remove="@(RuntimePackAsset)" Condition="'%(RuntimePackAsset.RuntimePackAlwaysCopyLocal)' != 'true'" /> + + + + + + + + + + + $(IntermediateOutputPath)$(MSBuildProjectName).AssemblyInfo$(DefaultLanguageSourceExtension) + true + + + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + + + + + + + <_InformationalVersionContainsPlus>false + <_InformationalVersionContainsPlus Condition="$(InformationalVersion.Contains('+'))">true + $(InformationalVersion)+$(SourceRevisionId) + $(InformationalVersion).$(SourceRevisionId) + + + + + + <_Parameter1>$(Company) + + + <_Parameter1>$(Configuration) + + + <_Parameter1>$(Copyright) + + + <_Parameter1>$(Description) + + + <_Parameter1>$(FileVersion) + + + <_Parameter1>$(InformationalVersion) + + + <_Parameter1>$(Product) + + + <_Parameter1>$(Trademark) + + + <_Parameter1>$(AssemblyTitle) + + + <_Parameter1>$(AssemblyVersion) + + + <_Parameter1>RepositoryUrl + <_Parameter2 Condition="'$(RepositoryUrl)' != ''">$(RepositoryUrl) + <_Parameter2 Condition="'$(RepositoryUrl)' == ''">$(PrivateRepositoryUrl) + + + <_Parameter1>$(NeutralLanguage) + + + %(InternalsVisibleTo.PublicKey) + + + <_Parameter1 Condition="'%(InternalsVisibleTo.Key)' != ''">%(InternalsVisibleTo.Identity), PublicKey=%(InternalsVisibleTo.Key) + <_Parameter1 Condition="'%(InternalsVisibleTo.Key)' == '' and '$(PublicKey)' != ''">%(InternalsVisibleTo.Identity), PublicKey=$(PublicKey) + <_Parameter1 Condition="'%(InternalsVisibleTo.Key)' == '' and '$(PublicKey)' == ''">%(InternalsVisibleTo.Identity) + + + <_Parameter1>%(AssemblyMetadata.Identity) + <_Parameter2>%(AssemblyMetadata.Value) + + + + + + <_Parameter1>$(TargetPlatformIdentifier)$(TargetPlatformVersion) + + + + + <_Parameter1>$(TargetPlatformIdentifier)$(SupportedOSPlatformVersion) + + + <_Parameter1>$(TargetPlatformIdentifier) + + + + + + + + + + $(IntermediateOutputPath)$(MSBuildProjectName).AssemblyInfoInputs.cache + + + + + + + + + + + + + + + + + + + + + + + + + + + $(AssemblyVersion) + $(Version) + + + + + + + + + $(IntermediateOutputPath)$(MSBuildProjectName).GlobalUsings.g$(DefaultLanguageSourceExtension) + + + + + + + + + + + + + + + + + + + <_GenerateSupportedRuntimeIntermediateAppConfig>$(IntermediateOutputPath)$(TargetFileName).withSupportedRuntime.config + + + + + + + + + + + + + + + + + + + + + + + + + + <_AllProjects Include="$(AdditionalProjects.Split('%3B'))" /> + <_AllProjects Include="$(MSBuildProjectFullPath)" /> + + + + + + + + + + %(PackageReference.Identity) + %(PackageReference.Version) + + StorePackageName=%(PackageReference.Identity); + StorePackageVersion=%(PackageReference.Version); + ComposeWorkingDir=$(ComposeWorkingDir); + PublishDir=$(PublishDir); + StoreStagingDir=$(StoreStagingDir); + TargetFramework=$(TargetFramework); + RuntimeIdentifier=$(RuntimeIdentifier); + JitPath=$(JitPath); + Crossgen=$(Crossgen); + SkipUnchangedFiles=$(SkipUnchangedFiles); + PreserveStoreLayout=$(PreserveStoreLayout); + CreateProfilingSymbols=$(CreateProfilingSymbols); + StoreSymbolsStagingDir=$(StoreSymbolsStagingDir); + DisableImplicitFrameworkReferences=false; + + + + + + + + + + + + + + + + + + + + + + + <_StoreArtifactContent> +@(ListOfPackageReference) + +]]> + + + + + + + + + + <_OptimizedResolvedFileToPublish Include="$(StoreStagingDir)\**\*.*" /> + <_OptimizedSymbolFileToPublish Include="$(StoreSymbolsStagingDir)\**\*.*" /> + + + + + + + + + + + + true + true + <_TFM Condition="'$(_TFM)' == ''">$(TargetFramework) + true + + + + + + $(UserProfileRuntimeStorePath) + <_ProfilingSymbolsDirectoryName>symbols + $([System.IO.Path]::Combine($(DefaultComposeDir), $(_ProfilingSymbolsDirectoryName))) + $([System.IO.Path]::Combine($(ComposeDir), $(_ProfilingSymbolsDirectoryName))) + $([System.IO.Path]::Combine($(ProfilingSymbolsDir), $(PlatformTarget))) + $(DefaultProfilingSymbolsDir) + $([System.IO.Path]::Combine($(ProfilingSymbolsDir), $(_TFM))) + $(ProfilingSymbolsDir)\ + $(DefaultComposeDir) + $([System.IO.Path]::Combine($(ComposeDir), $(PlatformTarget))) + $([System.IO.Path]::Combine($(ComposeDir), $(_TFM))) + $([System.IO.Path]::Combine($(ComposeDir),"artifact.xml")) + $([System.IO.Path]::GetFullPath($(ComposeDir))) + <_RandomFileName>$([System.IO.Path]::GetRandomFileName()) + $([System.IO.Path]::GetTempPath()) + $([System.IO.Path]::Combine($(TEMP), $(_RandomFileName))) + $([System.IO.Path]::GetFullPath($(ComposeWorkingDir))) + $([System.IO.Path]::Combine($(ComposeWorkingDir),"StagingDir")) + + $([System.IO.Path]::Combine($(ComposeWorkingDir),"SymbolsStagingDir")) + + $(PublishDir)\ + + + + false + true + + + + + + + + $(StorePackageVersion.Replace('*','-')) + $([System.IO.Path]::Combine($(ComposeWorkingDir),"$(StorePackageName)_$(StorePackageVersionForFolderName)")) + <_PackageProjFile>$([System.IO.Path]::Combine($(StoreWorkerWorkingDir), "Restore.csproj")) + $(StoreWorkerWorkingDir)\ + $(BaseIntermediateOutputPath)\project.assets.json + + + $(MicrosoftNETPlatformLibrary) + true + + + + + + + + + + + + + + + + + + + + + + + <_ManagedResolvedFileToPublishCandidates Include="@(ResolvedFileToPublish)" Condition="'%(ResolvedFileToPublish.AssetType)'=='runtime'" /> + <_UnOptimizedResolvedFileToPublish Include="@(ResolvedFileToPublish)" Condition="'%(ResolvedFileToPublish.AssetType)'!='runtime'" /> + + + true + + + + + + <_UnOptimizedResolvedFileToPublish Include="@(ResolvedFileToPublish)" /> + + + + + + + true + true + + + + + + + + + + + + + + + + + + + + + + true + true + false + true + false + true + 1 + + + + + + <_CoreclrResolvedPath Include="@(CrossgenResolvedAssembliesToPublish)" Condition="'%(CrossgenResolvedAssembliesToPublish.Filename)'=='coreclr'" /> + <_CoreclrResolvedPath Include="@(CrossgenResolvedAssembliesToPublish)" Condition="'%(CrossgenResolvedAssembliesToPublish.Filename)'=='libcoreclr'" /> + <_JitResolvedPath Include="@(CrossgenResolvedAssembliesToPublish)" Condition="'%(CrossgenResolvedAssembliesToPublish.Filename)'=='clrjit'" /> + <_JitResolvedPath Include="@(CrossgenResolvedAssembliesToPublish)" Condition="'%(CrossgenResolvedAssembliesToPublish.Filename)'=='libclrjit'" /> + + + + + + + + <_CoreclrPath>@(_CoreclrResolvedPath) + @(_JitResolvedPath) + <_CoreclrDir>$([System.IO.Path]::GetDirectoryName($(_CoreclrPath))) + <_CoreclrPkgDir>$([System.IO.Path]::Combine($(_CoreclrDir),"..\..\..\")) + $([System.IO.Path]::Combine($(_CoreclrPkgDir),"tools")) + + $([System.IO.Path]::Combine($(CrossgenDir),"crossgen")) + $([System.IO.Path]::Combine($(CrossgenDir),"crossgen.exe")) + + + + + + + + $([System.IO.Path]::GetFullPath($([System.IO.Path]::Combine($(_NetCoreRefDir), $([System.IO.Path]::GetFileName($(Crossgen))))))) + + + + + + + + CrossgenExe=$(Crossgen); + CrossgenJit=$(JitPath); + CrossgenInputAssembly=%(_ManagedResolvedFilesToOptimize.Fullpath); + CrossgenOutputAssembly=$(_RuntimeOptimizedDir)$(DirectorySeparatorChar)%(_ManagedResolvedFilesToOptimize.FileName)%(_ManagedResolvedFilesToOptimize.Extension); + CrossgenSubOutputPath=%(_ManagedResolvedFilesToOptimize.DestinationSubPath); + _RuntimeOptimizedDir=$(_RuntimeOptimizedDir); + PublishDir=$(StoreStagingDir); + CrossgenPlatformAssembliesPath=$(_RuntimeRefDir)$(PathSeparator)$(_NetCoreRefDir); + CreateProfilingSymbols=$(CreateProfilingSymbols); + StoreSymbolsStagingDir=$(StoreSymbolsStagingDir); + _RuntimeSymbolsDir=$(_RuntimeSymbolsDir) + + + + + + + + + + $([System.IO.Path]::GetDirectoryName($(_RuntimeSymbolsDir)\$(CrossgenSubOutputPath))) + $([System.IO.Path]::GetDirectoryName($(StoreSymbolsStagingDir)\$(CrossgenSubOutputPath))) + $(CrossgenExe) -nologo -readytorun -in "$(CrossgenInputAssembly)" -out "$(CrossgenOutputAssembly)" -jitpath "$(CrossgenJit)" -platform_assemblies_paths "$(CrossgenPlatformAssembliesPath)" + CreatePDB + CreatePerfMap + + + + + + + + + + + + <_ProfilingSymbols Include="$(CrossgenProfilingSymbolsOutputDirectory)\*" Condition="'$(CreateProfilingSymbols)' == 'true'" /> + + + + + + + + $([System.IO.Path]::PathSeparator) + $([System.IO.Path]::DirectorySeparatorChar) + + + + + + <_CrossProjFileDir>$([System.IO.Path]::Combine($(ComposeWorkingDir),"Optimize")) + <_NetCoreRefDir>$([System.IO.Path]::Combine($(_CrossProjFileDir), "netcoreapp")) + + + + + <_CrossProjAssetsFile>$([System.IO.Path]::Combine($(_CrossProjFileDir), project.assets.json)) + + + + + + <_RuntimeRefDir>$([System.IO.Path]::Combine($(StoreWorkerWorkingDir), "runtimeref")) + + <_RuntimeOptimizedDir>$([System.IO.Path]::Combine($(StoreWorkerWorkingDir), "runtimopt")) + + <_RuntimeSymbolsDir>$([System.IO.Path]::Combine($(StoreWorkerWorkingDir), "runtimesymbols")) + + + <_ManagedResolvedFilesToOptimize Include="@(_ManagedResolvedFileToPublishCandidates)" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(MSBuildThisFileDirectory)..\tasks\net7.0\Microsoft.NET.Sdk.Crossgen.dll + $(MSBuildThisFileDirectory)..\tasks\net472\Microsoft.NET.Sdk.Crossgen.dll + + + + + + + + + + + + + + + + + + + + + + + + <_ReadyToRunOutputPath>$(IntermediateOutputPath)R2R + + + + <_ReadyToRunImplementationAssemblies Include="@(ResolvedFileToPublish->WithMetadataValue('PostprocessAssembly', 'true'))" /> + + + + <_ReadyToRunImplementationAssemblies Include="@(_ManagedRuntimePackAssembly)" ReferenceOnly="true" /> + + + + + + <_ReadyToRunImplementationAssemblies Remove="@(_ReadyToRunImplementationAssemblies)" /> + <_ReadyToRunImplementationAssemblies Include="@(_ReadyToRunImplementationAssembliesWithoutConflicts)" /> + + + <_ReadyToRunPgoFiles Include="@(PublishReadyToRunPgoFiles)" /> + <_ReadyToRunPgoFiles Include="@(RuntimePackAsset)" Condition="'%(RuntimePackAsset.AssetType)' == 'pgodata' and '%(RuntimePackAsset.Extension)' == '.mibc' and '$(PublishReadyToRunUseRuntimePackOptimizationData)' == 'true'" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + <_ReadyToRunCompilerHasWarnings Condition="'$(_ReadyToRunWarningsDetected)' == 'true'">true + + + <_ReadyToRunCompilationFailures Condition="'$(_ReadyToRunCompilerExitCode)' != '' And $(_ReadyToRunCompilerExitCode) != 0" Include="@(_ReadyToRunCompileList)" /> + + + + + + + + + + + <_ReadyToRunCompilerHasWarnings Condition="'$(_ReadyToRunWarningsDetected)' == 'true'">true + + + <_ReadyToRunCompilationFailures Condition="'$(_ReadyToRunCompilerExitCode)' != '' And $(_ReadyToRunCompilerExitCode) != 0" Include="@(_ReadyToRunSymbolsCompileList)" /> + + + + + + + $(MSBuildThisFileDirectory)..\..\..\Microsoft.NETCoreSdk.BundledCliTools.props + + + + + + + <_ReferenceToObsoleteDotNetCliTool Include="@(DotNetCliToolReference)" /> + + <_ReferenceToObsoleteDotNetCliTool Remove="@(DotNetCliToolReference)" /> + + + + + + + + + true + <_GetChildProjectCopyToPublishDirectoryItems Condition="'$(_GetChildProjectCopyToPublishDirectoryItems)' == ''">true + true + + + + + true + true + <_FirstTargetFrameworkToSupportTrimming>net6.0 + <_FirstTargetFrameworkToSupportAot>net7.0 + <_FirstTargetFrameworkToSupportSingleFile>net6.0 + <_FirstTargetFrameworkVersionToSupportTrimAnalyzer>$([MSBuild]::GetTargetFrameworkVersion('$(_FirstTargetFrameworkToSupportTrimming)')) + <_FirstTargetFrameworkVersionToSupportAotAnalyzer>$([MSBuild]::GetTargetFrameworkVersion('$(_FirstTargetFrameworkToSupportAot)')) + <_FirstTargetFrameworkVersionToSupportSingleFileAnalyzer>$([MSBuild]::GetTargetFrameworkVersion('$(_FirstTargetFrameworkToSupportSingleFile)')) + + + + Always + + + + + + <_RequiresILLinkPack Condition="'$(_RequiresILLinkPack)' == '' And ( '$(PublishAot)' == 'true' Or '$(IsAotCompatible)' == 'true' Or '$(EnableAotAnalyzer)' == 'true' Or '$(PublishTrimmed)' == 'true' Or '$(IsTrimmable)' == 'true' Or '$(EnableTrimAnalyzer)' == 'true' Or '$(EnableSingleFileAnalyzer)' == 'true')">true + <_RequiresILLinkPack Condition="'$(_RequiresILLinkPack)' == ''">false + + + + + <_MinNonEolTargetFrameworkForTrimming>$(_MinimumNonEolSupportedNetCoreTargetFramework) + <_MinNonEolTargetFrameworkForSingleFile>$(_MinimumNonEolSupportedNetCoreTargetFramework) + + <_MinNonEolTargetFrameworkForAot>$(_MinimumNonEolSupportedNetCoreTargetFramework) + <_MinNonEolTargetFrameworkForAot Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(_FirstTargetFrameworkToSupportAot)', '$(_MinimumNonEolSupportedNetCoreTargetFramework)'))">$(_FirstTargetFrameworkToSupportAot) + + + <_TargetFramework Include="$(TargetFrameworks)" /> + <_DecomposedTargetFramework Include="@(_TargetFramework)"> + $([MSBuild]::IsTargetFrameworkCompatible('%(Identity)', '$(_FirstTargetFrameworkToSupportTrimming)')) + $([MSBuild]::IsTargetFrameworkCompatible('$(_MinNonEolTargetFrameworkForTrimming)', '%(Identity)')) + $([MSBuild]::IsTargetFrameworkCompatible('%(Identity)', '$(_FirstTargetFrameworkToSupportAot)')) + $([MSBuild]::IsTargetFrameworkCompatible('$(_MinNonEolTargetFrameworkForAot)', '%(Identity)')) + $([MSBuild]::IsTargetFrameworkCompatible('%(Identity)', '$(_FirstTargetFrameworkToSupportSingleFile)')) + $([MSBuild]::IsTargetFrameworkCompatible('$(_MinNonEolTargetFrameworkForSingleFile)', '%(Identity)')) + + <_TargetFrameworkToSilenceIsTrimmableUnsupportedWarning Include="@(_DecomposedTargetFramework)" Condition="'%(SupportsTrimming)' == 'true' And '%(SupportedByMinNonEolTargetFrameworkForTrimming)' == 'true'" /> + <_TargetFrameworkToSilenceIsAotCompatibleUnsupportedWarning Include="@(_DecomposedTargetFramework->'%(Identity)')" Condition="'%(SupportsAot)' == 'true' And '%(SupportedByMinNonEolTargetFrameworkForAot)' == 'true'" /> + <_TargetFrameworkToSilenceEnableSingleFileAnalyzerUnsupportedWarning Include="@(_DecomposedTargetFramework)" Condition="'%(SupportsSingleFile)' == 'true' And '%(SupportedByMinNonEolTargetFrameworkForSingleFile)' == 'true'" /> + + + + <_SilenceIsTrimmableUnsupportedWarning Condition="'$(_SilenceIsTrimmableUnsupportedWarning)' == '' And @(_TargetFrameworkToSilenceIsTrimmableUnsupportedWarning->Count()) > 0">true + <_SilenceIsAotCompatibleUnsupportedWarning Condition="'$(_SilenceIsAotCompatibleUnsupportedWarning)' == '' And @(_TargetFrameworkToSilenceIsAotCompatibleUnsupportedWarning->Count()) > 0">true + <_SilenceEnableSingleFileAnalyzerUnsupportedWarning Condition="'$(_SilenceEnableSingleFileAnalyzerUnsupportedWarning)' == '' And @(_TargetFrameworkToSilenceEnableSingleFileAnalyzerUnsupportedWarning->Count()) > 0">true + + + + + + + + <_BeforePublishNoBuildTargets> + BuildOnlySettings; + _PreventProjectReferencesFromBuilding; + ResolveReferences; + PrepareResourceNames; + ComputeIntermediateSatelliteAssemblies; + ComputeEmbeddedApphostPaths; + + <_CorePublishTargets> + PrepareForPublish; + ComputeAndCopyFilesToPublishDirectory; + $(PublishProtocolProviderTargets); + PublishItemsOutputGroup; + + <_PublishNoBuildAlternativeDependsOn>$(_BeforePublishNoBuildTargets);$(_CorePublishTargets) + + + + + + + + + + + + + + + + + + + false + + + + + + + + + + + + + + + + + $(PublishDir)\ + + + + + + + + + + + + <_OrphanPublishFileWrites Include="@(_PriorPublishFileWrites)" Exclude="@(_CurrentPublishFileWrites)" /> + + + + + + + + + + + + <_NormalizedPublishDir>$([MSBuild]::NormalizeDirectory($(PublishDir))) + + + + + + <_PublishCleanFile Condition="'$(PublishCleanFile)'==''">PublishOutputs.$(_NormalizedPublishDirHash.Substring(0, 10)).txt + + + + + + + + + + + + + + + + + + <_CurrentPublishFileWritesUnfiltered Include="@(ResolvedFileToPublish->'$(_NormalizedPublishDir)%(RelativePath)')" /> + <_CurrentPublishFileWritesUnfiltered Include="$(_NormalizedPublishDir)$(AssemblyName)$(_NativeExecutableExtension)" Condition="'$(UseAppHost)' == 'true'" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_ResolvedFileToPublishPreserveNewest Include="@(ResolvedFileToPublish)" Condition="'%(ResolvedFileToPublish.CopyToPublishDirectory)'=='PreserveNewest'" /> + <_ResolvedFileToPublishAlways Include="@(ResolvedFileToPublish)" Condition="'%(ResolvedFileToPublish.CopyToPublishDirectory)'=='Always'" /> + + + <_ResolvedUnbundledFileToPublishPreserveNewest Include="@(_ResolvedFileToPublishPreserveNewest)" Condition="'$(PublishSingleFile)' != 'true' or '%(_ResolvedFileToPublishPreserveNewest.ExcludeFromSingleFile)'=='true'" /> + <_ResolvedUnbundledFileToPublishAlways Include="@(_ResolvedFileToPublishAlways)" Condition="'$(PublishSingleFile)' != 'true' or '%(_ResolvedFileToPublishAlways.ExcludeFromSingleFile)'=='true'" /> + + + + + + + + true + true + false + + + + + + + + @(IntermediateAssembly->'%(Filename)%(Extension)') + PreserveNewest + + + + $(ProjectDepsFileName) + PreserveNewest + + + + $(ProjectRuntimeConfigFileName) + PreserveNewest + + + + @(AppConfigWithTargetPath->'%(TargetPath)') + PreserveNewest + + + + @(_DebugSymbolsIntermediatePath->'%(Filename)%(Extension)') + PreserveNewest + true + + + + %(IntermediateSatelliteAssembliesWithTargetPath.Culture)\%(Filename)%(Extension) + PreserveNewest + + + + %(Filename)%(Extension) + PreserveNewest + + + + + + + + + <_ResolvedCopyLocalPublishAssets Remove="@(_ResolvedCopyLocalPublishAssetsRemoved)" /> + + + + %(_ResolvedCopyLocalPublishAssets.DestinationSubDirectory)%(Filename)%(Extension) + PreserveNewest + + + + @(FinalDocFile->'%(Filename)%(Extension)') + PreserveNewest + + + + shims/%(_EmbeddedApphostPaths.ShimRuntimeIdentifier)/%(_EmbeddedApphostPaths.Filename)%(_EmbeddedApphostPaths.Extension) + PreserveNewest + + + <_FilesToDrop Include="@(ResolvedFileToPublish)" Condition="'$(PublishSingleFile)' == 'true' and '%(ResolvedFileToPublish.DropFromSingleFile)' == 'true'" /> + + + + + + + + + + + + <_ResolvedCopyLocalPublishAssets Include="@(RuntimePackAsset)" Condition="('$(SelfContained)' == 'true' Or '%(RuntimePackAsset.RuntimePackAlwaysCopyLocal)' == 'true') and '%(RuntimePackAsset.AssetType)' != 'pgodata'" /> + + + + <_ResolvedCopyLocalPublishAssets Remove="@(_NativeRestoredAppHostNETCore)" /> + + + <_ResolvedCopyLocalPublishAssets Include="@(_ResolvedCopyLocalBuildAssets)" Condition="'%(_ResolvedCopyLocalBuildAssets.CopyToPublishDirectory)' != 'false' " /> + + + + + + + + + + + + + <_PublishSatelliteResources Include="@(_ResolvedCopyLocalPublishAssets)" Condition="'%(_ResolvedCopyLocalPublishAssets.AssetType)' == 'resources'" /> + + + + + + <_ResolvedCopyLocalPublishAssets Remove="@(_PublishSatelliteResources)" /> + <_ResolvedCopyLocalPublishAssets Include="@(_FilteredPublishSatelliteResources)" /> + + + + + + <_ResolvedCopyLocalPublishAssets Include="@(ReferenceCopyLocalPaths)" Exclude="@(_ResolvedCopyLocalBuildAssets);@(RuntimePackAsset)" Condition="('$(PublishReferencesDocumentationFiles)' == 'true' or '%(ReferenceCopyLocalPaths.Extension)' != '.xml') and '%(ReferenceCopyLocalPaths.Private)' != 'false'"> + %(ReferenceCopyLocalPaths.DestinationSubDirectory)%(ReferenceCopyLocalPaths.Filename)%(ReferenceCopyLocalPaths.Extension) + + + + + + + + + + + + + + + + %(_SourceItemsToCopyToPublishDirectoryAlways.TargetPath) + Always + True + + + %(_SourceItemsToCopyToPublishDirectory.TargetPath) + PreserveNewest + True + + + + + + + + <_GCTPDIKeepDuplicates>false + <_GCTPDIKeepMetadata>CopyToPublishDirectory;ExcludeFromSingleFile;TargetPath + + + + + + + + <_SourceItemsToCopyToPublishDirectoryAlways KeepDuplicates=" '$(_GCTPDIKeepDuplicates)' != 'false' " KeepMetadata="$(_GCTPDIKeepMetadata)" Include="@(_AllChildProjectPublishItemsWithTargetPath->'%(FullPath)')" Condition="'%(_AllChildProjectPublishItemsWithTargetPath.CopyToPublishDirectory)'=='Always'" /> + <_SourceItemsToCopyToPublishDirectory KeepDuplicates=" '$(_GCTPDIKeepDuplicates)' != 'false' " KeepMetadata="$(_GCTPDIKeepMetadata)" Include="@(_AllChildProjectPublishItemsWithTargetPath->'%(FullPath)')" Condition="'%(_AllChildProjectPublishItemsWithTargetPath.CopyToPublishDirectory)'=='PreserveNewest'" /> + + + + <_AllChildProjectPublishItemsWithTargetPath Remove="@(_AllChildProjectPublishItemsWithTargetPath)" /> + + + + <_SourceItemsToCopyToPublishDirectoryAlways KeepMetadata="$(_GCTPDIKeepMetadata)" Include="@(ContentWithTargetPath->'%(FullPath)')" Condition="'%(ContentWithTargetPath.CopyToPublishDirectory)'=='Always'" /> + <_SourceItemsToCopyToPublishDirectory KeepMetadata="$(_GCTPDIKeepMetadata)" Include="@(ContentWithTargetPath->'%(FullPath)')" Condition="'%(ContentWithTargetPath.CopyToPublishDirectory)'=='PreserveNewest'" /> + + + <_SourceItemsToCopyToPublishDirectoryAlways KeepMetadata="$(_GCTPDIKeepMetadata)" Include="@(EmbeddedResource->'%(FullPath)')" Condition="'%(EmbeddedResource.CopyToPublishDirectory)'=='Always'" /> + <_SourceItemsToCopyToPublishDirectory KeepMetadata="$(_GCTPDIKeepMetadata)" Include="@(EmbeddedResource->'%(FullPath)')" Condition="'%(EmbeddedResource.CopyToPublishDirectory)'=='PreserveNewest'" /> + + + <_CompileItemsToPublish Include="@(Compile->'%(FullPath)')" Condition="'%(Compile.CopyToPublishDirectory)'=='Always' or '%(Compile.CopyToPublishDirectory)'=='PreserveNewest'" /> + + + + + + <_SourceItemsToCopyToPublishDirectoryAlways KeepMetadata="$(_GCTPDIKeepMetadata)" Include="@(_CompileItemsToPublishWithTargetPath)" Condition="'%(_CompileItemsToPublishWithTargetPath.CopyToPublishDirectory)'=='Always'" /> + <_SourceItemsToCopyToPublishDirectory KeepMetadata="$(_GCTPDIKeepMetadata)" Include="@(_CompileItemsToPublishWithTargetPath)" Condition="'%(_CompileItemsToPublishWithTargetPath.CopyToPublishDirectory)'=='PreserveNewest'" /> + + + <_SourceItemsToCopyToPublishDirectoryAlways KeepMetadata="$(_GCTPDIKeepMetadata)" Include="@(_NoneWithTargetPath->'%(FullPath)')" Condition="'%(_NoneWithTargetPath.CopyToPublishDirectory)'=='Always'" /> + <_SourceItemsToCopyToPublishDirectory KeepMetadata="$(_GCTPDIKeepMetadata)" Include="@(_NoneWithTargetPath->'%(FullPath)')" Condition="'%(_NoneWithTargetPath.CopyToPublishDirectory)'=='PreserveNewest'" /> + + + + <_SourceItemsToCopyToPublishDirectoryAlways Remove="$(AppHostIntermediatePath)" /> + <_SourceItemsToCopyToPublishDirectory Remove="$(AppHostIntermediatePath)" /> + + <_SourceItemsToCopyToPublishDirectoryAlways Include="$(SingleFileHostIntermediatePath)" CopyToOutputDirectory="Always" TargetPath="$(AssemblyName)$(_NativeExecutableExtension)" /> + + + + <_SourceItemsToCopyToPublishDirectoryAlways Remove="$(AppHostIntermediatePath)" /> + <_SourceItemsToCopyToPublishDirectory Remove="$(AppHostIntermediatePath)" /> + + <_SourceItemsToCopyToPublishDirectoryAlways Include="$(AppHostForPublishIntermediatePath)" CopyToOutputDirectory="Always" TargetPath="$(AssemblyName)$(_NativeExecutableExtension)" /> + + + + + + + + + + Always + + + PreserveNewest + + + Always + + + PreserveNewest + + + Always + + + PreserveNewest + + <_NoneWithTargetPath Condition="'%(_NoneWithTargetPath.CopyToOutputDirectory)'=='Always' and '%(_NoneWithTargetPath.CopyToPublishDirectory)' == ''"> + Always + + <_NoneWithTargetPath Condition="'%(_NoneWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest' and '%(_NoneWithTargetPath.CopyToPublishDirectory)' == ''"> + PreserveNewest + + + + + <_ComputeManagedRuntimePackAssembliesIfSelfContained>_ComputeManagedRuntimePackAssemblies + + + + + + + <_ManagedRuntimeAssembly Include="@(RuntimeCopyLocalItems)" /> + + <_ManagedRuntimeAssembly Include="@(UserRuntimeAssembly)" /> + + <_ManagedRuntimeAssembly Include="@(IntermediateAssembly)" /> + + + + <_ManagedRuntimeAssembly Include="@(_ManagedRuntimePackAssembly)" /> + + + + + + + + + + + + + + + <_ManagedRuntimePackAssembly Include="@(RuntimePackAsset)" Condition="'%(RuntimePackAsset.AssetType)' == 'runtime' or '%(RuntimePackAsset.Filename)' == 'System.Private.Corelib'" /> + + + + + + <_TrimRuntimeAssets Condition="'$(PublishSingleFile)' == 'true' and '$(SelfContained)' == 'true'">true + <_UseBuildDependencyFile Condition="'@(_ExcludeFromPublishPackageReference)' == '' and '@(RuntimeStorePackages)' == '' and '$(PreserveStoreLayout)' != 'true' and '$(PublishTrimmed)' != 'true' and '$(_TrimRuntimeAssets)' != 'true'">true + + + + + + <_FilesToBundle Include="@(ResolvedFileToPublish)" Condition="'%(ResolvedFileToPublish.ExcludeFromSingleFile)' != 'true'" /> + + + + $(AssemblyName)$(_NativeExecutableExtension) + $(PublishDir)$(PublishedSingleFileName) + + + + + + + + $(PublishedSingleFileName) + + + + + <_GenerateSingleFileBundlePropertyInputsCacheToHash Include="$(PublishedSingleFilePath)" /> + <_GenerateSingleFileBundlePropertyInputsCacheToHash Include="$(TraceSingleFileBundler)" /> + <_GenerateSingleFileBundlePropertyInputsCacheToHash Include="$(IncludeSymbolsInSingleFile)" /> + <_GenerateSingleFileBundlePropertyInputsCacheToHash Include="$(IncludeAllContentForSelfExtract)" /> + <_GenerateSingleFileBundlePropertyInputsCacheToHash Include="$(IncludeNativeLibrariesForSelfExtract)" /> + <_GenerateSingleFileBundlePropertyInputsCacheToHash Include="$(EnableCompressionInSingleFile)" /> + <_GenerateSingleFileBundlePropertyInputsCacheToHash Include="$(PublishedSingleFileName)" /> + <_GenerateSingleFileBundlePropertyInputsCacheToHash Include="$(RuntimeIdentifier)" /> + <_GenerateSingleFileBundlePropertyInputsCacheToHash Include="$(PublishDir)" /> + <_GenerateSingleFileBundlePropertyInputsCacheToHash Include="$(_TargetFrameworkVersionWithoutV)" /> + <_GenerateSingleFileBundlePropertyInputsCacheToHash Include="@(FilesToBundle)" /> + + + + + + + + + + false + false + false + $(IncludeAllContentForSelfExtract) + false + + + + + + + + + + + + + + $(PublishDepsFilePath) + $(IntermediateOutputPath)$(ProjectDepsFileName) + + + + + <_GeneratePublishDependencyFilePropertyInputsCacheToHash Include="$(PublishDepsFilePath)" /> + <_GeneratePublishDependencyFilePropertyInputsCacheToHash Include="$(PublishSingleFile)" /> + <_GeneratePublishDependencyFilePropertyInputsCacheToHash Include="$(MSBuildProjectFullPath)" /> + <_GeneratePublishDependencyFilePropertyInputsCacheToHash Include="$(ProjectAssetsFile)" /> + <_GeneratePublishDependencyFilePropertyInputsCacheToHash Include="$(IntermediateDepsFilePath)" /> + <_GeneratePublishDependencyFilePropertyInputsCacheToHash Include="$(TargetFramework)" /> + <_GeneratePublishDependencyFilePropertyInputsCacheToHash Include="$(AssemblyName)" /> + <_GeneratePublishDependencyFilePropertyInputsCacheToHash Include="$(TargetExt)" /> + <_GeneratePublishDependencyFilePropertyInputsCacheToHash Include="$(Version)" /> + <_GeneratePublishDependencyFilePropertyInputsCacheToHash Include="$(IncludeMainProjectInDepsFile)" /> + <_GeneratePublishDependencyFilePropertyInputsCacheToHash Include="$(RuntimeIdentifier)" /> + <_GeneratePublishDependencyFilePropertyInputsCacheToHash Include="$(MicrosoftNETPlatformLibrary)" /> + <_GeneratePublishDependencyFilePropertyInputsCacheToHash Include="$(SelfContained)" /> + <_GeneratePublishDependencyFilePropertyInputsCacheToHash Include="$(IncludeFileVersionsInDependencyFile)" /> + <_GeneratePublishDependencyFilePropertyInputsCacheToHash Include="$(RuntimeIdentifierGraphPath)" /> + <_GeneratePublishDependencyFilePropertyInputsCacheToHash Include="$(IncludeProjectsNotInAssetsFileInDepsFile)" /> + + + + + + + + + + + + + + $(PublishDir)$(ProjectDepsFileName) + <_IsSingleFilePublish Condition="'$(PublishSingleFile)' == ''">false + <_IsSingleFilePublish Condition="'$(PublishSingleFile)' != ''">$(PublishSingleFile) + + + + + + <_ResolvedNuGetFilesForPublish Include="@(NativeCopyLocalItems)" Condition="'%(NativeCopyLocalItems.CopyToPublishDirectory)' != 'false'" /> + <_ResolvedNuGetFilesForPublish Include="@(ResourceCopyLocalItems)" Condition="'%(ResourceCopyLocalItems.CopyToPublishDirectory)' != 'false'" /> + <_ResolvedNuGetFilesForPublish Include="@(RuntimeCopyLocalItems)" Condition="'%(RuntimeCopyLocalItems.CopyToPublishDirectory)' != 'false'" /> + <_ResolvedNuGetFilesForPublish Remove="@(_PublishConflictPackageFiles)" Condition="'%(_PublishConflictPackageFiles.ConflictItemType)' != 'Reference'" /> + + + + + $(ProjectDepsFileName) + + + + + + + + <_PackAsToolShimRuntimeIdentifiers Condition="@(_PackAsToolShimRuntimeIdentifiers) ==''" Include="$(PackAsToolShimRuntimeIdentifiers)" /> + + + + + + + + + + + + + + + + + + $(PublishItemsOutputGroupDependsOn); + ResolveReferences; + ComputeResolvedFilesToPublishList; + _ComputeFilesToBundle; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_ToolsSettingsFilePath>$(BaseIntermediateOutputPath)DotnetToolSettings.xml + true + <_PackToolPublishDependency Condition=" ('$(GeneratePackageOnBuild)' != 'true' and '$(NoBuild)' != 'true') and $(IsPublishable) == 'true' ">_PublishBuildAlternative + <_PackToolPublishDependency Condition=" ('$(GeneratePackageOnBuild)' == 'true' or '$(NoBuild)' == 'true') and $(IsPublishable) == 'true' ">$(_PublishNoBuildAlternativeDependsOn) + + + + <_GeneratedFiles Include="$(PublishDepsFilePath)" Condition="'$(GenerateDependencyFile)' != 'true' or '$(_UseBuildDependencyFile)' == 'true'" /> + <_GeneratedFiles Include="$(PublishRuntimeConfigFilePath)" /> + <_GeneratedFiles Include="$(_ToolsSettingsFilePath)" /> + + + + + + + tools/$(_NuGetShortFolderName)/any/%(_GeneratedFiles.RecursiveDir)%(_GeneratedFiles.Filename)%(_GeneratedFiles.Extension) + + + %(_ResolvedFileToPublishWithPackagePath.PackagePath) + + + + + $(TargetName) + $(TargetFileName) + <_GenerateToolsSettingsFileCacheFile Condition="'$(_GenerateToolsSettingsFileCacheFile)' == ''">$(IntermediateOutputPath)$(MSBuildProjectName).toolssettingsinput.cache + <_GenerateToolsSettingsFileCacheFile>$([MSBuild]::NormalizePath($(MSBuildProjectDirectory), $(_GenerateToolsSettingsFileCacheFile))) + + + + <_GenerateToolsSettingsFileInputCacheToHash Include="$(ToolEntryPoint)" /> + <_GenerateToolsSettingsFileInputCacheToHash Include="$(ToolCommandName)" /> + + + + + + + + + + + + + + + + + + + + + + <_ShimInputCacheFile Condition="'$(_ShimInputCacheFile)' == ''">$(IntermediateOutputPath)$(MSBuildProjectName).shiminput.cache + <_ShimInputCacheFile>$([MSBuild]::NormalizePath($(MSBuildProjectDirectory), $(_ShimInputCacheFile))) + <_ShimCreatedSentinelFile Condition="'$(_ShimCreatedSentinelFile)' == ''">$(IntermediateOutputPath)$(MSBuildProjectName).shimcreated.sentinel + <_ShimCreatedSentinelFile>$([MSBuild]::NormalizePath($(MSBuildProjectDirectory), $(_ShimCreatedSentinelFile))) + $(OutDir) + + + + + + + + + + + + + + + + + + + + + + <_GenerateShimsAssetsInput Include="$(_ShimInputCacheFile)" /> + <_GenerateShimsAssetsInput Include="@(_ApphostsForShimRuntimeIdentifiers)" /> + <_GenerateShimsAssetsInput Include="$(_ShimCreatedSentinelFile)" /> + <_GenerateShimsAssetsInput Include="$(ProjectAssetsFile)" /> + <_GenerateShimsAssetsInputCacheToHash Include="$(PackageId)" /> + <_GenerateShimsAssetsInputCacheToHash Include="$(Version)" /> + <_GenerateShimsAssetsInputCacheToHash Include="$(NuGetTargetMoniker)" /> + <_GenerateShimsAssetsInputCacheToHash Include="$(ToolCommandName)" /> + <_GenerateShimsAssetsInputCacheToHash Include="$(ToolEntryPoint)" /> + <_GenerateShimsAssetsInputCacheToHash Include="$(PackAsToolShimRuntimeIdentifiers)" /> + + + + + + + + + + + + + + + + + + + + refs + $(PreserveCompilationContext) + + + + + $(DefineConstants) + $(LangVersion) + $(PlatformTarget) + $(AllowUnsafeBlocks) + $(TreatWarningsAsErrors) + $(Optimize) + $(AssemblyOriginatorKeyFile) + $(DelaySign) + $(PublicSign) + $(DebugType) + $(OutputType) + $(GenerateDocumentationFile) + + + + + + + + + + + <_RefAssembliesToExclude Include="@(_ResolvedCopyLocalPublishAssets->'%(FullPath)')" /> + + <_RefAssembliesToExclude Include="@(_RuntimeItemsInRuntimeStore)" /> + + $(RefAssembliesFolderName)\%(Filename)%(Extension) + + + + + + + + + + + + + + + + + + Microsoft.CSharp|4.4.0; + Microsoft.Win32.Primitives|4.3.0; + Microsoft.Win32.Registry|4.4.0; + runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl|4.3.0; + runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl|4.3.0; + runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl|4.3.0; + runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl|4.3.0; + runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl|4.3.0; + runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple|4.3.0; + runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl|4.3.0; + runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl|4.3.0; + runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl|4.3.0; + runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl|4.3.0; + runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl|4.3.0; + System.AppContext|4.3.0; + System.Buffers|4.4.0; + System.Collections|4.3.0; + System.Collections.Concurrent|4.3.0; + System.Collections.Immutable|1.4.0; + System.Collections.NonGeneric|4.3.0; + System.Collections.Specialized|4.3.0; + System.ComponentModel|4.3.0; + System.ComponentModel.EventBasedAsync|4.3.0; + System.ComponentModel.Primitives|4.3.0; + System.ComponentModel.TypeConverter|4.3.0; + System.Console|4.3.0; + System.Data.Common|4.3.0; + System.Diagnostics.Contracts|4.3.0; + System.Diagnostics.Debug|4.3.0; + System.Diagnostics.DiagnosticSource|4.4.0; + System.Diagnostics.FileVersionInfo|4.3.0; + System.Diagnostics.Process|4.3.0; + System.Diagnostics.StackTrace|4.3.0; + System.Diagnostics.TextWriterTraceListener|4.3.0; + System.Diagnostics.Tools|4.3.0; + System.Diagnostics.TraceSource|4.3.0; + System.Diagnostics.Tracing|4.3.0; + System.Dynamic.Runtime|4.3.0; + System.Globalization|4.3.0; + System.Globalization.Calendars|4.3.0; + System.Globalization.Extensions|4.3.0; + System.IO|4.3.0; + System.IO.Compression|4.3.0; + System.IO.Compression.ZipFile|4.3.0; + System.IO.FileSystem|4.3.0; + System.IO.FileSystem.AccessControl|4.4.0; + System.IO.FileSystem.DriveInfo|4.3.0; + System.IO.FileSystem.Primitives|4.3.0; + System.IO.FileSystem.Watcher|4.3.0; + System.IO.IsolatedStorage|4.3.0; + System.IO.MemoryMappedFiles|4.3.0; + System.IO.Pipes|4.3.0; + System.IO.UnmanagedMemoryStream|4.3.0; + System.Linq|4.3.0; + System.Linq.Expressions|4.3.0; + System.Linq.Queryable|4.3.0; + System.Net.Http|4.3.0; + System.Net.NameResolution|4.3.0; + System.Net.Primitives|4.3.0; + System.Net.Requests|4.3.0; + System.Net.Security|4.3.0; + System.Net.Sockets|4.3.0; + System.Net.WebHeaderCollection|4.3.0; + System.ObjectModel|4.3.0; + System.Private.DataContractSerialization|4.3.0; + System.Reflection|4.3.0; + System.Reflection.Emit|4.3.0; + System.Reflection.Emit.ILGeneration|4.3.0; + System.Reflection.Emit.Lightweight|4.3.0; + System.Reflection.Extensions|4.3.0; + System.Reflection.Metadata|1.5.0; + System.Reflection.Primitives|4.3.0; + System.Reflection.TypeExtensions|4.3.0; + System.Resources.ResourceManager|4.3.0; + System.Runtime|4.3.0; + System.Runtime.Extensions|4.3.0; + System.Runtime.Handles|4.3.0; + System.Runtime.InteropServices|4.3.0; + System.Runtime.InteropServices.RuntimeInformation|4.3.0; + System.Runtime.Loader|4.3.0; + System.Runtime.Numerics|4.3.0; + System.Runtime.Serialization.Formatters|4.3.0; + System.Runtime.Serialization.Json|4.3.0; + System.Runtime.Serialization.Primitives|4.3.0; + System.Security.AccessControl|4.4.0; + System.Security.Claims|4.3.0; + System.Security.Cryptography.Algorithms|4.3.0; + System.Security.Cryptography.Cng|4.4.0; + System.Security.Cryptography.Csp|4.3.0; + System.Security.Cryptography.Encoding|4.3.0; + System.Security.Cryptography.OpenSsl|4.4.0; + System.Security.Cryptography.Primitives|4.3.0; + System.Security.Cryptography.X509Certificates|4.3.0; + System.Security.Cryptography.Xml|4.4.0; + System.Security.Principal|4.3.0; + System.Security.Principal.Windows|4.4.0; + System.Text.Encoding|4.3.0; + System.Text.Encoding.Extensions|4.3.0; + System.Text.RegularExpressions|4.3.0; + System.Threading|4.3.0; + System.Threading.Overlapped|4.3.0; + System.Threading.Tasks|4.3.0; + System.Threading.Tasks.Extensions|4.3.0; + System.Threading.Tasks.Parallel|4.3.0; + System.Threading.Thread|4.3.0; + System.Threading.ThreadPool|4.3.0; + System.Threading.Timer|4.3.0; + System.ValueTuple|4.3.0; + System.Xml.ReaderWriter|4.3.0; + System.Xml.XDocument|4.3.0; + System.Xml.XmlDocument|4.3.0; + System.Xml.XmlSerializer|4.3.0; + System.Xml.XPath|4.3.0; + System.Xml.XPath.XDocument|4.3.0; + + + + + Microsoft.Win32.Primitives|4.3.0; + System.AppContext|4.3.0; + System.Collections|4.3.0; + System.Collections.Concurrent|4.3.0; + System.Collections.Immutable|1.4.0; + System.Collections.NonGeneric|4.3.0; + System.Collections.Specialized|4.3.0; + System.ComponentModel|4.3.0; + System.ComponentModel.EventBasedAsync|4.3.0; + System.ComponentModel.Primitives|4.3.0; + System.ComponentModel.TypeConverter|4.3.0; + System.Console|4.3.0; + System.Data.Common|4.3.0; + System.Diagnostics.Contracts|4.3.0; + System.Diagnostics.Debug|4.3.0; + System.Diagnostics.FileVersionInfo|4.3.0; + System.Diagnostics.Process|4.3.0; + System.Diagnostics.StackTrace|4.3.0; + System.Diagnostics.TextWriterTraceListener|4.3.0; + System.Diagnostics.Tools|4.3.0; + System.Diagnostics.TraceSource|4.3.0; + System.Diagnostics.Tracing|4.3.0; + System.Dynamic.Runtime|4.3.0; + System.Globalization|4.3.0; + System.Globalization.Calendars|4.3.0; + System.Globalization.Extensions|4.3.0; + System.IO|4.3.0; + System.IO.Compression|4.3.0; + System.IO.Compression.ZipFile|4.3.0; + System.IO.FileSystem|4.3.0; + System.IO.FileSystem.DriveInfo|4.3.0; + System.IO.FileSystem.Primitives|4.3.0; + System.IO.FileSystem.Watcher|4.3.0; + System.IO.IsolatedStorage|4.3.0; + System.IO.MemoryMappedFiles|4.3.0; + System.IO.Pipes|4.3.0; + System.IO.UnmanagedMemoryStream|4.3.0; + System.Linq|4.3.0; + System.Linq.Expressions|4.3.0; + System.Linq.Queryable|4.3.0; + System.Net.Http|4.3.0; + System.Net.NameResolution|4.3.0; + System.Net.Primitives|4.3.0; + System.Net.Requests|4.3.0; + System.Net.Security|4.3.0; + System.Net.Sockets|4.3.0; + System.Net.WebHeaderCollection|4.3.0; + System.ObjectModel|4.3.0; + System.Private.DataContractSerialization|4.3.0; + System.Reflection|4.3.0; + System.Reflection.Emit|4.3.0; + System.Reflection.Emit.ILGeneration|4.3.0; + System.Reflection.Emit.Lightweight|4.3.0; + System.Reflection.Extensions|4.3.0; + System.Reflection.Primitives|4.3.0; + System.Reflection.TypeExtensions|4.3.0; + System.Resources.ResourceManager|4.3.0; + System.Runtime|4.3.0; + System.Runtime.Extensions|4.3.0; + System.Runtime.Handles|4.3.0; + System.Runtime.InteropServices|4.3.0; + System.Runtime.InteropServices.RuntimeInformation|4.3.0; + System.Runtime.Loader|4.3.0; + System.Runtime.Numerics|4.3.0; + System.Runtime.Serialization.Formatters|4.3.0; + System.Runtime.Serialization.Json|4.3.0; + System.Runtime.Serialization.Primitives|4.3.0; + System.Security.AccessControl|4.4.0; + System.Security.Claims|4.3.0; + System.Security.Cryptography.Algorithms|4.3.0; + System.Security.Cryptography.Csp|4.3.0; + System.Security.Cryptography.Encoding|4.3.0; + System.Security.Cryptography.Primitives|4.3.0; + System.Security.Cryptography.X509Certificates|4.3.0; + System.Security.Cryptography.Xml|4.4.0; + System.Security.Principal|4.3.0; + System.Security.Principal.Windows|4.4.0; + System.Text.Encoding|4.3.0; + System.Text.Encoding.Extensions|4.3.0; + System.Text.RegularExpressions|4.3.0; + System.Threading|4.3.0; + System.Threading.Overlapped|4.3.0; + System.Threading.Tasks|4.3.0; + System.Threading.Tasks.Extensions|4.3.0; + System.Threading.Tasks.Parallel|4.3.0; + System.Threading.Thread|4.3.0; + System.Threading.ThreadPool|4.3.0; + System.Threading.Timer|4.3.0; + System.ValueTuple|4.3.0; + System.Xml.ReaderWriter|4.3.0; + System.Xml.XDocument|4.3.0; + System.Xml.XmlDocument|4.3.0; + System.Xml.XmlSerializer|4.3.0; + System.Xml.XPath|4.3.0; + System.Xml.XPath.XDocument|4.3.0; + + + + + + + + + + + <_RuntimeAssetsForConflictResolution Include="@(RuntimeCopyLocalItems); @(NativeCopyLocalItems); @(ResourceCopyLocalItems); @(RuntimeTargetsCopyLocalItems)" Exclude="@(ReferenceCopyLocalPaths)" /> + + + + + + + + + + + + + + + + + + + + + + + + + <_ResolvedCopyLocalPublishAssets Remove="@(_ResolvedCopyLocalPublishAssets)" /> + <_ResolvedCopyLocalPublishAssets Include="@(_ResolvedCopyLocalPublishAssetsWithoutConflicts)" /> + + + + + + + + + + + + + + + Properties + + + $(Configuration.ToUpperInvariant()) + + $(ImplicitConfigurationDefine.Replace('-', '_')) + $(ImplicitConfigurationDefine.Replace('.', '_')) + $(ImplicitConfigurationDefine.Replace(' ', '_')) + $(DefineConstants);$(ImplicitConfigurationDefine) + + + $(DefineConstants);$(VersionlessImplicitFrameworkDefine);$(ImplicitFrameworkDefine);$(BackwardsCompatFrameworkDefine) + + + + + + + + $(WarningsAsErrors);SYSLIB0011 + + + + + + + + + + + + <_NoneAnalysisLevel>4.0 + + <_LatestAnalysisLevel>9.0 + <_PreviewAnalysisLevel>10.0 + latest + $(_TargetFrameworkVersionWithoutV) + + $([System.Text.RegularExpressions.Regex]::Replace($(AnalysisLevel), '-(.)*', '')) + $([System.Text.RegularExpressions.Regex]::Replace($(AnalysisLevel), '$(AnalysisLevelPrefix)-', '')) + + $(_NoneAnalysisLevel) + $(_LatestAnalysisLevel) + $(_PreviewAnalysisLevel) + + $(AnalysisLevelPrefix) + $(AnalysisLevel) + + + + 9999 + + 4 + + $(_TargetFrameworkVersionWithoutV.Substring(0, 1)) + + + + + true + + true + + true + + true + + false + + + + false + false + false + false + false + + + + + + + + <_NETAnalyzersSDKAssemblyVersion>9.0.0 + + + + CA1000;CA1001;CA1002;CA1003;CA1005;CA1008;CA1010;CA1012;CA1014;CA1016;CA1017;CA1018;CA1019;CA1021;CA1024;CA1027;CA1028;CA1030;CA1031;CA1032;CA1033;CA1034;CA1036;CA1040;CA1041;CA1043;CA1044;CA1045;CA1046;CA1047;CA1050;CA1051;CA1052;CA1054;CA1055;CA1056;CA1058;CA1060;CA1061;CA1062;CA1063;CA1064;CA1065;CA1066;CA1067;CA1068;CA1069;CA1070;CA1200;CA1303;CA1304;CA1305;CA1307;CA1308;CA1309;CA1310;CA1311;CA1401;CA1416;CA1417;CA1418;CA1419;CA1420;CA1421;CA1422;CA1501;CA1502;CA1505;CA1506;CA1507;CA1508;CA1509;CA1510;CA1511;CA1512;CA1513;CA1514;CA1515;CA1700;CA1707;CA1708;CA1710;CA1711;CA1712;CA1713;CA1715;CA1716;CA1720;CA1721;CA1724;CA1725;CA1727;CA1802;CA1805;CA1806;CA1810;CA1812;CA1813;CA1814;CA1815;CA1816;CA1819;CA1820;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1827;CA1828;CA1829;CA1830;CA1831;CA1832;CA1833;CA1834;CA1835;CA1836;CA1837;CA1838;CA1839;CA1840;CA1841;CA1842;CA1843;CA1844;CA1845;CA1846;CA1847;CA1848;CA1849;CA1850;CA1851;CA1852;CA1853;CA1854;CA1855;CA1856;CA1857;CA1858;CA1859;CA1860;CA1861;CA1862;CA1863;CA1864;CA1865;CA1866;CA1867;CA1868;CA1869;CA1870;CA1871;CA1872;CA2000;CA2002;CA2007;CA2008;CA2009;CA2011;CA2012;CA2013;CA2014;CA2015;CA2016;CA2017;CA2018;CA2019;CA2020;CA2021;CA2022;CA2100;CA2101;CA2119;CA2153;CA2200;CA2201;CA2207;CA2208;CA2211;CA2213;CA2214;CA2215;CA2216;CA2217;CA2218;CA2219;CA2224;CA2225;CA2226;CA2227;CA2231;CA2234;CA2235;CA2237;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2251;CA2252;CA2253;CA2254;CA2255;CA2256;CA2257;CA2258;CA2259;CA2260;CA2261;CA2262;CA2263;CA2264;CA2265;CA2300;CA2301;CA2302;CA2305;CA2310;CA2311;CA2312;CA2315;CA2321;CA2322;CA2326;CA2327;CA2328;CA2329;CA2330;CA2350;CA2351;CA2352;CA2353;CA2354;CA2355;CA2356;CA2361;CA2362;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3075;CA3076;CA3077;CA3147;CA5350;CA5351;CA5358;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;CA5404;CA5405 + $(CodeAnalysisTreatWarningsAsErrors) + $(WarningsNotAsErrors);$(CodeAnalysisRuleIds) + + + + + + + + $([System.Text.RegularExpressions.Regex]::Replace($(EffectiveAnalysisLevel), '(.0)*$', '')) + + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzers>$(AnalysisLevelSuffix) + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzers Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzers)' == ''">$(AnalysisMode) + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzers Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzers)' == 'AllEnabledByDefault'">All + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzers Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzers)' == 'AllDisabledByDefault'">None + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzers Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzers)' == ''">Default + + + $(CodeAnalysisTreatWarningsAsErrors) + + <_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzers_WarnAsErrorSuffix Condition="'$(EffectiveCodeAnalysisTreatWarningsAsErrors)' == 'true'">_warnaserror + + <_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzers Condition="'$(MicrosoftCodeAnalysisNetAnalyzersRulesVersion)' != ''">AnalysisLevel_$(MicrosoftCodeAnalysisNetAnalyzersRulesVersion.Replace(".","_"))_$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzers)$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzers_WarnAsErrorSuffix).globalconfig + <_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzers>$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzers.ToLowerInvariant()) + <_GlobalAnalyzerConfigDir_MicrosoftCodeAnalysisNetAnalyzers Condition="'$(_GlobalAnalyzerConfigDir_MicrosoftCodeAnalysisNetAnalyzers)' == ''">$(MSBuildThisFileDirectory)config + <_GlobalAnalyzerConfigFile_MicrosoftCodeAnalysisNetAnalyzers Condition="'$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzers)' != ''">$(_GlobalAnalyzerConfigDir_MicrosoftCodeAnalysisNetAnalyzers)\$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzers) + + + + + + + + + + $(AnalysisLevel) + + $([System.Text.RegularExpressions.Regex]::Replace($(AnalysisLevelDesign), '-(.)*', '')) + $([System.Text.RegularExpressions.Regex]::Replace($(AnalysisLevelDesign), '$(AnalysisLevelPrefixDesign)-', '')) + + $(_NoneAnalysisLevel) + $(_LatestAnalysisLevel) + $(_PreviewAnalysisLevel) + + $(AnalysisLevelPrefixDesign) + $(AnalysisLevelDesign) + + $([System.Text.RegularExpressions.Regex]::Replace($(EffectiveAnalysisLevelDesign), '(.0)*$', '')) + + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersDesign>$(AnalysisLevelSuffixDesign) + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersDesign Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersDesign)' == ''">$(AnalysisModeDesign) + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersDesign Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersDesign)' == 'AllEnabledByDefault'">All + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersDesign Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersDesign)' == 'AllDisabledByDefault'">None + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersDesign Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersDesign)' == ''">Default + + + $(CodeAnalysisTreatWarningsAsErrors) + + <_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersDesign_WarnAsErrorSuffix Condition="'$(EffectiveCodeAnalysisTreatWarningsAsErrors)' == 'true'">_warnaserror + + <_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersDesign Condition="'$(MicrosoftCodeAnalysisNetAnalyzersDesignRulesVersion)' != ''">AnalysisLevelDesign_$(MicrosoftCodeAnalysisNetAnalyzersDesignRulesVersion.Replace(".","_"))_$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersDesign)$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersDesign_WarnAsErrorSuffix).globalconfig + <_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersDesign>$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersDesign.ToLowerInvariant()) + <_GlobalAnalyzerConfigDir_MicrosoftCodeAnalysisNetAnalyzersDesign Condition="'$(_GlobalAnalyzerConfigDir_MicrosoftCodeAnalysisNetAnalyzersDesign)' == ''">$(MSBuildThisFileDirectory)config + <_GlobalAnalyzerConfigFile_MicrosoftCodeAnalysisNetAnalyzersDesign Condition="'$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersDesign)' != ''">$(_GlobalAnalyzerConfigDir_MicrosoftCodeAnalysisNetAnalyzersDesign)\$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersDesign) + + + + + + + + + + $(AnalysisLevel) + + $([System.Text.RegularExpressions.Regex]::Replace($(AnalysisLevelDocumentation), '-(.)*', '')) + $([System.Text.RegularExpressions.Regex]::Replace($(AnalysisLevelDocumentation), '$(AnalysisLevelPrefixDocumentation)-', '')) + + $(_NoneAnalysisLevel) + $(_LatestAnalysisLevel) + $(_PreviewAnalysisLevel) + + $(AnalysisLevelPrefixDocumentation) + $(AnalysisLevelDocumentation) + + $([System.Text.RegularExpressions.Regex]::Replace($(EffectiveAnalysisLevelDocumentation), '(.0)*$', '')) + + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersDocumentation>$(AnalysisLevelSuffixDocumentation) + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersDocumentation Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersDocumentation)' == ''">$(AnalysisModeDocumentation) + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersDocumentation Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersDocumentation)' == 'AllEnabledByDefault'">All + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersDocumentation Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersDocumentation)' == 'AllDisabledByDefault'">None + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersDocumentation Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersDocumentation)' == ''">Default + + + $(CodeAnalysisTreatWarningsAsErrors) + + <_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersDocumentation_WarnAsErrorSuffix Condition="'$(EffectiveCodeAnalysisTreatWarningsAsErrors)' == 'true'">_warnaserror + + <_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersDocumentation Condition="'$(MicrosoftCodeAnalysisNetAnalyzersDocumentationRulesVersion)' != ''">AnalysisLevelDocumentation_$(MicrosoftCodeAnalysisNetAnalyzersDocumentationRulesVersion.Replace(".","_"))_$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersDocumentation)$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersDocumentation_WarnAsErrorSuffix).globalconfig + <_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersDocumentation>$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersDocumentation.ToLowerInvariant()) + <_GlobalAnalyzerConfigDir_MicrosoftCodeAnalysisNetAnalyzersDocumentation Condition="'$(_GlobalAnalyzerConfigDir_MicrosoftCodeAnalysisNetAnalyzersDocumentation)' == ''">$(MSBuildThisFileDirectory)config + <_GlobalAnalyzerConfigFile_MicrosoftCodeAnalysisNetAnalyzersDocumentation Condition="'$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersDocumentation)' != ''">$(_GlobalAnalyzerConfigDir_MicrosoftCodeAnalysisNetAnalyzersDocumentation)\$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersDocumentation) + + + + + + + + + + $(AnalysisLevel) + + $([System.Text.RegularExpressions.Regex]::Replace($(AnalysisLevelGlobalization), '-(.)*', '')) + $([System.Text.RegularExpressions.Regex]::Replace($(AnalysisLevelGlobalization), '$(AnalysisLevelPrefixGlobalization)-', '')) + + $(_NoneAnalysisLevel) + $(_LatestAnalysisLevel) + $(_PreviewAnalysisLevel) + + $(AnalysisLevelPrefixGlobalization) + $(AnalysisLevelGlobalization) + + $([System.Text.RegularExpressions.Regex]::Replace($(EffectiveAnalysisLevelGlobalization), '(.0)*$', '')) + + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersGlobalization>$(AnalysisLevelSuffixGlobalization) + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersGlobalization Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersGlobalization)' == ''">$(AnalysisModeGlobalization) + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersGlobalization Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersGlobalization)' == 'AllEnabledByDefault'">All + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersGlobalization Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersGlobalization)' == 'AllDisabledByDefault'">None + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersGlobalization Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersGlobalization)' == ''">Default + + + $(CodeAnalysisTreatWarningsAsErrors) + + <_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersGlobalization_WarnAsErrorSuffix Condition="'$(EffectiveCodeAnalysisTreatWarningsAsErrors)' == 'true'">_warnaserror + + <_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersGlobalization Condition="'$(MicrosoftCodeAnalysisNetAnalyzersGlobalizationRulesVersion)' != ''">AnalysisLevelGlobalization_$(MicrosoftCodeAnalysisNetAnalyzersGlobalizationRulesVersion.Replace(".","_"))_$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersGlobalization)$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersGlobalization_WarnAsErrorSuffix).globalconfig + <_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersGlobalization>$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersGlobalization.ToLowerInvariant()) + <_GlobalAnalyzerConfigDir_MicrosoftCodeAnalysisNetAnalyzersGlobalization Condition="'$(_GlobalAnalyzerConfigDir_MicrosoftCodeAnalysisNetAnalyzersGlobalization)' == ''">$(MSBuildThisFileDirectory)config + <_GlobalAnalyzerConfigFile_MicrosoftCodeAnalysisNetAnalyzersGlobalization Condition="'$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersGlobalization)' != ''">$(_GlobalAnalyzerConfigDir_MicrosoftCodeAnalysisNetAnalyzersGlobalization)\$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersGlobalization) + + + + + + + + + + $(AnalysisLevel) + + $([System.Text.RegularExpressions.Regex]::Replace($(AnalysisLevelInteroperability), '-(.)*', '')) + $([System.Text.RegularExpressions.Regex]::Replace($(AnalysisLevelInteroperability), '$(AnalysisLevelPrefixInteroperability)-', '')) + + $(_NoneAnalysisLevel) + $(_LatestAnalysisLevel) + $(_PreviewAnalysisLevel) + + $(AnalysisLevelPrefixInteroperability) + $(AnalysisLevelInteroperability) + + $([System.Text.RegularExpressions.Regex]::Replace($(EffectiveAnalysisLevelInteroperability), '(.0)*$', '')) + + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersInteroperability>$(AnalysisLevelSuffixInteroperability) + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersInteroperability Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersInteroperability)' == ''">$(AnalysisModeInteroperability) + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersInteroperability Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersInteroperability)' == 'AllEnabledByDefault'">All + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersInteroperability Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersInteroperability)' == 'AllDisabledByDefault'">None + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersInteroperability Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersInteroperability)' == ''">Default + + + $(CodeAnalysisTreatWarningsAsErrors) + + <_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersInteroperability_WarnAsErrorSuffix Condition="'$(EffectiveCodeAnalysisTreatWarningsAsErrors)' == 'true'">_warnaserror + + <_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersInteroperability Condition="'$(MicrosoftCodeAnalysisNetAnalyzersInteroperabilityRulesVersion)' != ''">AnalysisLevelInteroperability_$(MicrosoftCodeAnalysisNetAnalyzersInteroperabilityRulesVersion.Replace(".","_"))_$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersInteroperability)$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersInteroperability_WarnAsErrorSuffix).globalconfig + <_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersInteroperability>$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersInteroperability.ToLowerInvariant()) + <_GlobalAnalyzerConfigDir_MicrosoftCodeAnalysisNetAnalyzersInteroperability Condition="'$(_GlobalAnalyzerConfigDir_MicrosoftCodeAnalysisNetAnalyzersInteroperability)' == ''">$(MSBuildThisFileDirectory)config + <_GlobalAnalyzerConfigFile_MicrosoftCodeAnalysisNetAnalyzersInteroperability Condition="'$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersInteroperability)' != ''">$(_GlobalAnalyzerConfigDir_MicrosoftCodeAnalysisNetAnalyzersInteroperability)\$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersInteroperability) + + + + + + + + + + $(AnalysisLevel) + + $([System.Text.RegularExpressions.Regex]::Replace($(AnalysisLevelMaintainability), '-(.)*', '')) + $([System.Text.RegularExpressions.Regex]::Replace($(AnalysisLevelMaintainability), '$(AnalysisLevelPrefixMaintainability)-', '')) + + $(_NoneAnalysisLevel) + $(_LatestAnalysisLevel) + $(_PreviewAnalysisLevel) + + $(AnalysisLevelPrefixMaintainability) + $(AnalysisLevelMaintainability) + + $([System.Text.RegularExpressions.Regex]::Replace($(EffectiveAnalysisLevelMaintainability), '(.0)*$', '')) + + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersMaintainability>$(AnalysisLevelSuffixMaintainability) + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersMaintainability Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersMaintainability)' == ''">$(AnalysisModeMaintainability) + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersMaintainability Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersMaintainability)' == 'AllEnabledByDefault'">All + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersMaintainability Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersMaintainability)' == 'AllDisabledByDefault'">None + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersMaintainability Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersMaintainability)' == ''">Default + + + $(CodeAnalysisTreatWarningsAsErrors) + + <_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersMaintainability_WarnAsErrorSuffix Condition="'$(EffectiveCodeAnalysisTreatWarningsAsErrors)' == 'true'">_warnaserror + + <_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersMaintainability Condition="'$(MicrosoftCodeAnalysisNetAnalyzersMaintainabilityRulesVersion)' != ''">AnalysisLevelMaintainability_$(MicrosoftCodeAnalysisNetAnalyzersMaintainabilityRulesVersion.Replace(".","_"))_$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersMaintainability)$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersMaintainability_WarnAsErrorSuffix).globalconfig + <_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersMaintainability>$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersMaintainability.ToLowerInvariant()) + <_GlobalAnalyzerConfigDir_MicrosoftCodeAnalysisNetAnalyzersMaintainability Condition="'$(_GlobalAnalyzerConfigDir_MicrosoftCodeAnalysisNetAnalyzersMaintainability)' == ''">$(MSBuildThisFileDirectory)config + <_GlobalAnalyzerConfigFile_MicrosoftCodeAnalysisNetAnalyzersMaintainability Condition="'$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersMaintainability)' != ''">$(_GlobalAnalyzerConfigDir_MicrosoftCodeAnalysisNetAnalyzersMaintainability)\$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersMaintainability) + + + + + + + + + + $(AnalysisLevel) + + $([System.Text.RegularExpressions.Regex]::Replace($(AnalysisLevelNaming), '-(.)*', '')) + $([System.Text.RegularExpressions.Regex]::Replace($(AnalysisLevelNaming), '$(AnalysisLevelPrefixNaming)-', '')) + + $(_NoneAnalysisLevel) + $(_LatestAnalysisLevel) + $(_PreviewAnalysisLevel) + + $(AnalysisLevelPrefixNaming) + $(AnalysisLevelNaming) + + $([System.Text.RegularExpressions.Regex]::Replace($(EffectiveAnalysisLevelNaming), '(.0)*$', '')) + + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersNaming>$(AnalysisLevelSuffixNaming) + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersNaming Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersNaming)' == ''">$(AnalysisModeNaming) + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersNaming Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersNaming)' == 'AllEnabledByDefault'">All + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersNaming Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersNaming)' == 'AllDisabledByDefault'">None + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersNaming Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersNaming)' == ''">Default + + + $(CodeAnalysisTreatWarningsAsErrors) + + <_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersNaming_WarnAsErrorSuffix Condition="'$(EffectiveCodeAnalysisTreatWarningsAsErrors)' == 'true'">_warnaserror + + <_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersNaming Condition="'$(MicrosoftCodeAnalysisNetAnalyzersNamingRulesVersion)' != ''">AnalysisLevelNaming_$(MicrosoftCodeAnalysisNetAnalyzersNamingRulesVersion.Replace(".","_"))_$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersNaming)$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersNaming_WarnAsErrorSuffix).globalconfig + <_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersNaming>$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersNaming.ToLowerInvariant()) + <_GlobalAnalyzerConfigDir_MicrosoftCodeAnalysisNetAnalyzersNaming Condition="'$(_GlobalAnalyzerConfigDir_MicrosoftCodeAnalysisNetAnalyzersNaming)' == ''">$(MSBuildThisFileDirectory)config + <_GlobalAnalyzerConfigFile_MicrosoftCodeAnalysisNetAnalyzersNaming Condition="'$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersNaming)' != ''">$(_GlobalAnalyzerConfigDir_MicrosoftCodeAnalysisNetAnalyzersNaming)\$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersNaming) + + + + + + + + + + $(AnalysisLevel) + + $([System.Text.RegularExpressions.Regex]::Replace($(AnalysisLevelPerformance), '-(.)*', '')) + $([System.Text.RegularExpressions.Regex]::Replace($(AnalysisLevelPerformance), '$(AnalysisLevelPrefixPerformance)-', '')) + + $(_NoneAnalysisLevel) + $(_LatestAnalysisLevel) + $(_PreviewAnalysisLevel) + + $(AnalysisLevelPrefixPerformance) + $(AnalysisLevelPerformance) + + $([System.Text.RegularExpressions.Regex]::Replace($(EffectiveAnalysisLevelPerformance), '(.0)*$', '')) + + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersPerformance>$(AnalysisLevelSuffixPerformance) + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersPerformance Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersPerformance)' == ''">$(AnalysisModePerformance) + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersPerformance Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersPerformance)' == 'AllEnabledByDefault'">All + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersPerformance Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersPerformance)' == 'AllDisabledByDefault'">None + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersPerformance Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersPerformance)' == ''">Default + + + $(CodeAnalysisTreatWarningsAsErrors) + + <_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersPerformance_WarnAsErrorSuffix Condition="'$(EffectiveCodeAnalysisTreatWarningsAsErrors)' == 'true'">_warnaserror + + <_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersPerformance Condition="'$(MicrosoftCodeAnalysisNetAnalyzersPerformanceRulesVersion)' != ''">AnalysisLevelPerformance_$(MicrosoftCodeAnalysisNetAnalyzersPerformanceRulesVersion.Replace(".","_"))_$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersPerformance)$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersPerformance_WarnAsErrorSuffix).globalconfig + <_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersPerformance>$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersPerformance.ToLowerInvariant()) + <_GlobalAnalyzerConfigDir_MicrosoftCodeAnalysisNetAnalyzersPerformance Condition="'$(_GlobalAnalyzerConfigDir_MicrosoftCodeAnalysisNetAnalyzersPerformance)' == ''">$(MSBuildThisFileDirectory)config + <_GlobalAnalyzerConfigFile_MicrosoftCodeAnalysisNetAnalyzersPerformance Condition="'$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersPerformance)' != ''">$(_GlobalAnalyzerConfigDir_MicrosoftCodeAnalysisNetAnalyzersPerformance)\$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersPerformance) + + + + + + + + + + $(AnalysisLevel) + + $([System.Text.RegularExpressions.Regex]::Replace($(AnalysisLevelReliability), '-(.)*', '')) + $([System.Text.RegularExpressions.Regex]::Replace($(AnalysisLevelReliability), '$(AnalysisLevelPrefixReliability)-', '')) + + $(_NoneAnalysisLevel) + $(_LatestAnalysisLevel) + $(_PreviewAnalysisLevel) + + $(AnalysisLevelPrefixReliability) + $(AnalysisLevelReliability) + + $([System.Text.RegularExpressions.Regex]::Replace($(EffectiveAnalysisLevelReliability), '(.0)*$', '')) + + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersReliability>$(AnalysisLevelSuffixReliability) + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersReliability Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersReliability)' == ''">$(AnalysisModeReliability) + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersReliability Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersReliability)' == 'AllEnabledByDefault'">All + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersReliability Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersReliability)' == 'AllDisabledByDefault'">None + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersReliability Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersReliability)' == ''">Default + + + $(CodeAnalysisTreatWarningsAsErrors) + + <_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersReliability_WarnAsErrorSuffix Condition="'$(EffectiveCodeAnalysisTreatWarningsAsErrors)' == 'true'">_warnaserror + + <_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersReliability Condition="'$(MicrosoftCodeAnalysisNetAnalyzersReliabilityRulesVersion)' != ''">AnalysisLevelReliability_$(MicrosoftCodeAnalysisNetAnalyzersReliabilityRulesVersion.Replace(".","_"))_$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersReliability)$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersReliability_WarnAsErrorSuffix).globalconfig + <_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersReliability>$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersReliability.ToLowerInvariant()) + <_GlobalAnalyzerConfigDir_MicrosoftCodeAnalysisNetAnalyzersReliability Condition="'$(_GlobalAnalyzerConfigDir_MicrosoftCodeAnalysisNetAnalyzersReliability)' == ''">$(MSBuildThisFileDirectory)config + <_GlobalAnalyzerConfigFile_MicrosoftCodeAnalysisNetAnalyzersReliability Condition="'$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersReliability)' != ''">$(_GlobalAnalyzerConfigDir_MicrosoftCodeAnalysisNetAnalyzersReliability)\$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersReliability) + + + + + + + + + + $(AnalysisLevel) + + $([System.Text.RegularExpressions.Regex]::Replace($(AnalysisLevelSecurity), '-(.)*', '')) + $([System.Text.RegularExpressions.Regex]::Replace($(AnalysisLevelSecurity), '$(AnalysisLevelPrefixSecurity)-', '')) + + $(_NoneAnalysisLevel) + $(_LatestAnalysisLevel) + $(_PreviewAnalysisLevel) + + $(AnalysisLevelPrefixSecurity) + $(AnalysisLevelSecurity) + + $([System.Text.RegularExpressions.Regex]::Replace($(EffectiveAnalysisLevelSecurity), '(.0)*$', '')) + + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersSecurity>$(AnalysisLevelSuffixSecurity) + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersSecurity Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersSecurity)' == ''">$(AnalysisModeSecurity) + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersSecurity Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersSecurity)' == 'AllEnabledByDefault'">All + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersSecurity Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersSecurity)' == 'AllDisabledByDefault'">None + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersSecurity Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersSecurity)' == ''">Default + + + $(CodeAnalysisTreatWarningsAsErrors) + + <_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersSecurity_WarnAsErrorSuffix Condition="'$(EffectiveCodeAnalysisTreatWarningsAsErrors)' == 'true'">_warnaserror + + <_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersSecurity Condition="'$(MicrosoftCodeAnalysisNetAnalyzersSecurityRulesVersion)' != ''">AnalysisLevelSecurity_$(MicrosoftCodeAnalysisNetAnalyzersSecurityRulesVersion.Replace(".","_"))_$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersSecurity)$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersSecurity_WarnAsErrorSuffix).globalconfig + <_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersSecurity>$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersSecurity.ToLowerInvariant()) + <_GlobalAnalyzerConfigDir_MicrosoftCodeAnalysisNetAnalyzersSecurity Condition="'$(_GlobalAnalyzerConfigDir_MicrosoftCodeAnalysisNetAnalyzersSecurity)' == ''">$(MSBuildThisFileDirectory)config + <_GlobalAnalyzerConfigFile_MicrosoftCodeAnalysisNetAnalyzersSecurity Condition="'$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersSecurity)' != ''">$(_GlobalAnalyzerConfigDir_MicrosoftCodeAnalysisNetAnalyzersSecurity)\$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersSecurity) + + + + + + + + + + $(AnalysisLevel) + + $([System.Text.RegularExpressions.Regex]::Replace($(AnalysisLevelUsage), '-(.)*', '')) + $([System.Text.RegularExpressions.Regex]::Replace($(AnalysisLevelUsage), '$(AnalysisLevelPrefixUsage)-', '')) + + $(_NoneAnalysisLevel) + $(_LatestAnalysisLevel) + $(_PreviewAnalysisLevel) + + $(AnalysisLevelPrefixUsage) + $(AnalysisLevelUsage) + + $([System.Text.RegularExpressions.Regex]::Replace($(EffectiveAnalysisLevelUsage), '(.0)*$', '')) + + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersUsage>$(AnalysisLevelSuffixUsage) + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersUsage Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersUsage)' == ''">$(AnalysisModeUsage) + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersUsage Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersUsage)' == 'AllEnabledByDefault'">All + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersUsage Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersUsage)' == 'AllDisabledByDefault'">None + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersUsage Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersUsage)' == ''">Default + + + $(CodeAnalysisTreatWarningsAsErrors) + + <_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersUsage_WarnAsErrorSuffix Condition="'$(EffectiveCodeAnalysisTreatWarningsAsErrors)' == 'true'">_warnaserror + + <_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersUsage Condition="'$(MicrosoftCodeAnalysisNetAnalyzersUsageRulesVersion)' != ''">AnalysisLevelUsage_$(MicrosoftCodeAnalysisNetAnalyzersUsageRulesVersion.Replace(".","_"))_$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisNetAnalyzersUsage)$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersUsage_WarnAsErrorSuffix).globalconfig + <_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersUsage>$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersUsage.ToLowerInvariant()) + <_GlobalAnalyzerConfigDir_MicrosoftCodeAnalysisNetAnalyzersUsage Condition="'$(_GlobalAnalyzerConfigDir_MicrosoftCodeAnalysisNetAnalyzersUsage)' == ''">$(MSBuildThisFileDirectory)config + <_GlobalAnalyzerConfigFile_MicrosoftCodeAnalysisNetAnalyzersUsage Condition="'$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersUsage)' != ''">$(_GlobalAnalyzerConfigDir_MicrosoftCodeAnalysisNetAnalyzersUsage)\$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisNetAnalyzersUsage) + + + + + + + + + + + + + + + + + + <_SupportedPlatformList>@(SupportedPlatform, ',') + + + + + + + + + $(CodeAnalysisTreatWarningsAsErrors) + $(WarningsNotAsErrors);$(CodeAnalysisRuleIds) + + + + + + + + + $(AnalysisLevel) + + $([System.Text.RegularExpressions.Regex]::Replace($(AnalysisLevelStyle), '-(.)*', '')) + $([System.Text.RegularExpressions.Regex]::Replace($(AnalysisLevelStyle), '$(AnalysisLevelPrefixStyle)-', '')) + + $(AnalysisLevelSuffix) + + $(AnalysisMode) + + $(_NoneAnalysisLevel) + $(_LatestAnalysisLevel) + $(_PreviewAnalysisLevel) + + $(AnalysisLevelPrefixStyle) + $(AnalysisLevelStyle) + + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisCSharpCodeStyle>$(AnalysisModeStyle) + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisCSharpCodeStyle Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisCSharpCodeStyle)' == ''">$(AnalysisLevelSuffixStyle) + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisCSharpCodeStyle Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisCSharpCodeStyle)' == 'AllEnabledByDefault'">All + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisCSharpCodeStyle Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisCSharpCodeStyle)' == 'AllDisabledByDefault'">None + <_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisCSharpCodeStyle Condition="'$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisCSharpCodeStyle)' == ''">Default + <_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisCSharpCodeStyle>AnalysisLevelStyle_$(_GlobalAnalyzerConfigAnalysisMode_MicrosoftCodeAnalysisCSharpCodeStyle).globalconfig + <_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisCSharpCodeStyle>$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisCSharpCodeStyle.ToLowerInvariant()) + <_GlobalAnalyzerConfigDir_MicrosoftCodeAnalysisCSharpCodeStyle Condition="'$(_GlobalAnalyzerConfigDir_MicrosoftCodeAnalysisCSharpCodeStyle)' == ''">$(MSBuildThisFileDirectory)config + <_GlobalAnalyzerConfigFile_MicrosoftCodeAnalysisCSharpCodeStyle Condition="'$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisCSharpCodeStyle)' != ''">$(_GlobalAnalyzerConfigDir_MicrosoftCodeAnalysisCSharpCodeStyle)\$(_GlobalAnalyzerConfigFileName_MicrosoftCodeAnalysisCSharpCodeStyle) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + true + + + $(AfterMicrosoftNETSdkTargets);$(MSBuildThisFileDirectory)../../Microsoft.NET.Sdk.WindowsDesktop/targets/Microsoft.NET.Sdk.WindowsDesktop.targets + + + + + + + + + + 0.0 + $(TargetPlatformIdentifier),Version=$(TargetPlatformVersion) + $([Microsoft.Build.Utilities.ToolLocationHelper]::GetPlatformSDKDisplayName($(TargetPlatformIdentifier), $(TargetPlatformVersion))) + + + + $(TargetPlatformVersion) + + + + + + + + + + + <_ResizetizerTaskAssemblyName>$(MSBuildThisFileDirectory)\Microsoft.Maui.Resizetizer.dll + + + + + + + + + + + + + + $(CleanDependsOn); + _CleanResizetizer; + + <_ResizetizerPlatformIdentifier>$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) + <_ResizetizerNoTargetPlatform Condition="'$(_ResizetizerPlatformIdentifier)' == ''">True + <_ResizetizerPlatformIsAndroid Condition="'$(_ResizetizerPlatformIdentifier)' == 'android'">True + <_ResizetizerPlatformIsiOS Condition="'$(_ResizetizerPlatformIdentifier)' == 'ios'">True + <_ResizetizerPlatformIsMacCatalyst Condition="'$(_ResizetizerPlatformIdentifier)' == 'maccatalyst'">True + <_ResizetizerPlatformIsmacOS Condition="'$(_ResizetizerPlatformIdentifier)' == 'macos'">True + <_ResizetizerPlatformIstvOS Condition="'$(_ResizetizerPlatformIdentifier)' == 'tvos'">True + <_ResizetizerPlatformIsWindows Condition="$(_ResizetizerPlatformIdentifier.Contains('windows')) == 'True'">True + <_ResizetizerPlatformIsTizen Condition="'$(_ResizetizerPlatformIdentifier)' == 'tizen'">True + <_ResizetizerIntermediateOutputPath Condition=" '$(_ResizetizerIntermediateOutputPath)' == '' ">$(IntermediateOutputPath) + <_ResizetizerInputsFile>$(_ResizetizerIntermediateOutputPath)mauiimage.inputs + <_ResizetizerOutputsFile>$(_ResizetizerIntermediateOutputPath)mauiimage.outputs + <_ResizetizerStampFile>$(_ResizetizerIntermediateOutputPath)mauiimage.stamp + <_MauiFontInputsFile>$(_ResizetizerIntermediateOutputPath)mauifont.inputs + <_MauiFontStampFile>$(_ResizetizerIntermediateOutputPath)mauifont.stamp + <_MauiSplashInputsFile>$(_ResizetizerIntermediateOutputPath)mauisplash.inputs + <_MauiSplashStampFile>$(_ResizetizerIntermediateOutputPath)mauisplash.stamp + <_MauiManifestStampFile>$(_ResizetizerIntermediateOutputPath)mauimanifest.stamp + <_ResizetizerIntermediateOutputRoot>$(_ResizetizerIntermediateOutputPath)resizetizer\ + <_MauiIntermediateImages>$(_ResizetizerIntermediateOutputRoot)r\ + <_MauiIntermediateFonts>$(_ResizetizerIntermediateOutputRoot)f\ + <_MauiIntermediateSplashScreen>$(_ResizetizerIntermediateOutputRoot)sp\ + <_MauiIntermediateManifest>$(_ResizetizerIntermediateOutputRoot)m\ + False + <_ResizetizerDefaultInvalidFilenamesErrorMessage>One or more invalid file names were detected. File names must be lowercase, start and end with a letter character, and contain only alphanumeric characters or underscores: + <_ResizetizerDefaultDuplicateFilenamesErrorMessage>One or more duplicate file names were detected. All image output filenames must be unique: + <_ResizetizerThrowsErrorOnInvalidFilename>true + <_ResizetizerThrowsErrorOnInvalidFilename Condition="'$(ResizetizerErrorOnInvalidFilename)' == 'false'">false + <_ResizetizerThrowsErrorOnDuplicateOutputFilename>true + <_ResizetizerThrowsErrorOnDuplicateOutputFilename Condition="'$(ResizetizerErrorOnDuplicateOutputFilename)' == 'false'">false + true + true + true + true + + + <_ResizetizerIsNetCore>True + <_ResizetizerIsAndroidApp Condition=" '$(_ResizetizerPlatformIsAndroid)' == 'True' And '$(AndroidApplication)' == 'True'">True + <_ResizetizerIsiOSApp Condition="( '$(_ResizetizerPlatformIsiOS)' == 'True' OR '$(_ResizetizerPlatformIsMacCatalyst)' == 'True' ) And ('$(OutputType)' == 'Exe' Or '$(IsAppExtension)' == 'True')">True + <_ResizetizerIsiOSSpecificApp Condition=" '$(_ResizetizerPlatformIsiOS)' == 'True' And ('$(OutputType)' == 'Exe' Or '$(IsAppExtension)' == 'True')">True + <_ResizetizerIsWPFApp Condition="'$(IsApplication)' == 'True' And '$(NuGetRuntimeIdentifier)' == 'win' And '$(_ResizetizerPlatformIsWindows)' == 'True'">True + <_ResizetizerIsWindowsAppSdk Condition="'$(MicrosoftWindowsAppSDKPackageDir)' != '' And '$(_ResizetizerPlatformIsWindows)' == 'True' And ('$(OutputType)' == 'WinExe' Or '$(OutputType)' == 'Exe')">True + <_ResizetizerIsTizenApp Condition="'$(_ResizetizerPlatformIsTizen)' == 'True' And ( '$(OutputType)' == 'Exe' )">True + + + <_ResizetizerIsCompatibleApp>True + + $(ResizetizeDependsOnTargets); + ResizetizeCollectItems; + ProcessMauiSplashScreens; + _ReadResizetizeImagesOutputs; + + + $(ProcessMauiFontsDependsOnTargets); + ResizetizeCollectItems; + ProcessMauiAssets; + ProcessMauiSplashScreens; + + + + + ios + + true + + $(CollectBundleResourcesDependsOn); + ResizetizeCollectItems; + + + $(CompileImageAssetsDependsOn); + ResizetizeCollectItems; + + + $(ResizetizeAfterTargets); + ResizetizeCollectItems; + + + ProcessMauiFonts; + ProcessMauiSplashScreens; + $(CollectAppManifestsDependsOn) + + + + + android + + $(ResizetizeCollectItemsBeforeTargets); + _ComputeAndroidResourcePaths; + + + $(ResizetizeAfterTargets); + ResizetizeCollectItems; + + + $(ProcessMauiFontsAfterTargets); + ResizetizeCollectItems; + + + + + uwp + + $(ResizetizeBeforeTargets); + AssignTargetPaths; + + + $(ProcessMauiFontsBeforeTargets); + AssignTargetPaths; + + + $(MauiGeneratePackageAppxManifestDependsOnTargets); + ResizetizeCollectItems; + + + + + wpf + + $(ResizetizeBeforeTargets); + FileClassification; + + + $(ProcessMauiFontsBeforeTargets); + FileClassification; + + + + + tizen + + $(ResizetizeBeforeTargets); + PrepareResources; + + + $(ResizetizeAfterTargets); + ResizetizeCollectItems; + + + $(ProcessMauiFontsAfterTargets); + ResizetizeCollectItems; + + + + + + + + + + + + + + + + + + + + + + + + + + <_ResizetizeCollectItemsProjectWithOIS Include="@(_ResolvedProjectReferencePaths->HasMetadata('OriginalProjectReferenceItemSpec'))" /> + + <_ResizetizeCollectItemsProjectWithItemSpec Include="@(_ResizetizeCollectItemsProjectWithOIS->'%(OriginalProjectReferenceItemSpec)')" /> + + <_ResizetizeCollectItemsProject Include="@(_ResizetizeCollectItemsProjectWithItemSpec->HasMetadata('NearestTargetFramework'))" /> + + <_ResizetizeCollectItemsProject Include="@(ProjectReference)" Exclude="@(_ResizetizeCollectItemsProject)" NearestTargetFramework="$(TargetFramework)" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_MauiSplashScreenWithHashes Update="@(_MauiSplashScreenWithHashes)" InputsFileHash="$(_MauiSplashInputsFileHash)" /> + + + + + + + + + + + <_MauiAssetWithLinkMetadata Include="@(MauiAsset)" Link="%(MauiAsset.LogicalName)" Condition="'%(MauiAsset.Link)' == '' And '%(MauiAsset.LogicalName)' != ''" /> + <_MauiAssetWithLinkMetadata Include="@(MauiAsset)" Condition="'%(MauiAsset.Link)' != '' Or '%(MauiAsset.LogicalName)' == ''" /> + + + + <_MauiAssetItemMetadata Condition="'$(_ResizetizerIsAndroidApp)' == 'True'">Link + <_MauiAssetItemMetadata Condition="'$(_ResizetizerIsiOSApp)' == 'True'">Link + <_MauiAssetItemMetadata Condition="'$(_ResizetizerIsWindowsAppSdk)' == 'True'">TargetPath + <_MauiAssetItemMetadata Condition="'$(_ResizetizerIsTizenApp)' == 'True'">TizenTpkFileName + + + + + + + + + + + + + + + + + + + + + <_MauiHasSplashScreens>false + <_MauiHasSplashScreens Condition="'@(MauiSplashScreen->Count())' != '0'">true + <_MauiShouldGenerateSplashScreen Condition="'$(_MauiHasSplashScreens)' == 'true'">true + <_MauiShouldGenerateSplashScreen Condition="'$(_ResizetizerIsiOSSpecificApp)' == 'True' and '$(_MauiHasSplashScreens)' != 'true' and '$(EnableBlankMauiSplashScreen)' == 'true'">true + + + + + + + <_MauiSplashAssets Include="$(_MauiIntermediateSplashScreen)**\*" /> + + $(_ResizetizerStampFile) + + + + + + <_MauiSplashScreenWithHashesInFilename Include="@(_MauiSplashScreenWithHashes->HasMetadata('Link'))" OriginalLink="%(_MauiSplashScreenWithHashes.Link)" Link="$([System.IO.Path]::GetFilenameWithoutExtension('%(_MauiSplashScreenWithHashes.Link)'))_%(InputsFileHash)$([System.IO.Path]::GetExtension('%(_MauiSplashScreenWithHashes.Link)'))" /> + <_MauiSplashScreenWithHashesInFilename Include="@(_MauiSplashScreenWithHashes)" Exclude="@(_MauiSplashScreenWithHashesInFilename)" Link="%(Filename)_%(InputsFileHash)%(Extension)" /> + + + + <_MauiIntermediateStoryboard>$(_MauiIntermediateSplashScreen)MauiSplash.storyboard + <_MauiIntermediatePList>$(_MauiIntermediateSplashScreen)MauiInfo.plist + + + + <_MauiSplashAssets Include="$(_MauiIntermediateSplashScreen)**\*" /> + <_MauiSplashStoryboard Include="$(_MauiIntermediateStoryboard)" /> + <_MauiSplashPList Include="$(_MauiIntermediatePList)" /> + <_MauiSplashImages Include="@(_MauiSplashAssets)" Exclude="@(_MauiSplashStoryboard);@(_MauiSplashPList)" /> + + + + + + + %(_MauiSplashImages.Filename)%(_MauiSplashImages.Extension) + %(_MauiSplashImages.Filename)%(_MauiSplashImages.Extension) + + + + + + <_MauiSplashAssets Include="$(_MauiIntermediateSplashScreen)**\*" /> + + %(_MauiSplashAssets.Filename)%(_MauiSplashAssets.Extension) + PreserveNewest + + + + + + <_MauiSplashAssets Include="$(_MauiIntermediateSplashScreen)**\*" /> + + <_MauiSplashScreens Include="$(_MauiIntermediateSplashScreen)splash\*" /> + + + + + + + + + + + + + + + <_MauiFontCopied Include="$(_MauiIntermediateFonts)*" /> + + + + + <_MauiFontBundleResource Include="@(_MauiFontCopied)"> + $([System.IO.Path]::GetFileName(%(_MauiFontCopied.Identity))) + $([System.IO.Path]::GetFileName(%(_MauiFontCopied.Identity))) + + + + + + + + <_MauiFontPListFiles Include="$(_MauiIntermediateFonts)MauiInfo.plist" Condition="Exists('$(_MauiIntermediateFonts)MauiInfo.plist')" /> + + + + + + + $([System.IO.Path]::GetFileName(%(_MauiFontCopied.Identity))) + + + + + + $([System.IO.Path]::GetFileName(%(_MauiFontCopied.Identity))) + PreserveNewest + + + + + + $([System.IO.Path]::GetFileName(%(_MauiFontCopied.Identity))) + $([System.IO.Path]::GetFileName(%(_MauiFontCopied.Identity))) + + + + + + + + + + + + + + + + + + + + + + <_MauiImageToProcess Include="@(MauiImage)" Condition=" '%(FileName)%(Extension)' != '.DS_Store' " /> + + + + + + + + + + <_ResizetizerCollectedImages Condition="'@(_CopiedResources->Count())' != '0'" Include="@(_CopiedResources)" /> + <_ResizetizerExistingImages Include="$(_MauiIntermediateImages)\**\*" /> + <_ResizetizerImagesToDelete Include="@(_ResizetizerExistingImages->'%(FullPath)')" /> + <_ResizetizerCollectedImages Condition="'@(_CopiedResources)' == ''" Include="@(_ResizetizerExistingImages->'%(FullPath)')" /> + <_ResizetizerImagesToDelete Remove="@(_ResizetizerCollectedImages)" /> + + + + + + + <_ResizetizerCollectedBundleResourceImages Include="@(_ResizetizerCollectedImages->'%(FullPath)')"> + %(_ResizetizerCollectedImages.Filename)%(_ResizetizerCollectedImages.Extension) + %(_ResizetizerCollectedImages.Filename)%(_ResizetizerCollectedImages.Extension) + + + + + Assets.xcassets\$([System.IO.Path]::GetFileName($([System.IO.Path]::GetDirectoryName(%(_ResizetizerCollectedBundleResourceImages.Identity)))))\%(_ResizetizerCollectedBundleResourceImages.Filename)%(_ResizetizerCollectedBundleResourceImages.Extension) + Assets.xcassets\$([System.IO.Path]::GetFileName($([System.IO.Path]::GetDirectoryName(%(_ResizetizerCollectedBundleResourceImages.Identity)))))\%(_ResizetizerCollectedBundleResourceImages.Filename)%(_ResizetizerCollectedBundleResourceImages.Extension) + Assets.xcassets\$([System.IO.Path]::GetFileName($([System.IO.Path]::GetDirectoryName(%(_ResizetizerCollectedBundleResourceImages.Identity)))))\%(_ResizetizerCollectedBundleResourceImages.Filename)%(_ResizetizerCollectedBundleResourceImages.Extension) + + + + + + + + $(_ResizetizerStampFile) + + + + + + + %(_ResizetizerCollectedImages.Filename)%(_ResizetizerCollectedImages.Extension) + PreserveNewest + + + <_MauiAppIconFile Include="@(_ResizetizerCollectedImages)" Condition="'%(Extension)' == '.ico'" /> + + + %(_MauiAppIconFile.Identity) + + + + + %(_ResizetizerCollectedImages.Filename)%(_ResizetizerCollectedImages.Extension) + %(_ResizetizerCollectedImages.Filename)%(_ResizetizerCollectedImages.Extension) + + + + + + $([System.IO.Path]::GetFullPath('$(_MauiIntermediateImages)')) + + + + + + $([MSBuild]::MakeRelative($(ResizetizerIntermediateOutputAbsolutePath), $([System.IO.Path]::GetFullPath('%(_ResizetizerCollectedImages.RelativeDir)')))) + + + + + + + + + + + + + + + + + <_MauiAppxManifest Include="@(AppxManifest)" /> + + + + + + + + <_MauiWindowsApplicationId Condition="'$(_MauiWindowsApplicationId)' == '' and '$(ApplicationIdGuid)' != ''">$(ApplicationIdGuid) + <_MauiWindowsApplicationId Condition="'$(_MauiWindowsApplicationId)' == '' and '$(ApplicationId)' != ''">$(ApplicationId) + + + + + + + + + <_MauiAppxManifest Remove="@(_MauiAppxManifest)" /> + <_MauiAppxManifest Include="$(_MauiIntermediateManifest)Package.appxmanifest" /> + + + + + + + + + + + + <_MauiAppxManifestContents>$([System.IO.File]::ReadAllText('$(_MauiIntermediateManifest)Package.appxmanifest')) + + + + + + + + + + + + + + + + <_Parameter1>Microsoft.Maui.ApplicationModel.AppInfo.PackageName + <_Parameter2>@(_MauiAppxManifestIdentity) + + + <_Parameter1>Microsoft.Maui.ApplicationModel.AppInfo.PublisherName + <_Parameter2>@(_MauiAppxManifestPublisher) + + + <_Parameter1>Microsoft.Maui.ApplicationModel.AppInfo.Name + <_Parameter2>@(_MauiAppxManifestDisplayName) + + + <_Parameter1>Microsoft.Maui.ApplicationModel.AppInfo.Version + <_Parameter2>@(_MauiAppxManifestVersion) + + + + + + + + $(_MauiIntermediateManifest)tizen-manifest.xml + + + + + + + + + + + + + + + $(MSBuildWarningsAsMessages);XA4218 + + + + 9.0.40 + 9.0 + 9.0.0 + + + + + + + + + + + + + + + + $(EnableDefaultItems) + $(EnableDefaultMauiItems) + $(EnableDefaultMauiItems) + $(EnableDefaultMauiItems) + + + + + + + + + true + True + True + + true + + false + + + + + + + + + + + <_MauiBindingInterceptorsSupport Condition=" '$(_MauiBindingInterceptorsSupport)' == '' and '$(DisableMauiAnalyzers)' != 'true' ">true + $(InterceptorsPreviewNamespaces);Microsoft.Maui.Controls.Generated + + + + + + + + + + + <_MauiTargetsImportedAgain Condition="'$(_MauiTargetsImported)'=='True'">True + <_MauiTargetsImported>True + true + false + + + + + + + + + + + + + + + + + <_MauiXamlWithResourceNames Remove="@(_MauiXamlWithResourceNames)" /> + <_MauiXamlWithTargetPath Remove="@(_MauiXamlWithTargetPath)" /> + <_MauiCssWithResourceNames Remove="@(_MauiCssWithResourceNames)" /> + <_MauiCssWithTargetPath Remove="@(_MauiCssWithTargetPath)" /> + + + + + + + + + + + + + + + + + <_MauiXamlWithResourceNames Remove="@(_MauiXamlWithResourceNames)" /> + <_MauiXamlWithTargetPath Remove="@(_MauiXamlWithTargetPath)" /> + <_MauiCssWithResourceNames Remove="@(_MauiCssWithResourceNames)" /> + <_MauiCssWithTargetPath Remove="@(_MauiCssWithTargetPath)" /> + + + + + + _MauiAddXamlEmbeddedResources; + $(PrepareResourcesDependsOn); + + + + + + + + + + + + $(CompileDependsOn); + XamlC; + + + + + <_MauiXamlCValidateOnly>$(MauiXamlCValidateOnly) + <_MauiXamlCValidateOnly Condition="'$(Configuration)' == 'Debug' AND '$(_MauiForceXamlCForDebug)' != 'True'">True + <_MauiXamlCValidateOnly Condition="'$(BuildingForLiveUnitTesting)' == 'True' ">True + true + <_MauiXamlCWarningsNotAsErrors>$(WarningsNotAsErrors) + <_MauiXamlCWarningsNotAsErrors Condition="'$(MauiStrictXamlCompilation)' != 'true'">$(_MauiXamlCWarningsNotAsErrors);XC0022;XC0023;XC0025 + <_MauiXamlCGenerateFullPaths Condition="'$(_MauiXamlCGenerateFullPaths)' == '' and '$(GenerateFullPaths)' == 'true'">true + <_MauiXamlCGenerateFullPaths Condition="'$(_MauiXamlCGenerateFullPaths)' == ''">false + <_MauiXamlCFullPathPrefix Condition="'$(_MauiXamlCFullPathPrefix)' == '' and '$(_MauiXamlCGenerateFullPaths)' == 'true'">$(MSBuildProjectDirectory) + + + + + + + + + + + + IncludeProguardForAndroid; + $(CoreCompileDependsOn); + + + ValidateTargetFrameworkVersionForMaui; + $(PrepareForBuildDependsOn); + + + + + + + + + + 10.0 + $(TargetFrameworkVersion.TrimStart('vV')) + + + + + + false + + + false + false + false + true + false + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + false + + + false + + + false + + + false + + + false + + + + + + + + + + <_MauiXamlToRemove Condition="'$(WindowsProjectFolder)' != ''" Include="$(WindowsProjectFolder)**/*.xaml" /> + + + + + + + + <_MauiOld_ResourcePrefix>$(_ResourcePrefix) + <_ResourcePrefix>Resources;$(_ResourcePrefix) + + + + + <_ResourcePrefix>$(_MauiOld_ResourcePrefix) + + + + + $(MSBuildExtensionsPath)\Microsoft\VisualStudio\Maui\Maui.DesignTime.targets + + + + + + + + + + $(MSBuildThisFileDirectory)..\tools\net472\Microsoft.DotNet.ApiCompat.Task.dll + $(MSBuildThisFileDirectory)..\tools\net9.0\Microsoft.DotNet.ApiCompat.Task.dll + + + + + + + + + + + + + + <_UseRoslynToolsetPackage Condition="'$(ApiCompatUseRoslynToolsetPackagePath)' == 'true' and '@(PackageReference->AnyHaveMetadataValue('Identity', 'Microsoft.Net.Compilers.Toolset'))' == 'true'">true + + $([System.IO.Path]::GetDirectoryName('$(CSharpCoreTargetsPath)')) + + $(RoslynTargetsPath) + + $([System.IO.Path]::Combine('$(RoslynAssembliesPath)', 'bincore')) + + + + $(GenerateCompatibilitySuppressionFile) + + + + + + + <_apiCompatDefaultProjectSuppressionFile>$([MSBuild]::NormalizePath('$(MSBuildProjectDirectory)', 'CompatibilitySuppressions.xml')) + + $(_apiCompatDefaultProjectSuppressionFile) + + + + + + + + + + + <_ApiCompatValidatePackageSemaphoreFile>$(IntermediateOutputPath)$(MSBuildThisFileName).semaphore + + CollectApiCompatInputs;_GetReferencePathFromInnerProjects;$(RunPackageValidationDependsOn) + + + + $(PackageId) + $([MSBuild]::NormalizePath('$(NuGetPackageRoot)', '$(PackageValidationBaselineName.ToLower())', '$(PackageValidationBaselineVersion)', '$(PackageValidationBaselineName.ToLower()).$(PackageValidationBaselineVersion).nupkg')) + <_packageValidationBaselinePath Condition="'$(DisablePackageBaselineValidation)' != 'true'">$(PackageValidationBaselinePath) + + + <_PackageTargetPath Include="@(NuGetPackOutput->WithMetadataValue('Extension', '.nupkg'))" Condition="!$([System.String]::new('%(Identity)').EndsWith('.symbols.nupkg'))" /> + + + + + + + + + + $(TargetPlatformMoniker) + + + + + + + + + + + + + + + $(MSBuildThisFileDirectory)..\..\NuGet.Build.Tasks.Pack\buildCrossTargeting\NuGet.Build.Tasks.Pack.targets + $(MSBuildThisFileDirectory)..\..\NuGet.Build.Tasks.Pack\build\NuGet.Build.Tasks.Pack.targets + true + + + + + + ..\CoreCLR\NuGet.Build.Tasks.Pack.dll + ..\Desktop\NuGet.Build.Tasks.Pack.dll + + + + + + + + + $(AssemblyName) + $(Version) + true + _LoadPackInputItems; _GetTargetFrameworksOutput; _WalkEachTargetPerFramework; _GetPackageFiles; $(GenerateNuspecDependsOn) + $(Description) + Package Description + false + true + true + tools + lib + content;contentFiles + $(BeforePack); _IntermediatePack; GenerateNuspec; $(PackDependsOn) + true + symbols.nupkg + DeterminePortableBuildCapabilities + false + false + .dll; .exe; .winmd; .json; .pri; .xml + $(DefaultAllowedOutputExtensionsInPackageBuildOutputFolder) ;$(AllowedOutputExtensionsInPackageBuildOutputFolder) + .pdb; .mdb; $(AllowedOutputExtensionsInPackageBuildOutputFolder); $(AllowedOutputExtensionsInSymbolsPackageBuildOutputFolder) + .pdb + false + + + $(GenerateNuspecDependsOn) + + + Build;$(GenerateNuspecDependsOn) + + + + + + + $(TargetFramework) + + + + $(MSBuildProjectExtensionsPath) + $(BaseOutputPath)$(Configuration)\ + $(BaseIntermediateOutputPath)$(Configuration)\ + + + + + + + + + + + + + + + + + + + + + + + + <_ProjectFrameworks /> + + + + + + <_TargetFrameworks Include="$(_ProjectFrameworks.Split(';'))" /> + + + + + + + <_PackageFilesToDelete Include="@(_OutputPackItems)" /> + + + + + + false + + + + + + + + + + + + + + + + + + + + true + + + + + + + + + + + + + + $(PrivateRepositoryUrl) + $(SourceRevisionId) + $(SourceBranchName) + + + + + + + $(MSBuildProjectFullPath) + + + + + + + + + + + + + + + + + <_ProjectPathWithVersion Include="$(MSBuildProjectFullPath)"> + $(PackageVersion) + 1.0.0 + + + + + + + + + + + + + + + + + + + + + + + + + + <_TfmWithDependenciesSuppressed Include="$(TargetFramework)" Condition="'$(SuppressDependenciesWhenPacking)' == 'true'" /> + + + + + + $(TargetFramework) + + + + + + + + + + + + + %(TfmSpecificPackageFile.RecursiveDir) + %(TfmSpecificPackageFile.BuildAction) + + + + + + <_TargetPathsToSymbolsWithTfm Include="@(DebugSymbolsProjectOutputGroupOutput)"> + $(TargetFramework) + + + + <_TargetPathsToSymbolsWithTfm Include="@(TfmSpecificDebugSymbolsFile)" /> + + + + + + <_PathToPriFile Include="$(ProjectPriFullPath)"> + $(ProjectPriFullPath) + $(ProjectPriFileName) + + + + + + + <_PackageFilesToExclude Include="@(Content)" Condition="'%(Content.Pack)' == 'false'" /> + + + + <_PackageFiles Include="@(Content)" Condition=" %(Content.Pack) != 'false' "> + Content + + <_PackageFiles Include="@(Compile)" Condition=" %(Compile.Pack) == 'true' "> + Compile + + <_PackageFiles Include="@(None)" Condition=" %(None.Pack) == 'true' "> + None + + <_PackageFiles Include="@(EmbeddedResource)" Condition=" %(EmbeddedResource.Pack) == 'true' "> + EmbeddedResource + + <_PackageFiles Include="@(ApplicationDefinition)" Condition=" %(ApplicationDefinition.Pack) == 'true' "> + ApplicationDefinition + + <_PackageFiles Include="@(Page)" Condition=" %(Page.Pack) == 'true' "> + Page + + <_PackageFiles Include="@(Resource)" Condition=" %(Resource.Pack) == 'true' "> + Resource + + <_PackageFiles Include="@(SplashScreen)" Condition=" %(SplashScreen.Pack) == 'true' "> + SplashScreen + + <_PackageFiles Include="@(DesignData)" Condition=" %(DesignData.Pack) == 'true' "> + DesignData + + <_PackageFiles Include="@(DesignDataWithDesignTimeCreatableTypes)" Condition=" %(DesignDataWithDesignTimeCreatableTypes.Pack) == 'true' "> + DesignDataWithDesignTimeCreatableTypes + + <_PackageFiles Include="@(CodeAnalysisDictionary)" Condition=" %(CodeAnalysisDictionary.Pack) == 'true' "> + CodeAnalysisDictionary + + <_PackageFiles Include="@(AndroidAsset)" Condition=" %(AndroidAsset.Pack) == 'true' "> + AndroidAsset + + <_PackageFiles Include="@(AndroidResource)" Condition=" %(AndroidResource.Pack) == 'true' "> + AndroidResource + + <_PackageFiles Include="@(BundleResource)" Condition=" %(BundleResource.Pack) == 'true' "> + BundleResource + + + + + + + <_IsNotSetContainersTargetsDir>false + <_IsNotSetContainersTargetsDir Condition=" '$(_ContainersTargetsDir)'=='' ">true + <_ContainersTargetsDir Condition="$(_IsNotSetContainersTargetsDir)">$(MSBuildThisFileDirectory)..\..\..\Containers\build\ + + + + + true + tasks + net9.0 + net472 + containerize + + $(MSBuildThisFileDirectory)..\$(ContainerTaskFolderName)\$(ContainerTaskFramework)\ + $(MSBuildThisFileDirectory)..\$(ContainerizeFolderName)\ + + $(ContainerCustomTasksFolder)$(MSBuildThisFileName).dll + + + + + + + + + <_IsSDKContainerAllowedVersion>false + + <_IsSDKContainerAllowedVersion Condition="$([MSBuild]::VersionGreaterThan($(NetCoreSdkVersion), 7.0.100)) OR ( $([MSBuild]::VersionEquals($(NetCoreSdkVersion), 7.0.100)) AND ( $(NETCoreSdkVersion.Contains('-preview.7')) OR $(NETCoreSdkVersion.Contains('-rc')) OR $(NETCoreSdkVersion.Contains('-')) == false ) )">true + <_ContainerIsTargetingNet8TFM>false + <_ContainerIsTargetingNet8TFM Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp' And $([MSBuild]::VersionGreaterThanOrEquals($(_TargetFrameworkVersionWithoutV), '8.0'))">true + <_ContainerIsSelfContained>false + <_ContainerIsSelfContained Condition="'$(SelfContained)' == 'true' or '$(PublishSelfContained)' == 'true'">true + + + + + + + + + + + + $(RuntimeIdentifier) + linux-$(NETCoreSdkPortableRuntimeIdentifier.Split('-')[1]) + <_ContainerIsUsingMicrosoftDefaultImages Condition="'$(ContainerBaseImage)' == ''">true + <_ContainerIsUsingMicrosoftDefaultImages Condition="'$(ContainerBaseImage)' != ''">false + + + + + + + + + $(RegistryUrl) + + $(PublishImageTag) + + $([System.DateTime]::UtcNow.ToString('yyyyMMddhhmmss')) + + + + + + + + + + + $(ContainerImageName) + + $(AssemblyName) + + latest + $([System.DateTime]::UtcNow.ToString('yyyyMMddhhmmss')) + + <_ContainerIsTargetingWindows>false + <_ContainerIsTargetingWindows Condition="$(ContainerRuntimeIdentifier.StartsWith('win'))">true + + /app/ + C:\app\ + + + + + + + + + + ContainerUser + + + + + + + + + + + + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + + + $(Description) + $(Authors) + $(PackageProjectUrl) + $(PackageProjectUrl) + $(PackageVersion) + $(PackageLicenseExpression) + $(Title) + + + + + + + + + + + + + + + + + + + + + + <_TrimmedRepositoryUrl Condition="'$(RepositoryType)' == 'git' and '$(PrivateRepositoryUrl)' != '' and $(PrivateRepositoryUrl.EndsWith('.git'))">$(PrivateRepositoryUrl.Substring(0, $(PrivateRepositoryUrl.LastIndexOf('.git')))) + <_TrimmedRepositoryUrl Condition="'$(_TrimmedRepositoryUrl)' == '' and '$(PrivateRepositoryUrl)' != ''">$(PrivateRepositoryUrl) + + + + + + + + + _ContainerVerifySDKVersion; + ComputeContainerConfig; + _CheckContainersPackage + + + + + + <_ContainersPackageIdentity>Microsoft.NET.Build.Containers + <_WebDefaultSdkVersion>7.0.300 + <_WorkerDefaultSdkVersion>8.0.100 + <_ConsoleDefaultSdkVersion>8.0.200 + + <_SdkCanPublishWeb>$([MSBuild]::VersionGreaterThanOrEquals('$(NETCoreSdkVersion)', '$(_WebDefaultSdkVersion)')) + <_SdkCanPublishWorker>$([MSBuild]::VersionGreaterThanOrEquals('$(NETCoreSdkVersion)', '$(_WorkerDefaultSdkVersion)')) + <_SdkCanPublishConsole>$([MSBuild]::VersionGreaterThanOrEquals('$(NETCoreSdkVersion)', '$(_ConsoleDefaultSdkVersion)')) + + <_ContainerPackageIsPresent>false + <_ContainerPackageIsPresent Condition="@(PackageReference->AnyHaveMetadataValue('Identity', '$(_ContainersPackageIdentity)'))">true + <_IsWebProject>false + <_IsWebProject Condition="@(ProjectCapability->AnyHaveMetadataValue('Identity', 'DotNetCoreWeb'))">true + <_IsWorkerProject>false + <_IsWorkerProject Condition="@(ProjectCapability->AnyHaveMetadataValue('Identity', 'DotNetCoreWorker'))">true + + + + true + + + + + $(NetCoreRoot) + dotnet + dotnet.exe + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/ControlGallery/App.xaml b/samples/ControlGallery/App.xaml deleted file mode 100644 index 8de3e73..0000000 --- a/samples/ControlGallery/App.xaml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - #512BD4 - #3B1F9E - #DFD8F7 - #2B0B98 - #E1E1E1 - #C8C8C8 - #ACACAC - #919191 - #6E6E6E - #404040 - #212121 - - - diff --git a/samples/ControlGallery/App.xaml.cs b/samples/ControlGallery/App.xaml.cs deleted file mode 100644 index 9270a3b..0000000 --- a/samples/ControlGallery/App.xaml.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace ControlGallery; - -public partial class App : Application -{ - public App() - { - InitializeComponent(); - MainPage = new AppShell(); - } -} diff --git a/samples/ControlGallery/AppShell.xaml b/samples/ControlGallery/AppShell.xaml deleted file mode 100644 index 40514a6..0000000 --- a/samples/ControlGallery/AppShell.xaml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/samples/ControlGallery/AppShell.xaml.cs b/samples/ControlGallery/AppShell.xaml.cs deleted file mode 100644 index 419d271..0000000 --- a/samples/ControlGallery/AppShell.xaml.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace ControlGallery; - -public partial class AppShell : Shell -{ - public AppShell() - { - InitializeComponent(); - } -} diff --git a/samples/ControlGallery/ControlGallery.csproj b/samples/ControlGallery/ControlGallery.csproj deleted file mode 100644 index 4b6880d..0000000 --- a/samples/ControlGallery/ControlGallery.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - net9.0 - Exe - enable - enable - ControlGallery - - - - - - - - - - - - - diff --git a/samples/ControlGallery/MauiProgram.cs b/samples/ControlGallery/MauiProgram.cs deleted file mode 100644 index ea6104d..0000000 --- a/samples/ControlGallery/MauiProgram.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.Maui.Hosting; -using OpenMaui.Platform.Linux.Hosting; - -namespace ControlGallery; - -public static class MauiProgram -{ - public static MauiApp CreateMauiApp() - { - var builder = MauiApp.CreateBuilder(); - builder - .UseMauiApp() - .UseOpenMauiLinux() - .ConfigureFonts(fonts => - { - fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); - fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); - }); - - return builder.Build(); - } -} diff --git a/samples/ControlGallery/Pages/ButtonsPage.xaml b/samples/ControlGallery/Pages/ButtonsPage.xaml deleted file mode 100644 index 0f03138..0000000 --- a/samples/ControlGallery/Pages/ButtonsPage.xaml +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - -