2025-12-19 09:30:16 +00:00
|
|
|
// Licensed to the .NET Foundation under one or more agreements.
|
|
|
|
|
// The .NET Foundation licenses this file to you under the MIT license.
|
|
|
|
|
|
2026-01-01 13:51:12 -05:00
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Net.Http;
|
|
|
|
|
using System.Threading;
|
|
|
|
|
using System.Threading.Tasks;
|
2025-12-19 09:30:16 +00:00
|
|
|
using SkiaSharp;
|
2026-01-01 13:51:12 -05:00
|
|
|
using Svg.Skia;
|
2025-12-19 09:30:16 +00:00
|
|
|
|
|
|
|
|
namespace Microsoft.Maui.Platform;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-01-01 13:51:12 -05:00
|
|
|
/// Skia-rendered image control with SVG support.
|
2025-12-19 09:30:16 +00:00
|
|
|
/// </summary>
|
|
|
|
|
public class SkiaImage : SkiaView
|
|
|
|
|
{
|
|
|
|
|
private SKBitmap? _bitmap;
|
|
|
|
|
private SKImage? _image;
|
|
|
|
|
private bool _isLoading;
|
2026-01-01 13:51:12 -05:00
|
|
|
private string? _currentFilePath;
|
|
|
|
|
private bool _isSvg;
|
|
|
|
|
private CancellationTokenSource? _loadCts;
|
|
|
|
|
private readonly object _loadLock = new object();
|
|
|
|
|
private double _svgLoadedWidth;
|
|
|
|
|
private double _svgLoadedHeight;
|
|
|
|
|
private bool _pendingSvgReload;
|
|
|
|
|
private SKRect _lastArrangedBounds;
|
2025-12-19 09:30:16 +00:00
|
|
|
|
|
|
|
|
public SKBitmap? Bitmap
|
|
|
|
|
{
|
|
|
|
|
get => _bitmap;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
_bitmap?.Dispose();
|
|
|
|
|
_bitmap = value;
|
|
|
|
|
_image?.Dispose();
|
|
|
|
|
_image = value != null ? SKImage.FromBitmap(value) : null;
|
|
|
|
|
Invalidate();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-01 13:51:12 -05:00
|
|
|
public Aspect Aspect { get; set; }
|
|
|
|
|
|
2025-12-19 09:30:16 +00:00
|
|
|
public bool IsOpaque { get; set; }
|
2026-01-01 13:51:12 -05:00
|
|
|
|
2025-12-19 09:30:16 +00:00
|
|
|
public bool IsLoading => _isLoading;
|
2026-01-01 13:51:12 -05:00
|
|
|
|
2025-12-19 09:30:16 +00:00
|
|
|
public bool IsAnimationPlaying { get; set; }
|
|
|
|
|
|
2026-01-01 13:51:12 -05:00
|
|
|
public new double WidthRequest
|
|
|
|
|
{
|
|
|
|
|
get => base.WidthRequest;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
base.WidthRequest = value;
|
|
|
|
|
ScheduleSvgReloadIfNeeded();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public new double HeightRequest
|
|
|
|
|
{
|
|
|
|
|
get => base.HeightRequest;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
base.HeightRequest = value;
|
|
|
|
|
ScheduleSvgReloadIfNeeded();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 09:30:16 +00:00
|
|
|
public event EventHandler? ImageLoaded;
|
|
|
|
|
public event EventHandler<ImageLoadingErrorEventArgs>? ImageLoadingError;
|
|
|
|
|
|
2026-01-01 13:51:12 -05:00
|
|
|
private void ScheduleSvgReloadIfNeeded()
|
|
|
|
|
{
|
|
|
|
|
if (_isSvg && !string.IsNullOrEmpty(_currentFilePath))
|
|
|
|
|
{
|
|
|
|
|
double widthRequest = WidthRequest;
|
|
|
|
|
double heightRequest = HeightRequest;
|
|
|
|
|
if (widthRequest > 0.0 && heightRequest > 0.0 &&
|
|
|
|
|
(Math.Abs(_svgLoadedWidth - widthRequest) > 0.5 || Math.Abs(_svgLoadedHeight - heightRequest) > 0.5) &&
|
|
|
|
|
!_pendingSvgReload)
|
|
|
|
|
{
|
|
|
|
|
_pendingSvgReload = true;
|
|
|
|
|
ReloadSvgDebounced();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task ReloadSvgDebounced()
|
|
|
|
|
{
|
|
|
|
|
await Task.Delay(10);
|
|
|
|
|
_pendingSvgReload = false;
|
|
|
|
|
if (!string.IsNullOrEmpty(_currentFilePath) && WidthRequest > 0.0 && HeightRequest > 0.0)
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine($"[SkiaImage] Reloading SVG at {WidthRequest}x{HeightRequest} (was {_svgLoadedWidth}x{_svgLoadedHeight})");
|
|
|
|
|
await LoadSvgAtSizeAsync(_currentFilePath, WidthRequest, HeightRequest);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 09:30:16 +00:00
|
|
|
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
|
|
|
|
|
{
|
|
|
|
|
// Draw background if not opaque
|
|
|
|
|
if (!IsOpaque && BackgroundColor != SKColors.Transparent)
|
|
|
|
|
{
|
|
|
|
|
using var bgPaint = new SKPaint
|
|
|
|
|
{
|
|
|
|
|
Color = BackgroundColor,
|
|
|
|
|
Style = SKPaintStyle.Fill
|
|
|
|
|
};
|
|
|
|
|
canvas.DrawRect(bounds, bgPaint);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-01 13:51:12 -05:00
|
|
|
if (_image == null)
|
|
|
|
|
return;
|
2025-12-19 09:30:16 +00:00
|
|
|
|
2026-01-01 13:51:12 -05:00
|
|
|
int width = _image.Width;
|
|
|
|
|
int height = _image.Height;
|
2025-12-19 09:30:16 +00:00
|
|
|
|
2026-01-01 13:51:12 -05:00
|
|
|
if (width <= 0 || height <= 0)
|
|
|
|
|
return;
|
2025-12-19 09:30:16 +00:00
|
|
|
|
2026-01-01 13:51:12 -05:00
|
|
|
SKRect destRect = CalculateDestRect(bounds, width, height);
|
2025-12-19 09:30:16 +00:00
|
|
|
|
|
|
|
|
using var paint = new SKPaint
|
|
|
|
|
{
|
|
|
|
|
IsAntialias = true,
|
|
|
|
|
FilterQuality = SKFilterQuality.High
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
canvas.DrawImage(_image, destRect, paint);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private SKRect CalculateDestRect(SKRect bounds, float imageWidth, float imageHeight)
|
|
|
|
|
{
|
|
|
|
|
switch (Aspect)
|
|
|
|
|
{
|
|
|
|
|
case Aspect.Fill:
|
|
|
|
|
return bounds;
|
|
|
|
|
|
|
|
|
|
case Aspect.AspectFit:
|
2026-01-01 13:51:12 -05:00
|
|
|
{
|
|
|
|
|
float scale = Math.Min(bounds.Width / imageWidth, bounds.Height / imageHeight);
|
|
|
|
|
float destWidth = imageWidth * scale;
|
|
|
|
|
float destHeight = imageHeight * scale;
|
|
|
|
|
float destX = bounds.Left + (bounds.Width - destWidth) / 2f;
|
|
|
|
|
float destY = bounds.Top + (bounds.Height - destHeight) / 2f;
|
2025-12-19 09:30:16 +00:00
|
|
|
return new SKRect(destX, destY, destX + destWidth, destY + destHeight);
|
2026-01-01 13:51:12 -05:00
|
|
|
}
|
2025-12-19 09:30:16 +00:00
|
|
|
|
|
|
|
|
case Aspect.AspectFill:
|
2026-01-01 13:51:12 -05:00
|
|
|
{
|
|
|
|
|
float scale = Math.Max(bounds.Width / imageWidth, bounds.Height / imageHeight);
|
|
|
|
|
float destWidth = imageWidth * scale;
|
|
|
|
|
float destHeight = imageHeight * scale;
|
|
|
|
|
float destX = bounds.Left + (bounds.Width - destWidth) / 2f;
|
|
|
|
|
float destY = bounds.Top + (bounds.Height - destHeight) / 2f;
|
2025-12-19 09:30:16 +00:00
|
|
|
return new SKRect(destX, destY, destX + destWidth, destY + destHeight);
|
2026-01-01 13:51:12 -05:00
|
|
|
}
|
2025-12-19 09:30:16 +00:00
|
|
|
|
|
|
|
|
case Aspect.Center:
|
2026-01-01 13:51:12 -05:00
|
|
|
{
|
|
|
|
|
float destX = bounds.Left + (bounds.Width - imageWidth) / 2f;
|
|
|
|
|
float destY = bounds.Top + (bounds.Height - imageHeight) / 2f;
|
2025-12-19 09:30:16 +00:00
|
|
|
return new SKRect(destX, destY, destX + imageWidth, destY + imageHeight);
|
2026-01-01 13:51:12 -05:00
|
|
|
}
|
2025-12-19 09:30:16 +00:00
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
return bounds;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task LoadFromFileAsync(string filePath)
|
|
|
|
|
{
|
|
|
|
|
_isLoading = true;
|
|
|
|
|
Invalidate();
|
2026-01-01 13:51:12 -05:00
|
|
|
Console.WriteLine($"[SkiaImage] LoadFromFileAsync: {filePath}, WidthRequest={WidthRequest}, HeightRequest={HeightRequest}");
|
2025-12-19 09:30:16 +00:00
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
2026-01-01 13:51:12 -05:00
|
|
|
List<string> searchPaths = new List<string>
|
2025-12-19 09:30:16 +00:00
|
|
|
{
|
2026-01-01 13:51:12 -05:00
|
|
|
filePath,
|
|
|
|
|
Path.Combine(AppContext.BaseDirectory, filePath),
|
|
|
|
|
Path.Combine(AppContext.BaseDirectory, "Resources", "Images", filePath),
|
|
|
|
|
Path.Combine(AppContext.BaseDirectory, "Resources", filePath)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Also try SVG if looking for PNG
|
|
|
|
|
if (filePath.EndsWith(".png", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
string svgPath = Path.ChangeExtension(filePath, ".svg");
|
|
|
|
|
searchPaths.Add(svgPath);
|
|
|
|
|
searchPaths.Add(Path.Combine(AppContext.BaseDirectory, svgPath));
|
|
|
|
|
searchPaths.Add(Path.Combine(AppContext.BaseDirectory, "Resources", "Images", svgPath));
|
|
|
|
|
searchPaths.Add(Path.Combine(AppContext.BaseDirectory, "Resources", svgPath));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
string? foundPath = null;
|
|
|
|
|
foreach (string path in searchPaths)
|
|
|
|
|
{
|
|
|
|
|
if (File.Exists(path))
|
2025-12-19 09:30:16 +00:00
|
|
|
{
|
2026-01-01 13:51:12 -05:00
|
|
|
foundPath = path;
|
|
|
|
|
Console.WriteLine("[SkiaImage] Found file at: " + path);
|
|
|
|
|
break;
|
2025-12-19 09:30:16 +00:00
|
|
|
}
|
2026-01-01 13:51:12 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (foundPath == null)
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine("[SkiaImage] File not found: " + filePath);
|
|
|
|
|
_isLoading = false;
|
|
|
|
|
_isSvg = false;
|
|
|
|
|
_currentFilePath = null;
|
|
|
|
|
ImageLoadingError?.Invoke(this, new ImageLoadingErrorEventArgs(new FileNotFoundException(filePath)));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_isSvg = foundPath.EndsWith(".svg", StringComparison.OrdinalIgnoreCase);
|
|
|
|
|
_currentFilePath = foundPath;
|
|
|
|
|
|
|
|
|
|
if (!_isSvg)
|
|
|
|
|
{
|
|
|
|
|
await Task.Run(() =>
|
|
|
|
|
{
|
|
|
|
|
using FileStream stream = File.OpenRead(foundPath);
|
|
|
|
|
SKBitmap? bitmap = SKBitmap.Decode(stream);
|
|
|
|
|
if (bitmap != null)
|
|
|
|
|
{
|
|
|
|
|
Bitmap = bitmap;
|
|
|
|
|
Console.WriteLine("[SkiaImage] Loaded image: " + foundPath);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
await LoadSvgAtSizeAsync(foundPath, WidthRequest, HeightRequest);
|
|
|
|
|
}
|
2025-12-19 09:30:16 +00:00
|
|
|
|
|
|
|
|
_isLoading = false;
|
|
|
|
|
ImageLoaded?.Invoke(this, EventArgs.Empty);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
_isLoading = false;
|
|
|
|
|
ImageLoadingError?.Invoke(this, new ImageLoadingErrorEventArgs(ex));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Invalidate();
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-01 13:51:12 -05:00
|
|
|
private async Task LoadSvgAtSizeAsync(string svgPath, double targetWidth, double targetHeight)
|
|
|
|
|
{
|
|
|
|
|
_loadCts?.Cancel();
|
|
|
|
|
CancellationTokenSource cts = new CancellationTokenSource();
|
|
|
|
|
_loadCts = cts;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
SKBitmap? newBitmap = null;
|
|
|
|
|
|
|
|
|
|
await Task.Run(() =>
|
|
|
|
|
{
|
|
|
|
|
if (cts.Token.IsCancellationRequested)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
using var svg = new SKSvg();
|
|
|
|
|
svg.Load(svgPath);
|
|
|
|
|
|
|
|
|
|
if (svg.Picture != null && !cts.Token.IsCancellationRequested)
|
|
|
|
|
{
|
|
|
|
|
SKRect cullRect = svg.Picture.CullRect;
|
|
|
|
|
|
|
|
|
|
float requestedWidth = (targetWidth > 0.0)
|
|
|
|
|
? (float)targetWidth
|
|
|
|
|
: ((cullRect.Width <= 24f) ? 24f : cullRect.Width);
|
|
|
|
|
|
|
|
|
|
float requestedHeight = (targetHeight > 0.0)
|
|
|
|
|
? (float)targetHeight
|
|
|
|
|
: ((cullRect.Height <= 24f) ? 24f : cullRect.Height);
|
|
|
|
|
|
|
|
|
|
float scale = Math.Min(requestedWidth / cullRect.Width, requestedHeight / cullRect.Height);
|
|
|
|
|
|
|
|
|
|
int bitmapWidth = Math.Max(1, (int)(cullRect.Width * scale));
|
|
|
|
|
int bitmapHeight = Math.Max(1, (int)(cullRect.Height * scale));
|
|
|
|
|
|
|
|
|
|
newBitmap = new SKBitmap(bitmapWidth, bitmapHeight, false);
|
|
|
|
|
|
|
|
|
|
using var canvas = new SKCanvas(newBitmap);
|
|
|
|
|
canvas.Clear(SKColors.Transparent);
|
|
|
|
|
canvas.Scale(scale);
|
|
|
|
|
canvas.DrawPicture(svg.Picture, null);
|
|
|
|
|
|
|
|
|
|
Console.WriteLine($"[SkiaImage] Loaded SVG: {svgPath} at {bitmapWidth}x{bitmapHeight} (requested {targetWidth}x{targetHeight})");
|
|
|
|
|
}
|
|
|
|
|
}, cts.Token);
|
|
|
|
|
|
|
|
|
|
if (!cts.Token.IsCancellationRequested && newBitmap != null)
|
|
|
|
|
{
|
|
|
|
|
_svgLoadedWidth = (targetWidth > 0.0) ? targetWidth : newBitmap.Width;
|
|
|
|
|
_svgLoadedHeight = (targetHeight > 0.0) ? targetHeight : newBitmap.Height;
|
|
|
|
|
Bitmap = newBitmap;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
newBitmap?.Dispose();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (OperationCanceledException)
|
|
|
|
|
{
|
|
|
|
|
// Cancellation is expected when reloading SVG at different sizes
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 09:30:16 +00:00
|
|
|
public async Task LoadFromStreamAsync(Stream stream)
|
|
|
|
|
{
|
|
|
|
|
_isLoading = true;
|
|
|
|
|
Invalidate();
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await Task.Run(() =>
|
|
|
|
|
{
|
2026-01-01 13:51:12 -05:00
|
|
|
SKBitmap? bitmap = SKBitmap.Decode(stream);
|
2025-12-19 09:30:16 +00:00
|
|
|
if (bitmap != null)
|
|
|
|
|
{
|
|
|
|
|
Bitmap = bitmap;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
_isLoading = false;
|
|
|
|
|
ImageLoaded?.Invoke(this, EventArgs.Empty);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
_isLoading = false;
|
|
|
|
|
ImageLoadingError?.Invoke(this, new ImageLoadingErrorEventArgs(ex));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Invalidate();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task LoadFromUriAsync(Uri uri)
|
|
|
|
|
{
|
|
|
|
|
_isLoading = true;
|
|
|
|
|
Invalidate();
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
2026-01-01 13:51:12 -05:00
|
|
|
using HttpClient httpClient = new HttpClient();
|
|
|
|
|
using MemoryStream stream = new MemoryStream(await httpClient.GetByteArrayAsync(uri));
|
|
|
|
|
SKBitmap? bitmap = SKBitmap.Decode(stream);
|
2025-12-19 09:30:16 +00:00
|
|
|
if (bitmap != null)
|
|
|
|
|
{
|
|
|
|
|
Bitmap = bitmap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_isLoading = false;
|
|
|
|
|
ImageLoaded?.Invoke(this, EventArgs.Empty);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
_isLoading = false;
|
|
|
|
|
ImageLoadingError?.Invoke(this, new ImageLoadingErrorEventArgs(ex));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Invalidate();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void LoadFromData(byte[] data)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2026-01-01 13:51:12 -05:00
|
|
|
using MemoryStream stream = new MemoryStream(data);
|
|
|
|
|
SKBitmap? bitmap = SKBitmap.Decode(stream);
|
2025-12-19 09:30:16 +00:00
|
|
|
if (bitmap != null)
|
|
|
|
|
{
|
|
|
|
|
Bitmap = bitmap;
|
|
|
|
|
}
|
|
|
|
|
ImageLoaded?.Invoke(this, EventArgs.Empty);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
ImageLoadingError?.Invoke(this, new ImageLoadingErrorEventArgs(ex));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Major production merge: GTK support, context menus, and dispatcher fixes
Core Infrastructure:
- Add Dispatching folder with LinuxDispatcher, LinuxDispatcherProvider, LinuxDispatcherTimer
- Add Native folder with P/Invoke wrappers (GTK, GLib, GDK, Cairo, WebKit)
- Add GTK host window system with GtkHostWindow and GtkSkiaSurfaceWidget
- Update LinuxApplication with GTK mode, theme handling, and icon support
- Fix duplicate LinuxDispatcher in LinuxMauiContext
Handlers:
- Add GtkWebViewManager and GtkWebViewPlatformView for GTK WebView
- Add FlexLayoutHandler and GestureManager
- Update multiple handlers with ToViewHandler fix and missing mappers
- Add MauiHandlerExtensions with ToViewHandler extension method
Views:
- Add SkiaContextMenu with hover, keyboard, and dark theme support
- Add LinuxDialogService with context menu management
- Add SkiaFlexLayout for flex container support
- Update SkiaShell with RefreshTheme, MauiShell, ContentRenderer
- Update SkiaWebView with SetMainWindow, ProcessGtkEvents
- Update SkiaImage with LoadFromBitmap method
Services:
- Add AppInfoService, ConnectivityService, DeviceDisplayService, DeviceInfoService
- Add GtkHostService, GtkContextMenuService, MauiIconGenerator
Window:
- Add CursorType enum and GtkHostWindow
- Update X11Window with SetIcon, SetCursor methods
Build: SUCCESS (0 errors)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 11:19:58 -05:00
|
|
|
/// <summary>
|
|
|
|
|
/// Loads the image from an SKBitmap.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public void LoadFromBitmap(SKBitmap bitmap)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2026-01-01 13:51:12 -05:00
|
|
|
_isSvg = false;
|
|
|
|
|
_currentFilePath = null;
|
Major production merge: GTK support, context menus, and dispatcher fixes
Core Infrastructure:
- Add Dispatching folder with LinuxDispatcher, LinuxDispatcherProvider, LinuxDispatcherTimer
- Add Native folder with P/Invoke wrappers (GTK, GLib, GDK, Cairo, WebKit)
- Add GTK host window system with GtkHostWindow and GtkSkiaSurfaceWidget
- Update LinuxApplication with GTK mode, theme handling, and icon support
- Fix duplicate LinuxDispatcher in LinuxMauiContext
Handlers:
- Add GtkWebViewManager and GtkWebViewPlatformView for GTK WebView
- Add FlexLayoutHandler and GestureManager
- Update multiple handlers with ToViewHandler fix and missing mappers
- Add MauiHandlerExtensions with ToViewHandler extension method
Views:
- Add SkiaContextMenu with hover, keyboard, and dark theme support
- Add LinuxDialogService with context menu management
- Add SkiaFlexLayout for flex container support
- Update SkiaShell with RefreshTheme, MauiShell, ContentRenderer
- Update SkiaWebView with SetMainWindow, ProcessGtkEvents
- Update SkiaImage with LoadFromBitmap method
Services:
- Add AppInfoService, ConnectivityService, DeviceDisplayService, DeviceInfoService
- Add GtkHostService, GtkContextMenuService, MauiIconGenerator
Window:
- Add CursorType enum and GtkHostWindow
- Update X11Window with SetIcon, SetCursor methods
Build: SUCCESS (0 errors)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 11:19:58 -05:00
|
|
|
Bitmap = bitmap;
|
|
|
|
|
_isLoading = false;
|
|
|
|
|
ImageLoaded?.Invoke(this, EventArgs.Empty);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
_isLoading = false;
|
|
|
|
|
ImageLoadingError?.Invoke(this, new ImageLoadingErrorEventArgs(ex));
|
|
|
|
|
}
|
|
|
|
|
Invalidate();
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-01 13:51:12 -05:00
|
|
|
public override void Arrange(SKRect bounds)
|
|
|
|
|
{
|
|
|
|
|
base.Arrange(bounds);
|
|
|
|
|
|
|
|
|
|
// If no explicit size requested and this is an SVG, check if we need to reload at larger size
|
|
|
|
|
if (!(base.WidthRequest > 0.0) || !(base.HeightRequest > 0.0))
|
|
|
|
|
{
|
|
|
|
|
if (_isSvg && !string.IsNullOrEmpty(_currentFilePath) && !_isLoading)
|
|
|
|
|
{
|
|
|
|
|
float width = bounds.Width;
|
|
|
|
|
float height = bounds.Height;
|
|
|
|
|
|
|
|
|
|
if ((width > _svgLoadedWidth * 1.1 || height > _svgLoadedHeight * 1.1) &&
|
|
|
|
|
width > 0f && height > 0f &&
|
|
|
|
|
(width != _lastArrangedBounds.Width || height != _lastArrangedBounds.Height))
|
|
|
|
|
{
|
|
|
|
|
_lastArrangedBounds = bounds;
|
|
|
|
|
Console.WriteLine($"[SkiaImage] Arrange detected larger bounds: {width}x{height} vs loaded {_svgLoadedWidth}x{_svgLoadedHeight}");
|
|
|
|
|
LoadSvgAtSizeAsync(_currentFilePath, width, height);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 09:30:16 +00:00
|
|
|
protected override SKSize MeasureOverride(SKSize availableSize)
|
|
|
|
|
{
|
2026-01-01 13:51:12 -05:00
|
|
|
double widthRequest = base.WidthRequest;
|
|
|
|
|
double heightRequest = base.HeightRequest;
|
|
|
|
|
|
|
|
|
|
// If both dimensions explicitly requested, use them
|
|
|
|
|
if (widthRequest > 0.0 && heightRequest > 0.0)
|
|
|
|
|
{
|
|
|
|
|
return new SKSize((float)widthRequest, (float)heightRequest);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If no image, return default or requested size
|
2025-12-19 09:30:16 +00:00
|
|
|
if (_image == null)
|
2026-01-01 13:51:12 -05:00
|
|
|
{
|
|
|
|
|
if (widthRequest > 0.0)
|
|
|
|
|
return new SKSize((float)widthRequest, (float)widthRequest);
|
|
|
|
|
if (heightRequest > 0.0)
|
|
|
|
|
return new SKSize((float)heightRequest, (float)heightRequest);
|
|
|
|
|
return new SKSize(100f, 100f);
|
|
|
|
|
}
|
2025-12-19 09:30:16 +00:00
|
|
|
|
2026-01-01 13:51:12 -05:00
|
|
|
float imageWidth = _image.Width;
|
|
|
|
|
float imageHeight = _image.Height;
|
2025-12-19 09:30:16 +00:00
|
|
|
|
2026-01-01 13:51:12 -05:00
|
|
|
// If only width requested, scale height proportionally
|
|
|
|
|
if (widthRequest > 0.0)
|
|
|
|
|
{
|
|
|
|
|
float scale = (float)widthRequest / imageWidth;
|
|
|
|
|
return new SKSize((float)widthRequest, imageHeight * scale);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If only height requested, scale width proportionally
|
|
|
|
|
if (heightRequest > 0.0)
|
|
|
|
|
{
|
|
|
|
|
float scale = (float)heightRequest / imageHeight;
|
|
|
|
|
return new SKSize(imageWidth * scale, (float)heightRequest);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Scale to fit available size
|
2025-12-19 09:30:16 +00:00
|
|
|
if (availableSize.Width < float.MaxValue && availableSize.Height < float.MaxValue)
|
|
|
|
|
{
|
2026-01-01 13:51:12 -05:00
|
|
|
float scale = Math.Min(availableSize.Width / imageWidth, availableSize.Height / imageHeight);
|
2025-12-19 09:30:16 +00:00
|
|
|
return new SKSize(imageWidth * scale, imageHeight * scale);
|
|
|
|
|
}
|
2026-01-01 13:51:12 -05:00
|
|
|
|
|
|
|
|
if (availableSize.Width < float.MaxValue)
|
2025-12-19 09:30:16 +00:00
|
|
|
{
|
2026-01-01 13:51:12 -05:00
|
|
|
float scale = availableSize.Width / imageWidth;
|
2025-12-19 09:30:16 +00:00
|
|
|
return new SKSize(availableSize.Width, imageHeight * scale);
|
|
|
|
|
}
|
2026-01-01 13:51:12 -05:00
|
|
|
|
|
|
|
|
if (availableSize.Height < float.MaxValue)
|
2025-12-19 09:30:16 +00:00
|
|
|
{
|
2026-01-01 13:51:12 -05:00
|
|
|
float scale = availableSize.Height / imageHeight;
|
2025-12-19 09:30:16 +00:00
|
|
|
return new SKSize(imageWidth * scale, availableSize.Height);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return new SKSize(imageWidth, imageHeight);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override void Dispose(bool disposing)
|
|
|
|
|
{
|
|
|
|
|
if (disposing)
|
|
|
|
|
{
|
|
|
|
|
_bitmap?.Dispose();
|
|
|
|
|
_image?.Dispose();
|
|
|
|
|
}
|
|
|
|
|
base.Dispose(disposing);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Event args for image loading errors.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public class ImageLoadingErrorEventArgs : EventArgs
|
|
|
|
|
{
|
|
|
|
|
public Exception Exception { get; }
|
|
|
|
|
|
|
|
|
|
public ImageLoadingErrorEventArgs(Exception exception)
|
|
|
|
|
{
|
|
|
|
|
Exception = exception;
|
|
|
|
|
}
|
|
|
|
|
}
|