From 433787ff807336b68a6e4ea17eb2c84b16383302 Mon Sep 17 00:00:00 2001 From: logikonline Date: Sat, 24 Jan 2026 03:18:08 +0000 Subject: [PATCH] Image fixes --- Handlers/ImageHandler.cs | 22 ++++ Hosting/LinuxViewRenderer.cs | 19 +++- Views/SkiaImage.cs | 189 ++++++++++++++++++++++++++++++++++- Views/SkiaLayoutView.cs | 25 ++++- Views/SkiaShell.cs | 105 ++++++++++++++++--- assets/logo_only.png | Bin 0 -> 8656 bytes assets/logo_only.svg | 44 ++++++++ 7 files changed, 383 insertions(+), 21 deletions(-) create mode 100644 assets/logo_only.png create mode 100644 assets/logo_only.svg diff --git a/Handlers/ImageHandler.cs b/Handlers/ImageHandler.cs index 6a1cad7..a961e2b 100644 --- a/Handlers/ImageHandler.cs +++ b/Handlers/ImageHandler.cs @@ -24,6 +24,8 @@ public partial class ImageHandler : ViewHandler [nameof(IView.Background)] = MapBackground, ["Width"] = MapWidth, ["Height"] = MapHeight, + ["HorizontalOptions"] = MapHorizontalOptions, + ["VerticalOptions"] = MapVerticalOptions, }; public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper) @@ -148,6 +150,26 @@ public partial class ImageHandler : ViewHandler } } + public static void MapHorizontalOptions(ImageHandler handler, IImage image) + { + if (handler.PlatformView is null) return; + + if (image is Image img) + { + handler.PlatformView.HorizontalOptions = img.HorizontalOptions; + } + } + + public static void MapVerticalOptions(ImageHandler handler, IImage image) + { + if (handler.PlatformView is null) return; + + if (image is Image img) + { + handler.PlatformView.VerticalOptions = img.VerticalOptions; + } + } + // Image source loading helper private ImageSourceServiceResultManager _sourceLoader = null!; diff --git a/Hosting/LinuxViewRenderer.cs b/Hosting/LinuxViewRenderer.cs index b206fba..38f79dc 100644 --- a/Hosting/LinuxViewRenderer.cs +++ b/Hosting/LinuxViewRenderer.cs @@ -201,9 +201,22 @@ public class LinuxViewRenderer } } - // Set flyout footer with version info - var version = Assembly.GetEntryAssembly()?.GetName().Version; - skiaShell.FlyoutFooterText = $"Version {version?.Major ?? 1}.{version?.Minor ?? 0}.{version?.Build ?? 0}"; + // Render flyout footer if present, otherwise use version text + if (shell.FlyoutFooter is View footerView) + { + var skiaFooter = RenderView(footerView); + if (skiaFooter != null) + { + skiaShell.FlyoutFooterView = skiaFooter; + skiaShell.FlyoutFooterHeight = (float)(footerView.HeightRequest > 0 ? footerView.HeightRequest : 40.0); + } + } + else + { + // Fallback: use assembly version as footer text + var version = Assembly.GetEntryAssembly()?.GetName().Version; + skiaShell.FlyoutFooterText = $"Version {version?.Major ?? 1}.{version?.Minor ?? 0}.{version?.Build ?? 0}"; + } // Process shell items into sections foreach (var item in shell.Items) diff --git a/Views/SkiaImage.cs b/Views/SkiaImage.cs index 08ed712..4932b1d 100644 --- a/Views/SkiaImage.cs +++ b/Views/SkiaImage.cs @@ -461,6 +461,23 @@ public class SkiaImage : SkiaView try { + // First try to load from embedded resources (MAUI standard pattern) + // MAUI converts SVG to PNG at build time, referenced as .png in XAML + var (stream, actualExtension) = TryLoadFromEmbeddedResource(filePath); + if (stream != null) + { + _isSvg = actualExtension.Equals(".svg", StringComparison.OrdinalIgnoreCase); + _currentFilePath = filePath; + _cacheKey = $"embedded:{filePath}"; + + using (stream) + { + await LoadFromStreamWithCacheAsync(stream, _cacheKey); + } + return; + } + + // Fall back to file system List searchPaths = new List { filePath, @@ -469,7 +486,8 @@ public class SkiaImage : SkiaView Path.Combine(AppContext.BaseDirectory, "Resources", filePath) }; - // Also try SVG if looking for PNG + // Also try SVG if looking for PNG (MAUI converts SVG to PNG at build time, + // but on Linux we load SVG directly) if (filePath.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) { string svgPath = Path.ChangeExtension(filePath, ".svg"); @@ -495,6 +513,7 @@ public class SkiaImage : SkiaView _isSvg = false; _currentFilePath = null; _cacheKey = null; + Console.WriteLine($"[SkiaImage] File not found: {filePath}"); ImageLoadingError?.Invoke(this, new ImageLoadingErrorEventArgs(new FileNotFoundException(filePath))); return; } @@ -1153,6 +1172,174 @@ public class SkiaImage : SkiaView } base.Dispose(disposing); } + + /// + /// Tries to load an image from embedded resources. + /// Follows MAUI convention: XAML references .png, but source can be .svg + /// + private static (Stream? stream, string extension) TryLoadFromEmbeddedResource(string filePath) + { + // Get the file name without path + string fileName = Path.GetFileName(filePath); + string fileNameWithoutExt = Path.GetFileNameWithoutExtension(fileName); + string requestedExt = Path.GetExtension(fileName); + + // Search all loaded assemblies for the resource + var assemblies = AppDomain.CurrentDomain.GetAssemblies() + .Where(a => !a.IsDynamic && !string.IsNullOrEmpty(a.Location)) + .ToList(); + + // Also include entry assembly + var entryAssembly = System.Reflection.Assembly.GetEntryAssembly(); + if (entryAssembly != null && !assemblies.Contains(entryAssembly)) + { + assemblies.Insert(0, entryAssembly); + } + + foreach (var assembly in assemblies) + { + try + { + var resourceNames = assembly.GetManifestResourceNames(); + + // Try exact match first + foreach (var resourceName in resourceNames) + { + if (resourceName.EndsWith(fileName, StringComparison.OrdinalIgnoreCase)) + { + var stream = assembly.GetManifestResourceStream(resourceName); + if (stream != null) + { + Console.WriteLine($"[SkiaImage] Loaded embedded resource: {resourceName}"); + return (stream, requestedExt); + } + } + } + + // If looking for .png, also try .svg (MAUI pattern) + if (requestedExt.Equals(".png", StringComparison.OrdinalIgnoreCase)) + { + string svgFileName = fileNameWithoutExt + ".svg"; + foreach (var resourceName in resourceNames) + { + if (resourceName.EndsWith(svgFileName, StringComparison.OrdinalIgnoreCase)) + { + var stream = assembly.GetManifestResourceStream(resourceName); + if (stream != null) + { + Console.WriteLine($"[SkiaImage] Loaded SVG as PNG substitute: {resourceName}"); + return (stream, ".svg"); + } + } + } + } + } + catch + { + // Skip assemblies that can't be inspected + } + } + + return (null, string.Empty); + } + + /// + /// Loads image from stream with caching support. + /// + private async Task LoadFromStreamWithCacheAsync(Stream stream, string cacheKey) + { + // Check cache first + if (_imageCache.TryGetValue(cacheKey, out var cached)) + { + cached.LastAccessed = DateTime.UtcNow; + if (cached.IsAnimated && cached.Frames != null) + { + _isAnimatedImage = true; + _animationFrames = cached.Frames; + _currentFrameIndex = 0; + if (cached.Frames.Count > 0 && cached.Frames[0].Bitmap != null) + { + _image?.Dispose(); + _image = SKImage.FromBitmap(cached.Frames[0].Bitmap); + } + if (IsAnimationPlaying) + { + StartAnimation(); + } + } + else if (cached.Bitmap != null) + { + _isAnimatedImage = false; + _bitmap = cached.Bitmap; + _image?.Dispose(); + _image = SKImage.FromBitmap(cached.Bitmap); + } + _isLoading = false; + ImageLoaded?.Invoke(this, EventArgs.Empty); + Invalidate(); + return; + } + + // Load from stream + using var memoryStream = new MemoryStream(); + await stream.CopyToAsync(memoryStream); + memoryStream.Position = 0; + + if (_isSvg) + { + // Load SVG using Svg.Skia + await Task.Run(() => + { + using var svg = new SKSvg(); + svg.Load(memoryStream); + + if (svg.Picture != null) + { + var cullRect = svg.Picture.CullRect; + int width = (int)(WidthRequest > 0 ? WidthRequest : cullRect.Width); + int height = (int)(HeightRequest > 0 ? HeightRequest : cullRect.Height); + + if (width <= 0) width = 64; + if (height <= 0) height = 64; + + float scale = Math.Min(width / cullRect.Width, height / cullRect.Height); + int bitmapWidth = Math.Max(1, (int)(cullRect.Width * scale)); + int bitmapHeight = Math.Max(1, (int)(cullRect.Height * scale)); + + var bitmap = new SKBitmap(bitmapWidth, bitmapHeight, false); + using var canvas = new SKCanvas(bitmap); + canvas.Clear(SKColors.Transparent); + canvas.Scale(scale); + // Translate to handle negative viewBox coordinates + canvas.Translate(-cullRect.Left, -cullRect.Top); + canvas.DrawPicture(svg.Picture, null); + + CacheAndSetBitmap(cacheKey, bitmap, false); + } + }); + } + else + { + // Load raster image + memoryStream.Position = 0; + await Task.Run(() => + { + using var codec = SKCodec.Create(memoryStream); + if (codec != null) + { + var bitmap = SKBitmap.Decode(codec, codec.Info); + if (bitmap != null) + { + CacheAndSetBitmap(cacheKey, bitmap, false); + } + } + }); + } + + _isLoading = false; + ImageLoaded?.Invoke(this, EventArgs.Empty); + Invalidate(); + } } /// diff --git a/Views/SkiaLayoutView.cs b/Views/SkiaLayoutView.cs index 6634eb0..6ea5574 100644 --- a/Views/SkiaLayoutView.cs +++ b/Views/SkiaLayoutView.cs @@ -443,9 +443,30 @@ public class SkiaStackLayout : SkiaLayoutView ? remainingHeight : Math.Min(childHeight, remainingHeight > 0 ? remainingHeight : childHeight); - childBoundsLeft = content.Left; + // Respect child's HorizontalOptions for vertical layouts + var useWidth = Math.Min(childWidth, contentWidth); + float childLeft = content.Left; + + var horizontalOptions = child.HorizontalOptions; + var alignmentValue = (int)horizontalOptions.Alignment; + + // LayoutAlignment: Start=0, Center=1, End=2, Fill=3 + if (alignmentValue == 1) // Center + { + childLeft = content.Left + (contentWidth - useWidth) / 2; + } + else if (alignmentValue == 2) // End + { + childLeft = content.Left + contentWidth - useWidth; + } + else if (alignmentValue == 3) // Fill + { + useWidth = contentWidth; + } + + childBoundsLeft = childLeft; childBoundsTop = content.Top + offset; - childBoundsWidth = contentWidth; + childBoundsWidth = useWidth; childBoundsHeight = useHeight; offset += useHeight + (float)Spacing; } diff --git a/Views/SkiaShell.cs b/Views/SkiaShell.cs index 9f2729a..a9f5cf3 100644 --- a/Views/SkiaShell.cs +++ b/Views/SkiaShell.cs @@ -298,10 +298,15 @@ public class SkiaShell : SkiaLayoutView public float FlyoutHeaderHeight { get; set; } = 140f; /// - /// Optional footer text in the flyout. + /// Optional footer text in the flyout (fallback if no FlyoutFooterView). /// public string? FlyoutFooterText { get; set; } + /// + /// Optional footer view in the flyout. + /// + public SkiaView? FlyoutFooterView { get; set; } + /// /// Height of the flyout footer. /// @@ -972,9 +977,31 @@ public class SkiaShell : SkiaLayoutView }; canvas.DrawRect(flyoutBounds, flyoutPaint); - // Draw flyout items - float itemY = flyoutBounds.Top + 80; + // Calculate header and footer heights + float headerHeight = FlyoutHeaderView != null ? FlyoutHeaderHeight : 0f; + float footerHeight = FlyoutFooterView != null ? FlyoutFooterHeight : + (!string.IsNullOrEmpty(FlyoutFooterText) ? FlyoutFooterHeight : 0f); + + // Draw flyout header if present + if (FlyoutHeaderView != null) + { + var headerBounds = new SKRect(flyoutBounds.Left, flyoutBounds.Top, flyoutBounds.Right, flyoutBounds.Top + headerHeight); + FlyoutHeaderView.Measure(new Size(headerBounds.Width, headerBounds.Height)); + FlyoutHeaderView.Arrange(new Rect(headerBounds.Left, headerBounds.Top, headerBounds.Width, headerBounds.Height)); + FlyoutHeaderView.Draw(canvas); + } + + // Draw flyout items with scrolling support float itemHeight = 48f; + float itemsAreaTop = flyoutBounds.Top + headerHeight; + float itemsAreaBottom = flyoutBounds.Bottom - footerHeight; + + // Clip to items area (between header and footer) + canvas.Save(); + canvas.ClipRect(new SKRect(flyoutBounds.Left, itemsAreaTop, flyoutBounds.Right, itemsAreaBottom)); + + // Apply scroll offset + float itemY = itemsAreaTop - _flyoutScrollOffset; using var itemTextPaint = new SKPaint { @@ -987,6 +1014,17 @@ public class SkiaShell : SkiaLayoutView var section = _sections[i]; bool isSelected = i == _selectedSectionIndex; + // Skip items that are scrolled above the visible area + if (itemY + itemHeight < itemsAreaTop) + { + itemY += itemHeight; + continue; + } + + // Stop if we're below the visible area + if (itemY > itemsAreaBottom) + break; + // Draw selection background if (isSelected) { @@ -1004,6 +1042,29 @@ public class SkiaShell : SkiaLayoutView itemY += itemHeight; } + + canvas.Restore(); + + // Draw flyout footer + if (FlyoutFooterView != null) + { + var footerBounds = new SKRect(flyoutBounds.Left, flyoutBounds.Bottom - footerHeight, flyoutBounds.Right, flyoutBounds.Bottom); + FlyoutFooterView.Measure(new Size(footerBounds.Width, footerBounds.Height)); + FlyoutFooterView.Arrange(new Rect(footerBounds.Left, footerBounds.Top, footerBounds.Width, footerBounds.Height)); + FlyoutFooterView.Draw(canvas); + } + else if (!string.IsNullOrEmpty(FlyoutFooterText)) + { + // Fallback: draw simple text footer + using var footerPaint = new SKPaint + { + TextSize = 12f, + Color = _flyoutTextColorSK.WithAlpha(180), + IsAntialias = true + }; + var footerY = flyoutBounds.Bottom - footerHeight / 2 + 4; + canvas.DrawText(FlyoutFooterText, flyoutBounds.Left + 16, footerY, footerPaint); + } } public override SkiaView? HitTest(float x, float y) @@ -1062,20 +1123,33 @@ public class SkiaShell : SkiaLayoutView if (flyoutBounds.Contains(e.X, e.Y)) { - // Check which section was tapped - float itemY = flyoutBounds.Top + 80; - float itemHeight = 48f; + // Calculate header and footer heights + float headerHeight = FlyoutHeaderView != null ? FlyoutHeaderHeight : 0f; + float footerHeight = FlyoutFooterView != null ? FlyoutFooterHeight : + (!string.IsNullOrEmpty(FlyoutFooterText) ? FlyoutFooterHeight : 0f); - for (int i = 0; i < _sections.Count; i++) + float itemsAreaTop = flyoutBounds.Top + headerHeight; + float itemsAreaBottom = flyoutBounds.Bottom - footerHeight; + + // Only check items if tap is in items area + if (e.Y >= itemsAreaTop && e.Y < itemsAreaBottom) { - if (e.Y >= itemY && e.Y < itemY + itemHeight) + // Apply scroll offset to find which item was tapped + float itemY = itemsAreaTop - _flyoutScrollOffset; + float itemHeight = 48f; + + for (int i = 0; i < _sections.Count; i++) { - NavigateToSection(i, 0); - FlyoutIsPresented = false; - e.Handled = true; - return; + if (e.Y >= itemY && e.Y < itemY + itemHeight) + { + NavigateToSection(i, 0); + FlyoutIsPresented = false; + _flyoutScrollOffset = 0; // Reset scroll when closing + e.Handled = true; + return; + } + itemY += itemHeight; } - itemY += itemHeight; } } else if (FlyoutIsPresented) @@ -1138,13 +1212,14 @@ public class SkiaShell : SkiaLayoutView if (flyoutBounds.Contains(e.X, e.Y)) { float headerHeight = FlyoutHeaderView != null ? FlyoutHeaderHeight : 0f; - float footerHeight = !string.IsNullOrEmpty(FlyoutFooterText) ? FlyoutFooterHeight : 0f; + float footerHeight = FlyoutFooterView != null ? FlyoutFooterHeight : + (!string.IsNullOrEmpty(FlyoutFooterText) ? FlyoutFooterHeight : 0f); float itemHeight = 48f; float totalItemsHeight = _sections.Count * itemHeight; float viewableHeight = flyoutBounds.Height - headerHeight - footerHeight; float maxScroll = Math.Max(0f, totalItemsHeight - viewableHeight); - _flyoutScrollOffset -= e.DeltaY * 30f; + _flyoutScrollOffset += e.DeltaY * 30f; _flyoutScrollOffset = Math.Max(0f, Math.Min(_flyoutScrollOffset, maxScroll)); Invalidate(); e.Handled = true; diff --git a/assets/logo_only.png b/assets/logo_only.png new file mode 100644 index 0000000000000000000000000000000000000000..fa1d567a57bcb52853f6848cbe7ce02b23e3ac4c GIT binary patch literal 8656 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 + + + + + + + +