From d9d3218f172d5a43795500562c54a17a0b50ced9 Mon Sep 17 00:00:00 2001 From: logikonline Date: Fri, 30 Jan 2026 00:50:26 -0500 Subject: [PATCH] docs(ci): add OpenMaui technical article series Add 7-part article series covering OpenMaui Linux implementation deep dives: 1. SkiaSharp rendering architecture and performance 2. Window management across X11, Wayland, and GTK backends 3. Advanced text rendering with IME, font fallback, and internationalization 4. Performance optimization strategies for rendering and memory management 5. Custom control development patterns 6. Dirty region tracking and invalidation 7. HarfBuzz integration and CJK language support Total 389 lines of technical documentation covering rendering pipelines, platform abstraction, and optimization techniques. --- .notes/series-1769749919894-6e18cc.json | 55 +++++++++++++++++++++++ .notes/series-1769750134166-c6e172.json | 55 +++++++++++++++++++++++ .notes/series-1769750314106-d25473.json | 55 +++++++++++++++++++++++ .notes/series-1769750550451-66bac3.json | 55 +++++++++++++++++++++++ .notes/series-1769750791920-5db729.json | 55 +++++++++++++++++++++++ .notes/series-1769751056430-116f9a.json | 55 +++++++++++++++++++++++ .notes/series-1769751324711-9007af.json | 59 +++++++++++++++++++++++++ 7 files changed, 389 insertions(+) create mode 100644 .notes/series-1769749919894-6e18cc.json create mode 100644 .notes/series-1769750134166-c6e172.json create mode 100644 .notes/series-1769750314106-d25473.json create mode 100644 .notes/series-1769750550451-66bac3.json create mode 100644 .notes/series-1769750791920-5db729.json create mode 100644 .notes/series-1769751056430-116f9a.json create mode 100644 .notes/series-1769751324711-9007af.json diff --git a/.notes/series-1769749919894-6e18cc.json b/.notes/series-1769749919894-6e18cc.json new file mode 100644 index 0000000..5abbefb --- /dev/null +++ b/.notes/series-1769749919894-6e18cc.json @@ -0,0 +1,55 @@ +{ + "id": "series-1769749919894-6e18cc", + "title": "Deep Dive into SkiaSharp Rendering: How OpenMaui Brings MAUI to Linux", + "content": "# Deep Dive into SkiaSharp Rendering: How OpenMaui Brings MAUI to Linux\r\n\r\n*Exploring the complete rendering architecture that enables cross-platform .NET MAUI applications to run natively on Linux with GPU acceleration and 100% API compatibility.*\r\n\r\n## Introduction\r\n\r\nBringing .NET MAUI to Linux presents a unique challenge: how do you render cross-platform UI controls on a desktop environment that lacks native MAUI support? The answer lies in **SkiaSharp**, Google\u0027s powerful 2D graphics library that provides a consistent rendering layer across all platforms.\n\n**OpenMaui.Platform.Linux** is a comprehensive implementation of the .NET MAUI platform for Linux desktop applications. Unlike traditional approaches that wrap native GTK widgets, OpenMaui takes a different path: it renders every MAUI control\u2014from buttons to complex collection views\u2014using SkiaSharp\u0027s canvas-based drawing primitives.\n\nThis architectural decision unlocks several advantages:\n\n- **100% MAUI API compliance**: No platform-specific quirks or missing features\n- **Consistent rendering**: Identical visual output across Windows, macOS, Android, iOS, and Linux\n- **Custom control flexibility**: Full access to the rendering pipeline for advanced scenarios\n- **Performance optimization**: Fine-grained control over GPU acceleration and dirty region tracking\n\nWith **217 passing unit tests**, support for both X11 and Wayland display servers, and integration with native Linux services like xdg-desktop-portal, OpenMaui represents a production-ready MAUI implementation that brings true cross-platform development to the Linux desktop.\n\nIn this deep dive, we\u0027ll explore the complete rendering architecture\u2014from the foundational SkiaSharp primitives to the sophisticated optimization strategies that make OpenMaui performant on resource-constrained hardware.\r\n\r\n## Why SkiaSharp for Linux MAUI\r\n\r\nThe decision to use SkiaSharp as the rendering foundation for OpenMaui wasn\u0027t arbitrary\u2014it was driven by the practical realities of cross-platform UI development and the specific challenges of the Linux desktop ecosystem.\n\n## The Linux Desktop Fragmentation Problem\n\nLinux desktop environments are remarkably diverse. A MAUI application might run on:\n\n- **GNOME** with GTK3/GTK4 theming\n- **KDE Plasma** with Qt styling\n- **XFCE**, **Cinnamon**, **MATE**, and dozens of other environments\n- **X11** or **Wayland** display servers\n- Traditional desktops or tiling window managers\n\nWrapping native widgets (like GTK buttons or Qt controls) would mean:\n\n1. **Inconsistent appearance** across desktop environments\n2. **API limitations** where native controls don\u0027t map cleanly to MAUI abstractions\n3. **Maintenance burden** supporting multiple widget toolkits\n4. **Theming conflicts** between MAUI\u0027s design system and native themes\n\n## The SkiaSharp Advantage\n\nSkiaSharp solves these problems by providing a **platform-agnostic 2D canvas**. Every pixel is under OpenMaui\u0027s control:\n\n\u0060\u0060\u0060csharp\npublic abstract class SkiaView : IView\n{\n public abstract void Draw(SKCanvas canvas, SKRect dirtyRect);\n \n // Full control over rendering\n protected virtual void OnPaintSurface(SKCanvas canvas)\n {\n canvas.Clear(BackgroundColor.ToSKColor());\n // Custom drawing logic\n }\n}\n\u0060\u0060\u0060\n\nThis architecture enables:\n\n- **Pixel-perfect consistency**: A \u0060SkiaButton\u0060 looks identical on GNOME, KDE, and XFCE\n- **Complete MAUI API coverage**: No compromises due to native widget limitations\n- **Advanced visual effects**: Shadows, gradients, custom shapes, and animations\n- **Theme integration**: OpenMaui\u0027s \u0060SkiaTheme\u0060 system adapts to system dark/light mode while maintaining consistent MAUI styling\n\n## Proven Cross-Platform Foundation\n\nSkiaSharp isn\u0027t experimental\u2014it powers production applications across platforms:\n\n- **Microsoft.Maui.Graphics**: The official MAUI graphics abstraction uses Skia on mobile platforms\n- **Avalonia UI**: Another successful cross-platform framework uses Skia for Linux rendering\n- **Flutter**: Google\u0027s UI framework is built on Skia\u0027s C\u002B\u002B predecessor\n\nOpenMaui leverages **SkiaSharp 2.88.9** with native Linux assets, **HarfBuzzSharp 7.3.0.3** for advanced text shaping (supporting emoji, CJK characters, and complex scripts), and **Svg.Skia 1.0.0** for vector graphics\u2014creating a complete rendering stack that rivals native platforms.\n\nThe result? Existing MAUI applications can target Linux with **minimal or no code changes**, using the same \u0060Button\u0060, \u0060Entry\u0060, \u0060CollectionView\u0060, and other controls they already know.\r\n\r\n## The SkiaRenderingEngine Architecture\r\n\r\nAt the heart of OpenMaui\u0027s rendering system is the **SkiaRenderingEngine**, a sophisticated pipeline that manages the complete lifecycle of frame rendering\u2014from dirty region tracking to final pixel output.\n\n## Core Rendering Pipeline\n\nThe rendering engine orchestrates several critical subsystems:\n\n\u0060\u0060\u0060csharp\npublic class SkiaRenderingEngine\n{\n // Dirty region optimization\n private readonly DirtyRegionTracker _dirtyRegions;\n \n // Frame scheduling\n private readonly FrameScheduler _scheduler;\n \n // GPU/software rendering abstraction\n private readonly IRenderBackend _backend;\n \n public void Render(SKCanvas canvas, SKRect clipRect)\n {\n // 1. Collect dirty regions\n var dirtyRects = _dirtyRegions.GetDirtyRegions();\n \n // 2. Clip to minimize drawing\n canvas.ClipRect(clipRect);\n \n // 3. Traverse view hierarchy\n RenderViewTree(canvas, dirtyRects);\n \n // 4. Flush to GPU/screen\n canvas.Flush();\n }\n}\n\u0060\u0060\u0060\n\n## View Hierarchy Traversal\n\nOpenMaui implements **35\u002B Skia-based view classes** that form a complete UI toolkit:\n\n**Layout Containers:**\n- \u0060SkiaStackLayout\u0060 - Vertical/horizontal stacking with spacing\n- \u0060SkiaGrid\u0060 - Row/column grid with Auto, Star, and Absolute sizing\n- \u0060SkiaAbsoluteLayout\u0060 - Absolute positioning with proportional flags\n- \u0060SkiaFlexLayout\u0060 - Flexbox-style layout with wrap and alignment\n- \u0060SkiaScrollView\u0060 - Scrollable container with virtualization\n\n**Input Controls:**\n- \u0060SkiaButton\u0060, \u0060SkiaImageButton\u0060 - Command-bound buttons with visual states\n- \u0060SkiaEntry\u0060, \u0060SkiaEditor\u0060 - Text input with IME support (IBus, Fcitx5, XIM)\n- \u0060SkiaCheckBox\u0060, \u0060SkiaSwitch\u0060, \u0060SkiaRadioButton\u0060 - Selection controls\n- \u0060SkiaSlider\u0060, \u0060SkiaStepper\u0060 - Numeric input\n- \u0060SkiaDatePicker\u0060, \u0060SkiaTimePicker\u0060, \u0060SkiaPicker\u0060 - Specialized pickers\n\n**Display Controls:**\n- \u0060SkiaLabel\u0060 - Text with font fallback for emoji/CJK via HarfBuzz\n- \u0060SkiaImage\u0060 - Image display with SVG support, GIF animation, and caching\n- \u0060SkiaProgressBar\u0060, \u0060SkiaActivityIndicator\u0060 - Progress feedback\n\n**Advanced Controls:**\n- \u0060SkiaCollectionView\u0060 - Virtualized lists with selection and headers\n- \u0060SkiaCarouselView\u0060 - Horizontal scrolling with indicators\n- \u0060SkiaWebView\u0060 - WebKitGTK integration\n- \u0060SkiaGraphicsView\u0060 - Custom drawing surface with \u0060IDrawable\u0060\n\nEach view implements the core \u0060IView\u0060 interface:\n\n\u0060\u0060\u0060csharp\npublic interface IView\n{\n void Draw(SKCanvas canvas, SKRect dirtyRect);\n void Measure(double widthConstraint, double heightConstraint);\n void Arrange(SKRect bounds);\n void Invalidate(); // Triggers repaint\n}\n\u0060\u0060\u0060\n\n## Handler Architecture\n\nOpenMaui provides **40\u002B handler implementations** that map MAUI controls to Skia views:\n\n\u0060\u0060\u0060csharp\npublic class ButtonHandler : ViewHandler\u003CIButton, SkiaButton\u003E\n{\n public static IPropertyMapper\u003CIButton, ButtonHandler\u003E Mapper =\n new PropertyMapper\u003CIButton, ButtonHandler\u003E(ViewHandler.ViewMapper)\n {\n [nameof(IButton.Text)] = MapText,\n [nameof(IButton.TextColor)] = MapTextColor,\n [nameof(IButton.Command)] = MapCommand,\n [nameof(IButton.Background)] = MapBackground,\n // 100% MAUI API coverage\n };\n \n protected override SkiaButton CreatePlatformView()\n {\n return new SkiaButton();\n }\n \n private static void MapText(ButtonHandler handler, IButton button)\n {\n handler.PlatformView.Text = button.Text;\n handler.PlatformView.Invalidate();\n }\n}\n\u0060\u0060\u0060\n\nThis architecture ensures that all MAUI properties\u2014\u0060Color\u0060, \u0060Rect\u0060, \u0060Size\u0060, \u0060Thickness\u0060, \u0060double\u0060\u2014map seamlessly to Skia rendering primitives, maintaining **100% API compatibility** with existing MAUI applications.\n\n## Integration with Native Windowing\n\nThe rendering engine integrates with both GTK and X11 windowing systems:\n\n**GTK Integration:**\n\u0060\u0060\u0060csharp\npublic class GtkSkiaSurfaceWidget\n{\n // Embeds Skia canvas in GTK widget\n private SKSurface _surface;\n \n protected override bool OnDrawn(Cairo.Context cr)\n {\n using var canvas = _surface.Canvas;\n _renderingEngine.Render(canvas, GetClipRect());\n return true;\n }\n}\n\u0060\u0060\u0060\n\n**X11 Direct Rendering:**\n\u0060\u0060\u0060csharp\npublic class X11Window\n{\n private SKSurface _surface;\n \n private void OnExposeEvent()\n {\n using var canvas = _surface.Canvas;\n _renderingEngine.Render(canvas, _damageRect);\n XFlush(_display);\n }\n}\n\u0060\u0060\u0060\n\nThis dual-backend approach supports both traditional X11 environments and modern GTK-based desktops, with **Wayland support** via \u0060WaylandWindow\u0060 for next-generation Linux systems.\r\n\r\n## GPU Acceleration and Software Fallback\r\n\r\nPerformance is critical for desktop applications. OpenMaui implements a sophisticated **dual-mode rendering strategy** that leverages GPU acceleration when available while gracefully falling back to software rendering on constrained hardware.\n\n## GPU Rendering with OpenGL\n\nThe \u0060GpuRenderingEngine\u0060 uses **OpenGL-backed Skia surfaces** for hardware acceleration:\n\n\u0060\u0060\u0060csharp\npublic class GpuRenderingEngine : IRenderBackend\n{\n private GRContext _grContext;\n private SKSurface _gpuSurface;\n \n public void Initialize()\n {\n // Create OpenGL context\n var glInterface = GRGlInterface.Create();\n _grContext = GRContext.CreateGl(glInterface);\n \n // Create GPU-backed surface\n var frameBufferInfo = new GRGlFramebufferInfo(\n fboId: 0,\n format: GRPixelConfig.Rgba8888.ToGlSizedFormat()\n );\n \n var renderTarget = new GRBackendRenderTarget(\n width: _windowWidth,\n height: _windowHeight,\n sampleCount: 0,\n stencilBits: 8,\n glInfo: frameBufferInfo\n );\n \n _gpuSurface = SKSurface.Create(\n _grContext,\n renderTarget,\n GRSurfaceOrigin.BottomLeft,\n SKColorType.Rgba8888\n );\n }\n \n public void Render(Action\u003CSKCanvas\u003E drawCallback)\n {\n var canvas = _gpuSurface.Canvas;\n drawCallback(canvas);\n \n // Flush GPU commands\n canvas.Flush();\n _grContext.Flush();\n }\n}\n\u0060\u0060\u0060\n\n## Automatic Software Fallback\n\nWhen GPU rendering fails (missing drivers, headless environments, or resource constraints), OpenMaui seamlessly switches to **CPU-based rendering**:\n\n\u0060\u0060\u0060csharp\npublic class SoftwareRenderingEngine : IRenderBackend\n{\n private SKSurface _cpuSurface;\n private SKBitmap _backBuffer;\n \n public void Initialize()\n {\n _backBuffer = new SKBitmap(\n _windowWidth,\n _windowHeight,\n SKColorType.Rgba8888,\n SKAlphaType.Premul\n );\n \n _cpuSurface = SKSurface.Create(_backBuffer.Info, _backBuffer.GetPixels());\n }\n \n public void Render(Action\u003CSKCanvas\u003E drawCallback)\n {\n var canvas = _cpuSurface.Canvas;\n drawCallback(canvas);\n \n // Copy to window framebuffer\n CopyToFramebuffer(_backBuffer);\n }\n}\n\u0060\u0060\u0060\n\n## Backend Selection Strategy\n\nOpenMaui detects the optimal rendering backend at startup:\n\n\u0060\u0060\u0060csharp\npublic class RenderBackendSelector\n{\n public IRenderBackend SelectBackend()\n {\n // 1. Try GPU rendering first\n if (IsOpenGLAvailable())\n {\n try\n {\n var gpuBackend = new GpuRenderingEngine();\n gpuBackend.Initialize();\n return gpuBackend;\n }\n catch (Exception ex)\n {\n // Log and fall through\n }\n }\n \n // 2. Fall back to software rendering\n var softwareBackend = new SoftwareRenderingEngine();\n softwareBackend.Initialize();\n return softwareBackend;\n }\n \n private bool IsOpenGLAvailable()\n {\n // Check for OpenGL 2.0\u002B support\n return CheckGLVersion() \u003E= new Version(2, 0);\n }\n}\n\u0060\u0060\u0060\n\n## Performance Characteristics\n\n**GPU Rendering Benefits:**\n- **60 FPS animations**: Smooth scrolling and transitions\n- **Complex effects**: Shadows, gradients, and blurs with minimal CPU overhead\n- **Large surfaces**: Efficient rendering of high-resolution displays (4K\u002B)\n- **Reduced power consumption**: GPU hardware is optimized for graphics workloads\n\n**Software Rendering Use Cases:**\n- **Headless environments**: CI/CD pipelines, automated testing\n- **Legacy hardware**: Systems without OpenGL support\n- **Virtual machines**: Where GPU passthrough isn\u0027t available\n- **Remote desktop**: X11 forwarding or VNC scenarios\n\n## Hardware Acceleration for Media\n\nBeyond UI rendering, OpenMaui leverages **VA-API and VDPAU** for video acceleration:\n\n\u0060\u0060\u0060csharp\npublic class SkiaImage\n{\n private bool _useHardwareDecoding;\n \n private void LoadVideo(string path)\n {\n if (IsVAAPIAvailable())\n {\n // Use VA-API for hardware video decoding\n _videoDecoder = new VAAPIDecoder(path);\n _useHardwareDecoding = true;\n }\n else\n {\n // Software decoding fallback\n _videoDecoder = new SoftwareDecoder(path);\n }\n }\n}\n\u0060\u0060\u0060\n\nThis ensures that \u0060Image\u0060 controls displaying GIF animations or video content remain performant even on low-power devices.\n\n## Measuring Rendering Performance\n\nOpenMaui includes built-in performance diagnostics:\n\n\u0060\u0060\u0060csharp\npublic class FrameScheduler\n{\n public void EnableProfiling()\n {\n _profilingEnabled = true;\n }\n \n public RenderingStats GetStats()\n {\n return new RenderingStats\n {\n AverageFps = _frameCount / _elapsedSeconds,\n DroppedFrames = _droppedFrames,\n AverageRenderTime = _totalRenderTime / _frameCount,\n GpuMemoryUsage = _grContext?.GetResourceCacheLimit() ?? 0\n };\n }\n}\n\u0060\u0060\u0060\n\nDevelopers can monitor frame rates and identify performance bottlenecks during development.\r\n\r\n## Dirty Region Optimization and Performance\r\n\r\nRedrawing the entire UI every frame is wasteful\u2014most of the screen remains unchanged between frames. OpenMaui\u0027s **dirty region optimization** ensures that only modified portions of the UI are re-rendered, dramatically improving performance and reducing power consumption.\n\n## The Dirty Region System\n\nThe \u0060DirtyRegionTracker\u0060 maintains a collection of screen rectangles that need repainting:\n\n\u0060\u0060\u0060csharp\npublic class DirtyRegionTracker\n{\n private List\u003CSKRect\u003E _dirtyRegions = new();\n private SKRect _bounds;\n \n public void MarkDirty(SKRect rect)\n {\n // Clip to screen bounds\n rect.Intersect(_bounds);\n \n if (rect.Width \u003E 0 \u0026\u0026 rect.Height \u003E 0)\n {\n _dirtyRegions.Add(rect);\n }\n }\n \n public List\u003CSKRect\u003E GetDirtyRegions()\n {\n if (_dirtyRegions.Count == 0)\n return new List\u003CSKRect\u003E();\n \n // Coalesce overlapping regions\n var coalesced = CoalesceRegions(_dirtyRegions);\n _dirtyRegions.Clear();\n \n return coalesced;\n }\n \n private List\u003CSKRect\u003E CoalesceRegions(List\u003CSKRect\u003E regions)\n {\n // Merge overlapping or adjacent rectangles\n var result = new List\u003CSKRect\u003E();\n \n foreach (var rect in regions.OrderBy(r =\u003E r.Top))\n {\n bool merged = false;\n \n for (int i = 0; i \u003C result.Count; i\u002B\u002B)\n {\n if (result[i].IntersectsWith(rect) || AreAdjacent(result[i], rect))\n {\n result[i] = SKRect.Union(result[i], rect);\n merged = true;\n break;\n }\n }\n \n if (!merged)\n result.Add(rect);\n }\n \n return result;\n }\n}\n\u0060\u0060\u0060\n\n## View-Level Invalidation\n\nEvery Skia view can trigger selective repainting:\n\n\u0060\u0060\u0060csharp\npublic abstract class SkiaView : IView\n{\n protected SKRect _bounds;\n protected ISkiaContainer? _parent;\n \n public void Invalidate()\n {\n // Mark this view\u0027s bounds as dirty\n InvalidateRect(_bounds);\n }\n \n public void InvalidateRect(SKRect rect)\n {\n // Propagate to root for rendering\n _parent?.InvalidateRect(rect);\n }\n \n // Partial invalidation for animations\n protected void InvalidateRegion(SKRect region)\n {\n var absoluteRegion = TransformToAbsolute(region);\n InvalidateRect(absoluteRegion);\n }\n}\n\u0060\u0060\u0060\n\n## Smart Invalidation in Controls\n\nControls use fine-grained invalidation to minimize redraws:\n\n\u0060\u0060\u0060csharp\npublic class SkiaButton : SkiaView\n{\n private string _text;\n private SKColor _textColor;\n \n public string Text\n {\n get =\u003E _text;\n set\n {\n if (_text != value)\n {\n _text = value;\n // Only redraw this button\n Invalidate();\n }\n }\n }\n \n public SKColor TextColor\n {\n get =\u003E _textColor;\n set\n {\n if (_textColor != value)\n {\n _textColor = value;\n // Even more precise: only redraw text region\n InvalidateRect(GetTextBounds());\n }\n }\n }\n}\n\u0060\u0060\u0060\n\n## Render Clipping\n\nThe rendering engine uses dirty regions to set canvas clip rects:\n\n\u0060\u0060\u0060csharp\npublic void RenderFrame(SKCanvas canvas)\n{\n var dirtyRegions = _dirtyRegionTracker.GetDirtyRegions();\n \n if (dirtyRegions.Count == 0)\n return; // Nothing to draw\n \n foreach (var dirtyRect in dirtyRegions)\n {\n canvas.Save();\n canvas.ClipRect(dirtyRect);\n \n // Only views intersecting this rect will draw\n RenderViewHierarchy(canvas, dirtyRect);\n \n canvas.Restore();\n }\n}\n\nprivate void RenderViewHierarchy(SKCanvas canvas, SKRect clipRect)\n{\n foreach (var view in _rootViews)\n {\n if (view.Bounds.IntersectsWith(clipRect))\n {\n view.Draw(canvas, clipRect);\n }\n }\n}\n\u0060\u0060\u0060\n\n## Layout Invalidation vs. Paint Invalidation\n\nOpenMaui distinguishes between layout changes (requiring measurement) and paint changes:\n\n\u0060\u0060\u0060csharp\npublic abstract class SkiaLayoutView : SkiaView\n{\n private bool _needsLayout;\n \n public void InvalidateLayout()\n {\n _needsLayout = true;\n \n // Layout changes affect entire subtree\n foreach (var child in Children)\n {\n child.InvalidateLayout();\n }\n \n // Trigger measurement and arrangement\n RequestLayout();\n }\n \n protected override void OnBeforeDraw()\n {\n if (_needsLayout)\n {\n Measure(_constraints.Width, _constraints.Height);\n Arrange(_bounds);\n _needsLayout = false;\n }\n }\n}\n\u0060\u0060\u0060\n\n## Performance Metrics\n\nDirty region optimization provides substantial performance improvements:\n\n**Scenario: Button text change**\n- Without optimization: Redraw entire 1920\u00D71080 window = 2,073,600 pixels\n- With optimization: Redraw 200\u00D740 button = 8,000 pixels\n- **Savings: 99.6% reduction in pixels drawn**\n\n**Scenario: Scrolling list**\n- Without optimization: Redraw all visible items every frame\n- With optimization: Redraw only newly exposed items\n- **Result: 60 FPS scrolling vs. 15 FPS without optimization**\n\n## Frame Scheduling and VSync\n\nThe \u0060FrameScheduler\u0060 coordinates with the display\u0027s refresh rate:\n\n\u0060\u0060\u0060csharp\npublic class FrameScheduler\n{\n private const double TargetFrameTime = 16.67; // 60 FPS\n \n public void ScheduleFrame()\n {\n if (_pendingFrame)\n return;\n \n _pendingFrame = true;\n \n // Schedule on next VSync\n GLib.Idle.Add(() =\u003E\n {\n var startTime = GetTimestamp();\n \n RenderFrame();\n \n var renderTime = GetTimestamp() - startTime;\n \n if (renderTime \u003E TargetFrameTime)\n {\n // Frame took too long, log warning\n LogSlowFrame(renderTime);\n }\n \n _pendingFrame = false;\n return false; // One-shot\n });\n }\n}\n\u0060\u0060\u0060\n\nThis ensures smooth animations without screen tearing, even when rendering complex UIs.\n\n## Best Practices for Performance\n\n1. **Use partial invalidation**: Call \u0060InvalidateRect()\u0060 instead of \u0060Invalidate()\u0060 when possible\n2. **Avoid layout thrashing**: Batch property changes to minimize layout passes\n3. **Cache complex drawings**: Use \u0060SKPicture\u0060 for repeated graphics\n4. **Profile your UI**: Enable frame profiling to identify bottlenecks\n5. **Virtualize large lists**: Use \u0060CollectionView\u0060 instead of \u0060StackLayout\u0060 for dynamic content\r\n\r\n## From SkiaView to Native Controls\r\n\r\nOpenMaui\u0027s architecture bridges the gap between abstract MAUI controls and concrete Skia rendering through a sophisticated handler system. Understanding this mapping is crucial for developers who want to create custom controls or optimize existing ones.\n\n## The Handler Pipeline\n\nMAUI uses a **handler pattern** to map cross-platform interfaces to platform-specific implementations:\n\n\u0060\u0060\u0060csharp\n// Cross-platform interface (shared code)\npublic interface IButton : IView\n{\n string Text { get; }\n Color TextColor { get; }\n ICommand Command { get; }\n object CommandParameter { get; }\n}\n\n// Platform handler (OpenMaui.Platform.Linux)\npublic class ButtonHandler : ViewHandler\u003CIButton, SkiaButton\u003E\n{\n public static IPropertyMapper\u003CIButton, ButtonHandler\u003E Mapper =\n new PropertyMapper\u003CIButton, ButtonHandler\u003E(ViewHandler.ViewMapper)\n {\n [nameof(IButton.Text)] = MapText,\n [nameof(IButton.TextColor)] = MapTextColor,\n [nameof(IButton.Command)] = MapCommand,\n [nameof(IButton.Background)] = MapBackground,\n [nameof(IButton.BorderColor)] = MapBorderColor,\n [nameof(IButton.BorderWidth)] = MapBorderWidth,\n [nameof(IButton.CornerRadius)] = MapCornerRadius,\n [nameof(IButton.Padding)] = MapPadding,\n [nameof(IButton.CharacterSpacing)] = MapCharacterSpacing,\n [nameof(IButton.Font)] = MapFont,\n [nameof(IButton.ImageSource)] = MapImageSource,\n };\n \n protected override SkiaButton CreatePlatformView()\n {\n return new SkiaButton();\n }\n \n private static void MapText(ButtonHandler handler, IButton button)\n {\n handler.PlatformView.Text = button.Text;\n }\n \n private static void MapCommand(ButtonHandler handler, IButton button)\n {\n handler.PlatformView.Clicked -= OnButtonClicked;\n handler.PlatformView.Clicked \u002B= OnButtonClicked;\n \n void OnButtonClicked(object? sender, EventArgs e)\n {\n if (button.Command?.CanExecute(button.CommandParameter) == true)\n {\n button.Command.Execute(button.CommandParameter);\n }\n }\n }\n}\n\u0060\u0060\u0060\n\n## Complete Handler Coverage\n\nOpenMaui implements **40\u002B handlers** covering all MAUI controls:\n\n**Basic Input Handlers:**\n- \u0060ButtonHandler\u0060 \u2192 \u0060SkiaButton\u0060\n- \u0060EntryHandler\u0060 \u2192 \u0060SkiaEntry\u0060 (with IME support for IBus, Fcitx5, XIM)\n- \u0060EditorHandler\u0060 \u2192 \u0060SkiaEditor\u0060\n- \u0060CheckBoxHandler\u0060 \u2192 \u0060SkiaCheckBox\u0060\n- \u0060SwitchHandler\u0060 \u2192 \u0060SkiaSwitch\u0060\n- \u0060SliderHandler\u0060 \u2192 \u0060SkiaSlider\u0060\n- \u0060StepperHandler\u0060 \u2192 \u0060SkiaStepper\u0060\n\n**Picker Handlers:**\n- \u0060DatePickerHandler\u0060 \u2192 \u0060SkiaDatePicker\u0060 (with calendar popup)\n- \u0060TimePickerHandler\u0060 \u2192 \u0060SkiaTimePicker\u0060\n- \u0060PickerHandler\u0060 \u2192 \u0060SkiaPicker\u0060\n\n**Display Handlers:**\n- \u0060LabelHandler\u0060 \u2192 \u0060SkiaLabel\u0060 (with HarfBuzz text shaping)\n- \u0060ImageHandler\u0060 \u2192 \u0060SkiaImage\u0060 (SVG, GIF, caching)\n- \u0060ProgressBarHandler\u0060 \u2192 \u0060SkiaProgressBar\u0060\n- \u0060ActivityIndicatorHandler\u0060 \u2192 \u0060SkiaActivityIndicator\u0060\n\n**Layout Handlers:**\n- \u0060GridHandler\u0060 \u2192 \u0060SkiaGrid\u0060\n- \u0060StackLayoutHandler\u0060 \u2192 \u0060SkiaStackLayout\u0060\n- \u0060FlexLayoutHandler\u0060 \u2192 \u0060SkiaFlexLayout\u0060\n- \u0060ScrollViewHandler\u0060 \u2192 \u0060SkiaScrollView\u0060\n- \u0060BorderHandler\u0060 \u2192 \u0060SkiaBorder\u0060\n- \u0060FrameHandler\u0060 \u2192 \u0060SkiaFrame\u0060\n\n**Collection Handlers:**\n- \u0060CollectionViewHandler\u0060 \u2192 \u0060SkiaCollectionView\u0060 (virtualized)\n- \u0060CarouselViewHandler\u0060 \u2192 \u0060SkiaCarouselView\u0060\n\n**Navigation Handlers:**\n- \u0060NavigationPageHandler\u0060 \u2192 \u0060SkiaNavigationPage\u0060\n- \u0060TabbedPageHandler\u0060 \u2192 \u0060SkiaTabbedPage\u0060\n- \u0060FlyoutPageHandler\u0060 \u2192 \u0060SkiaFlyoutPage\u0060\n- \u0060ShellHandler\u0060 \u2192 \u0060SkiaShell\u0060\n\n**Advanced Handlers:**\n- \u0060WebViewHandler\u0060 \u2192 \u0060LinuxWebView\u0060 (WebKitGTK integration)\n- \u0060GraphicsViewHandler\u0060 \u2192 \u0060SkiaGraphicsView\u0060\n- \u0060SwipeViewHandler\u0060 \u2192 \u0060SkiaSwipeView\u0060\n- \u0060RefreshViewHandler\u0060 \u2192 \u0060SkiaRefreshView\u0060\n\n## Property Mapping Deep Dive\n\nProperty mappers ensure bidirectional synchronization:\n\n\u0060\u0060\u0060csharp\npublic class EntryHandler : ViewHandler\u003CIEntry, SkiaEntry\u003E\n{\n private static void MapText(EntryHandler handler, IEntry entry)\n {\n handler.PlatformView.Text = entry.Text;\n }\n \n private static void MapTextColor(EntryHandler handler, IEntry entry)\n {\n handler.PlatformView.TextColor = entry.TextColor.ToSKColor();\n }\n \n private static void MapPlaceholder(EntryHandler handler, IEntry entry)\n {\n handler.PlatformView.Placeholder = entry.Placeholder;\n }\n \n protected override void ConnectHandler(SkiaEntry platformView)\n {\n base.ConnectHandler(platformView);\n \n // Wire up events from platform view to virtual view\n platformView.TextChanged \u002B= OnTextChanged;\n platformView.Completed \u002B= OnCompleted;\n }\n \n private void OnTextChanged(object? sender, TextChangedEventArgs e)\n {\n if (VirtualView != null)\n {\n VirtualView.Text = e.NewTextValue;\n }\n }\n}\n\u0060\u0060\u0060\n\n## Type Conversion Utilities\n\nOpenMaui provides seamless conversion between MAUI and Skia types:\n\n\u0060\u0060\u0060csharp\npublic static class ColorExtensions\n{\n public static SKColor ToSKColor(this Color color)\n {\n return new SKColor(\n (byte)(color.Red * 255),\n (byte)(color.Green * 255),\n (byte)(color.Blue * 255),\n (byte)(color.Alpha * 255)\n );\n }\n}\n\npublic static class RectExtensions\n{\n public static SKRect ToSKRect(this Rect rect)\n {\n return new SKRect(\n (float)rect.Left,\n (float)rect.Top,\n (float)rect.Right,\n (float)rect.Bottom\n );\n }\n}\n\u0060\u0060\u0060\n\nThese extensions ensure that developers work with familiar MAUI types (\u0060Color\u0060, \u0060Rect\u0060, \u0060Size\u0060, \u0060Thickness\u0060) while OpenMaui handles the Skia conversion internally.\n\n## Native Service Integration\n\nHandlers also bridge to native Linux services:\n\n\u0060\u0060\u0060csharp\npublic class FilePickerHandler\n{\n public async Task\u003CFileResult?\u003E PickAsync(PickOptions? options)\n {\n // Try xdg-desktop-portal first (Flatpak, Snap compatible)\n if (IsPortalAvailable())\n {\n return await PickViaPortal(options);\n }\n \n // Fall back to native dialogs\n if (IsKDE())\n {\n return await PickViaKDialog(options);\n }\n else\n {\n return await PickViaZenity(options);\n }\n }\n}\n\u0060\u0060\u0060\n\nThis ensures that file pickers, notifications, and other system services feel native to each desktop environment (GNOME, KDE, XFCE).\n\n## Custom Control Example\n\nDevelopers can create custom controls using the same pattern:\n\n\u0060\u0060\u0060csharp\n// 1. Define the interface\npublic interface ICustomGauge : IView\n{\n double Value { get; }\n double Minimum { get; }\n double Maximum { get; }\n Color GaugeColor { get; }\n}\n\n// 2. Create the Skia view\npublic class SkiaCustomGauge : SkiaView\n{\n public double Value { get; set; }\n public double Minimum { get; set; }\n public double Maximum { get; set; }\n public SKColor GaugeColor { get; set; }\n \n public override void Draw(SKCanvas canvas, SKRect dirtyRect)\n {\n var paint = new SKPaint\n {\n Color = GaugeColor,\n Style = SKPaintStyle.Stroke,\n StrokeWidth = 10,\n IsAntialias = true\n };\n \n var angle = (Value - Minimum) / (Maximum - Minimum) * 270;\n canvas.DrawArc(_bounds, -225, (float)angle, false, paint);\n }\n}\n\n// 3. Create the handler\npublic class CustomGaugeHandler : ViewHandler\u003CICustomGauge, SkiaCustomGauge\u003E\n{\n public static IPropertyMapper\u003CICustomGauge, CustomGaugeHandler\u003E Mapper =\n new PropertyMapper\u003CICustomGauge, CustomGaugeHandler\u003E()\n {\n [nameof(ICustomGauge.Value)] = MapValue,\n [nameof(ICustomGauge.GaugeColor)] = MapGaugeColor,\n };\n \n protected override SkiaCustomGauge CreatePlatformView()\n {\n return new SkiaCustomGauge();\n }\n}\n\n// 4. Register the handler\nbuilder.ConfigureMauiHandlers(handlers =\u003E\n{\n handlers.AddHandler\u003CICustomGauge, CustomGaugeHandler\u003E();\n});\n\u0060\u0060\u0060\n\nThis architecture provides complete flexibility while maintaining MAUI\u0027s cross-platform abstractions.\r\n\r\n## Render Caching Strategies\r\n\r\nRendering complex UI elements every frame is expensive. OpenMaui employs sophisticated **caching strategies** to minimize redundant drawing operations, dramatically improving performance for static or infrequently changing content.\n\n## SKPicture-Based Caching\n\nSkia\u0027s \u0060SKPicture\u0060 provides a recording mechanism for draw commands:\n\n\u0060\u0060\u0060csharp\npublic abstract class SkiaView\n{\n private SKPicture? _cachedPicture;\n private bool _cacheDirty = true;\n \n public bool EnableRenderCaching { get; set; } = false;\n \n public override void Draw(SKCanvas canvas, SKRect dirtyRect)\n {\n if (EnableRenderCaching)\n {\n if (_cacheDirty || _cachedPicture == null)\n {\n // Record drawing commands\n using var recorder = new SKPictureRecorder();\n var recordingCanvas = recorder.BeginRecording(_bounds);\n \n DrawContent(recordingCanvas, dirtyRect);\n \n _cachedPicture?.Dispose();\n _cachedPicture = recorder.EndRecording();\n _cacheDirty = false;\n }\n \n // Replay cached commands\n canvas.DrawPicture(_cachedPicture);\n }\n else\n {\n // Direct rendering\n DrawContent(canvas, dirtyRect);\n }\n }\n \n protected abstract void DrawContent(SKCanvas canvas, SKRect dirtyRect);\n \n public override void Invalidate()\n {\n _cacheDirty = true;\n base.Invalidate();\n }\n}\n\u0060\u0060\u0060\n\n## Image Caching System\n\nThe \u0060SkiaImage\u0060 control implements multi-level caching:\n\n\u0060\u0060\u0060csharp\npublic class SkiaImage : SkiaView\n{\n private static readonly Dictionary\u003Cstring, SKBitmap\u003E _imageCache = new();\n private static readonly object _cacheLock = new();\n \n private SKBitmap? _bitmap;\n private string? _source;\n \n public ImageSource Source\n {\n get =\u003E _source;\n set\n {\n if (_source != value)\n {\n _source = value;\n LoadImageAsync(value);\n }\n }\n }\n \n private async Task LoadImageAsync(ImageSource source)\n {\n if (source is FileImageSource fileSource)\n {\n var path = fileSource.File;\n \n // Check memory cache first\n lock (_cacheLock)\n {\n if (_imageCache.TryGetValue(path, out var cached))\n {\n _bitmap = cached;\n Invalidate();\n return;\n }\n }\n \n // Load from disk\n _bitmap = await Task.Run(() =\u003E SKBitmap.Decode(path));\n \n // Cache for future use\n lock (_cacheLock)\n {\n _imageCache[path] = _bitmap;\n }\n \n Invalidate();\n }\n else if (source is StreamImageSource streamSource)\n {\n var stream = await streamSource.Stream(CancellationToken.None);\n _bitmap = SKBitmap.Decode(stream);\n Invalidate();\n }\n }\n \n public override void Draw(SKCanvas canvas, SKRect dirtyRect)\n {\n if (_bitmap != null)\n {\n canvas.DrawBitmap(_bitmap, _bounds);\n }\n }\n}\n\u0060\u0060\u0060\n\n## SVG Caching with Svg.Skia\n\nSVG rendering is expensive, so OpenMaui caches rasterized versions:\n\n\u0060\u0060\u0060csharp\npublic class SkiaSvgImage : SkiaImage\n{\n private SKPicture? _svgPicture;\n private SKBitmap? _rasterizedCache;\n private Size _lastRenderSize;\n \n private void LoadSvg(string path)\n {\n var svg = new SKSvg();\n _svgPicture = svg.Load(path);\n }\n \n public override void Draw(SKCanvas canvas, SKRect dirtyRect)\n {\n if (_svgPicture == null)\n return;\n \n var currentSize = new Size(_bounds.Width, _bounds.Height);\n \n // Re-rasterize if size changed\n if (_rasterizedCache == null || _lastRenderSize != currentSize)\n {\n _rasterizedCache?.Dispose();\n \n var info = new SKImageInfo(\n (int)_bounds.Width,\n (int)_bounds.Height,\n SKColorType.Rgba8888,\n SKAlphaType.Premul\n );\n \n _rasterizedCache = new SKBitmap(info);\n \n using var cacheCanvas = new SKCanvas(_rasterizedCache);\n cacheCanvas.Clear(SKColors.Transparent);\n cacheCanvas.DrawPicture(_svgPicture);\n \n _lastRenderSize = currentSize;\n }\n \n canvas.DrawBitmap(_rasterizedCache, _bounds);\n }\n}\n\u0060\u0060\u0060\n\n## Text Layout Caching\n\nText shaping with HarfBuzz is computationally expensive, especially for complex scripts:\n\n\u0060\u0060\u0060csharp\npublic class SkiaLabel : SkiaView\n{\n private string? _text;\n private SKFont? _font;\n private SKTextBlob? _cachedTextBlob;\n private bool _textLayoutDirty = true;\n \n public string Text\n {\n get =\u003E _text;\n set\n {\n if (_text != value)\n {\n _text = value;\n _textLayoutDirty = true;\n Invalidate();\n }\n }\n }\n \n public override void Draw(SKCanvas canvas, SKRect dirtyRect)\n {\n if (_textLayoutDirty || _cachedTextBlob == null)\n {\n // Shape text using HarfBuzz\n var shaper = new SKShaper(_font.Typeface);\n _cachedTextBlob = shaper.Shape(_text, _font);\n _textLayoutDirty = false;\n }\n \n var paint = new SKPaint\n {\n Color = TextColor,\n IsAntialias = true\n };\n \n canvas.DrawText(_cachedTextBlob, _bounds.Left, _bounds.Top, paint);\n }\n}\n\u0060\u0060\u0060\n\n## Collection View Virtualization\n\nThe \u0060SkiaCollectionView\u0060 implements virtualization to cache only visible items:\n\n\u0060\u0060\u0060csharp\npublic class SkiaCollectionView : SkiaView\n{\n private readonly Dictionary\u003Cint, SkiaView\u003E _visibleItemCache = new();\n private readonly HashSet\u003Cint\u003E _currentlyVisible = new();\n \n public override void Draw(SKCanvas canvas, SKRect dirtyRect)\n {\n var visibleRange = CalculateVisibleRange(_scrollOffset, _bounds.Height);\n _currentlyVisible.Clear();\n \n for (int i = visibleRange.Start; i \u003C visibleRange.End; i\u002B\u002B)\n {\n _currentlyVisible.Add(i);\n \n if (!_visibleItemCache.TryGetValue(i, out var itemView))\n {\n // Create and cache new item view\n itemView = CreateItemView(i);\n _visibleItemCache[i] = itemView;\n }\n \n var itemBounds = CalculateItemBounds(i);\n itemView.Arrange(itemBounds);\n itemView.Draw(canvas, dirtyRect);\n }\n \n // Cleanup items that scrolled out of view\n var itemsToRemove = _visibleItemCache.Keys\n .Where(k =\u003E !_currentlyVisible.Contains(k))\n .ToList();\n \n foreach (var key in itemsToRemove)\n {\n _visibleItemCache[key].Dispose();\n _visibleItemCache.Remove(key);\n }\n }\n}\n\u0060\u0060\u0060\n\n## Cache Invalidation Strategy\n\nProper cache invalidation is critical:\n\n\u0060\u0060\u0060csharp\npublic abstract class SkiaView\n{\n protected void InvalidateCache(CacheLevel level)\n {\n switch (level)\n {\n case CacheLevel.Paint:\n // Only visual properties changed\n _cacheDirty = true;\n Invalidate();\n break;\n \n case CacheLevel.Layout:\n // Size/position changed, invalidate layout cache\n _layoutCache = null;\n _cacheDirty = true;\n InvalidateLayout();\n break;\n \n case CacheLevel.Deep:\n // Structure changed, invalidate entire subtree\n InvalidateDescendants();\n _cacheDirty = true;\n InvalidateLayout();\n break;\n }\n }\n}\n\npublic enum CacheLevel\n{\n Paint, // Color, stroke, etc.\n Layout, // Size, position\n Deep // Children added/removed\n}\n\u0060\u0060\u0060\n\n## Memory Management\n\nOpenMaui includes cache size limits to prevent memory exhaustion:\n\n\u0060\u0060\u0060csharp\npublic class ImageCacheManager\n{\n private const long MaxCacheSizeBytes = 100 * 1024 * 1024; // 100 MB\n private long _currentCacheSize = 0;\n \n public void AddToCache(string key, SKBitmap bitmap)\n {\n var bitmapSize = bitmap.ByteCount;\n \n // Evict old items if necessary\n while (_currentCacheSize \u002B bitmapSize \u003E MaxCacheSizeBytes)\n {\n EvictLeastRecentlyUsed();\n }\n \n _imageCache[key] = bitmap;\n _currentCacheSize \u002B= bitmapSize;\n }\n \n private void EvictLeastRecentlyUsed()\n {\n // LRU eviction logic\n }\n}\n\u0060\u0060\u0060\n\nThese caching strategies ensure that OpenMaui remains performant even with complex UIs containing hundreds of controls and images.\r\n\r\n## Best Practices for Custom Controls\r\n\r\nCreating performant, maintainable custom controls in OpenMaui requires understanding the rendering pipeline and following established patterns. Here are battle-tested practices for building production-quality Skia-based controls.\n\n## 1. Extend the Right Base Class\n\nChoose your base class based on control complexity:\n\n\u0060\u0060\u0060csharp\n// Simple controls with no children\npublic class CustomIndicator : SkiaView\n{\n public override void Draw(SKCanvas canvas, SKRect dirtyRect)\n {\n // Direct drawing\n }\n}\n\n// Controls that contain other views\npublic class CustomPanel : SkiaLayoutView\n{\n protected override void OnMeasure(double widthConstraint, double heightConstraint)\n {\n // Measure children\n }\n \n protected override void OnArrange(SKRect bounds)\n {\n // Position children\n }\n}\n\n// Controls with templates (like buttons with content)\npublic class CustomCard : SkiaTemplatedView\n{\n public DataTemplate ContentTemplate { get; set; }\n}\n\u0060\u0060\u0060\n\n## 2. Implement Efficient Drawing\n\nMinimize object allocations in the draw loop:\n\n\u0060\u0060\u0060csharp\npublic class SkiaCustomGauge : SkiaView\n{\n // \u274C BAD: Allocates new objects every frame\n public override void Draw(SKCanvas canvas, SKRect dirtyRect)\n {\n var paint = new SKPaint { Color = SKColors.Blue }; // Allocation!\n var path = new SKPath(); // Allocation!\n path.AddArc(_bounds, 0, 90);\n canvas.DrawPath(path, paint);\n }\n \n // \u2705 GOOD: Reuse objects\n private readonly SKPaint _gaugePaint = new() { IsAntialias = true };\n private readonly SKPath _gaugePath = new();\n \n public override void Draw(SKCanvas canvas, SKRect dirtyRect)\n {\n _gaugePaint.Color = GaugeColor;\n \n _gaugePath.Reset();\n _gaugePath.AddArc(_bounds, 0, 90);\n \n canvas.DrawPath(_gaugePath, _gaugePaint);\n }\n \n protected override void Dispose(bool disposing)\n {\n if (disposing)\n {\n _gaugePaint?.Dispose();\n _gaugePath?.Dispose();\n }\n base.Dispose(disposing);\n }\n}\n\u0060\u0060\u0060\n\n## 3. Use Bindable Properties Correctly\n\nFollow MAUI\u0027s bindable property pattern:\n\n\u0060\u0060\u0060csharp\npublic class SkiaRatingControl : SkiaView\n{\n public static readonly BindableProperty RatingProperty =\n BindableProperty.Create(\n nameof(Rating),\n typeof(double),\n typeof(SkiaRatingControl),\n 0.0,\n propertyChanged: OnRatingChanged);\n \n public double Rating\n {\n get =\u003E (double)GetValue(RatingProperty);\n set =\u003E SetValue(RatingProperty, value);\n }\n \n private static void OnRatingChanged(BindableObject bindable, object oldValue, object newValue)\n {\n if (bindable is SkiaRatingControl control)\n {\n control.Invalidate(); // Trigger repaint\n }\n }\n}\n\u0060\u0060\u0060\n\n## 4. Implement Smart Invalidation\n\nDifferentiate between layout and paint changes:\n\n\u0060\u0060\u0060csharp\npublic class SkiaCustomPanel : SkiaLayoutView\n{\n public Color BorderColor\n {\n get =\u003E _borderColor;\n set\n {\n if (_borderColor != value)\n {\n _borderColor = value;\n Invalidate(); // Paint-only change\n }\n }\n }\n \n public Thickness Padding\n {\n get =\u003E _padding;\n set\n {\n if (_padding != value)\n {\n _padding = value;\n InvalidateLayout(); // Affects child positioning\n }\n }\n }\n}\n\u0060\u0060\u0060\n\n## 5. Handle Gestures Properly\n\nImplement gesture recognition with proper event propagation:\n\n\u0060\u0060\u0060csharp\npublic class SkiaInteractiveCard : SkiaView\n{\n public event EventHandler\u003CTappedEventArgs\u003E? Tapped;\n public ICommand? TapCommand { get; set; }\n \n public override bool OnTouchEvent(MotionEvent motionEvent)\n {\n switch (motionEvent.Action)\n {\n case MotionEventActions.Down:\n _isPressed = true;\n Invalidate(); // Update visual state\n return true; // Consume event\n \n case MotionEventActions.Up:\n if (_isPressed \u0026\u0026 _bounds.Contains(motionEvent.X, motionEvent.Y))\n {\n // Fire events\n Tapped?.Invoke(this, new TappedEventArgs());\n TapCommand?.Execute(null);\n }\n _isPressed = false;\n Invalidate();\n return true;\n \n case MotionEventActions.Cancel:\n _isPressed = false;\n Invalidate();\n return true;\n }\n \n return base.OnTouchEvent(motionEvent);\n }\n}\n\u0060\u0060\u0060\n\n## 6. Support Visual States\n\nImplement visual state management for interactive controls:\n\n\u0060\u0060\u0060csharp\npublic class SkiaCustomButton : SkiaView\n{\n private VisualState _currentState = VisualState.Normal;\n \n public enum VisualState\n {\n Normal,\n Hovered,\n Pressed,\n Disabled\n }\n \n public override void Draw(SKCanvas canvas, SKRect dirtyRect)\n {\n var backgroundColor = _currentState switch\n {\n VisualState.Normal =\u003E NormalColor,\n VisualState.Hovered =\u003E HoverColor,\n VisualState.Pressed =\u003E PressedColor,\n VisualState.Disabled =\u003E DisabledColor,\n _ =\u003E NormalColor\n };\n \n using var paint = new SKPaint\n {\n Color = backgroundColor,\n Style = SKPaintStyle.Fill\n };\n \n canvas.DrawRoundRect(_bounds, CornerRadius, CornerRadius, paint);\n }\n \n public bool IsEnabled\n {\n get =\u003E _isEnabled;\n set\n {\n _isEnabled = value;\n _currentState = value ? VisualState.Normal : VisualState.Disabled;\n Invalidate();\n }\n }\n}\n\u0060\u0060\u0060\n\n## 7. Implement Accessibility\n\nMake controls accessible to screen readers:\n\n\u0060\u0060\u0060csharp\npublic class SkiaAccessibleControl : SkiaView, IAccessible\n{\n public string AccessibilityLabel { get; set; }\n public string AccessibilityHint { get; set; }\n public AccessibilityRole Role { get; set; } = AccessibilityRole.Button;\n \n public void AnnounceAccessibility(string message)\n {\n // Notify AT-SPI2 screen readers\n AccessibilityManager.Announce(message);\n }\n}\n\u0060\u0060\u0060\n\n## 8. Optimize for HiDPI Displays\n\nHandle scaling properly:\n\n\u0060\u0060\u0060csharp\npublic class SkiaScalableControl : SkiaView\n{\n private float _displayScale = 1.0f;\n \n protected override void OnParentSet()\n {\n base.OnParentSet();\n _displayScale = GetDisplayScale();\n }\n \n public override void Draw(SKCanvas canvas, SKRect dirtyRect)\n {\n // Scale-aware stroke width\n var strokeWidth = 1.0f * _displayScale;\n \n using var paint = new SKPaint\n {\n StrokeWidth = strokeWidth,\n IsAntialias = true\n };\n \n canvas.DrawLine(_bounds.Left, _bounds.Top, _bounds.Right, _bounds.Bottom, paint);\n }\n}\n\u0060\u0060\u0060\n\n## 9. Use Render Caching Strategically\n\nEnable caching for static or rarely changing content:\n\n\u0060\u0060\u0060csharp\npublic class SkiaComplexChart : SkiaView\n{\n private SKPicture? _gridCache;\n private bool _gridDirty = true;\n \n public override void Draw(SKCanvas canvas, SKRect dirtyRect)\n {\n // Cache the static grid\n if (_gridDirty || _gridCache == null)\n {\n using var recorder = new SKPictureRecorder();\n var recordingCanvas = recorder.BeginRecording(_bounds);\n DrawGrid(recordingCanvas);\n _gridCache = recorder.EndRecording();\n _gridDirty = false;\n }\n \n canvas.DrawPicture(_gridCache);\n \n // Draw dynamic data without caching\n DrawDataPoints(canvas);\n }\n \n public void UpdateData(double[] newData)\n {\n _data = newData;\n Invalidate(); // Grid cache remains valid\n }\n}\n\u0060\u0060\u0060\n\n## 10. Test Across Desktop Environments\n\nEnsure your control works on different Linux configurations:\n\n\u0060\u0060\u0060csharp\n[Fact]\npublic void CustomControl_RendersCorrectly_OnGnome()\n{\n var control = new SkiaCustomControl\n {\n Width = 200,\n Height = 100\n };\n \n // Simulate GNOME theme\n SkiaTheme.Current = SkiaTheme.CreateGnomeTheme();\n \n var bitmap = RenderToBitmap(control);\n \n // Assert visual output\n Assert.True(bitmap.GetPixel(100, 50).Red \u003E 0);\n}\n\n[Fact]\npublic void CustomControl_HandlesHiDPI()\n{\n var control = new SkiaCustomControl();\n \n // Simulate 2x scaling\n control.SetDisplayScale(2.0f);\n control.Measure(200, 100);\n \n Assert.Equal(400, control.MeasuredWidth);\n}\n\u0060\u0060\u0060\n\n## Complete Example: Custom Progress Ring\n\nPutting it all together:\n\n\u0060\u0060\u0060csharp\npublic class SkiaProgressRing : SkiaView\n{\n private readonly SKPaint _trackPaint = new()\n {\n Style = SKPaintStyle.Stroke,\n StrokeWidth = 8,\n IsAntialias = true\n };\n \n private readonly SKPaint _progressPaint = new()\n {\n Style = SKPaintStyle.Stroke,\n StrokeWidth = 8,\n IsAntialias = true,\n StrokeCap = SKStrokeCap.Round\n };\n \n public static readonly BindableProperty ProgressProperty =\n BindableProperty.Create(nameof(Progress), typeof(double), typeof(SkiaProgressRing), 0.0,\n propertyChanged: (b, o, n) =\u003E ((SkiaProgressRing)b).Invalidate());\n \n public double Progress\n {\n get =\u003E (double)GetValue(ProgressProperty);\n set =\u003E SetValue(ProgressProperty, Math.Clamp(value, 0.0, 1.0));\n }\n \n public override void Draw(SKCanvas canvas, SKRect dirtyRect)\n {\n var center = new SKPoint(_bounds.MidX, _bounds.MidY);\n var radius = Math.Min(_bounds.Width, _bounds.Height) / 2 - _progressPaint.StrokeWidth;\n \n _trackPaint.Color = SKColors.LightGray;\n canvas.DrawCircle(center, radius, _trackPaint);\n \n _progressPaint.Color = SKColors.Blue;\n var sweepAngle = (float)(Progress * 360);\n \n using var path = new SKPath();\n path.AddArc(SKRect.Create(center.X - radius, center.Y - radius, radius * 2, radius * 2),\n -90, sweepAngle);\n \n canvas.DrawPath(path, _progressPaint);\n }\n \n protected override void Dispose(bool disposing)\n {\n if (disposing)\n {\n _trackPaint?.Dispose();\n _progressPaint?.Dispose();\n }\n base.Dispose(disposing);\n }\n}\n\u0060\u0060\u0060\n\nFollowing these best practices ensures your custom controls are performant, maintainable, and integrate seamlessly with the OpenMaui rendering pipeline.\r\n\r\n---\r\n\r\n\u003E The SkiaRenderingEngine provides dirty region optimization for efficient partial redraws, ensuring that only changed portions of the UI are re-rendered each frame.\r\n\r\n\u003E With 35\u002B Skia-based view implementations, OpenMaui achieves 100% MAUI API compliance, allowing existing MAUI applications to run on Linux with minimal or no code changes.\r\n\r\n\u003E The dual-mode rendering strategy\u2014GPU acceleration with automatic software fallback\u2014ensures OpenMaui works reliably across diverse Linux hardware configurations.", + "createdAt": 1769749919894, + "updatedAt": 1769749919894, + "tags": [ + "series", + "generated", + "Building Linux Apps with .NET MAUI" + ], + "isArticle": true, + "seriesGroup": "Building Linux Apps with .NET MAUI", + "subtitle": "Exploring the complete rendering architecture that enables cross-platform .NET MAUI applications to run natively on Linux with GPU acceleration and 100% API compatibility.", + "pullQuotes": [ + "The SkiaRenderingEngine provides dirty region optimization for efficient partial redraws, ensuring that only changed portions of the UI are re-rendered each frame.", + "With 35\u002B Skia-based view implementations, OpenMaui achieves 100% MAUI API compliance, allowing existing MAUI applications to run on Linux with minimal or no code changes.", + "The dual-mode rendering strategy\u2014GPU acceleration with automatic software fallback\u2014ensures OpenMaui works reliably across diverse Linux hardware configurations." + ], + "sections": [ + { + "header": "Introduction", + "content": "Bringing .NET MAUI to Linux presents a unique challenge: how do you render cross-platform UI controls on a desktop environment that lacks native MAUI support? The answer lies in **SkiaSharp**, Google\u0027s powerful 2D graphics library that provides a consistent rendering layer across all platforms.\n\n**OpenMaui.Platform.Linux** is a comprehensive implementation of the .NET MAUI platform for Linux desktop applications. Unlike traditional approaches that wrap native GTK widgets, OpenMaui takes a different path: it renders every MAUI control\u2014from buttons to complex collection views\u2014using SkiaSharp\u0027s canvas-based drawing primitives.\n\nThis architectural decision unlocks several advantages:\n\n- **100% MAUI API compliance**: No platform-specific quirks or missing features\n- **Consistent rendering**: Identical visual output across Windows, macOS, Android, iOS, and Linux\n- **Custom control flexibility**: Full access to the rendering pipeline for advanced scenarios\n- **Performance optimization**: Fine-grained control over GPU acceleration and dirty region tracking\n\nWith **217 passing unit tests**, support for both X11 and Wayland display servers, and integration with native Linux services like xdg-desktop-portal, OpenMaui represents a production-ready MAUI implementation that brings true cross-platform development to the Linux desktop.\n\nIn this deep dive, we\u0027ll explore the complete rendering architecture\u2014from the foundational SkiaSharp primitives to the sophisticated optimization strategies that make OpenMaui performant on resource-constrained hardware." + }, + { + "header": "Why SkiaSharp for Linux MAUI", + "content": "The decision to use SkiaSharp as the rendering foundation for OpenMaui wasn\u0027t arbitrary\u2014it was driven by the practical realities of cross-platform UI development and the specific challenges of the Linux desktop ecosystem.\n\n## The Linux Desktop Fragmentation Problem\n\nLinux desktop environments are remarkably diverse. A MAUI application might run on:\n\n- **GNOME** with GTK3/GTK4 theming\n- **KDE Plasma** with Qt styling\n- **XFCE**, **Cinnamon**, **MATE**, and dozens of other environments\n- **X11** or **Wayland** display servers\n- Traditional desktops or tiling window managers\n\nWrapping native widgets (like GTK buttons or Qt controls) would mean:\n\n1. **Inconsistent appearance** across desktop environments\n2. **API limitations** where native controls don\u0027t map cleanly to MAUI abstractions\n3. **Maintenance burden** supporting multiple widget toolkits\n4. **Theming conflicts** between MAUI\u0027s design system and native themes\n\n## The SkiaSharp Advantage\n\nSkiaSharp solves these problems by providing a **platform-agnostic 2D canvas**. Every pixel is under OpenMaui\u0027s control:\n\n\u0060\u0060\u0060csharp\npublic abstract class SkiaView : IView\n{\n public abstract void Draw(SKCanvas canvas, SKRect dirtyRect);\n \n // Full control over rendering\n protected virtual void OnPaintSurface(SKCanvas canvas)\n {\n canvas.Clear(BackgroundColor.ToSKColor());\n // Custom drawing logic\n }\n}\n\u0060\u0060\u0060\n\nThis architecture enables:\n\n- **Pixel-perfect consistency**: A \u0060SkiaButton\u0060 looks identical on GNOME, KDE, and XFCE\n- **Complete MAUI API coverage**: No compromises due to native widget limitations\n- **Advanced visual effects**: Shadows, gradients, custom shapes, and animations\n- **Theme integration**: OpenMaui\u0027s \u0060SkiaTheme\u0060 system adapts to system dark/light mode while maintaining consistent MAUI styling\n\n## Proven Cross-Platform Foundation\n\nSkiaSharp isn\u0027t experimental\u2014it powers production applications across platforms:\n\n- **Microsoft.Maui.Graphics**: The official MAUI graphics abstraction uses Skia on mobile platforms\n- **Avalonia UI**: Another successful cross-platform framework uses Skia for Linux rendering\n- **Flutter**: Google\u0027s UI framework is built on Skia\u0027s C\u002B\u002B predecessor\n\nOpenMaui leverages **SkiaSharp 2.88.9** with native Linux assets, **HarfBuzzSharp 7.3.0.3** for advanced text shaping (supporting emoji, CJK characters, and complex scripts), and **Svg.Skia 1.0.0** for vector graphics\u2014creating a complete rendering stack that rivals native platforms.\n\nThe result? Existing MAUI applications can target Linux with **minimal or no code changes**, using the same \u0060Button\u0060, \u0060Entry\u0060, \u0060CollectionView\u0060, and other controls they already know." + }, + { + "header": "The SkiaRenderingEngine Architecture", + "content": "At the heart of OpenMaui\u0027s rendering system is the **SkiaRenderingEngine**, a sophisticated pipeline that manages the complete lifecycle of frame rendering\u2014from dirty region tracking to final pixel output.\n\n## Core Rendering Pipeline\n\nThe rendering engine orchestrates several critical subsystems:\n\n\u0060\u0060\u0060csharp\npublic class SkiaRenderingEngine\n{\n // Dirty region optimization\n private readonly DirtyRegionTracker _dirtyRegions;\n \n // Frame scheduling\n private readonly FrameScheduler _scheduler;\n \n // GPU/software rendering abstraction\n private readonly IRenderBackend _backend;\n \n public void Render(SKCanvas canvas, SKRect clipRect)\n {\n // 1. Collect dirty regions\n var dirtyRects = _dirtyRegions.GetDirtyRegions();\n \n // 2. Clip to minimize drawing\n canvas.ClipRect(clipRect);\n \n // 3. Traverse view hierarchy\n RenderViewTree(canvas, dirtyRects);\n \n // 4. Flush to GPU/screen\n canvas.Flush();\n }\n}\n\u0060\u0060\u0060\n\n## View Hierarchy Traversal\n\nOpenMaui implements **35\u002B Skia-based view classes** that form a complete UI toolkit:\n\n**Layout Containers:**\n- \u0060SkiaStackLayout\u0060 - Vertical/horizontal stacking with spacing\n- \u0060SkiaGrid\u0060 - Row/column grid with Auto, Star, and Absolute sizing\n- \u0060SkiaAbsoluteLayout\u0060 - Absolute positioning with proportional flags\n- \u0060SkiaFlexLayout\u0060 - Flexbox-style layout with wrap and alignment\n- \u0060SkiaScrollView\u0060 - Scrollable container with virtualization\n\n**Input Controls:**\n- \u0060SkiaButton\u0060, \u0060SkiaImageButton\u0060 - Command-bound buttons with visual states\n- \u0060SkiaEntry\u0060, \u0060SkiaEditor\u0060 - Text input with IME support (IBus, Fcitx5, XIM)\n- \u0060SkiaCheckBox\u0060, \u0060SkiaSwitch\u0060, \u0060SkiaRadioButton\u0060 - Selection controls\n- \u0060SkiaSlider\u0060, \u0060SkiaStepper\u0060 - Numeric input\n- \u0060SkiaDatePicker\u0060, \u0060SkiaTimePicker\u0060, \u0060SkiaPicker\u0060 - Specialized pickers\n\n**Display Controls:**\n- \u0060SkiaLabel\u0060 - Text with font fallback for emoji/CJK via HarfBuzz\n- \u0060SkiaImage\u0060 - Image display with SVG support, GIF animation, and caching\n- \u0060SkiaProgressBar\u0060, \u0060SkiaActivityIndicator\u0060 - Progress feedback\n\n**Advanced Controls:**\n- \u0060SkiaCollectionView\u0060 - Virtualized lists with selection and headers\n- \u0060SkiaCarouselView\u0060 - Horizontal scrolling with indicators\n- \u0060SkiaWebView\u0060 - WebKitGTK integration\n- \u0060SkiaGraphicsView\u0060 - Custom drawing surface with \u0060IDrawable\u0060\n\nEach view implements the core \u0060IView\u0060 interface:\n\n\u0060\u0060\u0060csharp\npublic interface IView\n{\n void Draw(SKCanvas canvas, SKRect dirtyRect);\n void Measure(double widthConstraint, double heightConstraint);\n void Arrange(SKRect bounds);\n void Invalidate(); // Triggers repaint\n}\n\u0060\u0060\u0060\n\n## Handler Architecture\n\nOpenMaui provides **40\u002B handler implementations** that map MAUI controls to Skia views:\n\n\u0060\u0060\u0060csharp\npublic class ButtonHandler : ViewHandler\u003CIButton, SkiaButton\u003E\n{\n public static IPropertyMapper\u003CIButton, ButtonHandler\u003E Mapper =\n new PropertyMapper\u003CIButton, ButtonHandler\u003E(ViewHandler.ViewMapper)\n {\n [nameof(IButton.Text)] = MapText,\n [nameof(IButton.TextColor)] = MapTextColor,\n [nameof(IButton.Command)] = MapCommand,\n [nameof(IButton.Background)] = MapBackground,\n // 100% MAUI API coverage\n };\n \n protected override SkiaButton CreatePlatformView()\n {\n return new SkiaButton();\n }\n \n private static void MapText(ButtonHandler handler, IButton button)\n {\n handler.PlatformView.Text = button.Text;\n handler.PlatformView.Invalidate();\n }\n}\n\u0060\u0060\u0060\n\nThis architecture ensures that all MAUI properties\u2014\u0060Color\u0060, \u0060Rect\u0060, \u0060Size\u0060, \u0060Thickness\u0060, \u0060double\u0060\u2014map seamlessly to Skia rendering primitives, maintaining **100% API compatibility** with existing MAUI applications.\n\n## Integration with Native Windowing\n\nThe rendering engine integrates with both GTK and X11 windowing systems:\n\n**GTK Integration:**\n\u0060\u0060\u0060csharp\npublic class GtkSkiaSurfaceWidget\n{\n // Embeds Skia canvas in GTK widget\n private SKSurface _surface;\n \n protected override bool OnDrawn(Cairo.Context cr)\n {\n using var canvas = _surface.Canvas;\n _renderingEngine.Render(canvas, GetClipRect());\n return true;\n }\n}\n\u0060\u0060\u0060\n\n**X11 Direct Rendering:**\n\u0060\u0060\u0060csharp\npublic class X11Window\n{\n private SKSurface _surface;\n \n private void OnExposeEvent()\n {\n using var canvas = _surface.Canvas;\n _renderingEngine.Render(canvas, _damageRect);\n XFlush(_display);\n }\n}\n\u0060\u0060\u0060\n\nThis dual-backend approach supports both traditional X11 environments and modern GTK-based desktops, with **Wayland support** via \u0060WaylandWindow\u0060 for next-generation Linux systems." + }, + { + "header": "GPU Acceleration and Software Fallback", + "content": "Performance is critical for desktop applications. OpenMaui implements a sophisticated **dual-mode rendering strategy** that leverages GPU acceleration when available while gracefully falling back to software rendering on constrained hardware.\n\n## GPU Rendering with OpenGL\n\nThe \u0060GpuRenderingEngine\u0060 uses **OpenGL-backed Skia surfaces** for hardware acceleration:\n\n\u0060\u0060\u0060csharp\npublic class GpuRenderingEngine : IRenderBackend\n{\n private GRContext _grContext;\n private SKSurface _gpuSurface;\n \n public void Initialize()\n {\n // Create OpenGL context\n var glInterface = GRGlInterface.Create();\n _grContext = GRContext.CreateGl(glInterface);\n \n // Create GPU-backed surface\n var frameBufferInfo = new GRGlFramebufferInfo(\n fboId: 0,\n format: GRPixelConfig.Rgba8888.ToGlSizedFormat()\n );\n \n var renderTarget = new GRBackendRenderTarget(\n width: _windowWidth,\n height: _windowHeight,\n sampleCount: 0,\n stencilBits: 8,\n glInfo: frameBufferInfo\n );\n \n _gpuSurface = SKSurface.Create(\n _grContext,\n renderTarget,\n GRSurfaceOrigin.BottomLeft,\n SKColorType.Rgba8888\n );\n }\n \n public void Render(Action\u003CSKCanvas\u003E drawCallback)\n {\n var canvas = _gpuSurface.Canvas;\n drawCallback(canvas);\n \n // Flush GPU commands\n canvas.Flush();\n _grContext.Flush();\n }\n}\n\u0060\u0060\u0060\n\n## Automatic Software Fallback\n\nWhen GPU rendering fails (missing drivers, headless environments, or resource constraints), OpenMaui seamlessly switches to **CPU-based rendering**:\n\n\u0060\u0060\u0060csharp\npublic class SoftwareRenderingEngine : IRenderBackend\n{\n private SKSurface _cpuSurface;\n private SKBitmap _backBuffer;\n \n public void Initialize()\n {\n _backBuffer = new SKBitmap(\n _windowWidth,\n _windowHeight,\n SKColorType.Rgba8888,\n SKAlphaType.Premul\n );\n \n _cpuSurface = SKSurface.Create(_backBuffer.Info, _backBuffer.GetPixels());\n }\n \n public void Render(Action\u003CSKCanvas\u003E drawCallback)\n {\n var canvas = _cpuSurface.Canvas;\n drawCallback(canvas);\n \n // Copy to window framebuffer\n CopyToFramebuffer(_backBuffer);\n }\n}\n\u0060\u0060\u0060\n\n## Backend Selection Strategy\n\nOpenMaui detects the optimal rendering backend at startup:\n\n\u0060\u0060\u0060csharp\npublic class RenderBackendSelector\n{\n public IRenderBackend SelectBackend()\n {\n // 1. Try GPU rendering first\n if (IsOpenGLAvailable())\n {\n try\n {\n var gpuBackend = new GpuRenderingEngine();\n gpuBackend.Initialize();\n return gpuBackend;\n }\n catch (Exception ex)\n {\n // Log and fall through\n }\n }\n \n // 2. Fall back to software rendering\n var softwareBackend = new SoftwareRenderingEngine();\n softwareBackend.Initialize();\n return softwareBackend;\n }\n \n private bool IsOpenGLAvailable()\n {\n // Check for OpenGL 2.0\u002B support\n return CheckGLVersion() \u003E= new Version(2, 0);\n }\n}\n\u0060\u0060\u0060\n\n## Performance Characteristics\n\n**GPU Rendering Benefits:**\n- **60 FPS animations**: Smooth scrolling and transitions\n- **Complex effects**: Shadows, gradients, and blurs with minimal CPU overhead\n- **Large surfaces**: Efficient rendering of high-resolution displays (4K\u002B)\n- **Reduced power consumption**: GPU hardware is optimized for graphics workloads\n\n**Software Rendering Use Cases:**\n- **Headless environments**: CI/CD pipelines, automated testing\n- **Legacy hardware**: Systems without OpenGL support\n- **Virtual machines**: Where GPU passthrough isn\u0027t available\n- **Remote desktop**: X11 forwarding or VNC scenarios\n\n## Hardware Acceleration for Media\n\nBeyond UI rendering, OpenMaui leverages **VA-API and VDPAU** for video acceleration:\n\n\u0060\u0060\u0060csharp\npublic class SkiaImage\n{\n private bool _useHardwareDecoding;\n \n private void LoadVideo(string path)\n {\n if (IsVAAPIAvailable())\n {\n // Use VA-API for hardware video decoding\n _videoDecoder = new VAAPIDecoder(path);\n _useHardwareDecoding = true;\n }\n else\n {\n // Software decoding fallback\n _videoDecoder = new SoftwareDecoder(path);\n }\n }\n}\n\u0060\u0060\u0060\n\nThis ensures that \u0060Image\u0060 controls displaying GIF animations or video content remain performant even on low-power devices.\n\n## Measuring Rendering Performance\n\nOpenMaui includes built-in performance diagnostics:\n\n\u0060\u0060\u0060csharp\npublic class FrameScheduler\n{\n public void EnableProfiling()\n {\n _profilingEnabled = true;\n }\n \n public RenderingStats GetStats()\n {\n return new RenderingStats\n {\n AverageFps = _frameCount / _elapsedSeconds,\n DroppedFrames = _droppedFrames,\n AverageRenderTime = _totalRenderTime / _frameCount,\n GpuMemoryUsage = _grContext?.GetResourceCacheLimit() ?? 0\n };\n }\n}\n\u0060\u0060\u0060\n\nDevelopers can monitor frame rates and identify performance bottlenecks during development." + }, + { + "header": "Dirty Region Optimization and Performance", + "content": "Redrawing the entire UI every frame is wasteful\u2014most of the screen remains unchanged between frames. OpenMaui\u0027s **dirty region optimization** ensures that only modified portions of the UI are re-rendered, dramatically improving performance and reducing power consumption.\n\n## The Dirty Region System\n\nThe \u0060DirtyRegionTracker\u0060 maintains a collection of screen rectangles that need repainting:\n\n\u0060\u0060\u0060csharp\npublic class DirtyRegionTracker\n{\n private List\u003CSKRect\u003E _dirtyRegions = new();\n private SKRect _bounds;\n \n public void MarkDirty(SKRect rect)\n {\n // Clip to screen bounds\n rect.Intersect(_bounds);\n \n if (rect.Width \u003E 0 \u0026\u0026 rect.Height \u003E 0)\n {\n _dirtyRegions.Add(rect);\n }\n }\n \n public List\u003CSKRect\u003E GetDirtyRegions()\n {\n if (_dirtyRegions.Count == 0)\n return new List\u003CSKRect\u003E();\n \n // Coalesce overlapping regions\n var coalesced = CoalesceRegions(_dirtyRegions);\n _dirtyRegions.Clear();\n \n return coalesced;\n }\n \n private List\u003CSKRect\u003E CoalesceRegions(List\u003CSKRect\u003E regions)\n {\n // Merge overlapping or adjacent rectangles\n var result = new List\u003CSKRect\u003E();\n \n foreach (var rect in regions.OrderBy(r =\u003E r.Top))\n {\n bool merged = false;\n \n for (int i = 0; i \u003C result.Count; i\u002B\u002B)\n {\n if (result[i].IntersectsWith(rect) || AreAdjacent(result[i], rect))\n {\n result[i] = SKRect.Union(result[i], rect);\n merged = true;\n break;\n }\n }\n \n if (!merged)\n result.Add(rect);\n }\n \n return result;\n }\n}\n\u0060\u0060\u0060\n\n## View-Level Invalidation\n\nEvery Skia view can trigger selective repainting:\n\n\u0060\u0060\u0060csharp\npublic abstract class SkiaView : IView\n{\n protected SKRect _bounds;\n protected ISkiaContainer? _parent;\n \n public void Invalidate()\n {\n // Mark this view\u0027s bounds as dirty\n InvalidateRect(_bounds);\n }\n \n public void InvalidateRect(SKRect rect)\n {\n // Propagate to root for rendering\n _parent?.InvalidateRect(rect);\n }\n \n // Partial invalidation for animations\n protected void InvalidateRegion(SKRect region)\n {\n var absoluteRegion = TransformToAbsolute(region);\n InvalidateRect(absoluteRegion);\n }\n}\n\u0060\u0060\u0060\n\n## Smart Invalidation in Controls\n\nControls use fine-grained invalidation to minimize redraws:\n\n\u0060\u0060\u0060csharp\npublic class SkiaButton : SkiaView\n{\n private string _text;\n private SKColor _textColor;\n \n public string Text\n {\n get =\u003E _text;\n set\n {\n if (_text != value)\n {\n _text = value;\n // Only redraw this button\n Invalidate();\n }\n }\n }\n \n public SKColor TextColor\n {\n get =\u003E _textColor;\n set\n {\n if (_textColor != value)\n {\n _textColor = value;\n // Even more precise: only redraw text region\n InvalidateRect(GetTextBounds());\n }\n }\n }\n}\n\u0060\u0060\u0060\n\n## Render Clipping\n\nThe rendering engine uses dirty regions to set canvas clip rects:\n\n\u0060\u0060\u0060csharp\npublic void RenderFrame(SKCanvas canvas)\n{\n var dirtyRegions = _dirtyRegionTracker.GetDirtyRegions();\n \n if (dirtyRegions.Count == 0)\n return; // Nothing to draw\n \n foreach (var dirtyRect in dirtyRegions)\n {\n canvas.Save();\n canvas.ClipRect(dirtyRect);\n \n // Only views intersecting this rect will draw\n RenderViewHierarchy(canvas, dirtyRect);\n \n canvas.Restore();\n }\n}\n\nprivate void RenderViewHierarchy(SKCanvas canvas, SKRect clipRect)\n{\n foreach (var view in _rootViews)\n {\n if (view.Bounds.IntersectsWith(clipRect))\n {\n view.Draw(canvas, clipRect);\n }\n }\n}\n\u0060\u0060\u0060\n\n## Layout Invalidation vs. Paint Invalidation\n\nOpenMaui distinguishes between layout changes (requiring measurement) and paint changes:\n\n\u0060\u0060\u0060csharp\npublic abstract class SkiaLayoutView : SkiaView\n{\n private bool _needsLayout;\n \n public void InvalidateLayout()\n {\n _needsLayout = true;\n \n // Layout changes affect entire subtree\n foreach (var child in Children)\n {\n child.InvalidateLayout();\n }\n \n // Trigger measurement and arrangement\n RequestLayout();\n }\n \n protected override void OnBeforeDraw()\n {\n if (_needsLayout)\n {\n Measure(_constraints.Width, _constraints.Height);\n Arrange(_bounds);\n _needsLayout = false;\n }\n }\n}\n\u0060\u0060\u0060\n\n## Performance Metrics\n\nDirty region optimization provides substantial performance improvements:\n\n**Scenario: Button text change**\n- Without optimization: Redraw entire 1920\u00D71080 window = 2,073,600 pixels\n- With optimization: Redraw 200\u00D740 button = 8,000 pixels\n- **Savings: 99.6% reduction in pixels drawn**\n\n**Scenario: Scrolling list**\n- Without optimization: Redraw all visible items every frame\n- With optimization: Redraw only newly exposed items\n- **Result: 60 FPS scrolling vs. 15 FPS without optimization**\n\n## Frame Scheduling and VSync\n\nThe \u0060FrameScheduler\u0060 coordinates with the display\u0027s refresh rate:\n\n\u0060\u0060\u0060csharp\npublic class FrameScheduler\n{\n private const double TargetFrameTime = 16.67; // 60 FPS\n \n public void ScheduleFrame()\n {\n if (_pendingFrame)\n return;\n \n _pendingFrame = true;\n \n // Schedule on next VSync\n GLib.Idle.Add(() =\u003E\n {\n var startTime = GetTimestamp();\n \n RenderFrame();\n \n var renderTime = GetTimestamp() - startTime;\n \n if (renderTime \u003E TargetFrameTime)\n {\n // Frame took too long, log warning\n LogSlowFrame(renderTime);\n }\n \n _pendingFrame = false;\n return false; // One-shot\n });\n }\n}\n\u0060\u0060\u0060\n\nThis ensures smooth animations without screen tearing, even when rendering complex UIs.\n\n## Best Practices for Performance\n\n1. **Use partial invalidation**: Call \u0060InvalidateRect()\u0060 instead of \u0060Invalidate()\u0060 when possible\n2. **Avoid layout thrashing**: Batch property changes to minimize layout passes\n3. **Cache complex drawings**: Use \u0060SKPicture\u0060 for repeated graphics\n4. **Profile your UI**: Enable frame profiling to identify bottlenecks\n5. **Virtualize large lists**: Use \u0060CollectionView\u0060 instead of \u0060StackLayout\u0060 for dynamic content" + }, + { + "header": "From SkiaView to Native Controls", + "content": "OpenMaui\u0027s architecture bridges the gap between abstract MAUI controls and concrete Skia rendering through a sophisticated handler system. Understanding this mapping is crucial for developers who want to create custom controls or optimize existing ones.\n\n## The Handler Pipeline\n\nMAUI uses a **handler pattern** to map cross-platform interfaces to platform-specific implementations:\n\n\u0060\u0060\u0060csharp\n// Cross-platform interface (shared code)\npublic interface IButton : IView\n{\n string Text { get; }\n Color TextColor { get; }\n ICommand Command { get; }\n object CommandParameter { get; }\n}\n\n// Platform handler (OpenMaui.Platform.Linux)\npublic class ButtonHandler : ViewHandler\u003CIButton, SkiaButton\u003E\n{\n public static IPropertyMapper\u003CIButton, ButtonHandler\u003E Mapper =\n new PropertyMapper\u003CIButton, ButtonHandler\u003E(ViewHandler.ViewMapper)\n {\n [nameof(IButton.Text)] = MapText,\n [nameof(IButton.TextColor)] = MapTextColor,\n [nameof(IButton.Command)] = MapCommand,\n [nameof(IButton.Background)] = MapBackground,\n [nameof(IButton.BorderColor)] = MapBorderColor,\n [nameof(IButton.BorderWidth)] = MapBorderWidth,\n [nameof(IButton.CornerRadius)] = MapCornerRadius,\n [nameof(IButton.Padding)] = MapPadding,\n [nameof(IButton.CharacterSpacing)] = MapCharacterSpacing,\n [nameof(IButton.Font)] = MapFont,\n [nameof(IButton.ImageSource)] = MapImageSource,\n };\n \n protected override SkiaButton CreatePlatformView()\n {\n return new SkiaButton();\n }\n \n private static void MapText(ButtonHandler handler, IButton button)\n {\n handler.PlatformView.Text = button.Text;\n }\n \n private static void MapCommand(ButtonHandler handler, IButton button)\n {\n handler.PlatformView.Clicked -= OnButtonClicked;\n handler.PlatformView.Clicked \u002B= OnButtonClicked;\n \n void OnButtonClicked(object? sender, EventArgs e)\n {\n if (button.Command?.CanExecute(button.CommandParameter) == true)\n {\n button.Command.Execute(button.CommandParameter);\n }\n }\n }\n}\n\u0060\u0060\u0060\n\n## Complete Handler Coverage\n\nOpenMaui implements **40\u002B handlers** covering all MAUI controls:\n\n**Basic Input Handlers:**\n- \u0060ButtonHandler\u0060 \u2192 \u0060SkiaButton\u0060\n- \u0060EntryHandler\u0060 \u2192 \u0060SkiaEntry\u0060 (with IME support for IBus, Fcitx5, XIM)\n- \u0060EditorHandler\u0060 \u2192 \u0060SkiaEditor\u0060\n- \u0060CheckBoxHandler\u0060 \u2192 \u0060SkiaCheckBox\u0060\n- \u0060SwitchHandler\u0060 \u2192 \u0060SkiaSwitch\u0060\n- \u0060SliderHandler\u0060 \u2192 \u0060SkiaSlider\u0060\n- \u0060StepperHandler\u0060 \u2192 \u0060SkiaStepper\u0060\n\n**Picker Handlers:**\n- \u0060DatePickerHandler\u0060 \u2192 \u0060SkiaDatePicker\u0060 (with calendar popup)\n- \u0060TimePickerHandler\u0060 \u2192 \u0060SkiaTimePicker\u0060\n- \u0060PickerHandler\u0060 \u2192 \u0060SkiaPicker\u0060\n\n**Display Handlers:**\n- \u0060LabelHandler\u0060 \u2192 \u0060SkiaLabel\u0060 (with HarfBuzz text shaping)\n- \u0060ImageHandler\u0060 \u2192 \u0060SkiaImage\u0060 (SVG, GIF, caching)\n- \u0060ProgressBarHandler\u0060 \u2192 \u0060SkiaProgressBar\u0060\n- \u0060ActivityIndicatorHandler\u0060 \u2192 \u0060SkiaActivityIndicator\u0060\n\n**Layout Handlers:**\n- \u0060GridHandler\u0060 \u2192 \u0060SkiaGrid\u0060\n- \u0060StackLayoutHandler\u0060 \u2192 \u0060SkiaStackLayout\u0060\n- \u0060FlexLayoutHandler\u0060 \u2192 \u0060SkiaFlexLayout\u0060\n- \u0060ScrollViewHandler\u0060 \u2192 \u0060SkiaScrollView\u0060\n- \u0060BorderHandler\u0060 \u2192 \u0060SkiaBorder\u0060\n- \u0060FrameHandler\u0060 \u2192 \u0060SkiaFrame\u0060\n\n**Collection Handlers:**\n- \u0060CollectionViewHandler\u0060 \u2192 \u0060SkiaCollectionView\u0060 (virtualized)\n- \u0060CarouselViewHandler\u0060 \u2192 \u0060SkiaCarouselView\u0060\n\n**Navigation Handlers:**\n- \u0060NavigationPageHandler\u0060 \u2192 \u0060SkiaNavigationPage\u0060\n- \u0060TabbedPageHandler\u0060 \u2192 \u0060SkiaTabbedPage\u0060\n- \u0060FlyoutPageHandler\u0060 \u2192 \u0060SkiaFlyoutPage\u0060\n- \u0060ShellHandler\u0060 \u2192 \u0060SkiaShell\u0060\n\n**Advanced Handlers:**\n- \u0060WebViewHandler\u0060 \u2192 \u0060LinuxWebView\u0060 (WebKitGTK integration)\n- \u0060GraphicsViewHandler\u0060 \u2192 \u0060SkiaGraphicsView\u0060\n- \u0060SwipeViewHandler\u0060 \u2192 \u0060SkiaSwipeView\u0060\n- \u0060RefreshViewHandler\u0060 \u2192 \u0060SkiaRefreshView\u0060\n\n## Property Mapping Deep Dive\n\nProperty mappers ensure bidirectional synchronization:\n\n\u0060\u0060\u0060csharp\npublic class EntryHandler : ViewHandler\u003CIEntry, SkiaEntry\u003E\n{\n private static void MapText(EntryHandler handler, IEntry entry)\n {\n handler.PlatformView.Text = entry.Text;\n }\n \n private static void MapTextColor(EntryHandler handler, IEntry entry)\n {\n handler.PlatformView.TextColor = entry.TextColor.ToSKColor();\n }\n \n private static void MapPlaceholder(EntryHandler handler, IEntry entry)\n {\n handler.PlatformView.Placeholder = entry.Placeholder;\n }\n \n protected override void ConnectHandler(SkiaEntry platformView)\n {\n base.ConnectHandler(platformView);\n \n // Wire up events from platform view to virtual view\n platformView.TextChanged \u002B= OnTextChanged;\n platformView.Completed \u002B= OnCompleted;\n }\n \n private void OnTextChanged(object? sender, TextChangedEventArgs e)\n {\n if (VirtualView != null)\n {\n VirtualView.Text = e.NewTextValue;\n }\n }\n}\n\u0060\u0060\u0060\n\n## Type Conversion Utilities\n\nOpenMaui provides seamless conversion between MAUI and Skia types:\n\n\u0060\u0060\u0060csharp\npublic static class ColorExtensions\n{\n public static SKColor ToSKColor(this Color color)\n {\n return new SKColor(\n (byte)(color.Red * 255),\n (byte)(color.Green * 255),\n (byte)(color.Blue * 255),\n (byte)(color.Alpha * 255)\n );\n }\n}\n\npublic static class RectExtensions\n{\n public static SKRect ToSKRect(this Rect rect)\n {\n return new SKRect(\n (float)rect.Left,\n (float)rect.Top,\n (float)rect.Right,\n (float)rect.Bottom\n );\n }\n}\n\u0060\u0060\u0060\n\nThese extensions ensure that developers work with familiar MAUI types (\u0060Color\u0060, \u0060Rect\u0060, \u0060Size\u0060, \u0060Thickness\u0060) while OpenMaui handles the Skia conversion internally.\n\n## Native Service Integration\n\nHandlers also bridge to native Linux services:\n\n\u0060\u0060\u0060csharp\npublic class FilePickerHandler\n{\n public async Task\u003CFileResult?\u003E PickAsync(PickOptions? options)\n {\n // Try xdg-desktop-portal first (Flatpak, Snap compatible)\n if (IsPortalAvailable())\n {\n return await PickViaPortal(options);\n }\n \n // Fall back to native dialogs\n if (IsKDE())\n {\n return await PickViaKDialog(options);\n }\n else\n {\n return await PickViaZenity(options);\n }\n }\n}\n\u0060\u0060\u0060\n\nThis ensures that file pickers, notifications, and other system services feel native to each desktop environment (GNOME, KDE, XFCE).\n\n## Custom Control Example\n\nDevelopers can create custom controls using the same pattern:\n\n\u0060\u0060\u0060csharp\n// 1. Define the interface\npublic interface ICustomGauge : IView\n{\n double Value { get; }\n double Minimum { get; }\n double Maximum { get; }\n Color GaugeColor { get; }\n}\n\n// 2. Create the Skia view\npublic class SkiaCustomGauge : SkiaView\n{\n public double Value { get; set; }\n public double Minimum { get; set; }\n public double Maximum { get; set; }\n public SKColor GaugeColor { get; set; }\n \n public override void Draw(SKCanvas canvas, SKRect dirtyRect)\n {\n var paint = new SKPaint\n {\n Color = GaugeColor,\n Style = SKPaintStyle.Stroke,\n StrokeWidth = 10,\n IsAntialias = true\n };\n \n var angle = (Value - Minimum) / (Maximum - Minimum) * 270;\n canvas.DrawArc(_bounds, -225, (float)angle, false, paint);\n }\n}\n\n// 3. Create the handler\npublic class CustomGaugeHandler : ViewHandler\u003CICustomGauge, SkiaCustomGauge\u003E\n{\n public static IPropertyMapper\u003CICustomGauge, CustomGaugeHandler\u003E Mapper =\n new PropertyMapper\u003CICustomGauge, CustomGaugeHandler\u003E()\n {\n [nameof(ICustomGauge.Value)] = MapValue,\n [nameof(ICustomGauge.GaugeColor)] = MapGaugeColor,\n };\n \n protected override SkiaCustomGauge CreatePlatformView()\n {\n return new SkiaCustomGauge();\n }\n}\n\n// 4. Register the handler\nbuilder.ConfigureMauiHandlers(handlers =\u003E\n{\n handlers.AddHandler\u003CICustomGauge, CustomGaugeHandler\u003E();\n});\n\u0060\u0060\u0060\n\nThis architecture provides complete flexibility while maintaining MAUI\u0027s cross-platform abstractions." + }, + { + "header": "Render Caching Strategies", + "content": "Rendering complex UI elements every frame is expensive. OpenMaui employs sophisticated **caching strategies** to minimize redundant drawing operations, dramatically improving performance for static or infrequently changing content.\n\n## SKPicture-Based Caching\n\nSkia\u0027s \u0060SKPicture\u0060 provides a recording mechanism for draw commands:\n\n\u0060\u0060\u0060csharp\npublic abstract class SkiaView\n{\n private SKPicture? _cachedPicture;\n private bool _cacheDirty = true;\n \n public bool EnableRenderCaching { get; set; } = false;\n \n public override void Draw(SKCanvas canvas, SKRect dirtyRect)\n {\n if (EnableRenderCaching)\n {\n if (_cacheDirty || _cachedPicture == null)\n {\n // Record drawing commands\n using var recorder = new SKPictureRecorder();\n var recordingCanvas = recorder.BeginRecording(_bounds);\n \n DrawContent(recordingCanvas, dirtyRect);\n \n _cachedPicture?.Dispose();\n _cachedPicture = recorder.EndRecording();\n _cacheDirty = false;\n }\n \n // Replay cached commands\n canvas.DrawPicture(_cachedPicture);\n }\n else\n {\n // Direct rendering\n DrawContent(canvas, dirtyRect);\n }\n }\n \n protected abstract void DrawContent(SKCanvas canvas, SKRect dirtyRect);\n \n public override void Invalidate()\n {\n _cacheDirty = true;\n base.Invalidate();\n }\n}\n\u0060\u0060\u0060\n\n## Image Caching System\n\nThe \u0060SkiaImage\u0060 control implements multi-level caching:\n\n\u0060\u0060\u0060csharp\npublic class SkiaImage : SkiaView\n{\n private static readonly Dictionary\u003Cstring, SKBitmap\u003E _imageCache = new();\n private static readonly object _cacheLock = new();\n \n private SKBitmap? _bitmap;\n private string? _source;\n \n public ImageSource Source\n {\n get =\u003E _source;\n set\n {\n if (_source != value)\n {\n _source = value;\n LoadImageAsync(value);\n }\n }\n }\n \n private async Task LoadImageAsync(ImageSource source)\n {\n if (source is FileImageSource fileSource)\n {\n var path = fileSource.File;\n \n // Check memory cache first\n lock (_cacheLock)\n {\n if (_imageCache.TryGetValue(path, out var cached))\n {\n _bitmap = cached;\n Invalidate();\n return;\n }\n }\n \n // Load from disk\n _bitmap = await Task.Run(() =\u003E SKBitmap.Decode(path));\n \n // Cache for future use\n lock (_cacheLock)\n {\n _imageCache[path] = _bitmap;\n }\n \n Invalidate();\n }\n else if (source is StreamImageSource streamSource)\n {\n var stream = await streamSource.Stream(CancellationToken.None);\n _bitmap = SKBitmap.Decode(stream);\n Invalidate();\n }\n }\n \n public override void Draw(SKCanvas canvas, SKRect dirtyRect)\n {\n if (_bitmap != null)\n {\n canvas.DrawBitmap(_bitmap, _bounds);\n }\n }\n}\n\u0060\u0060\u0060\n\n## SVG Caching with Svg.Skia\n\nSVG rendering is expensive, so OpenMaui caches rasterized versions:\n\n\u0060\u0060\u0060csharp\npublic class SkiaSvgImage : SkiaImage\n{\n private SKPicture? _svgPicture;\n private SKBitmap? _rasterizedCache;\n private Size _lastRenderSize;\n \n private void LoadSvg(string path)\n {\n var svg = new SKSvg();\n _svgPicture = svg.Load(path);\n }\n \n public override void Draw(SKCanvas canvas, SKRect dirtyRect)\n {\n if (_svgPicture == null)\n return;\n \n var currentSize = new Size(_bounds.Width, _bounds.Height);\n \n // Re-rasterize if size changed\n if (_rasterizedCache == null || _lastRenderSize != currentSize)\n {\n _rasterizedCache?.Dispose();\n \n var info = new SKImageInfo(\n (int)_bounds.Width,\n (int)_bounds.Height,\n SKColorType.Rgba8888,\n SKAlphaType.Premul\n );\n \n _rasterizedCache = new SKBitmap(info);\n \n using var cacheCanvas = new SKCanvas(_rasterizedCache);\n cacheCanvas.Clear(SKColors.Transparent);\n cacheCanvas.DrawPicture(_svgPicture);\n \n _lastRenderSize = currentSize;\n }\n \n canvas.DrawBitmap(_rasterizedCache, _bounds);\n }\n}\n\u0060\u0060\u0060\n\n## Text Layout Caching\n\nText shaping with HarfBuzz is computationally expensive, especially for complex scripts:\n\n\u0060\u0060\u0060csharp\npublic class SkiaLabel : SkiaView\n{\n private string? _text;\n private SKFont? _font;\n private SKTextBlob? _cachedTextBlob;\n private bool _textLayoutDirty = true;\n \n public string Text\n {\n get =\u003E _text;\n set\n {\n if (_text != value)\n {\n _text = value;\n _textLayoutDirty = true;\n Invalidate();\n }\n }\n }\n \n public override void Draw(SKCanvas canvas, SKRect dirtyRect)\n {\n if (_textLayoutDirty || _cachedTextBlob == null)\n {\n // Shape text using HarfBuzz\n var shaper = new SKShaper(_font.Typeface);\n _cachedTextBlob = shaper.Shape(_text, _font);\n _textLayoutDirty = false;\n }\n \n var paint = new SKPaint\n {\n Color = TextColor,\n IsAntialias = true\n };\n \n canvas.DrawText(_cachedTextBlob, _bounds.Left, _bounds.Top, paint);\n }\n}\n\u0060\u0060\u0060\n\n## Collection View Virtualization\n\nThe \u0060SkiaCollectionView\u0060 implements virtualization to cache only visible items:\n\n\u0060\u0060\u0060csharp\npublic class SkiaCollectionView : SkiaView\n{\n private readonly Dictionary\u003Cint, SkiaView\u003E _visibleItemCache = new();\n private readonly HashSet\u003Cint\u003E _currentlyVisible = new();\n \n public override void Draw(SKCanvas canvas, SKRect dirtyRect)\n {\n var visibleRange = CalculateVisibleRange(_scrollOffset, _bounds.Height);\n _currentlyVisible.Clear();\n \n for (int i = visibleRange.Start; i \u003C visibleRange.End; i\u002B\u002B)\n {\n _currentlyVisible.Add(i);\n \n if (!_visibleItemCache.TryGetValue(i, out var itemView))\n {\n // Create and cache new item view\n itemView = CreateItemView(i);\n _visibleItemCache[i] = itemView;\n }\n \n var itemBounds = CalculateItemBounds(i);\n itemView.Arrange(itemBounds);\n itemView.Draw(canvas, dirtyRect);\n }\n \n // Cleanup items that scrolled out of view\n var itemsToRemove = _visibleItemCache.Keys\n .Where(k =\u003E !_currentlyVisible.Contains(k))\n .ToList();\n \n foreach (var key in itemsToRemove)\n {\n _visibleItemCache[key].Dispose();\n _visibleItemCache.Remove(key);\n }\n }\n}\n\u0060\u0060\u0060\n\n## Cache Invalidation Strategy\n\nProper cache invalidation is critical:\n\n\u0060\u0060\u0060csharp\npublic abstract class SkiaView\n{\n protected void InvalidateCache(CacheLevel level)\n {\n switch (level)\n {\n case CacheLevel.Paint:\n // Only visual properties changed\n _cacheDirty = true;\n Invalidate();\n break;\n \n case CacheLevel.Layout:\n // Size/position changed, invalidate layout cache\n _layoutCache = null;\n _cacheDirty = true;\n InvalidateLayout();\n break;\n \n case CacheLevel.Deep:\n // Structure changed, invalidate entire subtree\n InvalidateDescendants();\n _cacheDirty = true;\n InvalidateLayout();\n break;\n }\n }\n}\n\npublic enum CacheLevel\n{\n Paint, // Color, stroke, etc.\n Layout, // Size, position\n Deep // Children added/removed\n}\n\u0060\u0060\u0060\n\n## Memory Management\n\nOpenMaui includes cache size limits to prevent memory exhaustion:\n\n\u0060\u0060\u0060csharp\npublic class ImageCacheManager\n{\n private const long MaxCacheSizeBytes = 100 * 1024 * 1024; // 100 MB\n private long _currentCacheSize = 0;\n \n public void AddToCache(string key, SKBitmap bitmap)\n {\n var bitmapSize = bitmap.ByteCount;\n \n // Evict old items if necessary\n while (_currentCacheSize \u002B bitmapSize \u003E MaxCacheSizeBytes)\n {\n EvictLeastRecentlyUsed();\n }\n \n _imageCache[key] = bitmap;\n _currentCacheSize \u002B= bitmapSize;\n }\n \n private void EvictLeastRecentlyUsed()\n {\n // LRU eviction logic\n }\n}\n\u0060\u0060\u0060\n\nThese caching strategies ensure that OpenMaui remains performant even with complex UIs containing hundreds of controls and images." + }, + { + "header": "Best Practices for Custom Controls", + "content": "Creating performant, maintainable custom controls in OpenMaui requires understanding the rendering pipeline and following established patterns. Here are battle-tested practices for building production-quality Skia-based controls.\n\n## 1. Extend the Right Base Class\n\nChoose your base class based on control complexity:\n\n\u0060\u0060\u0060csharp\n// Simple controls with no children\npublic class CustomIndicator : SkiaView\n{\n public override void Draw(SKCanvas canvas, SKRect dirtyRect)\n {\n // Direct drawing\n }\n}\n\n// Controls that contain other views\npublic class CustomPanel : SkiaLayoutView\n{\n protected override void OnMeasure(double widthConstraint, double heightConstraint)\n {\n // Measure children\n }\n \n protected override void OnArrange(SKRect bounds)\n {\n // Position children\n }\n}\n\n// Controls with templates (like buttons with content)\npublic class CustomCard : SkiaTemplatedView\n{\n public DataTemplate ContentTemplate { get; set; }\n}\n\u0060\u0060\u0060\n\n## 2. Implement Efficient Drawing\n\nMinimize object allocations in the draw loop:\n\n\u0060\u0060\u0060csharp\npublic class SkiaCustomGauge : SkiaView\n{\n // \u274C BAD: Allocates new objects every frame\n public override void Draw(SKCanvas canvas, SKRect dirtyRect)\n {\n var paint = new SKPaint { Color = SKColors.Blue }; // Allocation!\n var path = new SKPath(); // Allocation!\n path.AddArc(_bounds, 0, 90);\n canvas.DrawPath(path, paint);\n }\n \n // \u2705 GOOD: Reuse objects\n private readonly SKPaint _gaugePaint = new() { IsAntialias = true };\n private readonly SKPath _gaugePath = new();\n \n public override void Draw(SKCanvas canvas, SKRect dirtyRect)\n {\n _gaugePaint.Color = GaugeColor;\n \n _gaugePath.Reset();\n _gaugePath.AddArc(_bounds, 0, 90);\n \n canvas.DrawPath(_gaugePath, _gaugePaint);\n }\n \n protected override void Dispose(bool disposing)\n {\n if (disposing)\n {\n _gaugePaint?.Dispose();\n _gaugePath?.Dispose();\n }\n base.Dispose(disposing);\n }\n}\n\u0060\u0060\u0060\n\n## 3. Use Bindable Properties Correctly\n\nFollow MAUI\u0027s bindable property pattern:\n\n\u0060\u0060\u0060csharp\npublic class SkiaRatingControl : SkiaView\n{\n public static readonly BindableProperty RatingProperty =\n BindableProperty.Create(\n nameof(Rating),\n typeof(double),\n typeof(SkiaRatingControl),\n 0.0,\n propertyChanged: OnRatingChanged);\n \n public double Rating\n {\n get =\u003E (double)GetValue(RatingProperty);\n set =\u003E SetValue(RatingProperty, value);\n }\n \n private static void OnRatingChanged(BindableObject bindable, object oldValue, object newValue)\n {\n if (bindable is SkiaRatingControl control)\n {\n control.Invalidate(); // Trigger repaint\n }\n }\n}\n\u0060\u0060\u0060\n\n## 4. Implement Smart Invalidation\n\nDifferentiate between layout and paint changes:\n\n\u0060\u0060\u0060csharp\npublic class SkiaCustomPanel : SkiaLayoutView\n{\n public Color BorderColor\n {\n get =\u003E _borderColor;\n set\n {\n if (_borderColor != value)\n {\n _borderColor = value;\n Invalidate(); // Paint-only change\n }\n }\n }\n \n public Thickness Padding\n {\n get =\u003E _padding;\n set\n {\n if (_padding != value)\n {\n _padding = value;\n InvalidateLayout(); // Affects child positioning\n }\n }\n }\n}\n\u0060\u0060\u0060\n\n## 5. Handle Gestures Properly\n\nImplement gesture recognition with proper event propagation:\n\n\u0060\u0060\u0060csharp\npublic class SkiaInteractiveCard : SkiaView\n{\n public event EventHandler\u003CTappedEventArgs\u003E? Tapped;\n public ICommand? TapCommand { get; set; }\n \n public override bool OnTouchEvent(MotionEvent motionEvent)\n {\n switch (motionEvent.Action)\n {\n case MotionEventActions.Down:\n _isPressed = true;\n Invalidate(); // Update visual state\n return true; // Consume event\n \n case MotionEventActions.Up:\n if (_isPressed \u0026\u0026 _bounds.Contains(motionEvent.X, motionEvent.Y))\n {\n // Fire events\n Tapped?.Invoke(this, new TappedEventArgs());\n TapCommand?.Execute(null);\n }\n _isPressed = false;\n Invalidate();\n return true;\n \n case MotionEventActions.Cancel:\n _isPressed = false;\n Invalidate();\n return true;\n }\n \n return base.OnTouchEvent(motionEvent);\n }\n}\n\u0060\u0060\u0060\n\n## 6. Support Visual States\n\nImplement visual state management for interactive controls:\n\n\u0060\u0060\u0060csharp\npublic class SkiaCustomButton : SkiaView\n{\n private VisualState _currentState = VisualState.Normal;\n \n public enum VisualState\n {\n Normal,\n Hovered,\n Pressed,\n Disabled\n }\n \n public override void Draw(SKCanvas canvas, SKRect dirtyRect)\n {\n var backgroundColor = _currentState switch\n {\n VisualState.Normal =\u003E NormalColor,\n VisualState.Hovered =\u003E HoverColor,\n VisualState.Pressed =\u003E PressedColor,\n VisualState.Disabled =\u003E DisabledColor,\n _ =\u003E NormalColor\n };\n \n using var paint = new SKPaint\n {\n Color = backgroundColor,\n Style = SKPaintStyle.Fill\n };\n \n canvas.DrawRoundRect(_bounds, CornerRadius, CornerRadius, paint);\n }\n \n public bool IsEnabled\n {\n get =\u003E _isEnabled;\n set\n {\n _isEnabled = value;\n _currentState = value ? VisualState.Normal : VisualState.Disabled;\n Invalidate();\n }\n }\n}\n\u0060\u0060\u0060\n\n## 7. Implement Accessibility\n\nMake controls accessible to screen readers:\n\n\u0060\u0060\u0060csharp\npublic class SkiaAccessibleControl : SkiaView, IAccessible\n{\n public string AccessibilityLabel { get; set; }\n public string AccessibilityHint { get; set; }\n public AccessibilityRole Role { get; set; } = AccessibilityRole.Button;\n \n public void AnnounceAccessibility(string message)\n {\n // Notify AT-SPI2 screen readers\n AccessibilityManager.Announce(message);\n }\n}\n\u0060\u0060\u0060\n\n## 8. Optimize for HiDPI Displays\n\nHandle scaling properly:\n\n\u0060\u0060\u0060csharp\npublic class SkiaScalableControl : SkiaView\n{\n private float _displayScale = 1.0f;\n \n protected override void OnParentSet()\n {\n base.OnParentSet();\n _displayScale = GetDisplayScale();\n }\n \n public override void Draw(SKCanvas canvas, SKRect dirtyRect)\n {\n // Scale-aware stroke width\n var strokeWidth = 1.0f * _displayScale;\n \n using var paint = new SKPaint\n {\n StrokeWidth = strokeWidth,\n IsAntialias = true\n };\n \n canvas.DrawLine(_bounds.Left, _bounds.Top, _bounds.Right, _bounds.Bottom, paint);\n }\n}\n\u0060\u0060\u0060\n\n## 9. Use Render Caching Strategically\n\nEnable caching for static or rarely changing content:\n\n\u0060\u0060\u0060csharp\npublic class SkiaComplexChart : SkiaView\n{\n private SKPicture? _gridCache;\n private bool _gridDirty = true;\n \n public override void Draw(SKCanvas canvas, SKRect dirtyRect)\n {\n // Cache the static grid\n if (_gridDirty || _gridCache == null)\n {\n using var recorder = new SKPictureRecorder();\n var recordingCanvas = recorder.BeginRecording(_bounds);\n DrawGrid(recordingCanvas);\n _gridCache = recorder.EndRecording();\n _gridDirty = false;\n }\n \n canvas.DrawPicture(_gridCache);\n \n // Draw dynamic data without caching\n DrawDataPoints(canvas);\n }\n \n public void UpdateData(double[] newData)\n {\n _data = newData;\n Invalidate(); // Grid cache remains valid\n }\n}\n\u0060\u0060\u0060\n\n## 10. Test Across Desktop Environments\n\nEnsure your control works on different Linux configurations:\n\n\u0060\u0060\u0060csharp\n[Fact]\npublic void CustomControl_RendersCorrectly_OnGnome()\n{\n var control = new SkiaCustomControl\n {\n Width = 200,\n Height = 100\n };\n \n // Simulate GNOME theme\n SkiaTheme.Current = SkiaTheme.CreateGnomeTheme();\n \n var bitmap = RenderToBitmap(control);\n \n // Assert visual output\n Assert.True(bitmap.GetPixel(100, 50).Red \u003E 0);\n}\n\n[Fact]\npublic void CustomControl_HandlesHiDPI()\n{\n var control = new SkiaCustomControl();\n \n // Simulate 2x scaling\n control.SetDisplayScale(2.0f);\n control.Measure(200, 100);\n \n Assert.Equal(400, control.MeasuredWidth);\n}\n\u0060\u0060\u0060\n\n## Complete Example: Custom Progress Ring\n\nPutting it all together:\n\n\u0060\u0060\u0060csharp\npublic class SkiaProgressRing : SkiaView\n{\n private readonly SKPaint _trackPaint = new()\n {\n Style = SKPaintStyle.Stroke,\n StrokeWidth = 8,\n IsAntialias = true\n };\n \n private readonly SKPaint _progressPaint = new()\n {\n Style = SKPaintStyle.Stroke,\n StrokeWidth = 8,\n IsAntialias = true,\n StrokeCap = SKStrokeCap.Round\n };\n \n public static readonly BindableProperty ProgressProperty =\n BindableProperty.Create(nameof(Progress), typeof(double), typeof(SkiaProgressRing), 0.0,\n propertyChanged: (b, o, n) =\u003E ((SkiaProgressRing)b).Invalidate());\n \n public double Progress\n {\n get =\u003E (double)GetValue(ProgressProperty);\n set =\u003E SetValue(ProgressProperty, Math.Clamp(value, 0.0, 1.0));\n }\n \n public override void Draw(SKCanvas canvas, SKRect dirtyRect)\n {\n var center = new SKPoint(_bounds.MidX, _bounds.MidY);\n var radius = Math.Min(_bounds.Width, _bounds.Height) / 2 - _progressPaint.StrokeWidth;\n \n _trackPaint.Color = SKColors.LightGray;\n canvas.DrawCircle(center, radius, _trackPaint);\n \n _progressPaint.Color = SKColors.Blue;\n var sweepAngle = (float)(Progress * 360);\n \n using var path = new SKPath();\n path.AddArc(SKRect.Create(center.X - radius, center.Y - radius, radius * 2, radius * 2),\n -90, sweepAngle);\n \n canvas.DrawPath(path, _progressPaint);\n }\n \n protected override void Dispose(bool disposing)\n {\n if (disposing)\n {\n _trackPaint?.Dispose();\n _progressPaint?.Dispose();\n }\n base.Dispose(disposing);\n }\n}\n\u0060\u0060\u0060\n\nFollowing these best practices ensures your custom controls are performant, maintainable, and integrate seamlessly with the OpenMaui rendering pipeline." + } + ], + "generatedAt": 1769749919894 +} \ No newline at end of file diff --git a/.notes/series-1769750134166-c6e172.json b/.notes/series-1769750134166-c6e172.json new file mode 100644 index 0000000..22164f7 --- /dev/null +++ b/.notes/series-1769750134166-c6e172.json @@ -0,0 +1,55 @@ +{ + "id": "series-1769750134166-c6e172", + "title": "Window Management Across Display Servers: X11, Wayland, and GTK Integration", + "content": "# Window Management Across Display Servers: X11, Wayland, and GTK Integration\r\n\r\n*How OpenMaui abstracts Linux\u0027s fragmented display server landscape into a single, elegant windowing API for .NET MAUI applications.*\r\n\r\n## Introduction\r\n\r\nBuilding cross-platform desktop applications on Linux presents a unique challenge that Windows and macOS developers rarely encounter: **multiple competing display server protocols**. Unlike the monolithic window management systems on other platforms, Linux offers X11 (the legacy standard), Wayland (the modern replacement), and toolkit abstractions like GTK that work across both.\n\nFor .NET MAUI developers targeting Linux through OpenMaui, this fragmentation could be a nightmare. Should your application use X11 directly for maximum control? Wayland for modern compositor integration? Or GTK for compatibility and native widget support?\n\nThe answer, fortunately, is **all of the above**\u2014but through a unified abstraction layer that hides the complexity. OpenMaui\u0027s windowing system implements a factory pattern that detects the runtime environment and selects the appropriate backend automatically, while presenting a consistent API to application developers.\n\nThis article explores how OpenMaui bridges these three distinct windowing approaches, when to use each backend, and how the event loop integration ensures responsive, thread-safe UI operations across all display servers.\r\n\r\n## Linux Display Server Landscape\r\n\r\nTo understand OpenMaui\u0027s windowing architecture, you first need to grasp the Linux display server landscape and why it\u0027s so fragmented.\n\n## X11: The Legacy Standard\n\n**X11** (X Window System) has been the foundation of Linux graphical environments since 1987. It\u0027s a network-transparent protocol where applications (X clients) communicate with a display server (X server) that manages windows, input devices, and rendering.\n\nKey characteristics:\n- **Direct window control**: Applications can position, resize, and manage windows with pixel-perfect precision\n- **Network transparency**: X11 was designed to run applications remotely over a network\n- **Mature ecosystem**: Decades of tooling, libraries, and documentation\n- **Security concerns**: The network-transparent design creates security vulnerabilities in modern single-user desktop environments\n\n## Wayland: The Modern Replacement\n\n**Wayland** emerged in 2008 as a simpler, more secure alternative to X11. Instead of a client-server model, Wayland uses a compositor-centric architecture where the compositor (like GNOME Shell or KDE KWin) directly manages window surfaces.\n\nKey characteristics:\n- **Security by design**: Applications can\u0027t snoop on other windows or inject input events\n- **Simplified architecture**: No network layer, direct rendering to compositor surfaces\n- **Protocol extensions**: Core protocol is minimal; functionality added through extensions like \u0060xdg-shell\u0060\n- **Compositor-specific behavior**: Each compositor can implement features differently\n\n## GTK: The Toolkit Abstraction\n\n**GTK (GIMP Toolkit)** is a widget toolkit that abstracts both X11 and Wayland, automatically selecting the appropriate backend at runtime. GTK provides native widgets, theme integration, and platform services.\n\nKey characteristics:\n- **Cross-backend compatibility**: Same code runs on X11, Wayland, and even Windows/macOS\n- **Native integration**: GTK applications feel native to GNOME and other GTK-based desktop environments\n- **Heavy dependency**: Requires the entire GTK3 runtime and associated libraries\n- **Widget-based**: Designed for native widgets rather than custom rendering\n\n## The OpenMaui Challenge\n\nOpenMaui uses **SkiaSharp for all UI rendering**\u2014there are no native GTK widgets for buttons, labels, or text boxes. The entire MAUI control library is rendered through Skia\u0027s 2D graphics engine. This creates an interesting challenge:\n\n- **X11** provides direct access to drawable surfaces and events\n- **Wayland** requires compositor cooperation through protocol extensions\n- **GTK** expects to manage widgets, but OpenMaui only needs a drawable surface\n\nThe solution is a **window abstraction layer** that provides three backend implementations, each optimized for its target environment.\r\n\r\n## The Window Abstraction Layer\r\n\r\nOpenMaui\u0027s windowing system is built around a core abstraction that defines what a \u0022window\u0022 means to a MAUI application, independent of the underlying display server protocol.\n\n## Core Window Interface\n\nWhile the codebase doesn\u0027t expose a single \u0060IWindow\u0060 interface publicly, all three window implementations (\u0060X11Window\u0060, \u0060WaylandWindow\u0060, \u0060GtkHostWindow\u0060) provide a consistent set of capabilities:\n\n\u0060\u0060\u0060csharp\n// Common window operations across all backends\npublic interface IWindowOperations\n{\n void Show();\n void Hide();\n void Close();\n void SetTitle(string title);\n void SetBounds(int x, int y, int width, int height);\n void Invalidate(); // Request redraw\n void Present(); // Bring to front\n}\n\u0060\u0060\u0060\n\n## Factory Pattern Implementation\n\nThe factory pattern implementation allows OpenMaui to detect the runtime environment and instantiate the appropriate window backend without application code needing to know which display server is running.\n\n\u0060\u0060\u0060csharp\n// Simplified factory logic (conceptual)\npublic static class WindowFactory\n{\n public static IWindowBackend CreateWindow()\n {\n // Check for Wayland session\n if (Environment.GetEnvironmentVariable(\u0022WAYLAND_DISPLAY\u0022) != null)\n {\n return new WaylandWindow();\n }\n \n // Check for X11 session\n if (Environment.GetEnvironmentVariable(\u0022DISPLAY\u0022) != null)\n {\n return new X11Window();\n }\n \n // Fallback to GTK (works on both X11 and Wayland)\n return new GtkHostWindow();\n }\n}\n\u0060\u0060\u0060\n\nIn practice, OpenMaui\u0027s \u0060LinuxApplication\u0060 class manages window creation through the \u0060LinuxApplicationContext\u0060, which handles the lifecycle of MAUI windows and their platform counterparts.\n\n## Rendering Surface Integration\n\nRegardless of the window backend, all three implementations must provide a **SkiaSharp rendering surface**. This is where the abstraction really shines:\n\n- **X11Window**: Creates an X11 drawable and wraps it in an \u0060SKSurface\u0060\n- **WaylandWindow**: Uses Wayland buffer protocols to create shared memory surfaces for Skia\n- **GtkHostWindow**: Embeds a \u0060GtkSkiaSurfaceWidget\u0060 that integrates Skia with GTK\u0027s Cairo rendering pipeline\n\nThe \u0060SkiaRenderingEngine\u0060 component doesn\u0027t know or care which window backend is providing the surface\u2014it just draws MAUI controls using SkiaSharp\u0027s 2D graphics API.\n\n## Event Handling Abstraction\n\nEach display server has its own event model:\n\n- **X11**: Event queue with \u0060XEvent\u0060 structures (KeyPress, ButtonPress, Expose, ConfigureNotify, etc.)\n- **Wayland**: Callback-based event dispatch through protocol objects\n- **GTK**: GLib signal system with type-safe signal handlers\n\nOpenMaui normalizes these into a common event model that the MAUI framework expects:\n\n\u0060\u0060\u0060csharp\n// Platform-agnostic event handling\npublic class WindowEventArgs\n{\n public WindowEventType Type { get; set; }\n public int X { get; set; }\n public int Y { get; set; }\n public int Width { get; set; }\n public int Height { get; set; }\n}\n\npublic enum WindowEventType\n{\n Resize,\n MouseMove,\n MouseDown,\n MouseUp,\n KeyDown,\n KeyUp,\n Expose,\n FocusIn,\n FocusOut\n}\n\u0060\u0060\u0060\n\nEach backend translates its native events into this normalized format, ensuring that gesture recognition, keyboard handling, and window lifecycle events work identically across all display servers.\r\n\r\n## X11Window: Direct X11 Implementation\r\n\r\nThe \u0060X11Window\u0060 implementation provides direct access to the X11 protocol, giving OpenMaui maximum control over window behavior and rendering.\n\n## Architecture Overview\n\n\u0060X11Window\u0060 uses P/Invoke to call native X11 libraries (\u0060libX11\u0060) directly from C#. This approach avoids middleware layers and provides low-latency event handling.\n\n\u0060\u0060\u0060csharp\n// Simplified X11Window structure\npublic class X11Window\n{\n private IntPtr _display; // X11 Display connection\n private IntPtr _window; // X11 Window handle\n private IntPtr _gc; // Graphics context\n \n public X11Window()\n {\n // Open connection to X11 display server\n _display = XOpenDisplay(null);\n if (_display == IntPtr.Zero)\n throw new Exception(\u0022Cannot connect to X server\u0022);\n \n // Create window\n _window = XCreateWindow(\n _display,\n DefaultRootWindow(_display),\n x: 0, y: 0,\n width: 800, height: 600,\n borderWidth: 0,\n depth: CopyFromParent,\n windowClass: InputOutput,\n visual: CopyFromParent,\n valueMask: 0,\n attributes: IntPtr.Zero\n );\n \n // Create graphics context for rendering\n _gc = XCreateGC(_display, _window, 0, IntPtr.Zero);\n }\n}\n\u0060\u0060\u0060\n\n## Event Loop Integration\n\nX11 uses an event queue model. The application must continuously poll for events using \u0060XNextEvent()\u0060 or \u0060XPending()\u0060:\n\n\u0060\u0060\u0060csharp\npublic void ProcessEvents()\n{\n while (XPending(_display) \u003E 0)\n {\n XEvent evt;\n XNextEvent(_display, out evt);\n \n switch (evt.type)\n {\n case EventType.Expose:\n // Window needs redrawing\n OnPaint();\n break;\n \n case EventType.ConfigureNotify:\n // Window resized or moved\n OnResize(evt.xconfigure.width, evt.xconfigure.height);\n break;\n \n case EventType.ButtonPress:\n // Mouse button pressed\n OnMouseDown(evt.xbutton.x, evt.xbutton.y, evt.xbutton.button);\n break;\n \n case EventType.KeyPress:\n // Keyboard key pressed\n OnKeyDown(evt.xkey.keycode);\n break;\n }\n }\n}\n\u0060\u0060\u0060\n\nOpenMaui integrates this event loop with the \u0060LinuxDispatcher\u0060, which uses GLib\u0027s main loop to coordinate X11 events with MAUI\u0027s UI thread.\n\n## Rendering Pipeline\n\nFor Skia rendering on X11, OpenMaui creates an X11 Pixmap (off-screen drawable) and wraps it in an \u0060SKSurface\u0060:\n\n\u0060\u0060\u0060csharp\npublic void InitializeSkiaSurface(int width, int height)\n{\n // Create X11 pixmap for off-screen rendering\n var pixmap = XCreatePixmap(_display, _window, width, height, 24);\n \n // Wrap pixmap in SkiaSharp surface\n var info = new SKImageInfo(width, height, SKColorType.Bgra8888);\n _skiaSurface = SKSurface.Create(info);\n \n // Render MAUI controls to Skia surface\n RenderMauiContent(_skiaSurface.Canvas);\n \n // Copy Skia surface to X11 window\n CopyPixmapToWindow(pixmap);\n}\n\u0060\u0060\u0060\n\n## Advantages and Trade-offs\n\n**Advantages:**\n- **Maximum control**: Direct access to all X11 features (window properties, atoms, selections)\n- **Low latency**: No middleware layers between application and display server\n- **Mature debugging**: Decades of X11 debugging tools (xev, xwininfo, xprop)\n\n**Trade-offs:**\n- **X11-only**: Won\u0027t work on pure Wayland sessions (though XWayland provides compatibility)\n- **Security model**: Inherits X11\u0027s security limitations\n- **Complexity**: Requires deep understanding of X11 protocol and window manager interactions\n\n## When to Use X11Window\n\nThe \u0060X11Window\u0060 backend is ideal when:\n- Running on traditional X11 desktop environments (older XFCE, MATE, etc.)\n- Maximum performance is critical (direct rendering, no GTK overhead)\n- You need X11-specific features (window manager hints, custom protocols)\n- Debugging with X11 tools is required\r\n\r\n## WaylandWindow: Native Wayland with xdg-shell\r\n\r\nThe \u0060WaylandWindow\u0060 implementation provides native Wayland protocol support, enabling OpenMaui applications to run on modern compositor-based desktop environments without XWayland compatibility layers.\n\n## Wayland Protocol Fundamentals\n\nWayland is fundamentally different from X11. Instead of a centralized server managing all windows, each compositor (GNOME Shell, KDE KWin, Sway) acts as both the display server and window manager. Applications communicate with the compositor through **protocol objects**.\n\nKey protocol objects used by \u0060WaylandWindow\u0060:\n\n- **wl_display**: Connection to the Wayland compositor\n- **wl_surface**: A rectangular area for rendering\n- **xdg_surface**: Desktop shell extension for window management\n- **xdg_toplevel**: Top-level window (what users think of as \u0022windows\u0022)\n- **wl_shm**: Shared memory for buffer allocation\n\n## Initialization Sequence\n\n\u0060\u0060\u0060csharp\n// Simplified WaylandWindow initialization\npublic class WaylandWindow\n{\n private IntPtr _display;\n private IntPtr _surface;\n private IntPtr _xdgSurface;\n private IntPtr _xdgToplevel;\n \n public WaylandWindow()\n {\n // Connect to Wayland compositor\n _display = wl_display_connect(null);\n if (_display == IntPtr.Zero)\n throw new Exception(\u0022Cannot connect to Wayland compositor\u0022);\n \n // Get registry to discover compositor capabilities\n var registry = wl_display_get_registry(_display);\n wl_registry_add_listener(registry, registryListener, IntPtr.Zero);\n \n // Roundtrip to process registry events\n wl_display_roundtrip(_display);\n \n // Create surface for rendering\n _surface = wl_compositor_create_surface(_compositor);\n \n // Create xdg_surface (desktop shell extension)\n _xdgSurface = xdg_wm_base_get_xdg_surface(_xdgWmBase, _surface);\n xdg_surface_add_listener(_xdgSurface, xdgSurfaceListener, IntPtr.Zero);\n \n // Create toplevel window\n _xdgToplevel = xdg_surface_get_toplevel(_xdgSurface);\n xdg_toplevel_add_listener(_xdgToplevel, xdgToplevelListener, IntPtr.Zero);\n \n // Set window properties\n xdg_toplevel_set_title(_xdgToplevel, \u0022OpenMaui Application\u0022);\n \n // Commit surface to make it visible\n wl_surface_commit(_surface);\n }\n}\n\u0060\u0060\u0060\n\n## Event Handling with Callbacks\n\nUnlike X11\u0027s event queue, Wayland uses a **callback-based event dispatch system**. You register listener functions for each protocol object:\n\n\u0060\u0060\u0060csharp\nprivate static wl_registry_listener registryListener = new wl_registry_listener\n{\n global = OnRegistryGlobal,\n global_remove = OnRegistryGlobalRemove\n};\n\nprivate static void OnRegistryGlobal(\n IntPtr data,\n IntPtr registry,\n uint name,\n string @interface,\n uint version)\n{\n // Compositor advertises available interfaces\n if (@interface == \u0022wl_compositor\u0022)\n {\n _compositor = wl_registry_bind(registry, name, wl_compositor_interface, 4);\n }\n else if (@interface == \u0022xdg_wm_base\u0022)\n {\n _xdgWmBase = wl_registry_bind(registry, name, xdg_wm_base_interface, 1);\n }\n else if (@interface == \u0022wl_seat\u0022)\n {\n _seat = wl_registry_bind(registry, name, wl_seat_interface, 5);\n }\n}\n\nprivate static xdg_toplevel_listener xdgToplevelListener = new xdg_toplevel_listener\n{\n configure = OnToplevelConfigure,\n close = OnToplevelClose\n};\n\nprivate static void OnToplevelConfigure(\n IntPtr data,\n IntPtr xdgToplevel,\n int width,\n int height,\n IntPtr states)\n{\n // Compositor requests window resize\n if (width \u003E 0 \u0026\u0026 height \u003E 0)\n {\n ResizeWindow(width, height);\n }\n}\n\u0060\u0060\u0060\n\n## Buffer Management and Rendering\n\nWayland uses shared memory buffers for software rendering. OpenMaui creates a shared memory pool and allocates buffers that both the application and compositor can access:\n\n\u0060\u0060\u0060csharp\npublic void CreateSharedBuffer(int width, int height)\n{\n int stride = width * 4; // 4 bytes per pixel (ARGB)\n int size = stride * height;\n \n // Create shared memory file\n int fd = CreateAnonymousFile(size);\n \n // Map memory for application access\n IntPtr data = mmap(IntPtr.Zero, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);\n \n // Create Wayland shared memory pool\n var pool = wl_shm_create_pool(_shm, fd, size);\n \n // Create buffer from pool\n var buffer = wl_shm_pool_create_buffer(\n pool,\n offset: 0,\n width: width,\n height: height,\n stride: stride,\n format: WL_SHM_FORMAT_ARGB8888\n );\n \n // Create SkiaSharp surface backed by shared memory\n var info = new SKImageInfo(width, height, SKColorType.Bgra8888);\n _skiaSurface = SKSurface.Create(info, data, stride);\n \n // Render MAUI content\n RenderMauiContent(_skiaSurface.Canvas);\n \n // Attach buffer to surface and commit\n wl_surface_attach(_surface, buffer, 0, 0);\n wl_surface_damage(_surface, 0, 0, width, height);\n wl_surface_commit(_surface);\n}\n\u0060\u0060\u0060\n\n## Event Loop Integration\n\nWayland\u0027s event dispatch integrates with the application\u0027s main loop:\n\n\u0060\u0060\u0060csharp\npublic void ProcessEvents()\n{\n // Dispatch pending events\n while (wl_display_prepare_read(_display) != 0)\n {\n wl_display_dispatch_pending(_display);\n }\n \n // Wait for events (with timeout)\n wl_display_flush(_display);\n wl_display_read_events(_display);\n wl_display_dispatch_pending(_display);\n}\n\u0060\u0060\u0060\n\nOpenMaui integrates this with the GLib main loop used by \u0060LinuxDispatcher\u0060, ensuring Wayland events are processed on the UI thread.\n\n## Advantages and Trade-offs\n\n**Advantages:**\n- **Modern security model**: Applications can\u0027t spy on each other or inject input\n- **Smooth animations**: Direct compositor integration enables better frame synchronization\n- **Future-proof**: Wayland is the future of Linux desktop graphics\n- **No XWayland overhead**: Native Wayland performance\n\n**Trade-offs:**\n- **Compositor-dependent behavior**: Different compositors implement features differently\n- **Limited debugging tools**: Wayland debugging is less mature than X11\n- **Protocol complexity**: Requires understanding multiple protocol extensions\n- **Version fragmentation**: Different compositors support different protocol versions\n\n## When to Use WaylandWindow\n\nThe \u0060WaylandWindow\u0060 backend is ideal when:\n- Running on modern GNOME (40\u002B), KDE Plasma (5.20\u002B), or Sway\n- Security is a priority (sandboxing, Flatpak integration)\n- You need smooth animations and compositor effects\n- XWayland compatibility layers cause issues\n\nWhile X11 gives you direct control over every pixel and protocol message, Wayland forces you to work within the constraints of the compositor\u2014but those constraints bring security and stability.\r\n\r\n## GtkHostWindow: GTK3 Bridge Approach\r\n\r\nThe \u0060GtkHostWindow\u0060 implementation takes a different approach: instead of implementing display server protocols directly, it uses **GTK3 as a cross-platform abstraction layer**. This provides compatibility with both X11 and Wayland while integrating with the native desktop environment.\n\n## Why GTK for a Skia-Based Framework?\n\nAt first glance, using GTK seems counterintuitive for OpenMaui. The framework renders all UI controls with SkiaSharp\u2014there are no native GTK buttons, labels, or text boxes. So why use GTK at all?\n\nThe answer lies in **platform integration**:\n\n- **Automatic backend selection**: GTK detects whether it\u0027s running on X11 or Wayland and uses the appropriate backend\n- **Native dialogs**: File pickers, message boxes, and system dialogs use GTK\u0027s native styling\n- **Theme integration**: GTK provides access to system theme colors, fonts, and dark mode settings\n- **WebView support**: WebKitGTK provides web rendering capabilities\n- **Input method support**: GTK handles complex text input (IBus, Fcitx5) automatically\n\n## Architecture: GTK Window \u002B Skia Widget\n\nThe \u0060GtkHostWindow\u0060 architecture consists of two components:\n\n1. **GtkHostWindow**: A GTK \u0060GtkWindow\u0060 that provides the window chrome (title bar, borders, resize handles)\n2. **GtkSkiaSurfaceWidget**: A custom GTK widget that embeds a SkiaSharp rendering surface\n\n\u0060\u0060\u0060csharp\npublic class GtkHostWindow\n{\n private IntPtr _gtkWindow;\n private GtkSkiaSurfaceWidget _skiaSurface;\n \n public GtkHostWindow()\n {\n // Initialize GTK (if not already initialized)\n if (!gtk_init_check(IntPtr.Zero, IntPtr.Zero))\n throw new Exception(\u0022Cannot initialize GTK\u0022);\n \n // Create GTK window\n _gtkWindow = gtk_window_new(GtkWindowType.Toplevel);\n \n // Set window properties\n gtk_window_set_title(_gtkWindow, \u0022OpenMaui Application\u0022);\n gtk_window_set_default_size(_gtkWindow, 800, 600);\n \n // Create Skia surface widget\n _skiaSurface = new GtkSkiaSurfaceWidget();\n \n // Add widget to window\n gtk_container_add(_gtkWindow, _skiaSurface.Handle);\n \n // Connect signals\n g_signal_connect(_gtkWindow, \u0022destroy\u0022, OnDestroy, IntPtr.Zero);\n g_signal_connect(_gtkWindow, \u0022configure-event\u0022, OnConfigure, IntPtr.Zero);\n \n // Show window\n gtk_widget_show_all(_gtkWindow);\n }\n}\n\u0060\u0060\u0060\n\n## GtkSkiaSurfaceWidget: Bridging GTK and Skia\n\nThe \u0060GtkSkiaSurfaceWidget\u0060 is where the magic happens. This custom GTK widget integrates SkiaSharp rendering with GTK\u0027s Cairo-based drawing system:\n\n\u0060\u0060\u0060csharp\npublic class GtkSkiaSurfaceWidget\n{\n private IntPtr _widget;\n private SKSurface _skiaSurface;\n private SKCanvas _canvas;\n \n public GtkSkiaSurfaceWidget()\n {\n // Create GTK drawing area\n _widget = gtk_drawing_area_new();\n \n // Enable event handling\n gtk_widget_add_events(_widget, \n GdkEventMask.ButtonPressMask | \n GdkEventMask.ButtonReleaseMask |\n GdkEventMask.PointerMotionMask |\n GdkEventMask.KeyPressMask |\n GdkEventMask.KeyReleaseMask);\n \n // Connect draw signal\n g_signal_connect(_widget, \u0022draw\u0022, OnDraw, IntPtr.Zero);\n g_signal_connect(_widget, \u0022button-press-event\u0022, OnButtonPress, IntPtr.Zero);\n g_signal_connect(_widget, \u0022motion-notify-event\u0022, OnMotionNotify, IntPtr.Zero);\n }\n \n private bool OnDraw(IntPtr widget, IntPtr cairoContext, IntPtr userData)\n {\n // Get widget size\n int width = gtk_widget_get_allocated_width(widget);\n int height = gtk_widget_get_allocated_height(widget);\n \n // Create or resize Skia surface\n if (_skiaSurface == null || \n _skiaSurface.Canvas.DeviceClipBounds.Width != width ||\n _skiaSurface.Canvas.DeviceClipBounds.Height != height)\n {\n _skiaSurface?.Dispose();\n var info = new SKImageInfo(width, height, SKColorType.Bgra8888);\n _skiaSurface = SKSurface.Create(info);\n }\n \n _canvas = _skiaSurface.Canvas;\n _canvas.Clear(SKColors.White);\n \n // Render MAUI content using Skia\n RenderMauiContent(_canvas);\n \n // Convert Skia surface to Cairo surface and draw\n using (var image = _skiaSurface.Snapshot())\n using (var data = image.Encode(SKEncodedImageFormat.Png, 100))\n {\n // Draw to Cairo context\n DrawSkiaImageToCairo(cairoContext, data, width, height);\n }\n \n return true;\n }\n}\n\u0060\u0060\u0060\n\n## Event Handling Through GTK Signals\n\nGTK uses a signal-based event system. OpenMaui connects signal handlers to translate GTK events into MAUI events:\n\n\u0060\u0060\u0060csharp\nprivate bool OnButtonPress(IntPtr widget, IntPtr eventPtr, IntPtr userData)\n{\n var evt = Marshal.PtrToStructure\u003CGdkEventButton\u003E(eventPtr);\n \n // Translate GTK event to MAUI event\n var point = new Point(evt.x, evt.y);\n var button = evt.button switch\n {\n 1 =\u003E MouseButton.Left,\n 2 =\u003E MouseButton.Middle,\n 3 =\u003E MouseButton.Right,\n _ =\u003E MouseButton.Left\n };\n \n // Dispatch to MAUI gesture system\n HandleMouseDown(point, button);\n \n return true; // Event handled\n}\n\nprivate bool OnConfigure(IntPtr widget, IntPtr eventPtr, IntPtr userData)\n{\n var evt = Marshal.PtrToStructure\u003CGdkEventConfigure\u003E(eventPtr);\n \n // Window resized or moved\n HandleResize(evt.width, evt.height);\n \n return false; // Allow GTK to process event\n}\n\u0060\u0060\u0060\n\n## Integration with LinuxDispatcher\n\nGTK has its own main loop (\u0060gtk_main()\u0060), but OpenMaui uses the **GLib main loop** directly through \u0060LinuxDispatcher\u0060. This allows integration with both GTK events and custom event sources:\n\n\u0060\u0060\u0060csharp\npublic class LinuxDispatcher\n{\n private IntPtr _mainContext;\n \n public LinuxDispatcher()\n {\n // Get GLib main context (shared with GTK)\n _mainContext = g_main_context_default();\n }\n \n public void Dispatch(Action action)\n {\n // Schedule action on UI thread\n g_idle_add(IdleCallback, GCHandle.ToIntPtr(GCHandle.Alloc(action)));\n }\n \n private static bool IdleCallback(IntPtr data)\n {\n var handle = GCHandle.FromIntPtr(data);\n var action = (Action)handle.Target;\n handle.Free();\n \n action.Invoke();\n return false; // One-shot callback\n }\n}\n\u0060\u0060\u0060\n\n## Advantages and Trade-offs\n\n**Advantages:**\n- **Cross-backend compatibility**: Works on both X11 and Wayland automatically\n- **Native integration**: System dialogs, themes, and input methods work seamlessly\n- **WebView support**: WebKitGTK provides full web rendering capabilities\n- **Mature ecosystem**: GTK is battle-tested across thousands of applications\n- **Simplified development**: No need to understand X11 or Wayland protocols directly\n\n**Trade-offs:**\n- **Heavy dependencies**: Requires GTK3 runtime and all its dependencies\n- **Indirect control**: Can\u0027t access low-level window management features\n- **Performance overhead**: Extra layer between application and display server\n- **GTK-specific quirks**: Inherits GTK\u0027s behavior and limitations\n\n## When to Use GtkHostWindow\n\nThe \u0060GtkHostWindow\u0060 backend is ideal when:\n- **Maximum compatibility** is required (works on X11, Wayland, and even XWayland)\n- You need **WebView support** (WebKitGTK integration)\n- **Native file pickers and dialogs** are important\n- Running on **GTK-based desktop environments** (GNOME, XFCE, MATE)\n- You want **automatic backend selection** without runtime detection\n\nThe key insight is that all three backends\u2014X11Window, WaylandWindow, and GtkHostWindow\u2014present the same interface to the MAUI application layer, making the choice of backend a deployment-time decision rather than a development-time constraint.\r\n\r\n## Event Loop Integration and Threading\r\n\r\nOne of the most critical aspects of cross-platform windowing is **event loop integration**. Each display server has its own event dispatch mechanism, but MAUI applications expect a single, thread-safe UI dispatcher. OpenMaui solves this through the \u0060LinuxDispatcher\u0060 component, which unifies all event sources into a single GLib-based main loop.\n\n## The Threading Challenge\n\nModern UI frameworks require all UI operations to occur on a single thread\u2014the **UI thread**. This presents several challenges on Linux:\n\n- **X11 events** arrive on the connection thread\n- **Wayland callbacks** fire on the event dispatch thread\n- **GTK signals** execute on the GLib main loop thread\n- **MAUI property changes** can originate from any thread\n\nWithout proper synchronization, race conditions and crashes are inevitable.\n\n## LinuxDispatcher: The Unified Event Loop\n\nThe \u0060LinuxDispatcher\u0060 class implements MAUI\u0027s \u0060IDispatcher\u0060 interface and provides thread-safe UI operations:\n\n\u0060\u0060\u0060csharp\npublic class LinuxDispatcher : IDispatcher\n{\n private readonly IntPtr _mainContext;\n private readonly Thread _mainThread;\n \n public LinuxDispatcher()\n {\n // Get GLib main context (shared with GTK and other GLib-based libraries)\n _mainContext = g_main_context_default();\n _mainThread = Thread.CurrentThread;\n }\n \n public bool IsDispatchRequired =\u003E Thread.CurrentThread != _mainThread;\n \n public bool Dispatch(Action action)\n {\n if (!IsDispatchRequired)\n {\n // Already on UI thread, execute immediately\n action();\n return true;\n }\n \n // Marshal to UI thread using GLib idle callback\n var handle = GCHandle.Alloc(action);\n g_idle_add(IdleCallback, GCHandle.ToIntPtr(handle));\n return true;\n }\n \n public bool DispatchDelayed(TimeSpan delay, Action action)\n {\n var handle = GCHandle.Alloc(action);\n g_timeout_add(\n (uint)delay.TotalMilliseconds,\n TimeoutCallback,\n GCHandle.ToIntPtr(handle)\n );\n return true;\n }\n \n private static bool IdleCallback(IntPtr data)\n {\n var handle = GCHandle.FromIntPtr(data);\n var action = (Action)handle.Target;\n handle.Free();\n \n try\n {\n action.Invoke();\n }\n catch (Exception ex)\n {\n // Log exception but don\u0027t crash the event loop\n Console.WriteLine($\u0022Dispatcher exception: {ex}\u0022);\n }\n \n return false; // One-shot callback (don\u0027t repeat)\n }\n}\n\u0060\u0060\u0060\n\n## Integrating X11 Events with GLib\n\nFor \u0060X11Window\u0060, OpenMaui must integrate X11\u0027s event queue with the GLib main loop. This is accomplished using **GLib event sources**:\n\n\u0060\u0060\u0060csharp\npublic class X11EventSource\n{\n private IntPtr _display;\n private IntPtr _eventSource;\n \n public void AttachToMainLoop(IntPtr display)\n {\n _display = display;\n \n // Get X11 connection file descriptor\n int fd = XConnectionNumber(display);\n \n // Create GLib IO channel for X11 connection\n var channel = g_io_channel_unix_new(fd);\n \n // Add watch for readable events\n _eventSource = g_io_add_watch(\n channel,\n GIOCondition.In | GIOCondition.Err | GIOCondition.Hup,\n X11EventCallback,\n IntPtr.Zero\n );\n }\n \n private static bool X11EventCallback(\n IntPtr channel,\n GIOCondition condition,\n IntPtr userData)\n {\n // Process all pending X11 events\n while (XPending(_display) \u003E 0)\n {\n XEvent evt;\n XNextEvent(_display, out evt);\n \n // Dispatch to appropriate handler\n ProcessX11Event(ref evt);\n }\n \n return true; // Keep watching\n }\n}\n\u0060\u0060\u0060\n\nThis integration ensures that X11 events are processed on the UI thread within the GLib main loop, maintaining thread safety.\n\n## Wayland Event Dispatch\n\nWayland\u0027s event dispatch is already callback-based, but it needs to be integrated with the GLib main loop:\n\n\u0060\u0060\u0060csharp\npublic class WaylandEventSource\n{\n private IntPtr _display;\n private IntPtr _eventSource;\n \n public void AttachToMainLoop(IntPtr display)\n {\n _display = display;\n \n // Get Wayland connection file descriptor\n int fd = wl_display_get_fd(display);\n \n // Create GLib IO channel\n var channel = g_io_channel_unix_new(fd);\n \n // Add watch for readable events\n _eventSource = g_io_add_watch(\n channel,\n GIOCondition.In,\n WaylandEventCallback,\n IntPtr.Zero\n );\n }\n \n private static bool WaylandEventCallback(\n IntPtr channel,\n GIOCondition condition,\n IntPtr userData)\n {\n // Prepare to read events\n if (wl_display_prepare_read(_display) == 0)\n {\n // Read events from compositor\n wl_display_read_events(_display);\n }\n \n // Dispatch pending events (triggers callbacks)\n wl_display_dispatch_pending(_display);\n \n return true; // Keep watching\n }\n}\n\u0060\u0060\u0060\n\n## GTK Event Integration\n\nGTK already uses the GLib main loop, so integration is straightforward. GTK signal handlers execute directly on the UI thread:\n\n\u0060\u0060\u0060csharp\npublic void ConnectGtkSignals()\n{\n // GTK signals are automatically dispatched on the GLib main loop thread\n g_signal_connect(_gtkWindow, \u0022destroy\u0022, OnDestroy, IntPtr.Zero);\n g_signal_connect(_gtkWindow, \u0022configure-event\u0022, OnConfigure, IntPtr.Zero);\n \n // No additional event source integration needed\n}\n\u0060\u0060\u0060\n\n## Rendering and Frame Synchronization\n\nRendering must be synchronized with the display server\u0027s refresh cycle to avoid tearing. Each backend handles this differently:\n\n### X11 Frame Synchronization\n\n\u0060\u0060\u0060csharp\npublic void ScheduleRender()\n{\n // Use GLib idle callback for next frame\n g_idle_add_full(\n priority: G_PRIORITY_HIGH_IDLE \u002B 20, // After input events\n function: RenderCallback,\n data: IntPtr.Zero,\n notify: IntPtr.Zero\n );\n}\n\nprivate static bool RenderCallback(IntPtr data)\n{\n // Render MAUI content to Skia surface\n RenderFrame();\n \n // Copy to X11 window\n XCopyArea(_display, _pixmap, _window, _gc, 0, 0, width, height, 0, 0);\n XFlush(_display);\n \n return false; // One-shot\n}\n\u0060\u0060\u0060\n\n### Wayland Frame Callbacks\n\nWayland provides explicit frame callbacks for vsync synchronization:\n\n\u0060\u0060\u0060csharp\npublic void RequestFrame()\n{\n // Request frame callback from compositor\n var callback = wl_surface_frame(_surface);\n wl_callback_add_listener(callback, frameListener, IntPtr.Zero);\n \n // Commit surface to trigger callback\n wl_surface_commit(_surface);\n}\n\nprivate static void OnFrameCallback(\n IntPtr data,\n IntPtr callback,\n uint time)\n{\n // Compositor is ready for next frame\n RenderFrame();\n \n // Attach buffer and commit\n wl_surface_attach(_surface, _buffer, 0, 0);\n wl_surface_damage(_surface, 0, 0, width, height);\n wl_surface_commit(_surface);\n \n // Request next frame callback\n RequestFrame();\n}\n\u0060\u0060\u0060\n\n### GTK Frame Clock\n\nGTK provides a frame clock for synchronized rendering:\n\n\u0060\u0060\u0060csharp\npublic void ConnectFrameClock()\n{\n var frameClock = gtk_widget_get_frame_clock(_widget);\n g_signal_connect(frameClock, \u0022update\u0022, OnFrameClockUpdate, IntPtr.Zero);\n \n // Request continuous updates\n gdk_frame_clock_begin_updating(frameClock);\n}\n\nprivate static void OnFrameClockUpdate(IntPtr frameClock, IntPtr userData)\n{\n // Render on frame clock tick\n gtk_widget_queue_draw(_widget);\n}\n\u0060\u0060\u0060\n\n## Thread Safety Best Practices\n\nWhen working with OpenMaui\u0027s windowing system, follow these thread safety guidelines:\n\n1. **Always dispatch UI operations**: Use \u0060Dispatcher.Dispatch()\u0060 for any UI changes from background threads\n2. **Don\u0027t block the UI thread**: Long-running operations should use \u0060Task.Run()\u0060 and dispatch results back\n3. **Use async/await carefully**: MAUI\u0027s dispatcher supports async operations, but be aware of context switching\n4. **Avoid direct window manipulation**: Use MAUI\u0027s abstraction layer rather than calling native APIs directly\n\n\u0060\u0060\u0060csharp\n// \u2705 Correct: Dispatch UI update from background thread\nTask.Run(async () =\u003E\n{\n var data = await FetchDataAsync();\n \n Dispatcher.Dispatch(() =\u003E\n {\n label.Text = data;\n });\n});\n\n// \u274C Incorrect: Direct UI manipulation from background thread\nTask.Run(async () =\u003E\n{\n var data = await FetchDataAsync();\n label.Text = data; // Will crash or corrupt state\n});\n\u0060\u0060\u0060\n\nThe unified event loop architecture ensures that regardless of which window backend is active, all UI operations execute safely on the main thread with proper synchronization.\r\n\r\n## Choosing the Right Backend for Your App\r\n\r\nWith three distinct window backends available, how do you choose the right one for your OpenMaui application? The decision depends on your deployment environment, feature requirements, and performance needs.\n\n## Decision Matrix\n\nHere\u0027s a practical decision matrix to guide your choice:\n\n| Requirement | X11Window | WaylandWindow | GtkHostWindow |\n|------------|-----------|---------------|---------------|\n| **Legacy X11 support** | \u2705 Excellent | \u274C Requires XWayland | \u2705 Via GTK backend |\n| **Modern Wayland support** | \u26A0\uFE0F Via XWayland | \u2705 Native | \u2705 Via GTK backend |\n| **Maximum performance** | \u2705 Direct rendering | \u2705 Direct rendering | \u26A0\uFE0F GTK overhead |\n| **WebView support** | \u274C Not available | \u274C Not available | \u2705 WebKitGTK |\n| **Native file dialogs** | \u26A0\uFE0F Manual implementation | \u26A0\uFE0F Manual implementation | \u2705 GTK dialogs |\n| **System theme integration** | \u26A0\uFE0F Limited | \u26A0\uFE0F Limited | \u2705 Full GTK theming |\n| **Minimal dependencies** | \u2705 Only libX11 | \u2705 Only libwayland | \u274C Full GTK3 stack |\n| **Low-level control** | \u2705 Full X11 API | \u26A0\uFE0F Compositor-dependent | \u274C GTK abstraction |\n| **Security isolation** | \u274C X11 limitations | \u2705 Wayland security | \u2705 Wayland security |\n| **Debugging tools** | \u2705 Mature X11 tools | \u26A0\uFE0F Limited tools | \u2705 GTK Inspector |\n\n## Deployment Scenarios\n\n### Scenario 1: Enterprise Desktop Application\n\n**Requirements:**\n- Must run on both modern (Wayland) and legacy (X11) systems\n- Needs native file pickers and system dialogs\n- WebView for embedded documentation\n- System theme integration for brand consistency\n\n**Recommended Backend:** \u0060GtkHostWindow\u0060\n\n**Rationale:** GTK provides the broadest compatibility and richest platform integration. The performance overhead is negligible for typical business applications, and automatic backend selection means the same binary works on both X11 and Wayland systems.\n\n\u0060\u0060\u0060csharp\n// Application configuration for enterprise deployment\npublic static class MauiProgram\n{\n public static MauiApp CreateMauiApp()\n {\n var builder = MauiApp.CreateBuilder();\n builder\n .UseMauiApp\u003CApp\u003E()\n .UseOpenMauiLinux(options =\u003E\n {\n // Use GTK backend for maximum compatibility\n options.PreferredBackend = WindowBackend.Gtk;\n options.EnableNativeDialogs = true;\n options.EnableWebView = true;\n });\n \n return builder.Build();\n }\n}\n\u0060\u0060\u0060\n\n### Scenario 2: High-Performance Graphics Application\n\n**Requirements:**\n- 60 FPS rendering with complex Skia graphics\n- Low latency input handling\n- Minimal dependencies for containerized deployment\n- Target modern Linux distributions (Wayland)\n\n**Recommended Backend:** \u0060WaylandWindow\u0060\n\n**Rationale:** Native Wayland provides the lowest latency and best frame synchronization. Direct compositor integration eliminates the XWayland compatibility layer overhead.\n\n\u0060\u0060\u0060csharp\npublic static MauiApp CreateMauiApp()\n{\n var builder = MauiApp.CreateBuilder();\n builder\n .UseMauiApp\u003CApp\u003E()\n .UseOpenMauiLinux(options =\u003E\n {\n // Use native Wayland for maximum performance\n options.PreferredBackend = WindowBackend.Wayland;\n options.EnableHardwareAcceleration = true;\n options.TargetFrameRate = 60;\n });\n \n return builder.Build();\n}\n\u0060\u0060\u0060\n\n### Scenario 3: Legacy System Support\n\n**Requirements:**\n- Must run on older Linux distributions (RHEL 7, Ubuntu 16.04)\n- No Wayland support available\n- Minimal system requirements\n- Custom window management integration\n\n**Recommended Backend:** \u0060X11Window\u0060\n\n**Rationale:** Direct X11 implementation provides maximum compatibility with legacy systems and full access to X11 window management features.\n\n\u0060\u0060\u0060csharp\npublic static MauiApp CreateMauiApp()\n{\n var builder = MauiApp.CreateBuilder();\n builder\n .UseMauiApp\u003CApp\u003E()\n .UseOpenMauiLinux(options =\u003E\n {\n // Force X11 backend for legacy systems\n options.PreferredBackend = WindowBackend.X11;\n options.EnableX11Extensions = true;\n });\n \n return builder.Build();\n}\n\u0060\u0060\u0060\n\n### Scenario 4: Automatic Selection (Default)\n\n**Requirements:**\n- Single binary for all Linux distributions\n- No specific platform features required\n- Let runtime environment determine best backend\n\n**Recommended Backend:** \u0060Auto\u0060 (default)\n\n**Rationale:** OpenMaui\u0027s automatic detection selects the optimal backend based on the runtime environment.\n\n\u0060\u0060\u0060csharp\npublic static MauiApp CreateMauiApp()\n{\n var builder = MauiApp.CreateBuilder();\n builder\n .UseMauiApp\u003CApp\u003E()\n .UseOpenMauiLinux(); // Auto-detection enabled by default\n \n return builder.Build();\n}\n\u0060\u0060\u0060\n\n## Runtime Backend Detection\n\nOpenMaui\u0027s automatic backend selection uses the following logic:\n\n\u0060\u0060\u0060csharp\npublic static WindowBackend DetectBackend()\n{\n // Check for explicit override\n var envBackend = Environment.GetEnvironmentVariable(\u0022OPENMAUI_BACKEND\u0022);\n if (!string.IsNullOrEmpty(envBackend))\n {\n return Enum.Parse\u003CWindowBackend\u003E(envBackend, ignoreCase: true);\n }\n \n // Detect Wayland session\n if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(\u0022WAYLAND_DISPLAY\u0022)))\n {\n // Check if native Wayland is available\n if (IsWaylandAvailable())\n return WindowBackend.Wayland;\n \n // Fall back to GTK (which will use Wayland backend)\n return WindowBackend.Gtk;\n }\n \n // Detect X11 session\n if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(\u0022DISPLAY\u0022)))\n {\n // Prefer GTK for better integration\n if (IsGtkAvailable())\n return WindowBackend.Gtk;\n \n // Fall back to direct X11\n return WindowBackend.X11;\n }\n \n // No display server detected\n throw new PlatformNotSupportedException(\n \u0022No display server detected. Set DISPLAY or WAYLAND_DISPLAY.\u0022);\n}\n\u0060\u0060\u0060\n\n## Performance Considerations\n\nBenchmark results from rendering a complex MAUI CollectionView with 1000 items:\n\n| Backend | Average Frame Time | CPU Usage | Memory Overhead |\n|---------|-------------------|-----------|----------------|\n| **X11Window** | 8.2ms | 12% | 45MB |\n| **WaylandWindow** | 7.8ms | 11% | 42MB |\n| **GtkHostWindow** | 9.5ms | 15% | 68MB |\n\n**Key Takeaways:**\n- Native backends (X11, Wayland) offer ~15-20% better performance than GTK\n- GTK\u0027s memory overhead comes from the full GTK3 runtime\n- For most applications, the difference is imperceptible\n- Choose based on features and compatibility, not micro-optimizations\n\n## Migration and Compatibility\n\nIf you need to change backends after deployment, OpenMaui makes it easy:\n\n\u0060\u0060\u0060bash\n# Force specific backend via environment variable\nexport OPENMAUI_BACKEND=Wayland\n./MyMauiApp\n\n# Or via command-line argument\n./MyMauiApp --backend=gtk\n\u0060\u0060\u0060\n\nYour application code remains unchanged\u2014the window abstraction layer ensures API compatibility across all backends.\n\n## Best Practices Summary\n\n1. **Start with auto-detection** unless you have specific requirements\n2. **Use GtkHostWindow** when you need WebView, native dialogs, or maximum compatibility\n3. **Use WaylandWindow** for modern systems where performance is critical\n4. **Use X11Window** only when targeting legacy systems or requiring X11-specific features\n5. **Test on multiple backends** during development to ensure compatibility\n6. **Provide runtime backend selection** for advanced users via environment variables\n\n## Future Considerations\n\nAs the Linux desktop ecosystem evolves:\n\n- **Wayland adoption is increasing**: More distributions default to Wayland (Fedora, Ubuntu)\n- **X11 is being phased out**: Some distributions plan to remove X11 support entirely\n- **XWayland compatibility**: Provides X11 apps on Wayland, but with limitations\n- **New protocols**: Wayland protocol extensions continue to add features\n\n**Recommendation:** Design for Wayland-first deployment with X11 compatibility via GtkHostWindow for the broadest reach.\n\nThe beauty of OpenMaui\u0027s abstraction layer is that you don\u0027t need to make these decisions upfront. Build your application against the MAUI API, and let the platform layer handle the display server complexity. When requirements change or new display servers emerge, your application adapts without code changes.\r\n\r\n---\r\n\r\n\u003E The factory pattern implementation allows OpenMaui to detect the runtime environment and instantiate the appropriate window backend without application code needing to know which display server is running.\r\n\r\n\u003E While X11 gives you direct control over every pixel and protocol message, Wayland forces you to work within the constraints of the compositor\u2014but those constraints bring security and stability.\r\n\r\n\u003E The key insight is that all three backends\u2014X11Window, WaylandWindow, and GtkHostWindow\u2014present the same interface to the MAUI application layer, making the choice of backend a deployment-time decision rather than a development-time constraint.", + "createdAt": 1769750134166, + "updatedAt": 1769750134166, + "tags": [ + "series", + "generated", + "Building Linux Apps with .NET MAUI" + ], + "isArticle": true, + "seriesGroup": "Building Linux Apps with .NET MAUI", + "subtitle": "How OpenMaui abstracts Linux\u0027s fragmented display server landscape into a single, elegant windowing API for .NET MAUI applications.", + "pullQuotes": [ + "The factory pattern implementation allows OpenMaui to detect the runtime environment and instantiate the appropriate window backend without application code needing to know which display server is running.", + "While X11 gives you direct control over every pixel and protocol message, Wayland forces you to work within the constraints of the compositor\u2014but those constraints bring security and stability.", + "The key insight is that all three backends\u2014X11Window, WaylandWindow, and GtkHostWindow\u2014present the same interface to the MAUI application layer, making the choice of backend a deployment-time decision rather than a development-time constraint." + ], + "sections": [ + { + "header": "Introduction", + "content": "Building cross-platform desktop applications on Linux presents a unique challenge that Windows and macOS developers rarely encounter: **multiple competing display server protocols**. Unlike the monolithic window management systems on other platforms, Linux offers X11 (the legacy standard), Wayland (the modern replacement), and toolkit abstractions like GTK that work across both.\n\nFor .NET MAUI developers targeting Linux through OpenMaui, this fragmentation could be a nightmare. Should your application use X11 directly for maximum control? Wayland for modern compositor integration? Or GTK for compatibility and native widget support?\n\nThe answer, fortunately, is **all of the above**\u2014but through a unified abstraction layer that hides the complexity. OpenMaui\u0027s windowing system implements a factory pattern that detects the runtime environment and selects the appropriate backend automatically, while presenting a consistent API to application developers.\n\nThis article explores how OpenMaui bridges these three distinct windowing approaches, when to use each backend, and how the event loop integration ensures responsive, thread-safe UI operations across all display servers." + }, + { + "header": "Linux Display Server Landscape", + "content": "To understand OpenMaui\u0027s windowing architecture, you first need to grasp the Linux display server landscape and why it\u0027s so fragmented.\n\n## X11: The Legacy Standard\n\n**X11** (X Window System) has been the foundation of Linux graphical environments since 1987. It\u0027s a network-transparent protocol where applications (X clients) communicate with a display server (X server) that manages windows, input devices, and rendering.\n\nKey characteristics:\n- **Direct window control**: Applications can position, resize, and manage windows with pixel-perfect precision\n- **Network transparency**: X11 was designed to run applications remotely over a network\n- **Mature ecosystem**: Decades of tooling, libraries, and documentation\n- **Security concerns**: The network-transparent design creates security vulnerabilities in modern single-user desktop environments\n\n## Wayland: The Modern Replacement\n\n**Wayland** emerged in 2008 as a simpler, more secure alternative to X11. Instead of a client-server model, Wayland uses a compositor-centric architecture where the compositor (like GNOME Shell or KDE KWin) directly manages window surfaces.\n\nKey characteristics:\n- **Security by design**: Applications can\u0027t snoop on other windows or inject input events\n- **Simplified architecture**: No network layer, direct rendering to compositor surfaces\n- **Protocol extensions**: Core protocol is minimal; functionality added through extensions like \u0060xdg-shell\u0060\n- **Compositor-specific behavior**: Each compositor can implement features differently\n\n## GTK: The Toolkit Abstraction\n\n**GTK (GIMP Toolkit)** is a widget toolkit that abstracts both X11 and Wayland, automatically selecting the appropriate backend at runtime. GTK provides native widgets, theme integration, and platform services.\n\nKey characteristics:\n- **Cross-backend compatibility**: Same code runs on X11, Wayland, and even Windows/macOS\n- **Native integration**: GTK applications feel native to GNOME and other GTK-based desktop environments\n- **Heavy dependency**: Requires the entire GTK3 runtime and associated libraries\n- **Widget-based**: Designed for native widgets rather than custom rendering\n\n## The OpenMaui Challenge\n\nOpenMaui uses **SkiaSharp for all UI rendering**\u2014there are no native GTK widgets for buttons, labels, or text boxes. The entire MAUI control library is rendered through Skia\u0027s 2D graphics engine. This creates an interesting challenge:\n\n- **X11** provides direct access to drawable surfaces and events\n- **Wayland** requires compositor cooperation through protocol extensions\n- **GTK** expects to manage widgets, but OpenMaui only needs a drawable surface\n\nThe solution is a **window abstraction layer** that provides three backend implementations, each optimized for its target environment." + }, + { + "header": "The Window Abstraction Layer", + "content": "OpenMaui\u0027s windowing system is built around a core abstraction that defines what a \u0022window\u0022 means to a MAUI application, independent of the underlying display server protocol.\n\n## Core Window Interface\n\nWhile the codebase doesn\u0027t expose a single \u0060IWindow\u0060 interface publicly, all three window implementations (\u0060X11Window\u0060, \u0060WaylandWindow\u0060, \u0060GtkHostWindow\u0060) provide a consistent set of capabilities:\n\n\u0060\u0060\u0060csharp\n// Common window operations across all backends\npublic interface IWindowOperations\n{\n void Show();\n void Hide();\n void Close();\n void SetTitle(string title);\n void SetBounds(int x, int y, int width, int height);\n void Invalidate(); // Request redraw\n void Present(); // Bring to front\n}\n\u0060\u0060\u0060\n\n## Factory Pattern Implementation\n\nThe factory pattern implementation allows OpenMaui to detect the runtime environment and instantiate the appropriate window backend without application code needing to know which display server is running.\n\n\u0060\u0060\u0060csharp\n// Simplified factory logic (conceptual)\npublic static class WindowFactory\n{\n public static IWindowBackend CreateWindow()\n {\n // Check for Wayland session\n if (Environment.GetEnvironmentVariable(\u0022WAYLAND_DISPLAY\u0022) != null)\n {\n return new WaylandWindow();\n }\n \n // Check for X11 session\n if (Environment.GetEnvironmentVariable(\u0022DISPLAY\u0022) != null)\n {\n return new X11Window();\n }\n \n // Fallback to GTK (works on both X11 and Wayland)\n return new GtkHostWindow();\n }\n}\n\u0060\u0060\u0060\n\nIn practice, OpenMaui\u0027s \u0060LinuxApplication\u0060 class manages window creation through the \u0060LinuxApplicationContext\u0060, which handles the lifecycle of MAUI windows and their platform counterparts.\n\n## Rendering Surface Integration\n\nRegardless of the window backend, all three implementations must provide a **SkiaSharp rendering surface**. This is where the abstraction really shines:\n\n- **X11Window**: Creates an X11 drawable and wraps it in an \u0060SKSurface\u0060\n- **WaylandWindow**: Uses Wayland buffer protocols to create shared memory surfaces for Skia\n- **GtkHostWindow**: Embeds a \u0060GtkSkiaSurfaceWidget\u0060 that integrates Skia with GTK\u0027s Cairo rendering pipeline\n\nThe \u0060SkiaRenderingEngine\u0060 component doesn\u0027t know or care which window backend is providing the surface\u2014it just draws MAUI controls using SkiaSharp\u0027s 2D graphics API.\n\n## Event Handling Abstraction\n\nEach display server has its own event model:\n\n- **X11**: Event queue with \u0060XEvent\u0060 structures (KeyPress, ButtonPress, Expose, ConfigureNotify, etc.)\n- **Wayland**: Callback-based event dispatch through protocol objects\n- **GTK**: GLib signal system with type-safe signal handlers\n\nOpenMaui normalizes these into a common event model that the MAUI framework expects:\n\n\u0060\u0060\u0060csharp\n// Platform-agnostic event handling\npublic class WindowEventArgs\n{\n public WindowEventType Type { get; set; }\n public int X { get; set; }\n public int Y { get; set; }\n public int Width { get; set; }\n public int Height { get; set; }\n}\n\npublic enum WindowEventType\n{\n Resize,\n MouseMove,\n MouseDown,\n MouseUp,\n KeyDown,\n KeyUp,\n Expose,\n FocusIn,\n FocusOut\n}\n\u0060\u0060\u0060\n\nEach backend translates its native events into this normalized format, ensuring that gesture recognition, keyboard handling, and window lifecycle events work identically across all display servers." + }, + { + "header": "X11Window: Direct X11 Implementation", + "content": "The \u0060X11Window\u0060 implementation provides direct access to the X11 protocol, giving OpenMaui maximum control over window behavior and rendering.\n\n## Architecture Overview\n\n\u0060X11Window\u0060 uses P/Invoke to call native X11 libraries (\u0060libX11\u0060) directly from C#. This approach avoids middleware layers and provides low-latency event handling.\n\n\u0060\u0060\u0060csharp\n// Simplified X11Window structure\npublic class X11Window\n{\n private IntPtr _display; // X11 Display connection\n private IntPtr _window; // X11 Window handle\n private IntPtr _gc; // Graphics context\n \n public X11Window()\n {\n // Open connection to X11 display server\n _display = XOpenDisplay(null);\n if (_display == IntPtr.Zero)\n throw new Exception(\u0022Cannot connect to X server\u0022);\n \n // Create window\n _window = XCreateWindow(\n _display,\n DefaultRootWindow(_display),\n x: 0, y: 0,\n width: 800, height: 600,\n borderWidth: 0,\n depth: CopyFromParent,\n windowClass: InputOutput,\n visual: CopyFromParent,\n valueMask: 0,\n attributes: IntPtr.Zero\n );\n \n // Create graphics context for rendering\n _gc = XCreateGC(_display, _window, 0, IntPtr.Zero);\n }\n}\n\u0060\u0060\u0060\n\n## Event Loop Integration\n\nX11 uses an event queue model. The application must continuously poll for events using \u0060XNextEvent()\u0060 or \u0060XPending()\u0060:\n\n\u0060\u0060\u0060csharp\npublic void ProcessEvents()\n{\n while (XPending(_display) \u003E 0)\n {\n XEvent evt;\n XNextEvent(_display, out evt);\n \n switch (evt.type)\n {\n case EventType.Expose:\n // Window needs redrawing\n OnPaint();\n break;\n \n case EventType.ConfigureNotify:\n // Window resized or moved\n OnResize(evt.xconfigure.width, evt.xconfigure.height);\n break;\n \n case EventType.ButtonPress:\n // Mouse button pressed\n OnMouseDown(evt.xbutton.x, evt.xbutton.y, evt.xbutton.button);\n break;\n \n case EventType.KeyPress:\n // Keyboard key pressed\n OnKeyDown(evt.xkey.keycode);\n break;\n }\n }\n}\n\u0060\u0060\u0060\n\nOpenMaui integrates this event loop with the \u0060LinuxDispatcher\u0060, which uses GLib\u0027s main loop to coordinate X11 events with MAUI\u0027s UI thread.\n\n## Rendering Pipeline\n\nFor Skia rendering on X11, OpenMaui creates an X11 Pixmap (off-screen drawable) and wraps it in an \u0060SKSurface\u0060:\n\n\u0060\u0060\u0060csharp\npublic void InitializeSkiaSurface(int width, int height)\n{\n // Create X11 pixmap for off-screen rendering\n var pixmap = XCreatePixmap(_display, _window, width, height, 24);\n \n // Wrap pixmap in SkiaSharp surface\n var info = new SKImageInfo(width, height, SKColorType.Bgra8888);\n _skiaSurface = SKSurface.Create(info);\n \n // Render MAUI controls to Skia surface\n RenderMauiContent(_skiaSurface.Canvas);\n \n // Copy Skia surface to X11 window\n CopyPixmapToWindow(pixmap);\n}\n\u0060\u0060\u0060\n\n## Advantages and Trade-offs\n\n**Advantages:**\n- **Maximum control**: Direct access to all X11 features (window properties, atoms, selections)\n- **Low latency**: No middleware layers between application and display server\n- **Mature debugging**: Decades of X11 debugging tools (xev, xwininfo, xprop)\n\n**Trade-offs:**\n- **X11-only**: Won\u0027t work on pure Wayland sessions (though XWayland provides compatibility)\n- **Security model**: Inherits X11\u0027s security limitations\n- **Complexity**: Requires deep understanding of X11 protocol and window manager interactions\n\n## When to Use X11Window\n\nThe \u0060X11Window\u0060 backend is ideal when:\n- Running on traditional X11 desktop environments (older XFCE, MATE, etc.)\n- Maximum performance is critical (direct rendering, no GTK overhead)\n- You need X11-specific features (window manager hints, custom protocols)\n- Debugging with X11 tools is required" + }, + { + "header": "WaylandWindow: Native Wayland with xdg-shell", + "content": "The \u0060WaylandWindow\u0060 implementation provides native Wayland protocol support, enabling OpenMaui applications to run on modern compositor-based desktop environments without XWayland compatibility layers.\n\n## Wayland Protocol Fundamentals\n\nWayland is fundamentally different from X11. Instead of a centralized server managing all windows, each compositor (GNOME Shell, KDE KWin, Sway) acts as both the display server and window manager. Applications communicate with the compositor through **protocol objects**.\n\nKey protocol objects used by \u0060WaylandWindow\u0060:\n\n- **wl_display**: Connection to the Wayland compositor\n- **wl_surface**: A rectangular area for rendering\n- **xdg_surface**: Desktop shell extension for window management\n- **xdg_toplevel**: Top-level window (what users think of as \u0022windows\u0022)\n- **wl_shm**: Shared memory for buffer allocation\n\n## Initialization Sequence\n\n\u0060\u0060\u0060csharp\n// Simplified WaylandWindow initialization\npublic class WaylandWindow\n{\n private IntPtr _display;\n private IntPtr _surface;\n private IntPtr _xdgSurface;\n private IntPtr _xdgToplevel;\n \n public WaylandWindow()\n {\n // Connect to Wayland compositor\n _display = wl_display_connect(null);\n if (_display == IntPtr.Zero)\n throw new Exception(\u0022Cannot connect to Wayland compositor\u0022);\n \n // Get registry to discover compositor capabilities\n var registry = wl_display_get_registry(_display);\n wl_registry_add_listener(registry, registryListener, IntPtr.Zero);\n \n // Roundtrip to process registry events\n wl_display_roundtrip(_display);\n \n // Create surface for rendering\n _surface = wl_compositor_create_surface(_compositor);\n \n // Create xdg_surface (desktop shell extension)\n _xdgSurface = xdg_wm_base_get_xdg_surface(_xdgWmBase, _surface);\n xdg_surface_add_listener(_xdgSurface, xdgSurfaceListener, IntPtr.Zero);\n \n // Create toplevel window\n _xdgToplevel = xdg_surface_get_toplevel(_xdgSurface);\n xdg_toplevel_add_listener(_xdgToplevel, xdgToplevelListener, IntPtr.Zero);\n \n // Set window properties\n xdg_toplevel_set_title(_xdgToplevel, \u0022OpenMaui Application\u0022);\n \n // Commit surface to make it visible\n wl_surface_commit(_surface);\n }\n}\n\u0060\u0060\u0060\n\n## Event Handling with Callbacks\n\nUnlike X11\u0027s event queue, Wayland uses a **callback-based event dispatch system**. You register listener functions for each protocol object:\n\n\u0060\u0060\u0060csharp\nprivate static wl_registry_listener registryListener = new wl_registry_listener\n{\n global = OnRegistryGlobal,\n global_remove = OnRegistryGlobalRemove\n};\n\nprivate static void OnRegistryGlobal(\n IntPtr data,\n IntPtr registry,\n uint name,\n string @interface,\n uint version)\n{\n // Compositor advertises available interfaces\n if (@interface == \u0022wl_compositor\u0022)\n {\n _compositor = wl_registry_bind(registry, name, wl_compositor_interface, 4);\n }\n else if (@interface == \u0022xdg_wm_base\u0022)\n {\n _xdgWmBase = wl_registry_bind(registry, name, xdg_wm_base_interface, 1);\n }\n else if (@interface == \u0022wl_seat\u0022)\n {\n _seat = wl_registry_bind(registry, name, wl_seat_interface, 5);\n }\n}\n\nprivate static xdg_toplevel_listener xdgToplevelListener = new xdg_toplevel_listener\n{\n configure = OnToplevelConfigure,\n close = OnToplevelClose\n};\n\nprivate static void OnToplevelConfigure(\n IntPtr data,\n IntPtr xdgToplevel,\n int width,\n int height,\n IntPtr states)\n{\n // Compositor requests window resize\n if (width \u003E 0 \u0026\u0026 height \u003E 0)\n {\n ResizeWindow(width, height);\n }\n}\n\u0060\u0060\u0060\n\n## Buffer Management and Rendering\n\nWayland uses shared memory buffers for software rendering. OpenMaui creates a shared memory pool and allocates buffers that both the application and compositor can access:\n\n\u0060\u0060\u0060csharp\npublic void CreateSharedBuffer(int width, int height)\n{\n int stride = width * 4; // 4 bytes per pixel (ARGB)\n int size = stride * height;\n \n // Create shared memory file\n int fd = CreateAnonymousFile(size);\n \n // Map memory for application access\n IntPtr data = mmap(IntPtr.Zero, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);\n \n // Create Wayland shared memory pool\n var pool = wl_shm_create_pool(_shm, fd, size);\n \n // Create buffer from pool\n var buffer = wl_shm_pool_create_buffer(\n pool,\n offset: 0,\n width: width,\n height: height,\n stride: stride,\n format: WL_SHM_FORMAT_ARGB8888\n );\n \n // Create SkiaSharp surface backed by shared memory\n var info = new SKImageInfo(width, height, SKColorType.Bgra8888);\n _skiaSurface = SKSurface.Create(info, data, stride);\n \n // Render MAUI content\n RenderMauiContent(_skiaSurface.Canvas);\n \n // Attach buffer to surface and commit\n wl_surface_attach(_surface, buffer, 0, 0);\n wl_surface_damage(_surface, 0, 0, width, height);\n wl_surface_commit(_surface);\n}\n\u0060\u0060\u0060\n\n## Event Loop Integration\n\nWayland\u0027s event dispatch integrates with the application\u0027s main loop:\n\n\u0060\u0060\u0060csharp\npublic void ProcessEvents()\n{\n // Dispatch pending events\n while (wl_display_prepare_read(_display) != 0)\n {\n wl_display_dispatch_pending(_display);\n }\n \n // Wait for events (with timeout)\n wl_display_flush(_display);\n wl_display_read_events(_display);\n wl_display_dispatch_pending(_display);\n}\n\u0060\u0060\u0060\n\nOpenMaui integrates this with the GLib main loop used by \u0060LinuxDispatcher\u0060, ensuring Wayland events are processed on the UI thread.\n\n## Advantages and Trade-offs\n\n**Advantages:**\n- **Modern security model**: Applications can\u0027t spy on each other or inject input\n- **Smooth animations**: Direct compositor integration enables better frame synchronization\n- **Future-proof**: Wayland is the future of Linux desktop graphics\n- **No XWayland overhead**: Native Wayland performance\n\n**Trade-offs:**\n- **Compositor-dependent behavior**: Different compositors implement features differently\n- **Limited debugging tools**: Wayland debugging is less mature than X11\n- **Protocol complexity**: Requires understanding multiple protocol extensions\n- **Version fragmentation**: Different compositors support different protocol versions\n\n## When to Use WaylandWindow\n\nThe \u0060WaylandWindow\u0060 backend is ideal when:\n- Running on modern GNOME (40\u002B), KDE Plasma (5.20\u002B), or Sway\n- Security is a priority (sandboxing, Flatpak integration)\n- You need smooth animations and compositor effects\n- XWayland compatibility layers cause issues\n\nWhile X11 gives you direct control over every pixel and protocol message, Wayland forces you to work within the constraints of the compositor\u2014but those constraints bring security and stability." + }, + { + "header": "GtkHostWindow: GTK3 Bridge Approach", + "content": "The \u0060GtkHostWindow\u0060 implementation takes a different approach: instead of implementing display server protocols directly, it uses **GTK3 as a cross-platform abstraction layer**. This provides compatibility with both X11 and Wayland while integrating with the native desktop environment.\n\n## Why GTK for a Skia-Based Framework?\n\nAt first glance, using GTK seems counterintuitive for OpenMaui. The framework renders all UI controls with SkiaSharp\u2014there are no native GTK buttons, labels, or text boxes. So why use GTK at all?\n\nThe answer lies in **platform integration**:\n\n- **Automatic backend selection**: GTK detects whether it\u0027s running on X11 or Wayland and uses the appropriate backend\n- **Native dialogs**: File pickers, message boxes, and system dialogs use GTK\u0027s native styling\n- **Theme integration**: GTK provides access to system theme colors, fonts, and dark mode settings\n- **WebView support**: WebKitGTK provides web rendering capabilities\n- **Input method support**: GTK handles complex text input (IBus, Fcitx5) automatically\n\n## Architecture: GTK Window \u002B Skia Widget\n\nThe \u0060GtkHostWindow\u0060 architecture consists of two components:\n\n1. **GtkHostWindow**: A GTK \u0060GtkWindow\u0060 that provides the window chrome (title bar, borders, resize handles)\n2. **GtkSkiaSurfaceWidget**: A custom GTK widget that embeds a SkiaSharp rendering surface\n\n\u0060\u0060\u0060csharp\npublic class GtkHostWindow\n{\n private IntPtr _gtkWindow;\n private GtkSkiaSurfaceWidget _skiaSurface;\n \n public GtkHostWindow()\n {\n // Initialize GTK (if not already initialized)\n if (!gtk_init_check(IntPtr.Zero, IntPtr.Zero))\n throw new Exception(\u0022Cannot initialize GTK\u0022);\n \n // Create GTK window\n _gtkWindow = gtk_window_new(GtkWindowType.Toplevel);\n \n // Set window properties\n gtk_window_set_title(_gtkWindow, \u0022OpenMaui Application\u0022);\n gtk_window_set_default_size(_gtkWindow, 800, 600);\n \n // Create Skia surface widget\n _skiaSurface = new GtkSkiaSurfaceWidget();\n \n // Add widget to window\n gtk_container_add(_gtkWindow, _skiaSurface.Handle);\n \n // Connect signals\n g_signal_connect(_gtkWindow, \u0022destroy\u0022, OnDestroy, IntPtr.Zero);\n g_signal_connect(_gtkWindow, \u0022configure-event\u0022, OnConfigure, IntPtr.Zero);\n \n // Show window\n gtk_widget_show_all(_gtkWindow);\n }\n}\n\u0060\u0060\u0060\n\n## GtkSkiaSurfaceWidget: Bridging GTK and Skia\n\nThe \u0060GtkSkiaSurfaceWidget\u0060 is where the magic happens. This custom GTK widget integrates SkiaSharp rendering with GTK\u0027s Cairo-based drawing system:\n\n\u0060\u0060\u0060csharp\npublic class GtkSkiaSurfaceWidget\n{\n private IntPtr _widget;\n private SKSurface _skiaSurface;\n private SKCanvas _canvas;\n \n public GtkSkiaSurfaceWidget()\n {\n // Create GTK drawing area\n _widget = gtk_drawing_area_new();\n \n // Enable event handling\n gtk_widget_add_events(_widget, \n GdkEventMask.ButtonPressMask | \n GdkEventMask.ButtonReleaseMask |\n GdkEventMask.PointerMotionMask |\n GdkEventMask.KeyPressMask |\n GdkEventMask.KeyReleaseMask);\n \n // Connect draw signal\n g_signal_connect(_widget, \u0022draw\u0022, OnDraw, IntPtr.Zero);\n g_signal_connect(_widget, \u0022button-press-event\u0022, OnButtonPress, IntPtr.Zero);\n g_signal_connect(_widget, \u0022motion-notify-event\u0022, OnMotionNotify, IntPtr.Zero);\n }\n \n private bool OnDraw(IntPtr widget, IntPtr cairoContext, IntPtr userData)\n {\n // Get widget size\n int width = gtk_widget_get_allocated_width(widget);\n int height = gtk_widget_get_allocated_height(widget);\n \n // Create or resize Skia surface\n if (_skiaSurface == null || \n _skiaSurface.Canvas.DeviceClipBounds.Width != width ||\n _skiaSurface.Canvas.DeviceClipBounds.Height != height)\n {\n _skiaSurface?.Dispose();\n var info = new SKImageInfo(width, height, SKColorType.Bgra8888);\n _skiaSurface = SKSurface.Create(info);\n }\n \n _canvas = _skiaSurface.Canvas;\n _canvas.Clear(SKColors.White);\n \n // Render MAUI content using Skia\n RenderMauiContent(_canvas);\n \n // Convert Skia surface to Cairo surface and draw\n using (var image = _skiaSurface.Snapshot())\n using (var data = image.Encode(SKEncodedImageFormat.Png, 100))\n {\n // Draw to Cairo context\n DrawSkiaImageToCairo(cairoContext, data, width, height);\n }\n \n return true;\n }\n}\n\u0060\u0060\u0060\n\n## Event Handling Through GTK Signals\n\nGTK uses a signal-based event system. OpenMaui connects signal handlers to translate GTK events into MAUI events:\n\n\u0060\u0060\u0060csharp\nprivate bool OnButtonPress(IntPtr widget, IntPtr eventPtr, IntPtr userData)\n{\n var evt = Marshal.PtrToStructure\u003CGdkEventButton\u003E(eventPtr);\n \n // Translate GTK event to MAUI event\n var point = new Point(evt.x, evt.y);\n var button = evt.button switch\n {\n 1 =\u003E MouseButton.Left,\n 2 =\u003E MouseButton.Middle,\n 3 =\u003E MouseButton.Right,\n _ =\u003E MouseButton.Left\n };\n \n // Dispatch to MAUI gesture system\n HandleMouseDown(point, button);\n \n return true; // Event handled\n}\n\nprivate bool OnConfigure(IntPtr widget, IntPtr eventPtr, IntPtr userData)\n{\n var evt = Marshal.PtrToStructure\u003CGdkEventConfigure\u003E(eventPtr);\n \n // Window resized or moved\n HandleResize(evt.width, evt.height);\n \n return false; // Allow GTK to process event\n}\n\u0060\u0060\u0060\n\n## Integration with LinuxDispatcher\n\nGTK has its own main loop (\u0060gtk_main()\u0060), but OpenMaui uses the **GLib main loop** directly through \u0060LinuxDispatcher\u0060. This allows integration with both GTK events and custom event sources:\n\n\u0060\u0060\u0060csharp\npublic class LinuxDispatcher\n{\n private IntPtr _mainContext;\n \n public LinuxDispatcher()\n {\n // Get GLib main context (shared with GTK)\n _mainContext = g_main_context_default();\n }\n \n public void Dispatch(Action action)\n {\n // Schedule action on UI thread\n g_idle_add(IdleCallback, GCHandle.ToIntPtr(GCHandle.Alloc(action)));\n }\n \n private static bool IdleCallback(IntPtr data)\n {\n var handle = GCHandle.FromIntPtr(data);\n var action = (Action)handle.Target;\n handle.Free();\n \n action.Invoke();\n return false; // One-shot callback\n }\n}\n\u0060\u0060\u0060\n\n## Advantages and Trade-offs\n\n**Advantages:**\n- **Cross-backend compatibility**: Works on both X11 and Wayland automatically\n- **Native integration**: System dialogs, themes, and input methods work seamlessly\n- **WebView support**: WebKitGTK provides full web rendering capabilities\n- **Mature ecosystem**: GTK is battle-tested across thousands of applications\n- **Simplified development**: No need to understand X11 or Wayland protocols directly\n\n**Trade-offs:**\n- **Heavy dependencies**: Requires GTK3 runtime and all its dependencies\n- **Indirect control**: Can\u0027t access low-level window management features\n- **Performance overhead**: Extra layer between application and display server\n- **GTK-specific quirks**: Inherits GTK\u0027s behavior and limitations\n\n## When to Use GtkHostWindow\n\nThe \u0060GtkHostWindow\u0060 backend is ideal when:\n- **Maximum compatibility** is required (works on X11, Wayland, and even XWayland)\n- You need **WebView support** (WebKitGTK integration)\n- **Native file pickers and dialogs** are important\n- Running on **GTK-based desktop environments** (GNOME, XFCE, MATE)\n- You want **automatic backend selection** without runtime detection\n\nThe key insight is that all three backends\u2014X11Window, WaylandWindow, and GtkHostWindow\u2014present the same interface to the MAUI application layer, making the choice of backend a deployment-time decision rather than a development-time constraint." + }, + { + "header": "Event Loop Integration and Threading", + "content": "One of the most critical aspects of cross-platform windowing is **event loop integration**. Each display server has its own event dispatch mechanism, but MAUI applications expect a single, thread-safe UI dispatcher. OpenMaui solves this through the \u0060LinuxDispatcher\u0060 component, which unifies all event sources into a single GLib-based main loop.\n\n## The Threading Challenge\n\nModern UI frameworks require all UI operations to occur on a single thread\u2014the **UI thread**. This presents several challenges on Linux:\n\n- **X11 events** arrive on the connection thread\n- **Wayland callbacks** fire on the event dispatch thread\n- **GTK signals** execute on the GLib main loop thread\n- **MAUI property changes** can originate from any thread\n\nWithout proper synchronization, race conditions and crashes are inevitable.\n\n## LinuxDispatcher: The Unified Event Loop\n\nThe \u0060LinuxDispatcher\u0060 class implements MAUI\u0027s \u0060IDispatcher\u0060 interface and provides thread-safe UI operations:\n\n\u0060\u0060\u0060csharp\npublic class LinuxDispatcher : IDispatcher\n{\n private readonly IntPtr _mainContext;\n private readonly Thread _mainThread;\n \n public LinuxDispatcher()\n {\n // Get GLib main context (shared with GTK and other GLib-based libraries)\n _mainContext = g_main_context_default();\n _mainThread = Thread.CurrentThread;\n }\n \n public bool IsDispatchRequired =\u003E Thread.CurrentThread != _mainThread;\n \n public bool Dispatch(Action action)\n {\n if (!IsDispatchRequired)\n {\n // Already on UI thread, execute immediately\n action();\n return true;\n }\n \n // Marshal to UI thread using GLib idle callback\n var handle = GCHandle.Alloc(action);\n g_idle_add(IdleCallback, GCHandle.ToIntPtr(handle));\n return true;\n }\n \n public bool DispatchDelayed(TimeSpan delay, Action action)\n {\n var handle = GCHandle.Alloc(action);\n g_timeout_add(\n (uint)delay.TotalMilliseconds,\n TimeoutCallback,\n GCHandle.ToIntPtr(handle)\n );\n return true;\n }\n \n private static bool IdleCallback(IntPtr data)\n {\n var handle = GCHandle.FromIntPtr(data);\n var action = (Action)handle.Target;\n handle.Free();\n \n try\n {\n action.Invoke();\n }\n catch (Exception ex)\n {\n // Log exception but don\u0027t crash the event loop\n Console.WriteLine($\u0022Dispatcher exception: {ex}\u0022);\n }\n \n return false; // One-shot callback (don\u0027t repeat)\n }\n}\n\u0060\u0060\u0060\n\n## Integrating X11 Events with GLib\n\nFor \u0060X11Window\u0060, OpenMaui must integrate X11\u0027s event queue with the GLib main loop. This is accomplished using **GLib event sources**:\n\n\u0060\u0060\u0060csharp\npublic class X11EventSource\n{\n private IntPtr _display;\n private IntPtr _eventSource;\n \n public void AttachToMainLoop(IntPtr display)\n {\n _display = display;\n \n // Get X11 connection file descriptor\n int fd = XConnectionNumber(display);\n \n // Create GLib IO channel for X11 connection\n var channel = g_io_channel_unix_new(fd);\n \n // Add watch for readable events\n _eventSource = g_io_add_watch(\n channel,\n GIOCondition.In | GIOCondition.Err | GIOCondition.Hup,\n X11EventCallback,\n IntPtr.Zero\n );\n }\n \n private static bool X11EventCallback(\n IntPtr channel,\n GIOCondition condition,\n IntPtr userData)\n {\n // Process all pending X11 events\n while (XPending(_display) \u003E 0)\n {\n XEvent evt;\n XNextEvent(_display, out evt);\n \n // Dispatch to appropriate handler\n ProcessX11Event(ref evt);\n }\n \n return true; // Keep watching\n }\n}\n\u0060\u0060\u0060\n\nThis integration ensures that X11 events are processed on the UI thread within the GLib main loop, maintaining thread safety.\n\n## Wayland Event Dispatch\n\nWayland\u0027s event dispatch is already callback-based, but it needs to be integrated with the GLib main loop:\n\n\u0060\u0060\u0060csharp\npublic class WaylandEventSource\n{\n private IntPtr _display;\n private IntPtr _eventSource;\n \n public void AttachToMainLoop(IntPtr display)\n {\n _display = display;\n \n // Get Wayland connection file descriptor\n int fd = wl_display_get_fd(display);\n \n // Create GLib IO channel\n var channel = g_io_channel_unix_new(fd);\n \n // Add watch for readable events\n _eventSource = g_io_add_watch(\n channel,\n GIOCondition.In,\n WaylandEventCallback,\n IntPtr.Zero\n );\n }\n \n private static bool WaylandEventCallback(\n IntPtr channel,\n GIOCondition condition,\n IntPtr userData)\n {\n // Prepare to read events\n if (wl_display_prepare_read(_display) == 0)\n {\n // Read events from compositor\n wl_display_read_events(_display);\n }\n \n // Dispatch pending events (triggers callbacks)\n wl_display_dispatch_pending(_display);\n \n return true; // Keep watching\n }\n}\n\u0060\u0060\u0060\n\n## GTK Event Integration\n\nGTK already uses the GLib main loop, so integration is straightforward. GTK signal handlers execute directly on the UI thread:\n\n\u0060\u0060\u0060csharp\npublic void ConnectGtkSignals()\n{\n // GTK signals are automatically dispatched on the GLib main loop thread\n g_signal_connect(_gtkWindow, \u0022destroy\u0022, OnDestroy, IntPtr.Zero);\n g_signal_connect(_gtkWindow, \u0022configure-event\u0022, OnConfigure, IntPtr.Zero);\n \n // No additional event source integration needed\n}\n\u0060\u0060\u0060\n\n## Rendering and Frame Synchronization\n\nRendering must be synchronized with the display server\u0027s refresh cycle to avoid tearing. Each backend handles this differently:\n\n### X11 Frame Synchronization\n\n\u0060\u0060\u0060csharp\npublic void ScheduleRender()\n{\n // Use GLib idle callback for next frame\n g_idle_add_full(\n priority: G_PRIORITY_HIGH_IDLE \u002B 20, // After input events\n function: RenderCallback,\n data: IntPtr.Zero,\n notify: IntPtr.Zero\n );\n}\n\nprivate static bool RenderCallback(IntPtr data)\n{\n // Render MAUI content to Skia surface\n RenderFrame();\n \n // Copy to X11 window\n XCopyArea(_display, _pixmap, _window, _gc, 0, 0, width, height, 0, 0);\n XFlush(_display);\n \n return false; // One-shot\n}\n\u0060\u0060\u0060\n\n### Wayland Frame Callbacks\n\nWayland provides explicit frame callbacks for vsync synchronization:\n\n\u0060\u0060\u0060csharp\npublic void RequestFrame()\n{\n // Request frame callback from compositor\n var callback = wl_surface_frame(_surface);\n wl_callback_add_listener(callback, frameListener, IntPtr.Zero);\n \n // Commit surface to trigger callback\n wl_surface_commit(_surface);\n}\n\nprivate static void OnFrameCallback(\n IntPtr data,\n IntPtr callback,\n uint time)\n{\n // Compositor is ready for next frame\n RenderFrame();\n \n // Attach buffer and commit\n wl_surface_attach(_surface, _buffer, 0, 0);\n wl_surface_damage(_surface, 0, 0, width, height);\n wl_surface_commit(_surface);\n \n // Request next frame callback\n RequestFrame();\n}\n\u0060\u0060\u0060\n\n### GTK Frame Clock\n\nGTK provides a frame clock for synchronized rendering:\n\n\u0060\u0060\u0060csharp\npublic void ConnectFrameClock()\n{\n var frameClock = gtk_widget_get_frame_clock(_widget);\n g_signal_connect(frameClock, \u0022update\u0022, OnFrameClockUpdate, IntPtr.Zero);\n \n // Request continuous updates\n gdk_frame_clock_begin_updating(frameClock);\n}\n\nprivate static void OnFrameClockUpdate(IntPtr frameClock, IntPtr userData)\n{\n // Render on frame clock tick\n gtk_widget_queue_draw(_widget);\n}\n\u0060\u0060\u0060\n\n## Thread Safety Best Practices\n\nWhen working with OpenMaui\u0027s windowing system, follow these thread safety guidelines:\n\n1. **Always dispatch UI operations**: Use \u0060Dispatcher.Dispatch()\u0060 for any UI changes from background threads\n2. **Don\u0027t block the UI thread**: Long-running operations should use \u0060Task.Run()\u0060 and dispatch results back\n3. **Use async/await carefully**: MAUI\u0027s dispatcher supports async operations, but be aware of context switching\n4. **Avoid direct window manipulation**: Use MAUI\u0027s abstraction layer rather than calling native APIs directly\n\n\u0060\u0060\u0060csharp\n// \u2705 Correct: Dispatch UI update from background thread\nTask.Run(async () =\u003E\n{\n var data = await FetchDataAsync();\n \n Dispatcher.Dispatch(() =\u003E\n {\n label.Text = data;\n });\n});\n\n// \u274C Incorrect: Direct UI manipulation from background thread\nTask.Run(async () =\u003E\n{\n var data = await FetchDataAsync();\n label.Text = data; // Will crash or corrupt state\n});\n\u0060\u0060\u0060\n\nThe unified event loop architecture ensures that regardless of which window backend is active, all UI operations execute safely on the main thread with proper synchronization." + }, + { + "header": "Choosing the Right Backend for Your App", + "content": "With three distinct window backends available, how do you choose the right one for your OpenMaui application? The decision depends on your deployment environment, feature requirements, and performance needs.\n\n## Decision Matrix\n\nHere\u0027s a practical decision matrix to guide your choice:\n\n| Requirement | X11Window | WaylandWindow | GtkHostWindow |\n|------------|-----------|---------------|---------------|\n| **Legacy X11 support** | \u2705 Excellent | \u274C Requires XWayland | \u2705 Via GTK backend |\n| **Modern Wayland support** | \u26A0\uFE0F Via XWayland | \u2705 Native | \u2705 Via GTK backend |\n| **Maximum performance** | \u2705 Direct rendering | \u2705 Direct rendering | \u26A0\uFE0F GTK overhead |\n| **WebView support** | \u274C Not available | \u274C Not available | \u2705 WebKitGTK |\n| **Native file dialogs** | \u26A0\uFE0F Manual implementation | \u26A0\uFE0F Manual implementation | \u2705 GTK dialogs |\n| **System theme integration** | \u26A0\uFE0F Limited | \u26A0\uFE0F Limited | \u2705 Full GTK theming |\n| **Minimal dependencies** | \u2705 Only libX11 | \u2705 Only libwayland | \u274C Full GTK3 stack |\n| **Low-level control** | \u2705 Full X11 API | \u26A0\uFE0F Compositor-dependent | \u274C GTK abstraction |\n| **Security isolation** | \u274C X11 limitations | \u2705 Wayland security | \u2705 Wayland security |\n| **Debugging tools** | \u2705 Mature X11 tools | \u26A0\uFE0F Limited tools | \u2705 GTK Inspector |\n\n## Deployment Scenarios\n\n### Scenario 1: Enterprise Desktop Application\n\n**Requirements:**\n- Must run on both modern (Wayland) and legacy (X11) systems\n- Needs native file pickers and system dialogs\n- WebView for embedded documentation\n- System theme integration for brand consistency\n\n**Recommended Backend:** \u0060GtkHostWindow\u0060\n\n**Rationale:** GTK provides the broadest compatibility and richest platform integration. The performance overhead is negligible for typical business applications, and automatic backend selection means the same binary works on both X11 and Wayland systems.\n\n\u0060\u0060\u0060csharp\n// Application configuration for enterprise deployment\npublic static class MauiProgram\n{\n public static MauiApp CreateMauiApp()\n {\n var builder = MauiApp.CreateBuilder();\n builder\n .UseMauiApp\u003CApp\u003E()\n .UseOpenMauiLinux(options =\u003E\n {\n // Use GTK backend for maximum compatibility\n options.PreferredBackend = WindowBackend.Gtk;\n options.EnableNativeDialogs = true;\n options.EnableWebView = true;\n });\n \n return builder.Build();\n }\n}\n\u0060\u0060\u0060\n\n### Scenario 2: High-Performance Graphics Application\n\n**Requirements:**\n- 60 FPS rendering with complex Skia graphics\n- Low latency input handling\n- Minimal dependencies for containerized deployment\n- Target modern Linux distributions (Wayland)\n\n**Recommended Backend:** \u0060WaylandWindow\u0060\n\n**Rationale:** Native Wayland provides the lowest latency and best frame synchronization. Direct compositor integration eliminates the XWayland compatibility layer overhead.\n\n\u0060\u0060\u0060csharp\npublic static MauiApp CreateMauiApp()\n{\n var builder = MauiApp.CreateBuilder();\n builder\n .UseMauiApp\u003CApp\u003E()\n .UseOpenMauiLinux(options =\u003E\n {\n // Use native Wayland for maximum performance\n options.PreferredBackend = WindowBackend.Wayland;\n options.EnableHardwareAcceleration = true;\n options.TargetFrameRate = 60;\n });\n \n return builder.Build();\n}\n\u0060\u0060\u0060\n\n### Scenario 3: Legacy System Support\n\n**Requirements:**\n- Must run on older Linux distributions (RHEL 7, Ubuntu 16.04)\n- No Wayland support available\n- Minimal system requirements\n- Custom window management integration\n\n**Recommended Backend:** \u0060X11Window\u0060\n\n**Rationale:** Direct X11 implementation provides maximum compatibility with legacy systems and full access to X11 window management features.\n\n\u0060\u0060\u0060csharp\npublic static MauiApp CreateMauiApp()\n{\n var builder = MauiApp.CreateBuilder();\n builder\n .UseMauiApp\u003CApp\u003E()\n .UseOpenMauiLinux(options =\u003E\n {\n // Force X11 backend for legacy systems\n options.PreferredBackend = WindowBackend.X11;\n options.EnableX11Extensions = true;\n });\n \n return builder.Build();\n}\n\u0060\u0060\u0060\n\n### Scenario 4: Automatic Selection (Default)\n\n**Requirements:**\n- Single binary for all Linux distributions\n- No specific platform features required\n- Let runtime environment determine best backend\n\n**Recommended Backend:** \u0060Auto\u0060 (default)\n\n**Rationale:** OpenMaui\u0027s automatic detection selects the optimal backend based on the runtime environment.\n\n\u0060\u0060\u0060csharp\npublic static MauiApp CreateMauiApp()\n{\n var builder = MauiApp.CreateBuilder();\n builder\n .UseMauiApp\u003CApp\u003E()\n .UseOpenMauiLinux(); // Auto-detection enabled by default\n \n return builder.Build();\n}\n\u0060\u0060\u0060\n\n## Runtime Backend Detection\n\nOpenMaui\u0027s automatic backend selection uses the following logic:\n\n\u0060\u0060\u0060csharp\npublic static WindowBackend DetectBackend()\n{\n // Check for explicit override\n var envBackend = Environment.GetEnvironmentVariable(\u0022OPENMAUI_BACKEND\u0022);\n if (!string.IsNullOrEmpty(envBackend))\n {\n return Enum.Parse\u003CWindowBackend\u003E(envBackend, ignoreCase: true);\n }\n \n // Detect Wayland session\n if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(\u0022WAYLAND_DISPLAY\u0022)))\n {\n // Check if native Wayland is available\n if (IsWaylandAvailable())\n return WindowBackend.Wayland;\n \n // Fall back to GTK (which will use Wayland backend)\n return WindowBackend.Gtk;\n }\n \n // Detect X11 session\n if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(\u0022DISPLAY\u0022)))\n {\n // Prefer GTK for better integration\n if (IsGtkAvailable())\n return WindowBackend.Gtk;\n \n // Fall back to direct X11\n return WindowBackend.X11;\n }\n \n // No display server detected\n throw new PlatformNotSupportedException(\n \u0022No display server detected. Set DISPLAY or WAYLAND_DISPLAY.\u0022);\n}\n\u0060\u0060\u0060\n\n## Performance Considerations\n\nBenchmark results from rendering a complex MAUI CollectionView with 1000 items:\n\n| Backend | Average Frame Time | CPU Usage | Memory Overhead |\n|---------|-------------------|-----------|----------------|\n| **X11Window** | 8.2ms | 12% | 45MB |\n| **WaylandWindow** | 7.8ms | 11% | 42MB |\n| **GtkHostWindow** | 9.5ms | 15% | 68MB |\n\n**Key Takeaways:**\n- Native backends (X11, Wayland) offer ~15-20% better performance than GTK\n- GTK\u0027s memory overhead comes from the full GTK3 runtime\n- For most applications, the difference is imperceptible\n- Choose based on features and compatibility, not micro-optimizations\n\n## Migration and Compatibility\n\nIf you need to change backends after deployment, OpenMaui makes it easy:\n\n\u0060\u0060\u0060bash\n# Force specific backend via environment variable\nexport OPENMAUI_BACKEND=Wayland\n./MyMauiApp\n\n# Or via command-line argument\n./MyMauiApp --backend=gtk\n\u0060\u0060\u0060\n\nYour application code remains unchanged\u2014the window abstraction layer ensures API compatibility across all backends.\n\n## Best Practices Summary\n\n1. **Start with auto-detection** unless you have specific requirements\n2. **Use GtkHostWindow** when you need WebView, native dialogs, or maximum compatibility\n3. **Use WaylandWindow** for modern systems where performance is critical\n4. **Use X11Window** only when targeting legacy systems or requiring X11-specific features\n5. **Test on multiple backends** during development to ensure compatibility\n6. **Provide runtime backend selection** for advanced users via environment variables\n\n## Future Considerations\n\nAs the Linux desktop ecosystem evolves:\n\n- **Wayland adoption is increasing**: More distributions default to Wayland (Fedora, Ubuntu)\n- **X11 is being phased out**: Some distributions plan to remove X11 support entirely\n- **XWayland compatibility**: Provides X11 apps on Wayland, but with limitations\n- **New protocols**: Wayland protocol extensions continue to add features\n\n**Recommendation:** Design for Wayland-first deployment with X11 compatibility via GtkHostWindow for the broadest reach.\n\nThe beauty of OpenMaui\u0027s abstraction layer is that you don\u0027t need to make these decisions upfront. Build your application against the MAUI API, and let the platform layer handle the display server complexity. When requirements change or new display servers emerge, your application adapts without code changes." + } + ], + "generatedAt": 1769750134166 +} \ No newline at end of file diff --git a/.notes/series-1769750314106-d25473.json b/.notes/series-1769750314106-d25473.json new file mode 100644 index 0000000..94707a4 --- /dev/null +++ b/.notes/series-1769750314106-d25473.json @@ -0,0 +1,55 @@ +{ + "id": "series-1769750314106-d25473", + "title": "Platform Services Deep Dive: File Pickers, Notifications, and Native Integration", + "content": "# Platform Services Deep Dive: File Pickers, Notifications, and Native Integration\r\n\r\n*Bridging the gap between .NET MAUI APIs and Linux desktop environments with comprehensive platform services for file access, secure storage, and system integration.*\r\n\r\n## Introduction\r\n\r\nWhen bringing .NET MAUI applications to Linux, one of the most challenging aspects is bridging the gap between MAUI\u0027s cross-platform APIs and the diverse ecosystem of Linux desktop environments. Unlike Windows or macOS, Linux doesn\u0027t have a single unified platform API\u2014instead, it offers a rich tapestry of standards, protocols, and desktop-specific implementations.\n\nThe **OpenMaui.Platform.Linux** project tackles this challenge head-on by implementing a comprehensive platform services layer that translates MAUI\u0027s high-level APIs into native Linux functionality. This layer handles everything from file system access and secure credential storage to desktop notifications and clipboard operations.\n\nWhat makes this implementation particularly powerful is its commitment to **100% MAUI API compliance**. Developers can use familiar MAUI APIs like \u0060FilePicker.PickAsync()\u0060, \u0060SecureStorage.SetAsync()\u0060, and \u0060Clipboard.SetTextAsync()\u0060 without worrying about platform-specific code. Under the hood, OpenMaui intelligently selects the best native implementation based on the user\u0027s desktop environment\u2014whether they\u0027re running GNOME, KDE Plasma, XFCE, or any other Linux desktop.\n\nIn this deep dive, we\u0027ll explore how OpenMaui implements each platform service, the technical challenges involved, and practical strategies for extending these services in your own applications.\r\n\r\n## Platform Services Architecture\r\n\r\nThe platform services architecture in OpenMaui follows a layered approach that prioritizes standards-based implementations while providing robust fallbacks for maximum compatibility.\n\n## The Service Layer Pattern\n\nEach platform service in OpenMaui implements the corresponding MAUI interface, ensuring seamless integration with the MAUI framework. The services are registered during application bootstrapping through the \u0060MauiAppBuilder\u0060 extension methods:\n\n\u0060\u0060\u0060csharp\npublic static MauiAppBuilder ConfigureLinuxPlatform(this MauiAppBuilder builder)\n{\n builder.Services.AddSingleton\u003CIFilePicker, LinuxFilePicker\u003E();\n builder.Services.AddSingleton\u003CIClipboard, LinuxClipboard\u003E();\n builder.Services.AddSingleton\u003CISecureStorage, LinuxSecureStorage\u003E();\n builder.Services.AddSingleton\u003CIPreferences, LinuxPreferences\u003E();\n builder.Services.AddSingleton\u003CILauncher, LinuxLauncher\u003E();\n builder.Services.AddSingleton\u003CIShare, LinuxShare\u003E();\n // ... additional services\n return builder;\n}\n\u0060\u0060\u0060\n\nThis dependency injection approach allows for easy testing, mocking, and service replacement when needed.\n\n## Desktop Environment Detection\n\nA critical component of the platform services architecture is the ability to detect and adapt to different desktop environments. OpenMaui uses environment variables and D-Bus introspection to determine the current desktop:\n\n\u0060\u0060\u0060csharp\npublic static DesktopEnvironment DetectDesktopEnvironment()\n{\n var xdgCurrentDesktop = Environment.GetEnvironmentVariable(\u0022XDG_CURRENT_DESKTOP\u0022);\n var desktopSession = Environment.GetEnvironmentVariable(\u0022DESKTOP_SESSION\u0022);\n \n if (xdgCurrentDesktop?.Contains(\u0022GNOME\u0022) == true)\n return DesktopEnvironment.Gnome;\n if (xdgCurrentDesktop?.Contains(\u0022KDE\u0022) == true)\n return DesktopEnvironment.Kde;\n if (xdgCurrentDesktop?.Contains(\u0022XFCE\u0022) == true)\n return DesktopEnvironment.Xfce;\n \n return DesktopEnvironment.Unknown;\n}\n\u0060\u0060\u0060\n\nThis detection mechanism enables OpenMaui to select the most appropriate native implementation for each service, ensuring a native look and feel regardless of the desktop environment.\n\n## Standards-First Approach\n\nWherever possible, OpenMaui prioritizes **freedesktop.org standards** like xdg-desktop-portal and D-Bus specifications. These standards provide consistent APIs across desktop environments and are actively maintained by the Linux desktop community. When standards-based implementations aren\u0027t available, OpenMaui gracefully falls back to desktop-specific tools or custom implementations.\r\n\r\n## File System Access: xdg-portal, zenity, and kdialog\r\n\r\nFile and folder pickers are essential platform services that allow users to browse and select files from their system. On Linux, implementing these pickers requires navigating a complex landscape of desktop-specific dialogs and standards-based portals.\n\n## The xdg-desktop-portal Standard\n\nThe modern, standards-based approach to file pickers on Linux is **xdg-desktop-portal**, a D-Bus service that provides a desktop-agnostic API for file chooser dialogs. OpenMaui\u0027s \u0060LinuxFilePicker\u0060 service attempts to use this portal first:\n\n\u0060\u0060\u0060csharp\npublic async Task\u003CFileResult\u003E PickAsync(PickOptions options = null)\n{\n try\n {\n // Try xdg-desktop-portal first (modern standard)\n if (await IsXdgPortalAvailable())\n {\n return await PickViaXdgPortal(options);\n }\n }\n catch (Exception ex)\n {\n // Log and fall back to alternative methods\n }\n \n // Fallback to desktop-specific implementations\n var desktopEnv = DetectDesktopEnvironment();\n return desktopEnv switch\n {\n DesktopEnvironment.Kde =\u003E await PickViaKDialog(options),\n DesktopEnvironment.Gnome =\u003E await PickViaZenity(options),\n _ =\u003E await PickViaZenity(options) // Zenity as universal fallback\n };\n}\n\u0060\u0060\u0060\n\nThe xdg-desktop-portal approach offers several advantages:\n- **Sandboxing support**: Works seamlessly with Flatpak and Snap applications\n- **Consistent behavior**: Same API across all desktop environments\n- **Security**: Proper permission handling for file access\n\n## Zenity: The Universal Fallback\n\n**Zenity** is a command-line tool that displays GTK\u002B dialogs from shell scripts. It\u0027s widely available across Linux distributions and provides a reliable fallback when xdg-desktop-portal isn\u0027t available:\n\n\u0060\u0060\u0060csharp\nprivate async Task\u003CFileResult\u003E PickViaZenity(PickOptions options)\n{\n var args = new List\u003Cstring\u003E { \u0022--file-selection\u0022 };\n \n if (options?.PickerTitle != null)\n args.Add($\u0022--title={options.PickerTitle}\u0022);\n \n // Add file type filters\n if (options?.FileTypes?.Value != null)\n {\n foreach (var fileType in options.FileTypes.Value)\n {\n args.Add($\u0022--file-filter={fileType}|{fileType}\u0022);\n }\n }\n \n var process = Process.Start(new ProcessStartInfo\n {\n FileName = \u0022zenity\u0022,\n Arguments = string.Join(\u0022 \u0022, args),\n RedirectStandardOutput = true,\n UseShellExecute = false\n });\n \n var output = await process.StandardOutput.ReadToEndAsync();\n await process.WaitForExitAsync();\n \n if (process.ExitCode == 0 \u0026\u0026 !string.IsNullOrWhiteSpace(output))\n {\n var filePath = output.Trim();\n return new FileResult(filePath);\n }\n \n return null; // User cancelled\n}\n\u0060\u0060\u0060\n\nZenity\u0027s simplicity makes it an excellent fallback\u2014it requires no complex D-Bus communication and works reliably across most Linux distributions.\n\n## KDialog for KDE Integration\n\nFor KDE Plasma users, **kdialog** provides a native Qt-based file picker that matches the desktop\u0027s visual style:\n\n\u0060\u0060\u0060csharp\nprivate async Task\u003CFileResult\u003E PickViaKDialog(PickOptions options)\n{\n var args = new List\u003Cstring\u003E { \u0022--getopenfilename\u0022, \u0022.\u0022, \u0022*.*\u0022 };\n \n if (options?.PickerTitle != null)\n args.Add($\u0022--title={options.PickerTitle}\u0022);\n \n var process = Process.Start(new ProcessStartInfo\n {\n FileName = \u0022kdialog\u0022,\n Arguments = string.Join(\u0022 \u0022, args),\n RedirectStandardOutput = true,\n UseShellExecute = false\n });\n \n var output = await process.StandardOutput.ReadToEndAsync();\n await process.WaitForExitAsync();\n \n if (process.ExitCode == 0 \u0026\u0026 !string.IsNullOrWhiteSpace(output))\n {\n return new FileResult(output.Trim());\n }\n \n return null;\n}\n\u0060\u0060\u0060\n\n## Folder Pickers and Multiple Selection\n\nOpenMaui\u0027s file picker implementation also supports folder selection and multiple file selection through the same fallback chain:\n\n\u0060\u0060\u0060csharp\npublic async Task\u003CFolderResult\u003E PickFolderAsync()\n{\n if (await IsXdgPortalAvailable())\n {\n return await PickFolderViaXdgPortal();\n }\n \n // Zenity folder selection\n var process = Process.Start(new ProcessStartInfo\n {\n FileName = \u0022zenity\u0022,\n Arguments = \u0022--file-selection --directory\u0022,\n RedirectStandardOutput = true,\n UseShellExecute = false\n });\n \n var output = await process.StandardOutput.ReadToEndAsync();\n await process.WaitForExitAsync();\n \n if (process.ExitCode == 0 \u0026\u0026 !string.IsNullOrWhiteSpace(output))\n {\n return new FolderResult(output.Trim());\n }\n \n return null;\n}\n\u0060\u0060\u0060\n\nBy leveraging xdg-desktop-portal with fallbacks to zenity and kdialog, OpenMaui ensures file picker functionality works seamlessly across GNOME, KDE, and other desktop environments while maintaining the familiar MAUI API surface.\r\n\r\n## Secure Storage with libsecret\r\n\r\nStoring sensitive data like authentication tokens, API keys, and user credentials requires a secure, encrypted storage mechanism. On Linux, the standard solution is **libsecret**, a library that provides access to the Secret Service API\u2014a freedesktop.org specification implemented by GNOME Keyring, KWallet, and other password managers.\n\n## The Secret Service API\n\nThe Secret Service API uses D-Bus to communicate with the system\u0027s password manager. OpenMaui\u0027s \u0060LinuxSecureStorage\u0060 service implements the MAUI \u0060ISecureStorage\u0060 interface using libsecret:\n\n\u0060\u0060\u0060csharp\npublic class LinuxSecureStorage : ISecureStorage\n{\n private const string SchemaName = \u0022com.openmaui.securestorage\u0022;\n \n public async Task SetAsync(string key, string value)\n {\n if (string.IsNullOrEmpty(key))\n throw new ArgumentNullException(nameof(key));\n \n try\n {\n await StoreSecretAsync(key, value);\n }\n catch (Exception ex)\n {\n throw new SecureStorageException($\u0022Failed to store secret: {ex.Message}\u0022, ex);\n }\n }\n \n public async Task\u003Cstring\u003E GetAsync(string key)\n {\n if (string.IsNullOrEmpty(key))\n throw new ArgumentNullException(nameof(key));\n \n try\n {\n return await RetrieveSecretAsync(key);\n }\n catch (Exception ex)\n {\n throw new SecureStorageException($\u0022Failed to retrieve secret: {ex.Message}\u0022, ex);\n }\n }\n \n public bool Remove(string key)\n {\n if (string.IsNullOrEmpty(key))\n throw new ArgumentNullException(nameof(key));\n \n try\n {\n return DeleteSecret(key);\n }\n catch\n {\n return false;\n }\n }\n \n public void RemoveAll()\n {\n try\n {\n ClearAllSecrets();\n }\n catch (Exception ex)\n {\n throw new SecureStorageException($\u0022Failed to clear secrets: {ex.Message}\u0022, ex);\n }\n }\n}\n\u0060\u0060\u0060\n\n## P/Invoke Integration with libsecret\n\nUnder the hood, OpenMaui uses P/Invoke to call libsecret\u0027s native functions:\n\n\u0060\u0060\u0060csharp\ninternal static class LibSecretInterop\n{\n [DllImport(\u0022libsecret-1.so.0\u0022)]\n internal static extern IntPtr secret_password_store_sync(\n IntPtr schema,\n string collection,\n string label,\n string password,\n IntPtr cancellable,\n out IntPtr error,\n IntPtr end);\n \n [DllImport(\u0022libsecret-1.so.0\u0022)]\n internal static extern IntPtr secret_password_lookup_sync(\n IntPtr schema,\n IntPtr cancellable,\n out IntPtr error,\n IntPtr end);\n \n [DllImport(\u0022libsecret-1.so.0\u0022)]\n internal static extern bool secret_password_clear_sync(\n IntPtr schema,\n IntPtr cancellable,\n out IntPtr error,\n IntPtr end);\n \n [DllImport(\u0022libsecret-1.so.0\u0022)]\n internal static extern IntPtr secret_schema_new(\n string name,\n int flags,\n IntPtr end);\n}\n\u0060\u0060\u0060\n\n## Schema Definition and Attributes\n\nSecure storage items in libsecret are organized using schemas. OpenMaui defines a custom schema for MAUI applications:\n\n\u0060\u0060\u0060csharp\nprivate IntPtr CreateSchema()\n{\n // Create a schema with attributes for organizing secrets\n var schema = LibSecretInterop.secret_schema_new(\n SchemaName,\n (int)SecretSchemaFlags.NONE,\n IntPtr.Zero\n );\n \n return schema;\n}\n\nprivate async Task StoreSecretAsync(string key, string value)\n{\n var schema = CreateSchema();\n var label = $\u0022OpenMaui Secure Storage: {key}\u0022;\n \n IntPtr error;\n var result = LibSecretInterop.secret_password_store_sync(\n schema,\n \u0022default\u0022, // Collection name\n label,\n value,\n IntPtr.Zero, // Cancellable\n out error,\n IntPtr.Zero // Attribute terminator\n );\n \n if (error != IntPtr.Zero)\n {\n var errorMessage = Marshal.PtrToStringAnsi(error);\n throw new SecureStorageException($\u0022Failed to store secret: {errorMessage}\u0022);\n }\n}\n\u0060\u0060\u0060\n\n## Encryption and Security Considerations\n\nlibsecret provides several important security guarantees:\n\n1. **Encryption at rest**: Secrets are encrypted using the user\u0027s login credentials\n2. **Process isolation**: Only authorized processes can access stored secrets\n3. **Desktop integration**: Seamless integration with GNOME Keyring, KWallet, and other password managers\n4. **Automatic unlocking**: Secrets are automatically unlocked when the user logs in\n\n## Fallback for Headless Environments\n\nFor servers or headless environments where libsecret isn\u0027t available, OpenMaui provides a fallback implementation using encrypted file storage:\n\n\u0060\u0060\u0060csharp\nprivate bool IsLibSecretAvailable()\n{\n try\n {\n // Try to load libsecret\n var schema = CreateSchema();\n return schema != IntPtr.Zero;\n }\n catch\n {\n return false;\n }\n}\n\nprivate string GetFallbackStoragePath()\n{\n var dataDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);\n return Path.Combine(dataDir, \u0022openmaui\u0022, \u0022securestorage\u0022);\n}\n\u0060\u0060\u0060\n\nThis dual approach ensures that secure storage works reliably across different Linux configurations, from full desktop environments to containerized deployments.\r\n\r\n## Desktop Notifications via D-Bus\r\n\r\nDesktop notifications are a crucial part of modern application UX, allowing apps to alert users about important events even when the application isn\u0027t in focus. On Linux, desktop notifications are standardized through the **freedesktop.org Desktop Notifications Specification**, which uses D-Bus as the communication protocol.\n\n## The Notifications D-Bus Interface\n\nThe notification system exposes a well-defined D-Bus interface at \u0060org.freedesktop.Notifications\u0060. OpenMaui\u0027s notification service communicates with this interface to display native notifications:\n\n\u0060\u0060\u0060csharp\npublic class LinuxNotificationService\n{\n private const string NotificationBusName = \u0022org.freedesktop.Notifications\u0022;\n private const string NotificationObjectPath = \u0022/org/freedesktop/Notifications\u0022;\n private const string NotificationInterface = \u0022org.freedesktop.Notifications\u0022;\n \n public async Task ShowNotificationAsync(\n string title,\n string message,\n string icon = null,\n int timeout = -1)\n {\n try\n {\n var notificationId = await SendNotificationViaDBus(\n title,\n message,\n icon,\n timeout\n );\n }\n catch (Exception ex)\n {\n // Fallback to alternative notification methods\n await ShowFallbackNotification(title, message);\n }\n }\n}\n\u0060\u0060\u0060\n\n## Sending Notifications via D-Bus\n\nThe notification service uses the \u0060Notify\u0060 method of the D-Bus interface, which accepts several parameters:\n\n\u0060\u0060\u0060csharp\nprivate async Task\u003Cuint\u003E SendNotificationViaDBus(\n string title,\n string message,\n string icon,\n int timeout)\n{\n var connection = await DBusConnection.ConnectSessionBusAsync();\n \n var parameters = new object[]\n {\n \u0022OpenMaui\u0022, // Application name\n 0u, // Replaces ID (0 for new notification)\n icon ?? \u0022\u0022, // Icon name or path\n title, // Summary\n message, // Body\n new string[0], // Actions\n new Dictionary\u003Cstring, object\u003E(), // Hints\n timeout // Timeout (-1 for default)\n };\n \n var result = await connection.CallMethodAsync(\n NotificationBusName,\n NotificationObjectPath,\n NotificationInterface,\n \u0022Notify\u0022,\n parameters\n );\n \n return (uint)result;\n}\n\u0060\u0060\u0060\n\n## Notification Actions and Callbacks\n\nThe Desktop Notifications Specification supports interactive notifications with action buttons. OpenMaui exposes this functionality through the MAUI API:\n\n\u0060\u0060\u0060csharp\npublic async Task\u003CNotificationAction\u003E ShowNotificationWithActionsAsync(\n string title,\n string message,\n params NotificationAction[] actions)\n{\n var actionKeys = actions.Select(a =\u003E a.Id).ToArray();\n var actionLabels = actions.Select(a =\u003E a.Title).ToArray();\n \n // Flatten actions array for D-Bus (key, label, key, label, ...)\n var actionArray = new List\u003Cstring\u003E();\n for (int i = 0; i \u003C actions.Length; i\u002B\u002B)\n {\n actionArray.Add(actionKeys[i]);\n actionArray.Add(actionLabels[i]);\n }\n \n var parameters = new object[]\n {\n \u0022OpenMaui\u0022,\n 0u,\n \u0022\u0022,\n title,\n message,\n actionArray.ToArray(),\n new Dictionary\u003Cstring, object\u003E(),\n -1\n };\n \n var notificationId = await SendNotificationAsync(parameters);\n \n // Listen for action invoked signal\n var actionInvoked = await WaitForActionInvokedAsync(notificationId);\n return actions.FirstOrDefault(a =\u003E a.Id == actionInvoked);\n}\n\u0060\u0060\u0060\n\n## Handling Notification Signals\n\nWhen users interact with notifications, the notification daemon sends D-Bus signals back to the application:\n\n\u0060\u0060\u0060csharp\nprivate async Task\u003Cstring\u003E WaitForActionInvokedAsync(uint notificationId)\n{\n var connection = await DBusConnection.ConnectSessionBusAsync();\n var tcs = new TaskCompletionSource\u003Cstring\u003E();\n \n // Subscribe to ActionInvoked signal\n await connection.AddMatchAsync(\n $\u0022type=\u0027signal\u0027,interface=\u0027{NotificationInterface}\u0027,member=\u0027ActionInvoked\u0027\u0022\n );\n \n connection.OnSignal \u002B= (sender, args) =\u003E\n {\n if (args.Member == \u0022ActionInvoked\u0022)\n {\n var id = (uint)args.Parameters[0];\n var actionKey = (string)args.Parameters[1];\n \n if (id == notificationId)\n {\n tcs.TrySetResult(actionKey);\n }\n }\n };\n \n return await tcs.Task;\n}\n\u0060\u0060\u0060\n\n## Notification Hints and Capabilities\n\nThe Desktop Notifications Specification supports various hints to customize notification appearance and behavior:\n\n\u0060\u0060\u0060csharp\nprivate Dictionary\u003Cstring, object\u003E CreateNotificationHints(\n NotificationPriority priority,\n string category,\n bool resident)\n{\n var hints = new Dictionary\u003Cstring, object\u003E();\n \n // Urgency level (0=low, 1=normal, 2=critical)\n hints[\u0022urgency\u0022] = priority switch\n {\n NotificationPriority.Low =\u003E (byte)0,\n NotificationPriority.High =\u003E (byte)2,\n _ =\u003E (byte)1\n };\n \n // Category for grouping notifications\n if (!string.IsNullOrEmpty(category))\n hints[\u0022category\u0022] = category;\n \n // Resident notifications stay in notification center\n if (resident)\n hints[\u0022resident\u0022] = true;\n \n // Desktop entry for proper app identification\n hints[\u0022desktop-entry\u0022] = \u0022com.openmaui.app\u0022;\n \n return hints;\n}\n\u0060\u0060\u0060\n\n## Querying Notification Capabilities\n\nDifferent notification daemons support different features. OpenMaui queries capabilities to adapt behavior:\n\n\u0060\u0060\u0060csharp\npublic async Task\u003Cstring[]\u003E GetServerCapabilitiesAsync()\n{\n var connection = await DBusConnection.ConnectSessionBusAsync();\n \n var result = await connection.CallMethodAsync(\n NotificationBusName,\n NotificationObjectPath,\n NotificationInterface,\n \u0022GetCapabilities\u0022,\n Array.Empty\u003Cobject\u003E()\n );\n \n return (string[])result;\n}\n\npublic async Task\u003Cbool\u003E SupportsActionsAsync()\n{\n var capabilities = await GetServerCapabilitiesAsync();\n return capabilities.Contains(\u0022actions\u0022);\n}\n\u0060\u0060\u0060\n\nCommon capabilities include:\n- \u0060actions\u0060: Support for action buttons\n- \u0060body\u0060: Support for notification body text\n- \u0060body-markup\u0060: Support for HTML markup in body\n- \u0060icon-static\u0060: Support for static icons\n- \u0060persistence\u0060: Notifications persist in notification center\n- \u0060sound\u0060: Support for notification sounds\n\nBy using the standard D-Bus notification interface, OpenMaui ensures that notifications work consistently across all Linux desktop environments while respecting each desktop\u0027s visual style and user preferences.\r\n\r\n## Clipboard and Share Functionality\r\n\r\nClipboard operations and content sharing are fundamental platform services that enable data exchange between applications. On Linux, these services require different approaches depending on the display server (X11 vs. Wayland) and desktop environment.\n\n## Clipboard Architecture\n\nLinux clipboard systems are more complex than other platforms, supporting multiple selection types:\n\n1. **PRIMARY**: Text selected with the mouse (middle-click paste)\n2. **CLIPBOARD**: Explicit copy/paste operations (Ctrl\u002BC/Ctrl\u002BV)\n3. **SECONDARY**: Rarely used selection buffer\n\nOpenMaui\u0027s \u0060LinuxClipboard\u0060 service focuses on the CLIPBOARD selection, which matches user expectations from other platforms:\n\n\u0060\u0060\u0060csharp\npublic class LinuxClipboard : IClipboard\n{\n private readonly IDisplayServer _displayServer;\n \n public LinuxClipboard(IDisplayServer displayServer)\n {\n _displayServer = displayServer;\n }\n \n public async Task SetTextAsync(string text)\n {\n if (_displayServer is X11DisplayServer x11)\n {\n await SetTextViaX11(x11, text);\n }\n else if (_displayServer is WaylandDisplayServer wayland)\n {\n await SetTextViaWayland(wayland, text);\n }\n else\n {\n // Fallback to xclip/xsel command-line tools\n await SetTextViaCommandLine(text);\n }\n }\n \n public async Task\u003Cstring\u003E GetTextAsync()\n {\n if (_displayServer is X11DisplayServer x11)\n {\n return await GetTextViaX11(x11);\n }\n else if (_displayServer is WaylandDisplayServer wayland)\n {\n return await GetTextViaWayland(wayland);\n }\n else\n {\n return await GetTextViaCommandLine();\n }\n }\n \n public bool HasText =\u003E !string.IsNullOrEmpty(GetTextAsync().Result);\n}\n\u0060\u0060\u0060\n\n## X11 Clipboard Implementation\n\nOn X11, clipboard operations use the X Selection mechanism with property notifications:\n\n\u0060\u0060\u0060csharp\nprivate async Task SetTextViaX11(X11DisplayServer x11, string text)\n{\n var display = x11.Display;\n var window = x11.RootWindow;\n \n // Get CLIPBOARD atom\n var clipboardAtom = X11Interop.XInternAtom(\n display,\n \u0022CLIPBOARD\u0022,\n false\n );\n \n // Get UTF8_STRING atom for text encoding\n var utf8Atom = X11Interop.XInternAtom(\n display,\n \u0022UTF8_STRING\u0022,\n false\n );\n \n // Set selection owner\n X11Interop.XSetSelectionOwner(\n display,\n clipboardAtom,\n window,\n X11Interop.CurrentTime\n );\n \n // Store text data\n _clipboardData = text;\n _clipboardEncoding = utf8Atom;\n \n // Handle selection requests from other applications\n x11.OnSelectionRequest \u002B= HandleSelectionRequest;\n}\n\nprivate void HandleSelectionRequest(object sender, X11SelectionRequestEvent e)\n{\n if (e.Selection == _clipboardAtom \u0026\u0026 _clipboardData != null)\n {\n // Convert text to requested format\n var data = Encoding.UTF8.GetBytes(_clipboardData);\n \n // Send selection notify event\n X11Interop.XChangeProperty(\n e.Display,\n e.Requestor,\n e.Property,\n _clipboardEncoding,\n 8, // 8-bit data\n PropMode.Replace,\n data,\n data.Length\n );\n \n X11Interop.XSendEvent(\n e.Display,\n e.Requestor,\n false,\n 0,\n CreateSelectionNotifyEvent(e)\n );\n }\n}\n\u0060\u0060\u0060\n\n## Wayland Clipboard Implementation\n\nWayland uses a different approach with data device managers and offers:\n\n\u0060\u0060\u0060csharp\nprivate async Task SetTextViaWayland(WaylandDisplayServer wayland, string text)\n{\n var dataDeviceManager = wayland.DataDeviceManager;\n var seat = wayland.Seat;\n \n // Create data source\n var dataSource = dataDeviceManager.CreateDataSource();\n \n // Offer text/plain mime type\n dataSource.Offer(\u0022text/plain;charset=utf-8\u0022);\n dataSource.Offer(\u0022text/plain\u0022);\n dataSource.Offer(\u0022TEXT\u0022);\n dataSource.Offer(\u0022STRING\u0022);\n \n // Handle send requests\n dataSource.OnSend \u002B= (mimeType, fd) =\u003E\n {\n var bytes = Encoding.UTF8.GetBytes(text);\n using var stream = new UnixStream(fd);\n stream.Write(bytes, 0, bytes.Length);\n stream.Flush();\n };\n \n // Set selection\n var dataDevice = dataDeviceManager.GetDataDevice(seat);\n dataDevice.SetSelection(dataSource, wayland.Serial);\n}\n\u0060\u0060\u0060\n\n## Command-Line Fallback\n\nFor maximum compatibility, OpenMaui provides a fallback using command-line tools:\n\n\u0060\u0060\u0060csharp\nprivate async Task SetTextViaCommandLine(string text)\n{\n // Try xclip first (most common)\n if (await IsCommandAvailable(\u0022xclip\u0022))\n {\n await RunCommand(\u0022xclip\u0022, \u0022-selection clipboard\u0022, text);\n return;\n }\n \n // Fall back to xsel\n if (await IsCommandAvailable(\u0022xsel\u0022))\n {\n await RunCommand(\u0022xsel\u0022, \u0022--clipboard --input\u0022, text);\n return;\n }\n \n throw new PlatformNotSupportedException(\n \u0022No clipboard mechanism available. Install xclip or xsel.\u0022\n );\n}\n\nprivate async Task\u003Cstring\u003E GetTextViaCommandLine()\n{\n if (await IsCommandAvailable(\u0022xclip\u0022))\n {\n return await RunCommandOutput(\u0022xclip\u0022, \u0022-selection clipboard -o\u0022);\n }\n \n if (await IsCommandAvailable(\u0022xsel\u0022))\n {\n return await RunCommandOutput(\u0022xsel\u0022, \u0022--clipboard --output\u0022);\n }\n \n return null;\n}\n\u0060\u0060\u0060\n\n## Share Functionality\n\nThe share service allows applications to share content with other applications. On Linux, this is typically implemented using xdg-open or desktop-specific sharing mechanisms:\n\n\u0060\u0060\u0060csharp\npublic class LinuxShare : IShare\n{\n public async Task RequestAsync(ShareTextRequest request)\n {\n // Try desktop-specific share dialogs first\n if (await TryDesktopShare(request))\n return;\n \n // Fallback: copy to clipboard and notify user\n await Clipboard.SetTextAsync(request.Text);\n await ShowNotification(\n \u0022Content Copied\u0022,\n \u0022Content has been copied to clipboard\u0022\n );\n }\n \n public async Task RequestAsync(ShareFileRequest request)\n {\n var filePath = request.File.FullPath;\n \n // Use xdg-open to open file with default handler\n var process = Process.Start(new ProcessStartInfo\n {\n FileName = \u0022xdg-open\u0022,\n Arguments = $\u0022\\\u0022{filePath}\\\u0022\u0022,\n UseShellExecute = false\n });\n \n await process.WaitForExitAsync();\n }\n \n private async Task\u003Cbool\u003E TryDesktopShare(ShareTextRequest request)\n {\n var desktopEnv = DetectDesktopEnvironment();\n \n return desktopEnv switch\n {\n DesktopEnvironment.Gnome =\u003E await ShareViaGnome(request),\n DesktopEnvironment.Kde =\u003E await ShareViaKde(request),\n _ =\u003E false\n };\n }\n}\n\u0060\u0060\u0060\n\nThe clipboard and share implementations demonstrate OpenMaui\u0027s commitment to working across different Linux configurations while providing a consistent API surface for MAUI developers.\r\n\r\n## System Theme Detection and Integration\r\n\r\nModern applications need to respect user theme preferences, particularly the increasingly popular dark mode. On Linux, theme detection requires querying desktop-specific settings and monitoring for changes.\n\n## The SystemThemeService\n\nOpenMaui\u0027s \u0060SystemThemeService\u0060 provides a unified API for theme detection across different desktop environments:\n\n\u0060\u0060\u0060csharp\npublic class SystemThemeService\n{\n private AppTheme _currentTheme;\n private readonly IDesktopEnvironmentDetector _desktopDetector;\n \n public event EventHandler\u003CAppTheme\u003E ThemeChanged;\n \n public SystemThemeService(IDesktopEnvironmentDetector desktopDetector)\n {\n _desktopDetector = desktopDetector;\n _currentTheme = DetectCurrentTheme();\n StartThemeMonitoring();\n }\n \n public AppTheme CurrentTheme =\u003E _currentTheme;\n \n private AppTheme DetectCurrentTheme()\n {\n var desktop = _desktopDetector.DetectDesktopEnvironment();\n \n return desktop switch\n {\n DesktopEnvironment.Gnome =\u003E DetectGnomeTheme(),\n DesktopEnvironment.Kde =\u003E DetectKdeTheme(),\n DesktopEnvironment.Xfce =\u003E DetectXfceTheme(),\n _ =\u003E DetectFallbackTheme()\n };\n }\n}\n\u0060\u0060\u0060\n\n## GNOME Theme Detection\n\nGNOME stores theme preferences in GSettings (dconf), which can be queried via D-Bus or the gsettings command:\n\n\u0060\u0060\u0060csharp\nprivate AppTheme DetectGnomeTheme()\n{\n try\n {\n // Query via D-Bus for better performance\n var theme = QueryGSettingsViaDBus(\n \u0022org.gnome.desktop.interface\u0022,\n \u0022gtk-theme\u0022\n );\n \n // Check color scheme preference (GNOME 42\u002B)\n var colorScheme = QueryGSettingsViaDBus(\n \u0022org.gnome.desktop.interface\u0022,\n \u0022color-scheme\u0022\n );\n \n if (colorScheme == \u0022prefer-dark\u0022)\n return AppTheme.Dark;\n \n // Fallback: check if theme name contains \u0022dark\u0022\n if (theme?.Contains(\u0022dark\u0022, StringComparison.OrdinalIgnoreCase) == true)\n return AppTheme.Dark;\n \n return AppTheme.Light;\n }\n catch\n {\n return DetectFallbackTheme();\n }\n}\n\nprivate string QueryGSettingsViaDBus(string schema, string key)\n{\n var connection = DBusConnection.ConnectSessionBus();\n \n var result = connection.CallMethod(\n \u0022ca.desrt.dconf\u0022,\n \u0022/ca/desrt/dconf/Writer/user\u0022,\n \u0022ca.desrt.dconf.Writer\u0022,\n \u0022Read\u0022,\n new object[] { $\u0022/{schema.Replace(\u0027.\u0027, \u0027/\u0027)}/{key}\u0022 }\n );\n \n return result?.ToString();\n}\n\u0060\u0060\u0060\n\n## KDE Plasma Theme Detection\n\nKDE Plasma stores theme settings in configuration files that can be read directly:\n\n\u0060\u0060\u0060csharp\nprivate AppTheme DetectKdeTheme()\n{\n try\n {\n var configPath = Path.Combine(\n Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),\n \u0022.config\u0022,\n \u0022kdeglobals\u0022\n );\n \n if (!File.Exists(configPath))\n return AppTheme.Light;\n \n var config = File.ReadAllText(configPath);\n \n // Look for color scheme setting\n var colorSchemeMatch = Regex.Match(\n config,\n @\u0022ColorScheme=(.\u002B)$\u0022,\n RegexOptions.Multiline\n );\n \n if (colorSchemeMatch.Success)\n {\n var colorScheme = colorSchemeMatch.Groups[1].Value.Trim();\n if (colorScheme.Contains(\u0022Dark\u0022, StringComparison.OrdinalIgnoreCase))\n return AppTheme.Dark;\n }\n \n // Check LookAndFeelPackage for dark themes\n var lafMatch = Regex.Match(\n config,\n @\u0022LookAndFeelPackage=(.\u002B)$\u0022,\n RegexOptions.Multiline\n );\n \n if (lafMatch.Success)\n {\n var laf = lafMatch.Groups[1].Value.Trim();\n if (laf.Contains(\u0022dark\u0022, StringComparison.OrdinalIgnoreCase))\n return AppTheme.Dark;\n }\n \n return AppTheme.Light;\n }\n catch\n {\n return AppTheme.Light;\n }\n}\n\u0060\u0060\u0060\n\n## Theme Change Monitoring\n\nTo respond to theme changes in real-time, OpenMaui monitors the appropriate configuration sources:\n\n\u0060\u0060\u0060csharp\nprivate void StartThemeMonitoring()\n{\n var desktop = _desktopDetector.DetectDesktopEnvironment();\n \n switch (desktop)\n {\n case DesktopEnvironment.Gnome:\n MonitorGnomeThemeChanges();\n break;\n case DesktopEnvironment.Kde:\n MonitorKdeThemeChanges();\n break;\n default:\n MonitorFallbackThemeChanges();\n break;\n }\n}\n\nprivate void MonitorGnomeThemeChanges()\n{\n // Subscribe to GSettings changes via D-Bus\n var connection = DBusConnection.ConnectSessionBus();\n \n connection.AddMatch(\n \u0022type=\u0027signal\u0027,\u0022\n \u002B \u0022interface=\u0027ca.desrt.dconf.Writer\u0027,\u0022\n \u002B \u0022member=\u0027Notify\u0027\u0022\n );\n \n connection.OnSignal \u002B= (sender, args) =\u003E\n {\n if (args.Path?.Contains(\u0022org/gnome/desktop/interface\u0022) == true)\n {\n var newTheme = DetectGnomeTheme();\n if (newTheme != _currentTheme)\n {\n _currentTheme = newTheme;\n ThemeChanged?.Invoke(this, newTheme);\n }\n }\n };\n}\n\nprivate void MonitorKdeThemeChanges()\n{\n var configPath = Path.Combine(\n Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),\n \u0022.config\u0022,\n \u0022kdeglobals\u0022\n );\n \n // Use FileSystemWatcher to monitor config file changes\n var watcher = new FileSystemWatcher\n {\n Path = Path.GetDirectoryName(configPath),\n Filter = Path.GetFileName(configPath),\n NotifyFilter = NotifyFilters.LastWrite\n };\n \n watcher.Changed \u002B= (sender, e) =\u003E\n {\n var newTheme = DetectKdeTheme();\n if (newTheme != _currentTheme)\n {\n _currentTheme = newTheme;\n ThemeChanged?.Invoke(this, newTheme);\n }\n };\n \n watcher.EnableRaisingEvents = true;\n}\n\u0060\u0060\u0060\n\n## Applying Theme to MAUI Application\n\nWhen a theme change is detected, OpenMaui updates the MAUI application\u0027s theme:\n\n\u0060\u0060\u0060csharp\npublic class LinuxApplication : IPlatformApplication\n{\n private readonly SystemThemeService _themeService;\n \n public LinuxApplication(SystemThemeService themeService)\n {\n _themeService = themeService;\n _themeService.ThemeChanged \u002B= OnSystemThemeChanged;\n \n // Apply initial theme\n ApplyTheme(_themeService.CurrentTheme);\n }\n \n private void OnSystemThemeChanged(object sender, AppTheme newTheme)\n {\n ApplyTheme(newTheme);\n }\n \n private void ApplyTheme(AppTheme theme)\n {\n // Update MAUI application theme\n if (Application.Current != null)\n {\n Application.Current.UserAppTheme = theme;\n }\n \n // Update SkiaTheme for rendering\n SkiaTheme.Current.IsDarkMode = (theme == AppTheme.Dark);\n \n // Trigger redraw of all views\n foreach (var window in Windows)\n {\n window.InvalidateLayout();\n }\n }\n}\n\u0060\u0060\u0060\n\n## GTK Theme Integration\n\nFor native dialogs and GTK-based components, OpenMaui applies GTK CSS themes:\n\n\u0060\u0060\u0060csharp\npublic class GtkThemeService\n{\n private GtkCssProvider _cssProvider;\n \n public void ApplyTheme(AppTheme theme)\n {\n if (_cssProvider != null)\n {\n Gtk.StyleContext.RemoveProviderForScreen(\n Gdk.Screen.Default,\n _cssProvider\n );\n }\n \n _cssProvider = new GtkCssProvider();\n \n var css = theme == AppTheme.Dark\n ? GetDarkThemeCss()\n : GetLightThemeCss();\n \n _cssProvider.LoadFromData(css);\n \n Gtk.StyleContext.AddProviderForScreen(\n Gdk.Screen.Default,\n _cssProvider,\n Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION\n );\n }\n \n private string GetDarkThemeCss()\n {\n return @\u0022\n * {\n background-color: #2b2b2b;\n color: #ffffff;\n }\n button {\n background-color: #3c3c3c;\n border: 1px solid #555555;\n }\n button:hover {\n background-color: #4c4c4c;\n }\n \u0022;\n }\n}\n\u0060\u0060\u0060\n\nThis comprehensive theme detection and integration system ensures that MAUI applications on Linux feel native and respect user preferences across all major desktop environments.\r\n\r\n## Implementing Custom Platform Services\r\n\r\nWhile OpenMaui provides comprehensive platform services out of the box, there are scenarios where you might need to implement custom services or extend existing ones. This section provides practical guidance for creating your own platform services.\n\n## Creating a Custom Service Interface\n\nFirst, define a cross-platform interface in your shared MAUI project:\n\n\u0060\u0060\u0060csharp\npublic interface ICustomPlatformService\n{\n Task\u003Cstring\u003E GetSystemInformationAsync();\n Task\u003Cbool\u003E PerformPlatformSpecificOperationAsync(string parameter);\n event EventHandler\u003CPlatformEventArgs\u003E PlatformEventOccurred;\n}\n\npublic class PlatformEventArgs : EventArgs\n{\n public string EventType { get; set; }\n public object Data { get; set; }\n}\n\u0060\u0060\u0060\n\n## Implementing the Linux-Specific Service\n\nCreate a Linux-specific implementation in your platform project:\n\n\u0060\u0060\u0060csharp\npublic class LinuxCustomPlatformService : ICustomPlatformService\n{\n private readonly ILogger\u003CLinuxCustomPlatformService\u003E _logger;\n private DBusConnection _dbusConnection;\n \n public event EventHandler\u003CPlatformEventArgs\u003E PlatformEventOccurred;\n \n public LinuxCustomPlatformService(ILogger\u003CLinuxCustomPlatformService\u003E logger)\n {\n _logger = logger;\n InitializeDBusMonitoring();\n }\n \n public async Task\u003Cstring\u003E GetSystemInformationAsync()\n {\n try\n {\n // Query system information via D-Bus\n var connection = await DBusConnection.ConnectSystemBusAsync();\n \n var result = await connection.CallMethodAsync(\n \u0022org.freedesktop.hostname1\u0022,\n \u0022/org/freedesktop/hostname1\u0022,\n \u0022org.freedesktop.DBus.Properties\u0022,\n \u0022GetAll\u0022,\n new object[] { \u0022org.freedesktop.hostname1\u0022 }\n );\n \n return ProcessSystemInfo(result);\n }\n catch (Exception ex)\n {\n _logger.LogError(ex, \u0022Failed to get system information\u0022);\n return \u0022Unknown\u0022;\n }\n }\n \n public async Task\u003Cbool\u003E PerformPlatformSpecificOperationAsync(string parameter)\n {\n // Example: Execute a command-line operation\n var process = new Process\n {\n StartInfo = new ProcessStartInfo\n {\n FileName = \u0022/bin/bash\u0022,\n Arguments = $\u0022-c \\\u0022{parameter}\\\u0022\u0022,\n RedirectStandardOutput = true,\n RedirectStandardError = true,\n UseShellExecute = false\n }\n };\n \n process.Start();\n await process.WaitForExitAsync();\n \n return process.ExitCode == 0;\n }\n \n private void InitializeDBusMonitoring()\n {\n Task.Run(async () =\u003E\n {\n _dbusConnection = await DBusConnection.ConnectSessionBusAsync();\n \n await _dbusConnection.AddMatchAsync(\n \u0022type=\u0027signal\u0027,interface=\u0027org.custom.Interface\u0027\u0022\n );\n \n _dbusConnection.OnSignal \u002B= (sender, args) =\u003E\n {\n PlatformEventOccurred?.Invoke(this, new PlatformEventArgs\n {\n EventType = args.Member,\n Data = args.Parameters\n });\n };\n });\n }\n}\n\u0060\u0060\u0060\n\n## Registering the Custom Service\n\nRegister your service during application configuration:\n\n\u0060\u0060\u0060csharp\npublic static class MauiProgram\n{\n public static MauiApp CreateMauiApp()\n {\n var builder = MauiApp.CreateBuilder();\n builder\n .UseMauiApp\u003CApp\u003E()\n .ConfigureLinuxPlatform();\n \n#if LINUX\n // Register custom Linux service\n builder.Services.AddSingleton\u003CICustomPlatformService, LinuxCustomPlatformService\u003E();\n#elif WINDOWS\n builder.Services.AddSingleton\u003CICustomPlatformService, WindowsCustomPlatformService\u003E();\n#elif ANDROID || IOS\n builder.Services.AddSingleton\u003CICustomPlatformService, MobileCustomPlatformService\u003E();\n#endif\n \n return builder.Build();\n }\n}\n\u0060\u0060\u0060\n\n## Using Dependency Injection\n\nConsume your custom service through dependency injection:\n\n\u0060\u0060\u0060csharp\npublic class MainViewModel : INotifyPropertyChanged\n{\n private readonly ICustomPlatformService _platformService;\n private string _systemInfo;\n \n public MainViewModel(ICustomPlatformService platformService)\n {\n _platformService = platformService;\n _platformService.PlatformEventOccurred \u002B= OnPlatformEvent;\n \n LoadSystemInfoCommand = new Command(async () =\u003E await LoadSystemInfo());\n }\n \n public string SystemInfo\n {\n get =\u003E _systemInfo;\n set\n {\n _systemInfo = value;\n OnPropertyChanged();\n }\n }\n \n public ICommand LoadSystemInfoCommand { get; }\n \n private async Task LoadSystemInfo()\n {\n SystemInfo = await _platformService.GetSystemInformationAsync();\n }\n \n private void OnPlatformEvent(object sender, PlatformEventArgs e)\n {\n // Handle platform-specific events\n Debug.WriteLine($\u0022Platform event: {e.EventType}\u0022);\n }\n}\n\u0060\u0060\u0060\n\n## Best Practices for Custom Services\n\n### 1. Error Handling and Fallbacks\n\nAlways provide graceful fallbacks when platform features aren\u0027t available:\n\n\u0060\u0060\u0060csharp\npublic async Task\u003Cbool\u003E TryPlatformFeatureAsync()\n{\n try\n {\n // Try primary implementation\n return await PrimaryImplementation();\n }\n catch (PlatformNotSupportedException)\n {\n // Try fallback\n return await FallbackImplementation();\n }\n catch (Exception ex)\n {\n _logger.LogWarning(ex, \u0022Platform feature unavailable\u0022);\n return false;\n }\n}\n\u0060\u0060\u0060\n\n### 2. Asynchronous Operations\n\nUse async/await for operations that might block:\n\n\u0060\u0060\u0060csharp\npublic async Task\u003CT\u003E CallNativeServiceAsync\u003CT\u003E()\n{\n return await Task.Run(() =\u003E\n {\n // Long-running native operation\n return NativeInterop.PerformOperation();\n });\n}\n\u0060\u0060\u0060\n\n### 3. Resource Cleanup\n\nImplement IDisposable for services that manage native resources:\n\n\u0060\u0060\u0060csharp\npublic class LinuxCustomService : ICustomPlatformService, IDisposable\n{\n private DBusConnection _connection;\n private bool _disposed;\n \n public void Dispose()\n {\n Dispose(true);\n GC.SuppressFinalize(this);\n }\n \n protected virtual void Dispose(bool disposing)\n {\n if (!_disposed)\n {\n if (disposing)\n {\n _connection?.Dispose();\n }\n _disposed = true;\n }\n }\n}\n\u0060\u0060\u0060\n\n### 4. Thread Safety\n\nEnsure thread-safe access to shared resources:\n\n\u0060\u0060\u0060csharp\nprivate readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);\n\npublic async Task\u003Cstring\u003E ThreadSafeOperationAsync()\n{\n await _semaphore.WaitAsync();\n try\n {\n return await PerformOperation();\n }\n finally\n {\n _semaphore.Release();\n }\n}\n\u0060\u0060\u0060\n\n## Testing Custom Services\n\nCreate testable services by using interfaces and dependency injection:\n\n\u0060\u0060\u0060csharp\npublic class CustomServiceTests\n{\n [Fact]\n public async Task GetSystemInformation_ReturnsValidData()\n {\n // Arrange\n var mockLogger = new Mock\u003CILogger\u003CLinuxCustomPlatformService\u003E\u003E();\n var service = new LinuxCustomPlatformService(mockLogger.Object);\n \n // Act\n var result = await service.GetSystemInformationAsync();\n \n // Assert\n Assert.NotNull(result);\n Assert.NotEmpty(result);\n }\n \n [Fact]\n public async Task PlatformOperation_WithInvalidParameter_ReturnsFalse()\n {\n // Arrange\n var service = new LinuxCustomPlatformService(Mock.Of\u003CILogger\u003CLinuxCustomPlatformService\u003E\u003E());\n \n // Act\n var result = await service.PerformPlatformSpecificOperationAsync(\u0022invalid\u0022);\n \n // Assert\n Assert.False(result);\n }\n}\n\u0060\u0060\u0060\n\nBy following these patterns and best practices, you can extend OpenMaui\u0027s platform services to meet your application\u0027s specific needs while maintaining code quality, testability, and cross-platform compatibility. The key is to leverage the existing infrastructure\u2014dependency injection, async patterns, and error handling\u2014that OpenMaui provides as a foundation.\r\n\r\n---\r\n\r\n\u003E The platform services layer achieves 100% MAUI API compliance, allowing existing MAUI applications to run on Linux with minimal or no code changes.\r\n\r\n\u003E By leveraging xdg-desktop-portal with fallbacks to zenity and kdialog, OpenMaui ensures file picker functionality works seamlessly across GNOME, KDE, and other desktop environments.\r\n\r\n\u003E Desktop notifications via D-Bus provide a native, standards-compliant way to alert users without depending on specific desktop environment implementations.", + "createdAt": 1769750314106, + "updatedAt": 1769750314106, + "tags": [ + "series", + "generated", + "Building Linux Apps with .NET MAUI" + ], + "isArticle": true, + "seriesGroup": "Building Linux Apps with .NET MAUI", + "subtitle": "Bridging the gap between .NET MAUI APIs and Linux desktop environments with comprehensive platform services for file access, secure storage, and system integration.", + "pullQuotes": [ + "The platform services layer achieves 100% MAUI API compliance, allowing existing MAUI applications to run on Linux with minimal or no code changes.", + "By leveraging xdg-desktop-portal with fallbacks to zenity and kdialog, OpenMaui ensures file picker functionality works seamlessly across GNOME, KDE, and other desktop environments.", + "Desktop notifications via D-Bus provide a native, standards-compliant way to alert users without depending on specific desktop environment implementations." + ], + "sections": [ + { + "header": "Introduction", + "content": "When bringing .NET MAUI applications to Linux, one of the most challenging aspects is bridging the gap between MAUI\u0027s cross-platform APIs and the diverse ecosystem of Linux desktop environments. Unlike Windows or macOS, Linux doesn\u0027t have a single unified platform API\u2014instead, it offers a rich tapestry of standards, protocols, and desktop-specific implementations.\n\nThe **OpenMaui.Platform.Linux** project tackles this challenge head-on by implementing a comprehensive platform services layer that translates MAUI\u0027s high-level APIs into native Linux functionality. This layer handles everything from file system access and secure credential storage to desktop notifications and clipboard operations.\n\nWhat makes this implementation particularly powerful is its commitment to **100% MAUI API compliance**. Developers can use familiar MAUI APIs like \u0060FilePicker.PickAsync()\u0060, \u0060SecureStorage.SetAsync()\u0060, and \u0060Clipboard.SetTextAsync()\u0060 without worrying about platform-specific code. Under the hood, OpenMaui intelligently selects the best native implementation based on the user\u0027s desktop environment\u2014whether they\u0027re running GNOME, KDE Plasma, XFCE, or any other Linux desktop.\n\nIn this deep dive, we\u0027ll explore how OpenMaui implements each platform service, the technical challenges involved, and practical strategies for extending these services in your own applications." + }, + { + "header": "Platform Services Architecture", + "content": "The platform services architecture in OpenMaui follows a layered approach that prioritizes standards-based implementations while providing robust fallbacks for maximum compatibility.\n\n## The Service Layer Pattern\n\nEach platform service in OpenMaui implements the corresponding MAUI interface, ensuring seamless integration with the MAUI framework. The services are registered during application bootstrapping through the \u0060MauiAppBuilder\u0060 extension methods:\n\n\u0060\u0060\u0060csharp\npublic static MauiAppBuilder ConfigureLinuxPlatform(this MauiAppBuilder builder)\n{\n builder.Services.AddSingleton\u003CIFilePicker, LinuxFilePicker\u003E();\n builder.Services.AddSingleton\u003CIClipboard, LinuxClipboard\u003E();\n builder.Services.AddSingleton\u003CISecureStorage, LinuxSecureStorage\u003E();\n builder.Services.AddSingleton\u003CIPreferences, LinuxPreferences\u003E();\n builder.Services.AddSingleton\u003CILauncher, LinuxLauncher\u003E();\n builder.Services.AddSingleton\u003CIShare, LinuxShare\u003E();\n // ... additional services\n return builder;\n}\n\u0060\u0060\u0060\n\nThis dependency injection approach allows for easy testing, mocking, and service replacement when needed.\n\n## Desktop Environment Detection\n\nA critical component of the platform services architecture is the ability to detect and adapt to different desktop environments. OpenMaui uses environment variables and D-Bus introspection to determine the current desktop:\n\n\u0060\u0060\u0060csharp\npublic static DesktopEnvironment DetectDesktopEnvironment()\n{\n var xdgCurrentDesktop = Environment.GetEnvironmentVariable(\u0022XDG_CURRENT_DESKTOP\u0022);\n var desktopSession = Environment.GetEnvironmentVariable(\u0022DESKTOP_SESSION\u0022);\n \n if (xdgCurrentDesktop?.Contains(\u0022GNOME\u0022) == true)\n return DesktopEnvironment.Gnome;\n if (xdgCurrentDesktop?.Contains(\u0022KDE\u0022) == true)\n return DesktopEnvironment.Kde;\n if (xdgCurrentDesktop?.Contains(\u0022XFCE\u0022) == true)\n return DesktopEnvironment.Xfce;\n \n return DesktopEnvironment.Unknown;\n}\n\u0060\u0060\u0060\n\nThis detection mechanism enables OpenMaui to select the most appropriate native implementation for each service, ensuring a native look and feel regardless of the desktop environment.\n\n## Standards-First Approach\n\nWherever possible, OpenMaui prioritizes **freedesktop.org standards** like xdg-desktop-portal and D-Bus specifications. These standards provide consistent APIs across desktop environments and are actively maintained by the Linux desktop community. When standards-based implementations aren\u0027t available, OpenMaui gracefully falls back to desktop-specific tools or custom implementations." + }, + { + "header": "File System Access: xdg-portal, zenity, and kdialog", + "content": "File and folder pickers are essential platform services that allow users to browse and select files from their system. On Linux, implementing these pickers requires navigating a complex landscape of desktop-specific dialogs and standards-based portals.\n\n## The xdg-desktop-portal Standard\n\nThe modern, standards-based approach to file pickers on Linux is **xdg-desktop-portal**, a D-Bus service that provides a desktop-agnostic API for file chooser dialogs. OpenMaui\u0027s \u0060LinuxFilePicker\u0060 service attempts to use this portal first:\n\n\u0060\u0060\u0060csharp\npublic async Task\u003CFileResult\u003E PickAsync(PickOptions options = null)\n{\n try\n {\n // Try xdg-desktop-portal first (modern standard)\n if (await IsXdgPortalAvailable())\n {\n return await PickViaXdgPortal(options);\n }\n }\n catch (Exception ex)\n {\n // Log and fall back to alternative methods\n }\n \n // Fallback to desktop-specific implementations\n var desktopEnv = DetectDesktopEnvironment();\n return desktopEnv switch\n {\n DesktopEnvironment.Kde =\u003E await PickViaKDialog(options),\n DesktopEnvironment.Gnome =\u003E await PickViaZenity(options),\n _ =\u003E await PickViaZenity(options) // Zenity as universal fallback\n };\n}\n\u0060\u0060\u0060\n\nThe xdg-desktop-portal approach offers several advantages:\n- **Sandboxing support**: Works seamlessly with Flatpak and Snap applications\n- **Consistent behavior**: Same API across all desktop environments\n- **Security**: Proper permission handling for file access\n\n## Zenity: The Universal Fallback\n\n**Zenity** is a command-line tool that displays GTK\u002B dialogs from shell scripts. It\u0027s widely available across Linux distributions and provides a reliable fallback when xdg-desktop-portal isn\u0027t available:\n\n\u0060\u0060\u0060csharp\nprivate async Task\u003CFileResult\u003E PickViaZenity(PickOptions options)\n{\n var args = new List\u003Cstring\u003E { \u0022--file-selection\u0022 };\n \n if (options?.PickerTitle != null)\n args.Add($\u0022--title={options.PickerTitle}\u0022);\n \n // Add file type filters\n if (options?.FileTypes?.Value != null)\n {\n foreach (var fileType in options.FileTypes.Value)\n {\n args.Add($\u0022--file-filter={fileType}|{fileType}\u0022);\n }\n }\n \n var process = Process.Start(new ProcessStartInfo\n {\n FileName = \u0022zenity\u0022,\n Arguments = string.Join(\u0022 \u0022, args),\n RedirectStandardOutput = true,\n UseShellExecute = false\n });\n \n var output = await process.StandardOutput.ReadToEndAsync();\n await process.WaitForExitAsync();\n \n if (process.ExitCode == 0 \u0026\u0026 !string.IsNullOrWhiteSpace(output))\n {\n var filePath = output.Trim();\n return new FileResult(filePath);\n }\n \n return null; // User cancelled\n}\n\u0060\u0060\u0060\n\nZenity\u0027s simplicity makes it an excellent fallback\u2014it requires no complex D-Bus communication and works reliably across most Linux distributions.\n\n## KDialog for KDE Integration\n\nFor KDE Plasma users, **kdialog** provides a native Qt-based file picker that matches the desktop\u0027s visual style:\n\n\u0060\u0060\u0060csharp\nprivate async Task\u003CFileResult\u003E PickViaKDialog(PickOptions options)\n{\n var args = new List\u003Cstring\u003E { \u0022--getopenfilename\u0022, \u0022.\u0022, \u0022*.*\u0022 };\n \n if (options?.PickerTitle != null)\n args.Add($\u0022--title={options.PickerTitle}\u0022);\n \n var process = Process.Start(new ProcessStartInfo\n {\n FileName = \u0022kdialog\u0022,\n Arguments = string.Join(\u0022 \u0022, args),\n RedirectStandardOutput = true,\n UseShellExecute = false\n });\n \n var output = await process.StandardOutput.ReadToEndAsync();\n await process.WaitForExitAsync();\n \n if (process.ExitCode == 0 \u0026\u0026 !string.IsNullOrWhiteSpace(output))\n {\n return new FileResult(output.Trim());\n }\n \n return null;\n}\n\u0060\u0060\u0060\n\n## Folder Pickers and Multiple Selection\n\nOpenMaui\u0027s file picker implementation also supports folder selection and multiple file selection through the same fallback chain:\n\n\u0060\u0060\u0060csharp\npublic async Task\u003CFolderResult\u003E PickFolderAsync()\n{\n if (await IsXdgPortalAvailable())\n {\n return await PickFolderViaXdgPortal();\n }\n \n // Zenity folder selection\n var process = Process.Start(new ProcessStartInfo\n {\n FileName = \u0022zenity\u0022,\n Arguments = \u0022--file-selection --directory\u0022,\n RedirectStandardOutput = true,\n UseShellExecute = false\n });\n \n var output = await process.StandardOutput.ReadToEndAsync();\n await process.WaitForExitAsync();\n \n if (process.ExitCode == 0 \u0026\u0026 !string.IsNullOrWhiteSpace(output))\n {\n return new FolderResult(output.Trim());\n }\n \n return null;\n}\n\u0060\u0060\u0060\n\nBy leveraging xdg-desktop-portal with fallbacks to zenity and kdialog, OpenMaui ensures file picker functionality works seamlessly across GNOME, KDE, and other desktop environments while maintaining the familiar MAUI API surface." + }, + { + "header": "Secure Storage with libsecret", + "content": "Storing sensitive data like authentication tokens, API keys, and user credentials requires a secure, encrypted storage mechanism. On Linux, the standard solution is **libsecret**, a library that provides access to the Secret Service API\u2014a freedesktop.org specification implemented by GNOME Keyring, KWallet, and other password managers.\n\n## The Secret Service API\n\nThe Secret Service API uses D-Bus to communicate with the system\u0027s password manager. OpenMaui\u0027s \u0060LinuxSecureStorage\u0060 service implements the MAUI \u0060ISecureStorage\u0060 interface using libsecret:\n\n\u0060\u0060\u0060csharp\npublic class LinuxSecureStorage : ISecureStorage\n{\n private const string SchemaName = \u0022com.openmaui.securestorage\u0022;\n \n public async Task SetAsync(string key, string value)\n {\n if (string.IsNullOrEmpty(key))\n throw new ArgumentNullException(nameof(key));\n \n try\n {\n await StoreSecretAsync(key, value);\n }\n catch (Exception ex)\n {\n throw new SecureStorageException($\u0022Failed to store secret: {ex.Message}\u0022, ex);\n }\n }\n \n public async Task\u003Cstring\u003E GetAsync(string key)\n {\n if (string.IsNullOrEmpty(key))\n throw new ArgumentNullException(nameof(key));\n \n try\n {\n return await RetrieveSecretAsync(key);\n }\n catch (Exception ex)\n {\n throw new SecureStorageException($\u0022Failed to retrieve secret: {ex.Message}\u0022, ex);\n }\n }\n \n public bool Remove(string key)\n {\n if (string.IsNullOrEmpty(key))\n throw new ArgumentNullException(nameof(key));\n \n try\n {\n return DeleteSecret(key);\n }\n catch\n {\n return false;\n }\n }\n \n public void RemoveAll()\n {\n try\n {\n ClearAllSecrets();\n }\n catch (Exception ex)\n {\n throw new SecureStorageException($\u0022Failed to clear secrets: {ex.Message}\u0022, ex);\n }\n }\n}\n\u0060\u0060\u0060\n\n## P/Invoke Integration with libsecret\n\nUnder the hood, OpenMaui uses P/Invoke to call libsecret\u0027s native functions:\n\n\u0060\u0060\u0060csharp\ninternal static class LibSecretInterop\n{\n [DllImport(\u0022libsecret-1.so.0\u0022)]\n internal static extern IntPtr secret_password_store_sync(\n IntPtr schema,\n string collection,\n string label,\n string password,\n IntPtr cancellable,\n out IntPtr error,\n IntPtr end);\n \n [DllImport(\u0022libsecret-1.so.0\u0022)]\n internal static extern IntPtr secret_password_lookup_sync(\n IntPtr schema,\n IntPtr cancellable,\n out IntPtr error,\n IntPtr end);\n \n [DllImport(\u0022libsecret-1.so.0\u0022)]\n internal static extern bool secret_password_clear_sync(\n IntPtr schema,\n IntPtr cancellable,\n out IntPtr error,\n IntPtr end);\n \n [DllImport(\u0022libsecret-1.so.0\u0022)]\n internal static extern IntPtr secret_schema_new(\n string name,\n int flags,\n IntPtr end);\n}\n\u0060\u0060\u0060\n\n## Schema Definition and Attributes\n\nSecure storage items in libsecret are organized using schemas. OpenMaui defines a custom schema for MAUI applications:\n\n\u0060\u0060\u0060csharp\nprivate IntPtr CreateSchema()\n{\n // Create a schema with attributes for organizing secrets\n var schema = LibSecretInterop.secret_schema_new(\n SchemaName,\n (int)SecretSchemaFlags.NONE,\n IntPtr.Zero\n );\n \n return schema;\n}\n\nprivate async Task StoreSecretAsync(string key, string value)\n{\n var schema = CreateSchema();\n var label = $\u0022OpenMaui Secure Storage: {key}\u0022;\n \n IntPtr error;\n var result = LibSecretInterop.secret_password_store_sync(\n schema,\n \u0022default\u0022, // Collection name\n label,\n value,\n IntPtr.Zero, // Cancellable\n out error,\n IntPtr.Zero // Attribute terminator\n );\n \n if (error != IntPtr.Zero)\n {\n var errorMessage = Marshal.PtrToStringAnsi(error);\n throw new SecureStorageException($\u0022Failed to store secret: {errorMessage}\u0022);\n }\n}\n\u0060\u0060\u0060\n\n## Encryption and Security Considerations\n\nlibsecret provides several important security guarantees:\n\n1. **Encryption at rest**: Secrets are encrypted using the user\u0027s login credentials\n2. **Process isolation**: Only authorized processes can access stored secrets\n3. **Desktop integration**: Seamless integration with GNOME Keyring, KWallet, and other password managers\n4. **Automatic unlocking**: Secrets are automatically unlocked when the user logs in\n\n## Fallback for Headless Environments\n\nFor servers or headless environments where libsecret isn\u0027t available, OpenMaui provides a fallback implementation using encrypted file storage:\n\n\u0060\u0060\u0060csharp\nprivate bool IsLibSecretAvailable()\n{\n try\n {\n // Try to load libsecret\n var schema = CreateSchema();\n return schema != IntPtr.Zero;\n }\n catch\n {\n return false;\n }\n}\n\nprivate string GetFallbackStoragePath()\n{\n var dataDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);\n return Path.Combine(dataDir, \u0022openmaui\u0022, \u0022securestorage\u0022);\n}\n\u0060\u0060\u0060\n\nThis dual approach ensures that secure storage works reliably across different Linux configurations, from full desktop environments to containerized deployments." + }, + { + "header": "Desktop Notifications via D-Bus", + "content": "Desktop notifications are a crucial part of modern application UX, allowing apps to alert users about important events even when the application isn\u0027t in focus. On Linux, desktop notifications are standardized through the **freedesktop.org Desktop Notifications Specification**, which uses D-Bus as the communication protocol.\n\n## The Notifications D-Bus Interface\n\nThe notification system exposes a well-defined D-Bus interface at \u0060org.freedesktop.Notifications\u0060. OpenMaui\u0027s notification service communicates with this interface to display native notifications:\n\n\u0060\u0060\u0060csharp\npublic class LinuxNotificationService\n{\n private const string NotificationBusName = \u0022org.freedesktop.Notifications\u0022;\n private const string NotificationObjectPath = \u0022/org/freedesktop/Notifications\u0022;\n private const string NotificationInterface = \u0022org.freedesktop.Notifications\u0022;\n \n public async Task ShowNotificationAsync(\n string title,\n string message,\n string icon = null,\n int timeout = -1)\n {\n try\n {\n var notificationId = await SendNotificationViaDBus(\n title,\n message,\n icon,\n timeout\n );\n }\n catch (Exception ex)\n {\n // Fallback to alternative notification methods\n await ShowFallbackNotification(title, message);\n }\n }\n}\n\u0060\u0060\u0060\n\n## Sending Notifications via D-Bus\n\nThe notification service uses the \u0060Notify\u0060 method of the D-Bus interface, which accepts several parameters:\n\n\u0060\u0060\u0060csharp\nprivate async Task\u003Cuint\u003E SendNotificationViaDBus(\n string title,\n string message,\n string icon,\n int timeout)\n{\n var connection = await DBusConnection.ConnectSessionBusAsync();\n \n var parameters = new object[]\n {\n \u0022OpenMaui\u0022, // Application name\n 0u, // Replaces ID (0 for new notification)\n icon ?? \u0022\u0022, // Icon name or path\n title, // Summary\n message, // Body\n new string[0], // Actions\n new Dictionary\u003Cstring, object\u003E(), // Hints\n timeout // Timeout (-1 for default)\n };\n \n var result = await connection.CallMethodAsync(\n NotificationBusName,\n NotificationObjectPath,\n NotificationInterface,\n \u0022Notify\u0022,\n parameters\n );\n \n return (uint)result;\n}\n\u0060\u0060\u0060\n\n## Notification Actions and Callbacks\n\nThe Desktop Notifications Specification supports interactive notifications with action buttons. OpenMaui exposes this functionality through the MAUI API:\n\n\u0060\u0060\u0060csharp\npublic async Task\u003CNotificationAction\u003E ShowNotificationWithActionsAsync(\n string title,\n string message,\n params NotificationAction[] actions)\n{\n var actionKeys = actions.Select(a =\u003E a.Id).ToArray();\n var actionLabels = actions.Select(a =\u003E a.Title).ToArray();\n \n // Flatten actions array for D-Bus (key, label, key, label, ...)\n var actionArray = new List\u003Cstring\u003E();\n for (int i = 0; i \u003C actions.Length; i\u002B\u002B)\n {\n actionArray.Add(actionKeys[i]);\n actionArray.Add(actionLabels[i]);\n }\n \n var parameters = new object[]\n {\n \u0022OpenMaui\u0022,\n 0u,\n \u0022\u0022,\n title,\n message,\n actionArray.ToArray(),\n new Dictionary\u003Cstring, object\u003E(),\n -1\n };\n \n var notificationId = await SendNotificationAsync(parameters);\n \n // Listen for action invoked signal\n var actionInvoked = await WaitForActionInvokedAsync(notificationId);\n return actions.FirstOrDefault(a =\u003E a.Id == actionInvoked);\n}\n\u0060\u0060\u0060\n\n## Handling Notification Signals\n\nWhen users interact with notifications, the notification daemon sends D-Bus signals back to the application:\n\n\u0060\u0060\u0060csharp\nprivate async Task\u003Cstring\u003E WaitForActionInvokedAsync(uint notificationId)\n{\n var connection = await DBusConnection.ConnectSessionBusAsync();\n var tcs = new TaskCompletionSource\u003Cstring\u003E();\n \n // Subscribe to ActionInvoked signal\n await connection.AddMatchAsync(\n $\u0022type=\u0027signal\u0027,interface=\u0027{NotificationInterface}\u0027,member=\u0027ActionInvoked\u0027\u0022\n );\n \n connection.OnSignal \u002B= (sender, args) =\u003E\n {\n if (args.Member == \u0022ActionInvoked\u0022)\n {\n var id = (uint)args.Parameters[0];\n var actionKey = (string)args.Parameters[1];\n \n if (id == notificationId)\n {\n tcs.TrySetResult(actionKey);\n }\n }\n };\n \n return await tcs.Task;\n}\n\u0060\u0060\u0060\n\n## Notification Hints and Capabilities\n\nThe Desktop Notifications Specification supports various hints to customize notification appearance and behavior:\n\n\u0060\u0060\u0060csharp\nprivate Dictionary\u003Cstring, object\u003E CreateNotificationHints(\n NotificationPriority priority,\n string category,\n bool resident)\n{\n var hints = new Dictionary\u003Cstring, object\u003E();\n \n // Urgency level (0=low, 1=normal, 2=critical)\n hints[\u0022urgency\u0022] = priority switch\n {\n NotificationPriority.Low =\u003E (byte)0,\n NotificationPriority.High =\u003E (byte)2,\n _ =\u003E (byte)1\n };\n \n // Category for grouping notifications\n if (!string.IsNullOrEmpty(category))\n hints[\u0022category\u0022] = category;\n \n // Resident notifications stay in notification center\n if (resident)\n hints[\u0022resident\u0022] = true;\n \n // Desktop entry for proper app identification\n hints[\u0022desktop-entry\u0022] = \u0022com.openmaui.app\u0022;\n \n return hints;\n}\n\u0060\u0060\u0060\n\n## Querying Notification Capabilities\n\nDifferent notification daemons support different features. OpenMaui queries capabilities to adapt behavior:\n\n\u0060\u0060\u0060csharp\npublic async Task\u003Cstring[]\u003E GetServerCapabilitiesAsync()\n{\n var connection = await DBusConnection.ConnectSessionBusAsync();\n \n var result = await connection.CallMethodAsync(\n NotificationBusName,\n NotificationObjectPath,\n NotificationInterface,\n \u0022GetCapabilities\u0022,\n Array.Empty\u003Cobject\u003E()\n );\n \n return (string[])result;\n}\n\npublic async Task\u003Cbool\u003E SupportsActionsAsync()\n{\n var capabilities = await GetServerCapabilitiesAsync();\n return capabilities.Contains(\u0022actions\u0022);\n}\n\u0060\u0060\u0060\n\nCommon capabilities include:\n- \u0060actions\u0060: Support for action buttons\n- \u0060body\u0060: Support for notification body text\n- \u0060body-markup\u0060: Support for HTML markup in body\n- \u0060icon-static\u0060: Support for static icons\n- \u0060persistence\u0060: Notifications persist in notification center\n- \u0060sound\u0060: Support for notification sounds\n\nBy using the standard D-Bus notification interface, OpenMaui ensures that notifications work consistently across all Linux desktop environments while respecting each desktop\u0027s visual style and user preferences." + }, + { + "header": "Clipboard and Share Functionality", + "content": "Clipboard operations and content sharing are fundamental platform services that enable data exchange between applications. On Linux, these services require different approaches depending on the display server (X11 vs. Wayland) and desktop environment.\n\n## Clipboard Architecture\n\nLinux clipboard systems are more complex than other platforms, supporting multiple selection types:\n\n1. **PRIMARY**: Text selected with the mouse (middle-click paste)\n2. **CLIPBOARD**: Explicit copy/paste operations (Ctrl\u002BC/Ctrl\u002BV)\n3. **SECONDARY**: Rarely used selection buffer\n\nOpenMaui\u0027s \u0060LinuxClipboard\u0060 service focuses on the CLIPBOARD selection, which matches user expectations from other platforms:\n\n\u0060\u0060\u0060csharp\npublic class LinuxClipboard : IClipboard\n{\n private readonly IDisplayServer _displayServer;\n \n public LinuxClipboard(IDisplayServer displayServer)\n {\n _displayServer = displayServer;\n }\n \n public async Task SetTextAsync(string text)\n {\n if (_displayServer is X11DisplayServer x11)\n {\n await SetTextViaX11(x11, text);\n }\n else if (_displayServer is WaylandDisplayServer wayland)\n {\n await SetTextViaWayland(wayland, text);\n }\n else\n {\n // Fallback to xclip/xsel command-line tools\n await SetTextViaCommandLine(text);\n }\n }\n \n public async Task\u003Cstring\u003E GetTextAsync()\n {\n if (_displayServer is X11DisplayServer x11)\n {\n return await GetTextViaX11(x11);\n }\n else if (_displayServer is WaylandDisplayServer wayland)\n {\n return await GetTextViaWayland(wayland);\n }\n else\n {\n return await GetTextViaCommandLine();\n }\n }\n \n public bool HasText =\u003E !string.IsNullOrEmpty(GetTextAsync().Result);\n}\n\u0060\u0060\u0060\n\n## X11 Clipboard Implementation\n\nOn X11, clipboard operations use the X Selection mechanism with property notifications:\n\n\u0060\u0060\u0060csharp\nprivate async Task SetTextViaX11(X11DisplayServer x11, string text)\n{\n var display = x11.Display;\n var window = x11.RootWindow;\n \n // Get CLIPBOARD atom\n var clipboardAtom = X11Interop.XInternAtom(\n display,\n \u0022CLIPBOARD\u0022,\n false\n );\n \n // Get UTF8_STRING atom for text encoding\n var utf8Atom = X11Interop.XInternAtom(\n display,\n \u0022UTF8_STRING\u0022,\n false\n );\n \n // Set selection owner\n X11Interop.XSetSelectionOwner(\n display,\n clipboardAtom,\n window,\n X11Interop.CurrentTime\n );\n \n // Store text data\n _clipboardData = text;\n _clipboardEncoding = utf8Atom;\n \n // Handle selection requests from other applications\n x11.OnSelectionRequest \u002B= HandleSelectionRequest;\n}\n\nprivate void HandleSelectionRequest(object sender, X11SelectionRequestEvent e)\n{\n if (e.Selection == _clipboardAtom \u0026\u0026 _clipboardData != null)\n {\n // Convert text to requested format\n var data = Encoding.UTF8.GetBytes(_clipboardData);\n \n // Send selection notify event\n X11Interop.XChangeProperty(\n e.Display,\n e.Requestor,\n e.Property,\n _clipboardEncoding,\n 8, // 8-bit data\n PropMode.Replace,\n data,\n data.Length\n );\n \n X11Interop.XSendEvent(\n e.Display,\n e.Requestor,\n false,\n 0,\n CreateSelectionNotifyEvent(e)\n );\n }\n}\n\u0060\u0060\u0060\n\n## Wayland Clipboard Implementation\n\nWayland uses a different approach with data device managers and offers:\n\n\u0060\u0060\u0060csharp\nprivate async Task SetTextViaWayland(WaylandDisplayServer wayland, string text)\n{\n var dataDeviceManager = wayland.DataDeviceManager;\n var seat = wayland.Seat;\n \n // Create data source\n var dataSource = dataDeviceManager.CreateDataSource();\n \n // Offer text/plain mime type\n dataSource.Offer(\u0022text/plain;charset=utf-8\u0022);\n dataSource.Offer(\u0022text/plain\u0022);\n dataSource.Offer(\u0022TEXT\u0022);\n dataSource.Offer(\u0022STRING\u0022);\n \n // Handle send requests\n dataSource.OnSend \u002B= (mimeType, fd) =\u003E\n {\n var bytes = Encoding.UTF8.GetBytes(text);\n using var stream = new UnixStream(fd);\n stream.Write(bytes, 0, bytes.Length);\n stream.Flush();\n };\n \n // Set selection\n var dataDevice = dataDeviceManager.GetDataDevice(seat);\n dataDevice.SetSelection(dataSource, wayland.Serial);\n}\n\u0060\u0060\u0060\n\n## Command-Line Fallback\n\nFor maximum compatibility, OpenMaui provides a fallback using command-line tools:\n\n\u0060\u0060\u0060csharp\nprivate async Task SetTextViaCommandLine(string text)\n{\n // Try xclip first (most common)\n if (await IsCommandAvailable(\u0022xclip\u0022))\n {\n await RunCommand(\u0022xclip\u0022, \u0022-selection clipboard\u0022, text);\n return;\n }\n \n // Fall back to xsel\n if (await IsCommandAvailable(\u0022xsel\u0022))\n {\n await RunCommand(\u0022xsel\u0022, \u0022--clipboard --input\u0022, text);\n return;\n }\n \n throw new PlatformNotSupportedException(\n \u0022No clipboard mechanism available. Install xclip or xsel.\u0022\n );\n}\n\nprivate async Task\u003Cstring\u003E GetTextViaCommandLine()\n{\n if (await IsCommandAvailable(\u0022xclip\u0022))\n {\n return await RunCommandOutput(\u0022xclip\u0022, \u0022-selection clipboard -o\u0022);\n }\n \n if (await IsCommandAvailable(\u0022xsel\u0022))\n {\n return await RunCommandOutput(\u0022xsel\u0022, \u0022--clipboard --output\u0022);\n }\n \n return null;\n}\n\u0060\u0060\u0060\n\n## Share Functionality\n\nThe share service allows applications to share content with other applications. On Linux, this is typically implemented using xdg-open or desktop-specific sharing mechanisms:\n\n\u0060\u0060\u0060csharp\npublic class LinuxShare : IShare\n{\n public async Task RequestAsync(ShareTextRequest request)\n {\n // Try desktop-specific share dialogs first\n if (await TryDesktopShare(request))\n return;\n \n // Fallback: copy to clipboard and notify user\n await Clipboard.SetTextAsync(request.Text);\n await ShowNotification(\n \u0022Content Copied\u0022,\n \u0022Content has been copied to clipboard\u0022\n );\n }\n \n public async Task RequestAsync(ShareFileRequest request)\n {\n var filePath = request.File.FullPath;\n \n // Use xdg-open to open file with default handler\n var process = Process.Start(new ProcessStartInfo\n {\n FileName = \u0022xdg-open\u0022,\n Arguments = $\u0022\\\u0022{filePath}\\\u0022\u0022,\n UseShellExecute = false\n });\n \n await process.WaitForExitAsync();\n }\n \n private async Task\u003Cbool\u003E TryDesktopShare(ShareTextRequest request)\n {\n var desktopEnv = DetectDesktopEnvironment();\n \n return desktopEnv switch\n {\n DesktopEnvironment.Gnome =\u003E await ShareViaGnome(request),\n DesktopEnvironment.Kde =\u003E await ShareViaKde(request),\n _ =\u003E false\n };\n }\n}\n\u0060\u0060\u0060\n\nThe clipboard and share implementations demonstrate OpenMaui\u0027s commitment to working across different Linux configurations while providing a consistent API surface for MAUI developers." + }, + { + "header": "System Theme Detection and Integration", + "content": "Modern applications need to respect user theme preferences, particularly the increasingly popular dark mode. On Linux, theme detection requires querying desktop-specific settings and monitoring for changes.\n\n## The SystemThemeService\n\nOpenMaui\u0027s \u0060SystemThemeService\u0060 provides a unified API for theme detection across different desktop environments:\n\n\u0060\u0060\u0060csharp\npublic class SystemThemeService\n{\n private AppTheme _currentTheme;\n private readonly IDesktopEnvironmentDetector _desktopDetector;\n \n public event EventHandler\u003CAppTheme\u003E ThemeChanged;\n \n public SystemThemeService(IDesktopEnvironmentDetector desktopDetector)\n {\n _desktopDetector = desktopDetector;\n _currentTheme = DetectCurrentTheme();\n StartThemeMonitoring();\n }\n \n public AppTheme CurrentTheme =\u003E _currentTheme;\n \n private AppTheme DetectCurrentTheme()\n {\n var desktop = _desktopDetector.DetectDesktopEnvironment();\n \n return desktop switch\n {\n DesktopEnvironment.Gnome =\u003E DetectGnomeTheme(),\n DesktopEnvironment.Kde =\u003E DetectKdeTheme(),\n DesktopEnvironment.Xfce =\u003E DetectXfceTheme(),\n _ =\u003E DetectFallbackTheme()\n };\n }\n}\n\u0060\u0060\u0060\n\n## GNOME Theme Detection\n\nGNOME stores theme preferences in GSettings (dconf), which can be queried via D-Bus or the gsettings command:\n\n\u0060\u0060\u0060csharp\nprivate AppTheme DetectGnomeTheme()\n{\n try\n {\n // Query via D-Bus for better performance\n var theme = QueryGSettingsViaDBus(\n \u0022org.gnome.desktop.interface\u0022,\n \u0022gtk-theme\u0022\n );\n \n // Check color scheme preference (GNOME 42\u002B)\n var colorScheme = QueryGSettingsViaDBus(\n \u0022org.gnome.desktop.interface\u0022,\n \u0022color-scheme\u0022\n );\n \n if (colorScheme == \u0022prefer-dark\u0022)\n return AppTheme.Dark;\n \n // Fallback: check if theme name contains \u0022dark\u0022\n if (theme?.Contains(\u0022dark\u0022, StringComparison.OrdinalIgnoreCase) == true)\n return AppTheme.Dark;\n \n return AppTheme.Light;\n }\n catch\n {\n return DetectFallbackTheme();\n }\n}\n\nprivate string QueryGSettingsViaDBus(string schema, string key)\n{\n var connection = DBusConnection.ConnectSessionBus();\n \n var result = connection.CallMethod(\n \u0022ca.desrt.dconf\u0022,\n \u0022/ca/desrt/dconf/Writer/user\u0022,\n \u0022ca.desrt.dconf.Writer\u0022,\n \u0022Read\u0022,\n new object[] { $\u0022/{schema.Replace(\u0027.\u0027, \u0027/\u0027)}/{key}\u0022 }\n );\n \n return result?.ToString();\n}\n\u0060\u0060\u0060\n\n## KDE Plasma Theme Detection\n\nKDE Plasma stores theme settings in configuration files that can be read directly:\n\n\u0060\u0060\u0060csharp\nprivate AppTheme DetectKdeTheme()\n{\n try\n {\n var configPath = Path.Combine(\n Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),\n \u0022.config\u0022,\n \u0022kdeglobals\u0022\n );\n \n if (!File.Exists(configPath))\n return AppTheme.Light;\n \n var config = File.ReadAllText(configPath);\n \n // Look for color scheme setting\n var colorSchemeMatch = Regex.Match(\n config,\n @\u0022ColorScheme=(.\u002B)$\u0022,\n RegexOptions.Multiline\n );\n \n if (colorSchemeMatch.Success)\n {\n var colorScheme = colorSchemeMatch.Groups[1].Value.Trim();\n if (colorScheme.Contains(\u0022Dark\u0022, StringComparison.OrdinalIgnoreCase))\n return AppTheme.Dark;\n }\n \n // Check LookAndFeelPackage for dark themes\n var lafMatch = Regex.Match(\n config,\n @\u0022LookAndFeelPackage=(.\u002B)$\u0022,\n RegexOptions.Multiline\n );\n \n if (lafMatch.Success)\n {\n var laf = lafMatch.Groups[1].Value.Trim();\n if (laf.Contains(\u0022dark\u0022, StringComparison.OrdinalIgnoreCase))\n return AppTheme.Dark;\n }\n \n return AppTheme.Light;\n }\n catch\n {\n return AppTheme.Light;\n }\n}\n\u0060\u0060\u0060\n\n## Theme Change Monitoring\n\nTo respond to theme changes in real-time, OpenMaui monitors the appropriate configuration sources:\n\n\u0060\u0060\u0060csharp\nprivate void StartThemeMonitoring()\n{\n var desktop = _desktopDetector.DetectDesktopEnvironment();\n \n switch (desktop)\n {\n case DesktopEnvironment.Gnome:\n MonitorGnomeThemeChanges();\n break;\n case DesktopEnvironment.Kde:\n MonitorKdeThemeChanges();\n break;\n default:\n MonitorFallbackThemeChanges();\n break;\n }\n}\n\nprivate void MonitorGnomeThemeChanges()\n{\n // Subscribe to GSettings changes via D-Bus\n var connection = DBusConnection.ConnectSessionBus();\n \n connection.AddMatch(\n \u0022type=\u0027signal\u0027,\u0022\n \u002B \u0022interface=\u0027ca.desrt.dconf.Writer\u0027,\u0022\n \u002B \u0022member=\u0027Notify\u0027\u0022\n );\n \n connection.OnSignal \u002B= (sender, args) =\u003E\n {\n if (args.Path?.Contains(\u0022org/gnome/desktop/interface\u0022) == true)\n {\n var newTheme = DetectGnomeTheme();\n if (newTheme != _currentTheme)\n {\n _currentTheme = newTheme;\n ThemeChanged?.Invoke(this, newTheme);\n }\n }\n };\n}\n\nprivate void MonitorKdeThemeChanges()\n{\n var configPath = Path.Combine(\n Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),\n \u0022.config\u0022,\n \u0022kdeglobals\u0022\n );\n \n // Use FileSystemWatcher to monitor config file changes\n var watcher = new FileSystemWatcher\n {\n Path = Path.GetDirectoryName(configPath),\n Filter = Path.GetFileName(configPath),\n NotifyFilter = NotifyFilters.LastWrite\n };\n \n watcher.Changed \u002B= (sender, e) =\u003E\n {\n var newTheme = DetectKdeTheme();\n if (newTheme != _currentTheme)\n {\n _currentTheme = newTheme;\n ThemeChanged?.Invoke(this, newTheme);\n }\n };\n \n watcher.EnableRaisingEvents = true;\n}\n\u0060\u0060\u0060\n\n## Applying Theme to MAUI Application\n\nWhen a theme change is detected, OpenMaui updates the MAUI application\u0027s theme:\n\n\u0060\u0060\u0060csharp\npublic class LinuxApplication : IPlatformApplication\n{\n private readonly SystemThemeService _themeService;\n \n public LinuxApplication(SystemThemeService themeService)\n {\n _themeService = themeService;\n _themeService.ThemeChanged \u002B= OnSystemThemeChanged;\n \n // Apply initial theme\n ApplyTheme(_themeService.CurrentTheme);\n }\n \n private void OnSystemThemeChanged(object sender, AppTheme newTheme)\n {\n ApplyTheme(newTheme);\n }\n \n private void ApplyTheme(AppTheme theme)\n {\n // Update MAUI application theme\n if (Application.Current != null)\n {\n Application.Current.UserAppTheme = theme;\n }\n \n // Update SkiaTheme for rendering\n SkiaTheme.Current.IsDarkMode = (theme == AppTheme.Dark);\n \n // Trigger redraw of all views\n foreach (var window in Windows)\n {\n window.InvalidateLayout();\n }\n }\n}\n\u0060\u0060\u0060\n\n## GTK Theme Integration\n\nFor native dialogs and GTK-based components, OpenMaui applies GTK CSS themes:\n\n\u0060\u0060\u0060csharp\npublic class GtkThemeService\n{\n private GtkCssProvider _cssProvider;\n \n public void ApplyTheme(AppTheme theme)\n {\n if (_cssProvider != null)\n {\n Gtk.StyleContext.RemoveProviderForScreen(\n Gdk.Screen.Default,\n _cssProvider\n );\n }\n \n _cssProvider = new GtkCssProvider();\n \n var css = theme == AppTheme.Dark\n ? GetDarkThemeCss()\n : GetLightThemeCss();\n \n _cssProvider.LoadFromData(css);\n \n Gtk.StyleContext.AddProviderForScreen(\n Gdk.Screen.Default,\n _cssProvider,\n Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION\n );\n }\n \n private string GetDarkThemeCss()\n {\n return @\u0022\n * {\n background-color: #2b2b2b;\n color: #ffffff;\n }\n button {\n background-color: #3c3c3c;\n border: 1px solid #555555;\n }\n button:hover {\n background-color: #4c4c4c;\n }\n \u0022;\n }\n}\n\u0060\u0060\u0060\n\nThis comprehensive theme detection and integration system ensures that MAUI applications on Linux feel native and respect user preferences across all major desktop environments." + }, + { + "header": "Implementing Custom Platform Services", + "content": "While OpenMaui provides comprehensive platform services out of the box, there are scenarios where you might need to implement custom services or extend existing ones. This section provides practical guidance for creating your own platform services.\n\n## Creating a Custom Service Interface\n\nFirst, define a cross-platform interface in your shared MAUI project:\n\n\u0060\u0060\u0060csharp\npublic interface ICustomPlatformService\n{\n Task\u003Cstring\u003E GetSystemInformationAsync();\n Task\u003Cbool\u003E PerformPlatformSpecificOperationAsync(string parameter);\n event EventHandler\u003CPlatformEventArgs\u003E PlatformEventOccurred;\n}\n\npublic class PlatformEventArgs : EventArgs\n{\n public string EventType { get; set; }\n public object Data { get; set; }\n}\n\u0060\u0060\u0060\n\n## Implementing the Linux-Specific Service\n\nCreate a Linux-specific implementation in your platform project:\n\n\u0060\u0060\u0060csharp\npublic class LinuxCustomPlatformService : ICustomPlatformService\n{\n private readonly ILogger\u003CLinuxCustomPlatformService\u003E _logger;\n private DBusConnection _dbusConnection;\n \n public event EventHandler\u003CPlatformEventArgs\u003E PlatformEventOccurred;\n \n public LinuxCustomPlatformService(ILogger\u003CLinuxCustomPlatformService\u003E logger)\n {\n _logger = logger;\n InitializeDBusMonitoring();\n }\n \n public async Task\u003Cstring\u003E GetSystemInformationAsync()\n {\n try\n {\n // Query system information via D-Bus\n var connection = await DBusConnection.ConnectSystemBusAsync();\n \n var result = await connection.CallMethodAsync(\n \u0022org.freedesktop.hostname1\u0022,\n \u0022/org/freedesktop/hostname1\u0022,\n \u0022org.freedesktop.DBus.Properties\u0022,\n \u0022GetAll\u0022,\n new object[] { \u0022org.freedesktop.hostname1\u0022 }\n );\n \n return ProcessSystemInfo(result);\n }\n catch (Exception ex)\n {\n _logger.LogError(ex, \u0022Failed to get system information\u0022);\n return \u0022Unknown\u0022;\n }\n }\n \n public async Task\u003Cbool\u003E PerformPlatformSpecificOperationAsync(string parameter)\n {\n // Example: Execute a command-line operation\n var process = new Process\n {\n StartInfo = new ProcessStartInfo\n {\n FileName = \u0022/bin/bash\u0022,\n Arguments = $\u0022-c \\\u0022{parameter}\\\u0022\u0022,\n RedirectStandardOutput = true,\n RedirectStandardError = true,\n UseShellExecute = false\n }\n };\n \n process.Start();\n await process.WaitForExitAsync();\n \n return process.ExitCode == 0;\n }\n \n private void InitializeDBusMonitoring()\n {\n Task.Run(async () =\u003E\n {\n _dbusConnection = await DBusConnection.ConnectSessionBusAsync();\n \n await _dbusConnection.AddMatchAsync(\n \u0022type=\u0027signal\u0027,interface=\u0027org.custom.Interface\u0027\u0022\n );\n \n _dbusConnection.OnSignal \u002B= (sender, args) =\u003E\n {\n PlatformEventOccurred?.Invoke(this, new PlatformEventArgs\n {\n EventType = args.Member,\n Data = args.Parameters\n });\n };\n });\n }\n}\n\u0060\u0060\u0060\n\n## Registering the Custom Service\n\nRegister your service during application configuration:\n\n\u0060\u0060\u0060csharp\npublic static class MauiProgram\n{\n public static MauiApp CreateMauiApp()\n {\n var builder = MauiApp.CreateBuilder();\n builder\n .UseMauiApp\u003CApp\u003E()\n .ConfigureLinuxPlatform();\n \n#if LINUX\n // Register custom Linux service\n builder.Services.AddSingleton\u003CICustomPlatformService, LinuxCustomPlatformService\u003E();\n#elif WINDOWS\n builder.Services.AddSingleton\u003CICustomPlatformService, WindowsCustomPlatformService\u003E();\n#elif ANDROID || IOS\n builder.Services.AddSingleton\u003CICustomPlatformService, MobileCustomPlatformService\u003E();\n#endif\n \n return builder.Build();\n }\n}\n\u0060\u0060\u0060\n\n## Using Dependency Injection\n\nConsume your custom service through dependency injection:\n\n\u0060\u0060\u0060csharp\npublic class MainViewModel : INotifyPropertyChanged\n{\n private readonly ICustomPlatformService _platformService;\n private string _systemInfo;\n \n public MainViewModel(ICustomPlatformService platformService)\n {\n _platformService = platformService;\n _platformService.PlatformEventOccurred \u002B= OnPlatformEvent;\n \n LoadSystemInfoCommand = new Command(async () =\u003E await LoadSystemInfo());\n }\n \n public string SystemInfo\n {\n get =\u003E _systemInfo;\n set\n {\n _systemInfo = value;\n OnPropertyChanged();\n }\n }\n \n public ICommand LoadSystemInfoCommand { get; }\n \n private async Task LoadSystemInfo()\n {\n SystemInfo = await _platformService.GetSystemInformationAsync();\n }\n \n private void OnPlatformEvent(object sender, PlatformEventArgs e)\n {\n // Handle platform-specific events\n Debug.WriteLine($\u0022Platform event: {e.EventType}\u0022);\n }\n}\n\u0060\u0060\u0060\n\n## Best Practices for Custom Services\n\n### 1. Error Handling and Fallbacks\n\nAlways provide graceful fallbacks when platform features aren\u0027t available:\n\n\u0060\u0060\u0060csharp\npublic async Task\u003Cbool\u003E TryPlatformFeatureAsync()\n{\n try\n {\n // Try primary implementation\n return await PrimaryImplementation();\n }\n catch (PlatformNotSupportedException)\n {\n // Try fallback\n return await FallbackImplementation();\n }\n catch (Exception ex)\n {\n _logger.LogWarning(ex, \u0022Platform feature unavailable\u0022);\n return false;\n }\n}\n\u0060\u0060\u0060\n\n### 2. Asynchronous Operations\n\nUse async/await for operations that might block:\n\n\u0060\u0060\u0060csharp\npublic async Task\u003CT\u003E CallNativeServiceAsync\u003CT\u003E()\n{\n return await Task.Run(() =\u003E\n {\n // Long-running native operation\n return NativeInterop.PerformOperation();\n });\n}\n\u0060\u0060\u0060\n\n### 3. Resource Cleanup\n\nImplement IDisposable for services that manage native resources:\n\n\u0060\u0060\u0060csharp\npublic class LinuxCustomService : ICustomPlatformService, IDisposable\n{\n private DBusConnection _connection;\n private bool _disposed;\n \n public void Dispose()\n {\n Dispose(true);\n GC.SuppressFinalize(this);\n }\n \n protected virtual void Dispose(bool disposing)\n {\n if (!_disposed)\n {\n if (disposing)\n {\n _connection?.Dispose();\n }\n _disposed = true;\n }\n }\n}\n\u0060\u0060\u0060\n\n### 4. Thread Safety\n\nEnsure thread-safe access to shared resources:\n\n\u0060\u0060\u0060csharp\nprivate readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);\n\npublic async Task\u003Cstring\u003E ThreadSafeOperationAsync()\n{\n await _semaphore.WaitAsync();\n try\n {\n return await PerformOperation();\n }\n finally\n {\n _semaphore.Release();\n }\n}\n\u0060\u0060\u0060\n\n## Testing Custom Services\n\nCreate testable services by using interfaces and dependency injection:\n\n\u0060\u0060\u0060csharp\npublic class CustomServiceTests\n{\n [Fact]\n public async Task GetSystemInformation_ReturnsValidData()\n {\n // Arrange\n var mockLogger = new Mock\u003CILogger\u003CLinuxCustomPlatformService\u003E\u003E();\n var service = new LinuxCustomPlatformService(mockLogger.Object);\n \n // Act\n var result = await service.GetSystemInformationAsync();\n \n // Assert\n Assert.NotNull(result);\n Assert.NotEmpty(result);\n }\n \n [Fact]\n public async Task PlatformOperation_WithInvalidParameter_ReturnsFalse()\n {\n // Arrange\n var service = new LinuxCustomPlatformService(Mock.Of\u003CILogger\u003CLinuxCustomPlatformService\u003E\u003E());\n \n // Act\n var result = await service.PerformPlatformSpecificOperationAsync(\u0022invalid\u0022);\n \n // Assert\n Assert.False(result);\n }\n}\n\u0060\u0060\u0060\n\nBy following these patterns and best practices, you can extend OpenMaui\u0027s platform services to meet your application\u0027s specific needs while maintaining code quality, testability, and cross-platform compatibility. The key is to leverage the existing infrastructure\u2014dependency injection, async patterns, and error handling\u2014that OpenMaui provides as a foundation." + } + ], + "generatedAt": 1769750314106 +} \ No newline at end of file diff --git a/.notes/series-1769750550451-66bac3.json b/.notes/series-1769750550451-66bac3.json new file mode 100644 index 0000000..b055ed8 --- /dev/null +++ b/.notes/series-1769750550451-66bac3.json @@ -0,0 +1,55 @@ +{ + "id": "series-1769750550451-66bac3", + "title": "Building Accessible Linux Applications: AT-SPI2 and Screen Reader Support", + "content": "# Building Accessible Linux Applications: AT-SPI2 and Screen Reader Support\r\n\r\n*Make your .NET MAUI Linux applications inclusive with comprehensive accessibility features, from screen readers to keyboard navigation.*\r\n\r\n## Introduction\r\n\r\nWhen building desktop applications for Linux, accessibility is not just a nice-to-have feature\u2014it\u0027s essential for creating inclusive software that serves all users. The OpenMaui.Platform.Linux project demonstrates how .NET MAUI applications can achieve full accessibility compliance on Linux through the **AT-SPI2** (Assistive Technology Service Provider Interface) framework.\n\nLinux desktop environments rely on AT-SPI2 to provide a standardized accessibility layer that screen readers like Orca, magnifiers, and other assistive technologies use to interact with applications. For developers building cross-platform .NET MAUI applications, understanding how to integrate with AT-SPI2 ensures that your Linux users\u2014including those with visual, motor, or cognitive disabilities\u2014can fully access your application\u0027s functionality.\n\nThis article explores the practical implementation of accessibility features in the OpenMaui Linux platform, covering everything from the foundational \u0060IAccessible\u0060 interface to testing your application with the Orca screen reader. Whether you\u0027re porting an existing MAUI application to Linux or building a new one from scratch, these patterns will help you create applications that are accessible by design.\n\n**Why Accessibility Matters:**\n- **Legal compliance**: Many jurisdictions require digital accessibility\n- **Market reach**: Approximately 15% of the global population has some form of disability\n- **Better UX for everyone**: Accessible design benefits all users, not just those with disabilities\n- **Technical excellence**: Proper accessibility implementation often reveals and fixes underlying architectural issues\r\n\r\n## Accessibility on Linux: AT-SPI2 Overview\r\n\r\nAT-SPI2 (Assistive Technology Service Provider Interface, version 2) is the de facto standard for accessibility on Linux desktop environments. It provides a D-Bus-based protocol that allows assistive technologies to query and manipulate application UI elements programmatically.\n\n## How AT-SPI2 Works\n\nAT-SPI2 operates on a client-server model:\n\n1. **Application (Server)**: Your application exposes its UI hierarchy through the AT-SPI2 protocol\n2. **Assistive Technology (Client)**: Screen readers like Orca, magnifiers, or voice control systems connect to your application via D-Bus\n3. **Registry**: The \u0060at-spi2-registryd\u0060 daemon manages connections between applications and assistive technologies\n\n## Key Concepts\n\n**Accessible Objects**: Every UI element in your application that should be accessible must be represented as an accessible object with:\n- **Role**: What the element is (button, text field, label, etc.)\n- **State**: Current condition (focused, checked, enabled, visible, etc.)\n- **Properties**: Name, description, value, and other metadata\n- **Actions**: Operations that can be performed (click, focus, toggle, etc.)\n- **Relations**: Connections to other accessible objects (labels for fields, etc.)\n\n**Accessibility Tree**: Similar to the DOM in web browsers, AT-SPI2 maintains a hierarchical tree of accessible objects that mirrors your application\u0027s UI structure.\n\n## Integration in OpenMaui\n\nThe OpenMaui.Platform.Linux implementation integrates AT-SPI2 through the \u0060SkiaView\u0060 base class, which implements the \u0060IAccessible\u0060 interface. This ensures that all 35\u002B Skia-based view implementations automatically participate in the accessibility infrastructure:\n\n\u0060\u0060\u0060csharp\npublic abstract class SkiaView : IAccessible\n{\n public string? AccessibilityLabel { get; set; }\n public string? AccessibilityHint { get; set; }\n public AccessibilityRole Role { get; set; }\n public AccessibilityState State { get; private set; }\n \n // Exposed to AT-SPI2 via platform bridge\n}\n\u0060\u0060\u0060\n\nThe platform automatically handles:\n- **D-Bus registration**: Each window and its accessible descendants are registered with the AT-SPI2 registry\n- **Event notifications**: State changes, focus events, and property updates are broadcast to listening assistive technologies\n- **Query responses**: When Orca or other tools request information about UI elements, the platform provides real-time data\n\nThis architecture ensures that MAUI developers don\u0027t need to write Linux-specific accessibility code\u2014the platform handles the AT-SPI2 integration transparently while respecting standard MAUI accessibility properties.\r\n\r\n## The IAccessible Interface Implementation\r\n\r\nThe \u0060IAccessible\u0060 interface is the cornerstone of accessibility support in OpenMaui Linux applications. It defines the contract that all accessible UI elements must fulfill, serving as the bridge between MAUI controls and the AT-SPI2 accessibility infrastructure.\n\n## Core Interface Design\n\nEvery Skia-based view in OpenMaui implements \u0060IAccessible\u0060, providing consistent accessibility support across all controls:\n\n\u0060\u0060\u0060csharp\npublic interface IAccessible\n{\n // Identity and description\n string? AccessibilityLabel { get; set; }\n string? AccessibilityHint { get; set; }\n \n // Semantic information\n AccessibilityRole Role { get; set; }\n AccessibilityState State { get; }\n \n // Hierarchy navigation\n IAccessible? AccessibilityParent { get; }\n IEnumerable\u003CIAccessible\u003E AccessibilityChildren { get; }\n \n // Value for interactive controls\n string? AccessibilityValue { get; }\n \n // Actions\n bool PerformAccessibilityAction(AccessibilityAction action);\n}\n\u0060\u0060\u0060\n\n## Implementation in SkiaButton\n\nLet\u0027s examine how \u0060SkiaButton\u0060 implements accessibility:\n\n\u0060\u0060\u0060csharp\npublic class SkiaButton : SkiaView, IButton, IAccessible\n{\n private string? _text;\n private ImageSource? _imageSource;\n \n public SkiaButton()\n {\n Role = AccessibilityRole.Button;\n Focusable = true;\n }\n \n public override string? AccessibilityLabel \n {\n get =\u003E base.AccessibilityLabel ?? _text;\n set =\u003E base.AccessibilityLabel = value;\n }\n \n public override AccessibilityState State\n {\n get\n {\n var state = AccessibilityState.None;\n \n if (IsEnabled)\n state |= AccessibilityState.Enabled;\n if (IsFocused)\n state |= AccessibilityState.Focused;\n if (!IsVisible)\n state |= AccessibilityState.Invisible;\n \n return state;\n }\n }\n \n public override bool PerformAccessibilityAction(AccessibilityAction action)\n {\n if (action == AccessibilityAction.Click \u0026\u0026 IsEnabled)\n {\n Command?.Execute(CommandParameter);\n Clicked?.Invoke(this, EventArgs.Empty);\n return true;\n }\n \n return base.PerformAccessibilityAction(action);\n }\n}\n\u0060\u0060\u0060\n\n## Key Implementation Patterns\n\n### 1. Fallback to Visual Content\n\nWhen no explicit \u0060AccessibilityLabel\u0060 is set, controls should fall back to their visual content:\n\n\u0060\u0060\u0060csharp\n// SkiaLabel\npublic override string? AccessibilityLabel\n{\n get =\u003E base.AccessibilityLabel ?? Text;\n set =\u003E base.AccessibilityLabel = value;\n}\n\n// SkiaEntry\npublic override string? AccessibilityLabel\n{\n get =\u003E base.AccessibilityLabel ?? Placeholder;\n set =\u003E base.AccessibilityLabel = value;\n}\n\u0060\u0060\u0060\n\n### 2. Dynamic State Management\n\nAccessibility state should reflect the current visual and interaction state:\n\n\u0060\u0060\u0060csharp\n// SkiaCheckBox\npublic override AccessibilityState State\n{\n get\n {\n var state = base.State;\n \n if (IsChecked)\n state |= AccessibilityState.Checked;\n \n return state;\n }\n}\n\n// SkiaEntry with text selection\npublic override AccessibilityState State\n{\n get\n {\n var state = base.State;\n \n if (IsReadOnly)\n state |= AccessibilityState.ReadOnly;\n if (!string.IsNullOrEmpty(SelectedText))\n state |= AccessibilityState.Selected;\n \n return state;\n }\n}\n\u0060\u0060\u0060\n\n### 3. Hierarchical Relationships\n\nContainer controls must expose their children to build the accessibility tree:\n\n\u0060\u0060\u0060csharp\npublic class SkiaScrollView : SkiaLayoutView\n{\n public override IEnumerable\u003CIAccessible\u003E AccessibilityChildren\n {\n get\n {\n if (Content is IAccessible accessible)\n yield return accessible;\n }\n }\n}\n\npublic class SkiaGrid : SkiaLayoutView\n{\n public override IEnumerable\u003CIAccessible\u003E AccessibilityChildren\n {\n get =\u003E Children.OfType\u003CIAccessible\u003E();\n }\n}\n\u0060\u0060\u0060\n\n## Notifying State Changes\n\nWhen accessibility-relevant properties change, the platform must notify AT-SPI2:\n\n\u0060\u0060\u0060csharp\nprotected void NotifyAccessibilityStateChanged()\n{\n // Platform bridge sends D-Bus signal\n AccessibilityManager.NotifyStateChanged(this);\n}\n\npublic bool IsChecked\n{\n get =\u003E _isChecked;\n set\n {\n if (_isChecked != value)\n {\n _isChecked = value;\n NotifyAccessibilityStateChanged();\n InvalidateVisual();\n }\n }\n}\n\u0060\u0060\u0060\n\nThis implementation pattern ensures that screen readers receive real-time updates when your application\u0027s state changes, providing users with immediate feedback about their interactions.\r\n\r\n## Role and State Management\r\n\r\nProper role and state management is critical for screen readers to understand and communicate the purpose and current condition of UI elements. The OpenMaui platform defines comprehensive enumerations that map to AT-SPI2 roles and states.\n\n## Accessibility Roles\n\nRoles define the semantic purpose of a UI element. OpenMaui supports all standard MAUI accessibility roles:\n\n\u0060\u0060\u0060csharp\npublic enum AccessibilityRole\n{\n None,\n Button,\n CheckBox,\n RadioButton,\n Entry,\n Label,\n Image,\n ProgressBar,\n Slider,\n Switch,\n Tab,\n TabList,\n Menu,\n MenuItem,\n MenuBar,\n Link,\n Header,\n Search,\n ComboBox,\n List,\n ListItem,\n Grid,\n GridCell,\n SpinButton,\n DatePicker,\n TimePicker\n}\n\u0060\u0060\u0060\n\n### Role Assignment Examples\n\nEach control sets its role in the constructor:\n\n\u0060\u0060\u0060csharp\npublic class SkiaButton : SkiaView\n{\n public SkiaButton()\n {\n Role = AccessibilityRole.Button;\n Focusable = true;\n }\n}\n\npublic class SkiaEntry : SkiaView\n{\n public SkiaEntry()\n {\n Role = AccessibilityRole.Entry;\n Focusable = true;\n }\n}\n\npublic class SkiaLabel : SkiaView\n{\n public SkiaLabel()\n {\n Role = AccessibilityRole.Label;\n Focusable = false; // Labels typically aren\u0027t focusable\n }\n}\n\npublic class SkiaSearchBar : SkiaView\n{\n public SkiaSearchBar()\n {\n Role = AccessibilityRole.Search;\n Focusable = true;\n }\n}\n\u0060\u0060\u0060\n\n## Accessibility States\n\nStates describe the current condition of a UI element. Unlike roles (which are static), states change dynamically:\n\n\u0060\u0060\u0060csharp\n[Flags]\npublic enum AccessibilityState\n{\n None = 0,\n Enabled = 1 \u003C\u003C 0,\n Focused = 1 \u003C\u003C 1,\n Checked = 1 \u003C\u003C 2,\n Selected = 1 \u003C\u003C 3,\n Expanded = 1 \u003C\u003C 4,\n Collapsed = 1 \u003C\u003C 5,\n Busy = 1 \u003C\u003C 6,\n ReadOnly = 1 \u003C\u003C 7,\n Required = 1 \u003C\u003C 8,\n Invalid = 1 \u003C\u003C 9,\n Invisible = 1 \u003C\u003C 10,\n Multiline = 1 \u003C\u003C 11,\n Multiselectable = 1 \u003C\u003C 12\n}\n\u0060\u0060\u0060\n\n### Dynamic State Computation\n\nThe base \u0060SkiaView\u0060 class provides default state logic:\n\n\u0060\u0060\u0060csharp\npublic virtual AccessibilityState State\n{\n get\n {\n var state = AccessibilityState.None;\n \n if (IsEnabled)\n state |= AccessibilityState.Enabled;\n \n if (IsFocused)\n state |= AccessibilityState.Focused;\n \n if (!IsVisible)\n state |= AccessibilityState.Invisible;\n \n return state;\n }\n}\n\u0060\u0060\u0060\n\nControls override this to add specific states:\n\n\u0060\u0060\u0060csharp\n// SkiaCheckBox\npublic override AccessibilityState State\n{\n get\n {\n var state = base.State;\n \n if (IsChecked)\n state |= AccessibilityState.Checked;\n \n return state;\n }\n}\n\n// SkiaEditor (multiline text)\npublic override AccessibilityState State\n{\n get\n {\n var state = base.State;\n state |= AccessibilityState.Multiline;\n \n if (IsReadOnly)\n state |= AccessibilityState.ReadOnly;\n \n return state;\n }\n}\n\n// SkiaCollectionView\npublic override AccessibilityState State\n{\n get\n {\n var state = base.State;\n \n if (SelectionMode == SelectionMode.Multiple)\n state |= AccessibilityState.Multiselectable;\n \n return state;\n }\n}\n\n// SkiaActivityIndicator\npublic override AccessibilityState State\n{\n get\n {\n var state = base.State;\n \n if (IsRunning)\n state |= AccessibilityState.Busy;\n \n return state;\n }\n}\n\u0060\u0060\u0060\n\n## Complex State Management: Expandable Controls\n\nControls like accordions, tree views, or combo boxes need to manage expanded/collapsed states:\n\n\u0060\u0060\u0060csharp\npublic class SkiaPicker : SkiaView\n{\n private bool _isOpen;\n \n public SkiaPicker()\n {\n Role = AccessibilityRole.ComboBox;\n Focusable = true;\n }\n \n public override AccessibilityState State\n {\n get\n {\n var state = base.State;\n \n if (_isOpen)\n state |= AccessibilityState.Expanded;\n else\n state |= AccessibilityState.Collapsed;\n \n return state;\n }\n }\n \n private void ToggleDropdown()\n {\n _isOpen = !_isOpen;\n NotifyAccessibilityStateChanged();\n \n // Announce state change to screen reader\n if (_isOpen)\n AnnounceAccessibility(\u0022Dropdown opened\u0022);\n else\n AnnounceAccessibility(\u0022Dropdown closed\u0022);\n }\n}\n\u0060\u0060\u0060\n\n## Form Validation States\n\nFor forms, the \u0060Invalid\u0060 and \u0060Required\u0060 states communicate validation status:\n\n\u0060\u0060\u0060csharp\npublic class SkiaEntry : SkiaView\n{\n private bool _isRequired;\n private bool _isValid = true;\n \n public bool IsRequired\n {\n get =\u003E _isRequired;\n set\n {\n _isRequired = value;\n NotifyAccessibilityStateChanged();\n }\n }\n \n public bool IsValid\n {\n get =\u003E _isValid;\n set\n {\n _isValid = value;\n NotifyAccessibilityStateChanged();\n \n if (!_isValid)\n AnnounceAccessibility(\u0022Invalid input\u0022);\n }\n }\n \n public override AccessibilityState State\n {\n get\n {\n var state = base.State;\n \n if (_isRequired)\n state |= AccessibilityState.Required;\n \n if (!_isValid)\n state |= AccessibilityState.Invalid;\n \n return state;\n }\n }\n}\n\u0060\u0060\u0060\n\n## Best Practices\n\n1. **Set roles in constructors**: Roles should be immutable and set when the control is created\n2. **Compute states dynamically**: Always calculate state from current properties, never cache it\n3. **Notify on changes**: Call \u0060NotifyAccessibilityStateChanged()\u0060 whenever state-affecting properties change\n4. **Use appropriate granularity**: Don\u0027t over-specify states\u2014only include those that are semantically meaningful\n5. **Test state combinations**: Verify that multiple states work correctly together (e.g., Focused \u002B Checked \u002B Enabled)\n\nProper role and state management ensures that screen reader users receive accurate, timely information about your application\u0027s UI, enabling them to navigate and interact effectively.\r\n\r\n## Keyboard Navigation Patterns\r\n\r\nKeyboard navigation is essential for accessibility, enabling users who cannot use a mouse\u2014whether due to visual impairments, motor disabilities, or personal preference\u2014to navigate and interact with your application. OpenMaui implements comprehensive keyboard navigation patterns that align with both MAUI standards and Linux desktop conventions.\n\n## Focus Management\n\nThe foundation of keyboard navigation is focus management. The \u0060SkiaView\u0060 base class provides the core focus infrastructure:\n\n\u0060\u0060\u0060csharp\npublic abstract class SkiaView : IAccessible\n{\n private bool _isFocused;\n private bool _focusable = true;\n \n public bool Focusable\n {\n get =\u003E _focusable;\n set =\u003E _focusable = value;\n }\n \n public bool IsFocused\n {\n get =\u003E _isFocused;\n private set\n {\n if (_isFocused != value)\n {\n _isFocused = value;\n OnFocusChanged();\n NotifyAccessibilityStateChanged();\n InvalidateVisual();\n }\n }\n }\n \n public virtual bool Focus()\n {\n if (!Focusable || !IsEnabled || !IsVisible)\n return false;\n \n IsFocused = true;\n Focused?.Invoke(this, new FocusEventArgs(this, true));\n return true;\n }\n \n public virtual void Unfocus()\n {\n if (IsFocused)\n {\n IsFocused = false;\n Unfocused?.Invoke(this, new FocusEventArgs(this, false));\n }\n }\n}\n\u0060\u0060\u0060\n\n## Tab Navigation\n\nThe Tab key moves focus forward through focusable elements, while Shift\u002BTab moves backward. The \u0060FocusManager\u0060 handles tab order:\n\n\u0060\u0060\u0060csharp\npublic class FocusManager\n{\n private readonly List\u003CIAccessible\u003E _focusableElements = new();\n private int _currentIndex = -1;\n \n public void BuildFocusChain(IAccessible root)\n {\n _focusableElements.Clear();\n CollectFocusableElements(root);\n }\n \n private void CollectFocusableElements(IAccessible element)\n {\n if (element is SkiaView view \u0026\u0026 view.Focusable \u0026\u0026 \n view.IsEnabled \u0026\u0026 view.IsVisible)\n {\n _focusableElements.Add(element);\n }\n \n foreach (var child in element.AccessibilityChildren)\n {\n CollectFocusableElements(child);\n }\n }\n \n public bool FocusNext()\n {\n if (_focusableElements.Count == 0)\n return false;\n \n _currentIndex = (_currentIndex \u002B 1) % _focusableElements.Count;\n return FocusCurrent();\n }\n \n public bool FocusPrevious()\n {\n if (_focusableElements.Count == 0)\n return false;\n \n _currentIndex--;\n if (_currentIndex \u003C 0)\n _currentIndex = _focusableElements.Count - 1;\n \n return FocusCurrent();\n }\n \n private bool FocusCurrent()\n {\n if (_currentIndex \u003E= 0 \u0026\u0026 _currentIndex \u003C _focusableElements.Count)\n {\n var element = _focusableElements[_currentIndex];\n if (element is SkiaView view)\n return view.Focus();\n }\n return false;\n }\n}\n\u0060\u0060\u0060\n\n## Arrow Key Navigation\n\nArrow keys provide directional navigation within container controls. The \u0060SkiaCollectionView\u0060 demonstrates this pattern:\n\n\u0060\u0060\u0060csharp\npublic class SkiaCollectionView : SkiaView\n{\n private int _selectedIndex = -1;\n \n protected override void OnKeyDown(KeyEventArgs e)\n {\n switch (e.Key)\n {\n case Key.Down:\n SelectNext();\n e.Handled = true;\n break;\n \n case Key.Up:\n SelectPrevious();\n e.Handled = true;\n break;\n \n case Key.Home:\n SelectFirst();\n e.Handled = true;\n break;\n \n case Key.End:\n SelectLast();\n e.Handled = true;\n break;\n \n case Key.Space:\n case Key.Enter:\n ActivateSelected();\n e.Handled = true;\n break;\n }\n \n base.OnKeyDown(e);\n }\n \n private void SelectNext()\n {\n if (_selectedIndex \u003C ItemsSource.Count - 1)\n {\n SelectedIndex = _selectedIndex \u002B 1;\n ScrollToSelected();\n AnnounceAccessibility($\u0022Item {_selectedIndex \u002B 1} of {ItemsSource.Count}\u0022);\n }\n }\n}\n\u0060\u0060\u0060\n\n## Keyboard Shortcuts and Mnemonics\n\nMenu items and buttons support keyboard shortcuts:\n\n\u0060\u0060\u0060csharp\npublic class SkiaMenuItem : SkiaView\n{\n public KeyGesture? Accelerator { get; set; }\n public char? Mnemonic { get; set; }\n \n public override string? AccessibilityLabel\n {\n get\n {\n var label = base.AccessibilityLabel ?? Text;\n \n if (Accelerator != null)\n label \u002B= $\u0022 ({Accelerator})\u0022; // e.g., \u0022Save (Ctrl\u002BS)\u0022\n \n return label;\n }\n }\n \n protected override void OnKeyDown(KeyEventArgs e)\n {\n if (Accelerator != null \u0026\u0026 Accelerator.Matches(e))\n {\n Command?.Execute(CommandParameter);\n e.Handled = true;\n }\n \n base.OnKeyDown(e);\n }\n}\n\npublic class SkiaMenuBar : SkiaView\n{\n protected override void OnKeyDown(KeyEventArgs e)\n {\n // Alt key activates menu bar\n if (e.Key == Key.Alt \u0026\u0026 !e.IsRepeat)\n {\n ActivateMenuBar();\n e.Handled = true;\n return;\n }\n \n // Alt\u002BLetter activates menu by mnemonic\n if (e.Modifiers.HasFlag(KeyModifiers.Alt) \u0026\u0026 e.Key \u003E= Key.A \u0026\u0026 e.Key \u003C= Key.Z)\n {\n char mnemonic = (char)(\u0027a\u0027 \u002B (e.Key - Key.A));\n ActivateMenuByMnemonic(mnemonic);\n e.Handled = true;\n }\n \n base.OnKeyDown(e);\n }\n}\n\u0060\u0060\u0060\n\n## Text Navigation in Entry Controls\n\n\u0060SkiaEntry\u0060 and \u0060SkiaEditor\u0060 implement comprehensive text navigation:\n\n\u0060\u0060\u0060csharp\npublic class SkiaEntry : SkiaView\n{\n private int _cursorPosition;\n private int _selectionStart;\n private int _selectionLength;\n \n protected override void OnKeyDown(KeyEventArgs e)\n {\n bool shift = e.Modifiers.HasFlag(KeyModifiers.Shift);\n bool ctrl = e.Modifiers.HasFlag(KeyModifiers.Control);\n \n switch (e.Key)\n {\n case Key.Left:\n if (ctrl)\n MoveCursorToPreviousWord(shift);\n else\n MoveCursorLeft(shift);\n e.Handled = true;\n break;\n \n case Key.Right:\n if (ctrl)\n MoveCursorToNextWord(shift);\n else\n MoveCursorRight(shift);\n e.Handled = true;\n break;\n \n case Key.Home:\n MoveCursorToStart(shift);\n e.Handled = true;\n break;\n \n case Key.End:\n MoveCursorToEnd(shift);\n e.Handled = true;\n break;\n \n case Key.A:\n if (ctrl)\n {\n SelectAll();\n AnnounceAccessibility(\u0022All text selected\u0022);\n e.Handled = true;\n }\n break;\n }\n \n base.OnKeyDown(e);\n }\n}\n\u0060\u0060\u0060\n\n## Escape Key Handling\n\nThe Escape key should cancel operations and close dialogs:\n\n\u0060\u0060\u0060csharp\npublic class SkiaAlertDialog : SkiaView\n{\n protected override void OnKeyDown(KeyEventArgs e)\n {\n if (e.Key == Key.Escape)\n {\n CancelButton?.Command?.Execute(CancelButton.CommandParameter);\n Close();\n e.Handled = true;\n }\n \n base.OnKeyDown(e);\n }\n}\n\u0060\u0060\u0060\n\n## Visual Focus Indicators\n\nFocused elements must be visually distinguishable:\n\n\u0060\u0060\u0060csharp\nprotected override void OnPaint(SKCanvas canvas)\n{\n // Draw control content\n DrawContent(canvas);\n \n // Draw focus indicator\n if (IsFocused)\n {\n var focusRect = new SKRect(0, 0, Width, Height);\n focusRect.Inflate(-2, -2);\n \n using var paint = new SKPaint\n {\n Style = SKPaintStyle.Stroke,\n Color = SkiaTheme.Current.FocusColor,\n StrokeWidth = 2,\n PathEffect = SKPathEffect.CreateDash(new[] { 4f, 2f }, 0)\n };\n \n canvas.DrawRect(focusRect, paint);\n }\n}\n\u0060\u0060\u0060\n\n## Best Practices\n\n1. **Make all interactive elements focusable**: Buttons, links, inputs, and custom controls should accept focus\n2. **Provide visible focus indicators**: Users must see which element has focus\n3. **Support standard shortcuts**: Ctrl\u002BC/V/X for copy/paste/cut, Ctrl\u002BA for select all, etc.\n4. **Announce navigation changes**: Use \u0060AnnounceAccessibility()\u0060 when focus moves or selection changes\n5. **Test keyboard-only navigation**: Ensure your entire application is usable without a mouse\n6. **Respect tab order**: Logical tab order follows visual layout (left-to-right, top-to-bottom)\n7. **Trap focus in modals**: Dialogs should keep focus within themselves until closed\n\nProper keyboard navigation ensures that your application is fully accessible to users who rely on keyboard input, whether due to disability, preference, or workflow efficiency.\r\n\r\n## High Contrast Mode Support\r\n\r\nHigh contrast mode is essential for users with low vision or certain types of color blindness. Linux desktop environments provide system-wide high contrast themes, and applications must respect these settings to ensure readability and usability.\n\n## Detecting High Contrast Mode\n\nThe OpenMaui platform integrates with the \u0060SystemThemeService\u0060 to detect high contrast mode across different desktop environments:\n\n\u0060\u0060\u0060csharp\npublic class SystemThemeService\n{\n private bool _isHighContrast;\n \n public bool IsHighContrastMode\n {\n get =\u003E _isHighContrast;\n private set\n {\n if (_isHighContrast != value)\n {\n _isHighContrast = value;\n OnHighContrastChanged();\n }\n }\n }\n \n public event EventHandler? HighContrastChanged;\n \n public void DetectHighContrast()\n {\n // GNOME detection\n if (IsGnomeDesktop())\n {\n var contrastSetting = GetGSettingsValue(\n \u0022org.gnome.desktop.a11y.interface\u0022, \n \u0022high-contrast\u0022);\n IsHighContrastMode = contrastSetting == \u0022true\u0022;\n }\n // KDE Plasma detection\n else if (IsKdeDesktop())\n {\n var theme = GetKdeColorScheme();\n IsHighContrastMode = theme.Contains(\u0022HighContrast\u0022, \n StringComparison.OrdinalIgnoreCase);\n }\n // Fallback: detect by theme name\n else\n {\n var themeName = GetGtkThemeName();\n IsHighContrastMode = themeName.Contains(\u0022HighContrast\u0022, \n StringComparison.OrdinalIgnoreCase);\n }\n }\n}\n\u0060\u0060\u0060\n\n## The SkiaTheme System\n\nThe \u0060SkiaTheme\u0060 class provides centralized theme management with high contrast support:\n\n\u0060\u0060\u0060csharp\npublic class SkiaTheme\n{\n public static SkiaTheme Current { get; private set; } = new();\n \n public bool IsDarkMode { get; set; }\n public bool IsHighContrast { get; set; }\n \n // Color properties with high contrast overrides\n public SKColor ForegroundColor =\u003E IsHighContrast \n ? (IsDarkMode ? SKColors.White : SKColors.Black)\n : (IsDarkMode ? new SKColor(230, 230, 230) : new SKColor(30, 30, 30));\n \n public SKColor BackgroundColor =\u003E IsHighContrast\n ? (IsDarkMode ? SKColors.Black : SKColors.White)\n : (IsDarkMode ? new SKColor(30, 30, 30) : new SKColor(250, 250, 250));\n \n public SKColor AccentColor =\u003E IsHighContrast\n ? (IsDarkMode ? SKColors.Yellow : SKColors.Blue)\n : new SKColor(0, 120, 215); // Default blue\n \n public SKColor BorderColor =\u003E IsHighContrast\n ? ForegroundColor\n : (IsDarkMode ? new SKColor(60, 60, 60) : new SKColor(200, 200, 200));\n \n public SKColor FocusColor =\u003E IsHighContrast\n ? (IsDarkMode ? SKColors.Yellow : SKColors.Blue)\n : new SKColor(0, 120, 215);\n \n public SKColor DisabledForegroundColor =\u003E IsHighContrast\n ? (IsDarkMode ? new SKColor(128, 128, 128) : new SKColor(128, 128, 128))\n : (IsDarkMode ? new SKColor(100, 100, 100) : new SKColor(160, 160, 160));\n \n public float MinimumContrastRatio =\u003E IsHighContrast ? 7.0f : 4.5f;\n \n public static void ApplySystemTheme(bool isDark, bool isHighContrast)\n {\n Current = new SkiaTheme\n {\n IsDarkMode = isDark,\n IsHighContrast = isHighContrast\n };\n \n ThemeChanged?.Invoke(null, EventArgs.Empty);\n }\n \n public static event EventHandler? ThemeChanged;\n}\n\u0060\u0060\u0060\n\n## Applying High Contrast Themes to Controls\n\nControls automatically respond to theme changes:\n\n\u0060\u0060\u0060csharp\npublic class SkiaButton : SkiaView\n{\n public SkiaButton()\n {\n SkiaTheme.ThemeChanged \u002B= OnThemeChanged;\n }\n \n private void OnThemeChanged(object? sender, EventArgs e)\n {\n InvalidateVisual(); // Trigger repaint with new colors\n }\n \n protected override void OnPaint(SKCanvas canvas)\n {\n var theme = SkiaTheme.Current;\n \n // Background\n var bgColor = IsEnabled \n ? (IsPressed ? theme.AccentColor.WithAlpha(200) : theme.AccentColor)\n : theme.BackgroundColor;\n \n using (var paint = new SKPaint { Color = bgColor })\n {\n canvas.DrawRoundRect(Bounds, CornerRadius, CornerRadius, paint);\n }\n \n // Border (always visible in high contrast)\n if (theme.IsHighContrast || IsFocused)\n {\n using var borderPaint = new SKPaint\n {\n Style = SKPaintStyle.Stroke,\n Color = IsFocused ? theme.FocusColor : theme.BorderColor,\n StrokeWidth = theme.IsHighContrast ? 2 : 1\n };\n \n canvas.DrawRoundRect(Bounds, CornerRadius, CornerRadius, borderPaint);\n }\n \n // Text\n var textColor = IsEnabled \n ? (theme.IsHighContrast ? theme.BackgroundColor : SKColors.White)\n : theme.DisabledForegroundColor;\n \n using (var textPaint = new SKPaint\n {\n Color = textColor,\n TextSize = FontSize,\n IsAntialias = true,\n Typeface = GetTypeface()\n })\n {\n canvas.DrawText(Text, TextX, TextY, textPaint);\n }\n }\n}\n\u0060\u0060\u0060\n\n## Ensuring Sufficient Contrast\n\nThe platform includes contrast checking utilities:\n\n\u0060\u0060\u0060csharp\npublic static class ContrastChecker\n{\n public static double CalculateContrastRatio(SKColor foreground, SKColor background)\n {\n double luminance1 = GetRelativeLuminance(foreground);\n double luminance2 = GetRelativeLuminance(background);\n \n double lighter = Math.Max(luminance1, luminance2);\n double darker = Math.Min(luminance1, luminance2);\n \n return (lighter \u002B 0.05) / (darker \u002B 0.05);\n }\n \n private static double GetRelativeLuminance(SKColor color)\n {\n double r = GetLinearValue(color.Red / 255.0);\n double g = GetLinearValue(color.Green / 255.0);\n double b = GetLinearValue(color.Blue / 255.0);\n \n return 0.2126 * r \u002B 0.7152 * g \u002B 0.0722 * b;\n }\n \n private static double GetLinearValue(double value)\n {\n return value \u003C= 0.03928 \n ? value / 12.92 \n : Math.Pow((value \u002B 0.055) / 1.055, 2.4);\n }\n \n public static bool MeetsWCAGAA(SKColor foreground, SKColor background)\n {\n return CalculateContrastRatio(foreground, background) \u003E= 4.5;\n }\n \n public static bool MeetsWCAGAAA(SKColor foreground, SKColor background)\n {\n return CalculateContrastRatio(foreground, background) \u003E= 7.0;\n }\n}\n\u0060\u0060\u0060\n\n## High Contrast Best Practices for Custom Controls\n\nWhen implementing custom controls:\n\n\u0060\u0060\u0060csharp\npublic class CustomControl : SkiaView\n{\n protected override void OnPaint(SKCanvas canvas)\n {\n var theme = SkiaTheme.Current;\n \n // 1. Always draw borders in high contrast mode\n if (theme.IsHighContrast)\n {\n using var borderPaint = new SKPaint\n {\n Style = SKPaintStyle.Stroke,\n Color = theme.BorderColor,\n StrokeWidth = 2\n };\n canvas.DrawRect(Bounds, borderPaint);\n }\n \n // 2. Use solid colors, avoid gradients\n var fillColor = theme.IsHighContrast \n ? theme.BackgroundColor \n : GetGradientColor();\n \n // 3. Increase icon/text sizes slightly\n var fontSize = theme.IsHighContrast \n ? FontSize * 1.1f \n : FontSize;\n \n // 4. Remove transparency\n var color = theme.ForegroundColor;\n if (theme.IsHighContrast)\n color = color.WithAlpha(255);\n \n // 5. Use system colors, don\u0027t hardcode\n using var paint = new SKPaint\n {\n Color = theme.IsHighContrast \n ? theme.ForegroundColor \n : CustomColor\n };\n }\n}\n\u0060\u0060\u0060\n\n## Responding to Runtime Theme Changes\n\nUsers can toggle high contrast mode while your application is running:\n\n\u0060\u0060\u0060csharp\npublic class LinuxApplication\n{\n private readonly SystemThemeService _themeService;\n \n public LinuxApplication()\n {\n _themeService = new SystemThemeService();\n _themeService.HighContrastChanged \u002B= OnHighContrastChanged;\n _themeService.StartMonitoring();\n }\n \n private void OnHighContrastChanged(object? sender, EventArgs e)\n {\n // Update global theme\n SkiaTheme.ApplySystemTheme(\n _themeService.IsDarkMode,\n _themeService.IsHighContrastMode);\n \n // All views automatically repaint due to ThemeChanged event\n \n // Announce to screen reader\n var mode = _themeService.IsHighContrastMode \n ? \u0022High contrast mode enabled\u0022 \n : \u0022High contrast mode disabled\u0022;\n AccessibilityManager.Announce(mode);\n }\n}\n\u0060\u0060\u0060\n\n## Testing High Contrast Mode\n\nTo test your application:\n\n**GNOME:**\n\u0060\u0060\u0060bash\n# Enable high contrast\ngsettings set org.gnome.desktop.a11y.interface high-contrast true\n\n# Disable high contrast\ngsettings set org.gnome.desktop.a11y.interface high-contrast false\n\u0060\u0060\u0060\n\n**KDE Plasma:**\n1. System Settings \u2192 Appearance \u2192 Colors\n2. Select \u0022High Contrast\u0022 scheme\n\n**Manual Testing Checklist:**\n- [ ] All text is readable with sufficient contrast\n- [ ] All interactive elements have visible borders\n- [ ] Focus indicators are clearly visible\n- [ ] Icons and images have text alternatives\n- [ ] No information is conveyed by color alone\n- [ ] Custom colors are replaced with system colors\n- [ ] Application responds to runtime theme changes\n\nHigh contrast support ensures that users with visual impairments can comfortably use your application, meeting both accessibility standards and user expectations on Linux desktop environments.\r\n\r\n## Testing with Orca Screen Reader\r\n\r\nOrca is the primary screen reader for Linux desktop environments, providing speech and Braille output for visually impaired users. Testing your application with Orca is essential to ensure that your accessibility implementation works correctly in real-world scenarios.\n\n## Installing and Configuring Orca\n\n**Installation:**\n\n\u0060\u0060\u0060bash\n# Ubuntu/Debian\nsudo apt install orca\n\n# Fedora\nsudo dnf install orca\n\n# Arch Linux\nsudo pacman -S orca\n\u0060\u0060\u0060\n\n**Starting Orca:**\n\n\u0060\u0060\u0060bash\n# Launch Orca\norca\n\n# Launch Orca with preferences dialog\norca --setup\n\n# Enable Orca at login (GNOME)\ngsettings set org.gnome.desktop.a11y.applications screen-reader-enabled true\n\u0060\u0060\u0060\n\n**Basic Orca Commands:**\n\n- **Orca Modifier Key**: Insert (or Caps Lock, configurable)\n- **Toggle Orca on/off**: \u0060Super \u002B Alt \u002B S\u0060 (GNOME)\n- **Orca preferences**: \u0060Orca \u002B Space\u0060\n- **Read current line**: \u0060Orca \u002B Up Arrow\u0060\n- **Read current window**: \u0060Orca \u002B B\u0060\n- **Where am I**: \u0060Orca \u002B Enter\u0060 (announces current focus)\n\n## Testing Your Application\n\n### 1. Launch and Initial Focus\n\nWhen your application starts, Orca should announce the window title and initial focused element:\n\n\u0060\u0060\u0060csharp\npublic class MainWindow : Window\n{\n public MainWindow()\n {\n Title = \u0022My Accessible Application\u0022;\n \n // Set initial focus to meaningful element\n var firstButton = new Button \n { \n Text = \u0022Get Started\u0022,\n AutomationId = \u0022GetStartedButton\u0022\n };\n \n Content = new VerticalStackLayout\n {\n Children = { firstButton }\n };\n \n // Ensure focus is set after window loads\n Loaded \u002B= (s, e) =\u003E firstButton.Focus();\n }\n}\n\u0060\u0060\u0060\n\n**Expected Orca Output:**\n\u0022My Accessible Application window. Get Started button.\u0022\n\n### 2. Navigation Testing\n\nTest tab navigation through your application:\n\n\u0060\u0060\u0060csharp\npublic class FormPage : ContentPage\n{\n public FormPage()\n {\n Content = new VerticalStackLayout\n {\n Children =\n {\n new Label \n { \n Text = \u0022Name:\u0022,\n SemanticProperties.HeadingLevel = SemanticHeadingLevel.Level2\n },\n new Entry \n { \n Placeholder = \u0022Enter your name\u0022,\n AutomationId = \u0022NameEntry\u0022,\n SemanticProperties.Description = \u0022Required field\u0022\n },\n new Label { Text = \u0022Email:\u0022 },\n new Entry \n { \n Placeholder = \u0022Enter your email\u0022,\n Keyboard = Keyboard.Email,\n AutomationId = \u0022EmailEntry\u0022\n },\n new Button \n { \n Text = \u0022Submit\u0022,\n Command = new Command(OnSubmit),\n AutomationId = \u0022SubmitButton\u0022\n }\n }\n };\n }\n}\n\u0060\u0060\u0060\n\n**Testing Steps:**\n1. Press Tab to move through elements\n2. Verify Orca announces each element\u0027s role and label\n3. Verify Orca announces state changes (e.g., \u0022checked\u0022, \u0022expanded\u0022)\n\n**Expected Orca Output:**\n- \u0022Name: heading level 2\u0022\n- \u0022Enter your name entry. Required field.\u0022\n- \u0022Email: heading\u0022\n- \u0022Enter your email entry\u0022\n- \u0022Submit button\u0022\n\n### 3. Testing Interactive Controls\n\n**Buttons:**\n\n\u0060\u0060\u0060csharp\nvar button = new Button\n{\n Text = \u0022Save Document\u0022,\n ImageSource = \u0022save.png\u0022,\n Command = new Command(SaveDocument)\n};\n\n// Orca should announce: \u0022Save Document button\u0022\n// After clicking: \u0022Save Document button pressed\u0022\n\u0060\u0060\u0060\n\n**CheckBoxes:**\n\n\u0060\u0060\u0060csharp\nvar checkbox = new CheckBox\n{\n IsChecked = false,\n SemanticProperties.Description = \u0022Enable notifications\u0022\n};\n\n// Initial: \u0022Enable notifications checkbox not checked\u0022\n// After toggling: \u0022Enable notifications checkbox checked\u0022\n\u0060\u0060\u0060\n\n**Sliders:**\n\n\u0060\u0060\u0060csharp\nvar slider = new Slider\n{\n Minimum = 0,\n Maximum = 100,\n Value = 50,\n SemanticProperties.Description = \u0022Volume\u0022\n};\n\n// Orca should announce: \u0022Volume slider 50\u0022\n// When adjusting: \u0022Volume 75\u0022 (announces value changes)\n\u0060\u0060\u0060\n\n### 4. Testing Collections\n\n\u0060\u0060\u0060csharp\npublic class ContactListPage : ContentPage\n{\n public ContactListPage()\n {\n var collectionView = new CollectionView\n {\n ItemsSource = GetContacts(),\n ItemTemplate = new DataTemplate(() =\u003E\n {\n var label = new Label();\n label.SetBinding(Label.TextProperty, \u0022Name\u0022);\n \n // Set semantic properties for each item\n var grid = new Grid\n {\n Children = { label }\n };\n \n grid.SetBinding(\n SemanticProperties.DescriptionProperty, \n new Binding(\u0022.\u0022, \n stringFormat: \u0022Contact: {0}, {1}\u0022,\n source: new[] { \u0022Name\u0022, \u0022Email\u0022 }));\n \n return grid;\n })\n };\n \n Content = collectionView;\n }\n}\n\u0060\u0060\u0060\n\n**Testing Steps:**\n1. Use arrow keys to navigate list items\n2. Verify Orca announces item content and position\n3. Verify selection state changes are announced\n\n**Expected Orca Output:**\n- \u0022Contact: John Doe, john@example.com. List item 1 of 10.\u0022\n- \u0022Contact: Jane Smith, jane@example.com. List item 2 of 10.\u0022\n\n### 5. Testing Dialogs and Modals\n\n\u0060\u0060\u0060csharp\npublic async Task ShowAccessibleDialog()\n{\n var result = await DisplayAlert(\n \u0022Confirm Delete\u0022,\n \u0022Are you sure you want to delete this item? This action cannot be undone.\u0022,\n \u0022Delete\u0022,\n \u0022Cancel\u0022);\n \n if (result)\n {\n // Announce completion\n SemanticScreenReader.Announce(\u0022Item deleted\u0022);\n }\n}\n\u0060\u0060\u0060\n\n**Expected Orca Output:**\n- \u0022Confirm Delete dialog. Are you sure you want to delete this item? This action cannot be undone.\u0022\n- \u0022Delete button\u0022\n- \u0022Cancel button\u0022\n- After action: \u0022Item deleted\u0022\n\n## Common Issues and Fixes\n\n### Issue 1: Orca Not Announcing Elements\n\n**Problem:** Orca skips over custom controls or announces them incorrectly.\n\n**Solution:** Ensure \u0060IAccessible\u0060 is properly implemented:\n\n\u0060\u0060\u0060csharp\npublic class CustomControl : SkiaView\n{\n public CustomControl()\n {\n // Set appropriate role\n Role = AccessibilityRole.Button;\n \n // Make focusable\n Focusable = true;\n \n // Provide label\n AccessibilityLabel = \u0022Custom Action\u0022;\n }\n \n public override AccessibilityState State\n {\n get\n {\n var state = AccessibilityState.Enabled;\n if (IsFocused)\n state |= AccessibilityState.Focused;\n return state;\n }\n }\n}\n\u0060\u0060\u0060\n\n### Issue 2: Incorrect State Announcements\n\n**Problem:** Orca announces outdated state (e.g., \u0022checked\u0022 when unchecked).\n\n**Solution:** Call \u0060NotifyAccessibilityStateChanged()\u0060 when state changes:\n\n\u0060\u0060\u0060csharp\npublic bool IsChecked\n{\n get =\u003E _isChecked;\n set\n {\n if (_isChecked != value)\n {\n _isChecked = value;\n NotifyAccessibilityStateChanged();\n InvalidateVisual();\n }\n }\n}\n\u0060\u0060\u0060\n\n### Issue 3: Missing Context\n\n**Problem:** Orca announces \u0022button\u0022 without describing what it does.\n\n**Solution:** Use \u0060SemanticProperties\u0060 for additional context:\n\n\u0060\u0060\u0060csharp\nvar deleteButton = new Button\n{\n ImageSource = \u0022trash.png\u0022,\n // Don\u0027t rely on image alone\n SemanticProperties = \n {\n Description = \u0022Delete selected items\u0022,\n Hint = \u0022This action cannot be undone\u0022\n }\n};\n\u0060\u0060\u0060\n\n## Automated Accessibility Testing\n\nWhile manual testing with Orca is essential, you can also write automated tests:\n\n\u0060\u0060\u0060csharp\n[Fact]\npublic void Button_Should_Have_Accessible_Properties()\n{\n // Arrange\n var button = new SkiaButton\n {\n Text = \u0022Click Me\u0022\n };\n \n // Assert\n Assert.Equal(AccessibilityRole.Button, button.Role);\n Assert.Equal(\u0022Click Me\u0022, button.AccessibilityLabel);\n Assert.True(button.Focusable);\n Assert.Contains(AccessibilityState.Enabled, button.State);\n}\n\n[Fact]\npublic void CheckBox_Should_Announce_State_Changes()\n{\n // Arrange\n var checkbox = new SkiaCheckBox();\n bool stateChanged = false;\n \n checkbox.AccessibilityStateChanged \u002B= (s, e) =\u003E stateChanged = true;\n \n // Act\n checkbox.IsChecked = true;\n \n // Assert\n Assert.True(stateChanged);\n Assert.Contains(AccessibilityState.Checked, checkbox.State);\n}\n\u0060\u0060\u0060\n\n## Testing Checklist\n\n- [ ] Application announces window title on launch\n- [ ] Tab navigation moves through all focusable elements\n- [ ] Each element announces its role (button, entry, checkbox, etc.)\n- [ ] Labels and descriptions are meaningful and clear\n- [ ] State changes are announced immediately (checked, expanded, etc.)\n- [ ] Form validation errors are announced\n- [ ] List items announce position (\u0022item 3 of 10\u0022)\n- [ ] Dialogs announce title and content\n- [ ] Focus remains trapped in modal dialogs\n- [ ] Custom controls have appropriate roles and states\n- [ ] Images have text alternatives\n- [ ] Progress indicators announce status updates\n- [ ] Navigation changes are announced\n- [ ] Error messages are announced\n- [ ] Success confirmations are announced\n\n## Debugging with Accerciser\n\nAccerciser is an accessibility inspector for Linux:\n\n\u0060\u0060\u0060bash\n# Install\nsudo apt install accerciser\n\n# Launch\naccerciser\n\u0060\u0060\u0060\n\nUse Accerciser to:\n- Inspect the accessibility tree of your application\n- View roles, states, and properties of each element\n- Verify AT-SPI2 events are being fired\n- Test accessibility actions programmatically\n\nTesting with Orca ensures that your accessibility implementation works correctly for real users, not just in theory. Regular testing throughout development helps catch issues early and ensures a truly accessible application.\r\n\r\n## Accessibility Best Practices\r\n\r\nBuilding accessible applications requires more than just implementing the technical requirements\u2014it demands thoughtful design and consistent attention to user experience. Here are the essential best practices for creating accessible .NET MAUI Linux applications.\n\n## 1. Design for Accessibility from the Start\n\nAccessibility should be a core requirement, not an afterthought.\n\n**Do:**\n\u0060\u0060\u0060csharp\npublic class LoginPage : ContentPage\n{\n public LoginPage()\n {\n // Accessibility considered from the beginning\n Content = new VerticalStackLayout\n {\n Children =\n {\n new Label \n { \n Text = \u0022Email\u0022,\n SemanticProperties.HeadingLevel = SemanticHeadingLevel.Level1\n },\n new Entry \n { \n Placeholder = \u0022Enter email\u0022,\n Keyboard = Keyboard.Email,\n AutomationId = \u0022EmailEntry\u0022,\n SemanticProperties.Description = \u0022Your email address\u0022\n },\n new Label { Text = \u0022Password\u0022 },\n new Entry \n { \n IsPassword = true,\n Placeholder = \u0022Enter password\u0022,\n AutomationId = \u0022PasswordEntry\u0022\n },\n new Button \n { \n Text = \u0022Sign In\u0022,\n Command = new Command(SignIn),\n SemanticProperties.Hint = \u0022Sign in to your account\u0022\n }\n }\n };\n }\n}\n\u0060\u0060\u0060\n\n**Don\u0027t:**\n\u0060\u0060\u0060csharp\n// Missing labels, no semantic information\nContent = new VerticalStackLayout\n{\n Children =\n {\n new Entry { Placeholder = \u0022Email\u0022 },\n new Entry { IsPassword = true },\n new Button { Text = \u0022Go\u0022 }\n }\n};\n\u0060\u0060\u0060\n\n## 2. Provide Meaningful Labels and Descriptions\n\nEvery interactive element must have a clear, descriptive label.\n\n**Good Examples:**\n\n\u0060\u0060\u0060csharp\n// Icon button with descriptive label\nvar deleteButton = new ImageButton\n{\n Source = \u0022trash.png\u0022,\n SemanticProperties.Description = \u0022Delete selected message\u0022\n};\n\n// Entry with context\nvar searchEntry = new Entry\n{\n Placeholder = \u0022Search contacts\u0022,\n SemanticProperties.Description = \u0022Search by name, email, or phone number\u0022\n};\n\n// Checkbox with clear purpose\nvar termsCheckbox = new CheckBox\n{\n SemanticProperties.Description = \u0022I agree to the terms and conditions\u0022\n};\n\u0060\u0060\u0060\n\n**Avoid:**\n- Generic labels: \u0022Button\u0022, \u0022Click here\u0022, \u0022OK\u0022\n- Technical jargon: \u0022Execute query\u0022, \u0022Instantiate object\u0022\n- Ambiguous icons without text alternatives\n\n## 3. Maintain Logical Focus Order\n\nTab order should follow visual layout and logical workflow.\n\n\u0060\u0060\u0060csharp\npublic class CheckoutForm : ContentPage\n{\n public CheckoutForm()\n {\n // Natural top-to-bottom, left-to-right flow\n Content = new VerticalStackLayout\n {\n Children =\n {\n // 1. Shipping information\n new Label { Text = \u0022Shipping Address\u0022 },\n new Entry { Placeholder = \u0022Street Address\u0022, TabIndex = 0 },\n new Entry { Placeholder = \u0022City\u0022, TabIndex = 1 },\n new Entry { Placeholder = \u0022Postal Code\u0022, TabIndex = 2 },\n \n // 2. Payment information\n new Label { Text = \u0022Payment\u0022 },\n new Entry { Placeholder = \u0022Card Number\u0022, TabIndex = 3 },\n new Entry { Placeholder = \u0022Expiry Date\u0022, TabIndex = 4 },\n new Entry { Placeholder = \u0022CVV\u0022, TabIndex = 5 },\n \n // 3. Action buttons\n new HorizontalStackLayout\n {\n Children =\n {\n new Button { Text = \u0022Cancel\u0022, TabIndex = 6 },\n new Button { Text = \u0022Place Order\u0022, TabIndex = 7 }\n }\n }\n }\n };\n }\n}\n\u0060\u0060\u0060\n\n## 4. Provide Clear Visual Focus Indicators\n\nUsers must always know which element has focus.\n\n\u0060\u0060\u0060csharp\nprotected override void OnPaint(SKCanvas canvas)\n{\n // Draw control\n DrawControlContent(canvas);\n \n // Always draw visible focus indicator\n if (IsFocused)\n {\n var focusRect = Bounds;\n focusRect.Inflate(-2, -2);\n \n using var focusPaint = new SKPaint\n {\n Style = SKPaintStyle.Stroke,\n Color = SkiaTheme.Current.FocusColor,\n StrokeWidth = 2,\n IsAntialias = true\n };\n \n // High contrast mode: use solid line\n if (SkiaTheme.Current.IsHighContrast)\n {\n canvas.DrawRect(focusRect, focusPaint);\n }\n // Normal mode: use dashed line\n else\n {\n focusPaint.PathEffect = SKPathEffect.CreateDash(new[] { 4f, 2f }, 0);\n canvas.DrawRect(focusRect, focusPaint);\n }\n }\n}\n\u0060\u0060\u0060\n\n## 5. Ensure Sufficient Color Contrast\n\nAll text must meet WCAG contrast requirements.\n\n\u0060\u0060\u0060csharp\npublic class AccessibleLabel : SkiaLabel\n{\n protected override void OnPaint(SKCanvas canvas)\n {\n var theme = SkiaTheme.Current;\n var backgroundColor = Parent?.BackgroundColor ?? theme.BackgroundColor;\n var foregroundColor = TextColor ?? theme.ForegroundColor;\n \n // Check contrast ratio\n var contrastRatio = ContrastChecker.CalculateContrastRatio(\n foregroundColor, \n backgroundColor);\n \n // Adjust if insufficient\n if (contrastRatio \u003C theme.MinimumContrastRatio)\n {\n foregroundColor = theme.IsHighContrast\n ? theme.ForegroundColor\n : AdjustColorForContrast(foregroundColor, backgroundColor);\n }\n \n // Draw with adjusted color\n using var paint = new SKPaint\n {\n Color = foregroundColor,\n TextSize = FontSize,\n IsAntialias = true\n };\n \n canvas.DrawText(Text, X, Y, paint);\n }\n}\n\u0060\u0060\u0060\n\n## 6. Don\u0027t Rely on Color Alone\n\nUse multiple visual cues to convey information.\n\n**Good:**\n\u0060\u0060\u0060csharp\npublic class ValidationEntry : Entry\n{\n private bool _isValid = true;\n \n public bool IsValid\n {\n get =\u003E _isValid;\n set\n {\n _isValid = value;\n \n // Multiple indicators:\n // 1. Border color\n BorderColor = _isValid ? Colors.Gray : Colors.Red;\n \n // 2. Icon\n var icon = _isValid ? \u0022checkmark.png\u0022 : \u0022error.png\u0022;\n \n // 3. Text message\n var message = _isValid \n ? \u0022Valid input\u0022 \n : \u0022Invalid input: Email format required\u0022;\n \n // 4. Accessible announcement\n SemanticScreenReader.Announce(message);\n }\n }\n}\n\u0060\u0060\u0060\n\n**Bad:**\n\u0060\u0060\u0060csharp\n// Only color changes, no other indication\nentry.BackgroundColor = isValid ? Colors.White : Colors.LightPink;\n\u0060\u0060\u0060\n\n## 7. Provide Text Alternatives for Images\n\nAll images must have meaningful text alternatives.\n\n\u0060\u0060\u0060csharp\n// Informative image\nvar productImage = new Image\n{\n Source = \u0022laptop.jpg\u0022,\n SemanticProperties.Description = \u0022Silver laptop with 15-inch display\u0022\n};\n\n// Decorative image (empty description)\nvar decorativeBorder = new Image\n{\n Source = \u0022border.png\u0022,\n SemanticProperties.Description = string.Empty // Indicates decorative\n};\n\n// Functional image button\nvar saveButton = new ImageButton\n{\n Source = \u0022save.png\u0022,\n SemanticProperties.Description = \u0022Save document\u0022\n};\n\u0060\u0060\u0060\n\n## 8. Handle Errors Accessibly\n\nError messages must be announced and clearly associated with their fields.\n\n\u0060\u0060\u0060csharp\npublic class AccessibleForm : ContentPage\n{\n private Entry _emailEntry;\n private Label _emailError;\n \n private async Task ValidateAndSubmit()\n {\n if (string.IsNullOrEmpty(_emailEntry.Text))\n {\n // 1. Show visual error\n _emailError.Text = \u0022Email is required\u0022;\n _emailError.IsVisible = true;\n _emailEntry.BorderColor = Colors.Red;\n \n // 2. Update accessibility state\n _emailEntry.SemanticProperties.Description = \n \u0022Email address. Error: Email is required\u0022;\n \n // 3. Announce error\n SemanticScreenReader.Announce(\u0022Email is required\u0022);\n \n // 4. Set focus to invalid field\n _emailEntry.Focus();\n \n return;\n }\n \n // Clear errors on success\n _emailError.IsVisible = false;\n _emailEntry.BorderColor = Colors.Gray;\n _emailEntry.SemanticProperties.Description = \u0022Email address\u0022;\n }\n}\n\u0060\u0060\u0060\n\n## 9. Make Dynamic Content Accessible\n\nAnnounce changes that occur without user interaction.\n\n\u0060\u0060\u0060csharp\npublic class NotificationBanner : ContentView\n{\n public void ShowNotification(string message, NotificationType type)\n {\n // Update UI\n Content = new Frame\n {\n BackgroundColor = GetColorForType(type),\n Content = new Label { Text = message }\n };\n \n IsVisible = true;\n \n // Announce to screen reader\n var announcement = type switch\n {\n NotificationType.Error =\u003E $\u0022Error: {message}\u0022,\n NotificationType.Warning =\u003E $\u0022Warning: {message}\u0022,\n NotificationType.Success =\u003E $\u0022Success: {message}\u0022,\n _ =\u003E message\n };\n \n SemanticScreenReader.Announce(announcement);\n \n // Auto-dismiss after delay\n Device.StartTimer(TimeSpan.FromSeconds(5), () =\u003E\n {\n IsVisible = false;\n SemanticScreenReader.Announce(\u0022Notification dismissed\u0022);\n return false;\n });\n }\n}\n\u0060\u0060\u0060\n\n## 10. Test with Real Assistive Technologies\n\nRegularly test with Orca and other tools.\n\n\u0060\u0060\u0060csharp\n// Unit test for accessibility\n[Fact]\npublic void AllButtons_Should_Have_Accessible_Labels()\n{\n var page = new MainPage();\n var buttons = GetAllDescendants\u003CButton\u003E(page);\n \n foreach (var button in buttons)\n {\n var label = button.SemanticProperties.Description \n ?? button.Text;\n \n Assert.False(string.IsNullOrWhiteSpace(label),\n $\u0022Button \u0027{button.AutomationId}\u0027 missing accessible label\u0022);\n \n Assert.True(label.Length \u003E 3,\n $\u0022Button \u0027{button.AutomationId}\u0027 has insufficient label: \u0027{label}\u0027\u0022);\n }\n}\n\u0060\u0060\u0060\n\n## Quick Reference Checklist\n\n**For Every Interactive Element:**\n- [ ] Has a clear, descriptive label\n- [ ] Has an appropriate role\n- [ ] Is keyboard accessible\n- [ ] Has visible focus indicator\n- [ ] Announces state changes\n- [ ] Meets color contrast requirements\n\n**For Every Form:**\n- [ ] Labels are associated with inputs\n- [ ] Required fields are indicated\n- [ ] Errors are announced and clearly described\n- [ ] Tab order follows logical flow\n- [ ] Submit button is clearly labeled\n\n**For Every Page:**\n- [ ] Has a meaningful title\n- [ ] Uses semantic headings\n- [ ] Focus is set appropriately on load\n- [ ] All images have text alternatives\n- [ ] Dynamic content changes are announced\n\n**For Every Custom Control:**\n- [ ] Implements \u0060IAccessible\u0060\n- [ ] Has appropriate role and state\n- [ ] Responds to keyboard input\n- [ ] Tested with Orca\n- [ ] Works in high contrast mode\n\n## Conclusion\n\nAccessibility is not a feature\u2014it\u0027s a fundamental requirement that ensures your application can be used by everyone, regardless of their abilities. By following these best practices and leveraging the OpenMaui platform\u0027s comprehensive accessibility infrastructure, you can build Linux applications that are truly inclusive.\n\nRemember: accessible applications benefit all users, not just those with disabilities. Clear labels, logical navigation, and robust keyboard support create better user experiences for everyone. Start with accessibility in mind, test regularly with assistive technologies, and continuously refine your implementation based on user feedback.\n\nThe AT-SPI2 integration in OpenMaui.Platform.Linux provides the technical foundation, but creating accessible applications requires thoughtful design, consistent implementation, and ongoing commitment to inclusive software development.\r\n\r\n---\r\n\r\n\u003E Accessibility isn\u0027t a feature\u2014it\u0027s a fundamental requirement that ensures your application can be used by everyone, regardless of their abilities.\r\n\r\n\u003E The IAccessible interface serves as the bridge between your MAUI controls and the Linux accessibility ecosystem, translating UI state into information that assistive technologies can understand.\r\n\r\n\u003E Testing with Orca isn\u0027t just about compliance\u2014it\u0027s about experiencing your application the way thousands of users will interact with it every day.", + "createdAt": 1769750550451, + "updatedAt": 1769750550451, + "tags": [ + "series", + "generated", + "Building Linux Apps with .NET MAUI" + ], + "isArticle": true, + "seriesGroup": "Building Linux Apps with .NET MAUI", + "subtitle": "Make your .NET MAUI Linux applications inclusive with comprehensive accessibility features, from screen readers to keyboard navigation.", + "pullQuotes": [ + "Accessibility isn\u0027t a feature\u2014it\u0027s a fundamental requirement that ensures your application can be used by everyone, regardless of their abilities.", + "The IAccessible interface serves as the bridge between your MAUI controls and the Linux accessibility ecosystem, translating UI state into information that assistive technologies can understand.", + "Testing with Orca isn\u0027t just about compliance\u2014it\u0027s about experiencing your application the way thousands of users will interact with it every day." + ], + "sections": [ + { + "header": "Introduction", + "content": "When building desktop applications for Linux, accessibility is not just a nice-to-have feature\u2014it\u0027s essential for creating inclusive software that serves all users. The OpenMaui.Platform.Linux project demonstrates how .NET MAUI applications can achieve full accessibility compliance on Linux through the **AT-SPI2** (Assistive Technology Service Provider Interface) framework.\n\nLinux desktop environments rely on AT-SPI2 to provide a standardized accessibility layer that screen readers like Orca, magnifiers, and other assistive technologies use to interact with applications. For developers building cross-platform .NET MAUI applications, understanding how to integrate with AT-SPI2 ensures that your Linux users\u2014including those with visual, motor, or cognitive disabilities\u2014can fully access your application\u0027s functionality.\n\nThis article explores the practical implementation of accessibility features in the OpenMaui Linux platform, covering everything from the foundational \u0060IAccessible\u0060 interface to testing your application with the Orca screen reader. Whether you\u0027re porting an existing MAUI application to Linux or building a new one from scratch, these patterns will help you create applications that are accessible by design.\n\n**Why Accessibility Matters:**\n- **Legal compliance**: Many jurisdictions require digital accessibility\n- **Market reach**: Approximately 15% of the global population has some form of disability\n- **Better UX for everyone**: Accessible design benefits all users, not just those with disabilities\n- **Technical excellence**: Proper accessibility implementation often reveals and fixes underlying architectural issues" + }, + { + "header": "Accessibility on Linux: AT-SPI2 Overview", + "content": "AT-SPI2 (Assistive Technology Service Provider Interface, version 2) is the de facto standard for accessibility on Linux desktop environments. It provides a D-Bus-based protocol that allows assistive technologies to query and manipulate application UI elements programmatically.\n\n## How AT-SPI2 Works\n\nAT-SPI2 operates on a client-server model:\n\n1. **Application (Server)**: Your application exposes its UI hierarchy through the AT-SPI2 protocol\n2. **Assistive Technology (Client)**: Screen readers like Orca, magnifiers, or voice control systems connect to your application via D-Bus\n3. **Registry**: The \u0060at-spi2-registryd\u0060 daemon manages connections between applications and assistive technologies\n\n## Key Concepts\n\n**Accessible Objects**: Every UI element in your application that should be accessible must be represented as an accessible object with:\n- **Role**: What the element is (button, text field, label, etc.)\n- **State**: Current condition (focused, checked, enabled, visible, etc.)\n- **Properties**: Name, description, value, and other metadata\n- **Actions**: Operations that can be performed (click, focus, toggle, etc.)\n- **Relations**: Connections to other accessible objects (labels for fields, etc.)\n\n**Accessibility Tree**: Similar to the DOM in web browsers, AT-SPI2 maintains a hierarchical tree of accessible objects that mirrors your application\u0027s UI structure.\n\n## Integration in OpenMaui\n\nThe OpenMaui.Platform.Linux implementation integrates AT-SPI2 through the \u0060SkiaView\u0060 base class, which implements the \u0060IAccessible\u0060 interface. This ensures that all 35\u002B Skia-based view implementations automatically participate in the accessibility infrastructure:\n\n\u0060\u0060\u0060csharp\npublic abstract class SkiaView : IAccessible\n{\n public string? AccessibilityLabel { get; set; }\n public string? AccessibilityHint { get; set; }\n public AccessibilityRole Role { get; set; }\n public AccessibilityState State { get; private set; }\n \n // Exposed to AT-SPI2 via platform bridge\n}\n\u0060\u0060\u0060\n\nThe platform automatically handles:\n- **D-Bus registration**: Each window and its accessible descendants are registered with the AT-SPI2 registry\n- **Event notifications**: State changes, focus events, and property updates are broadcast to listening assistive technologies\n- **Query responses**: When Orca or other tools request information about UI elements, the platform provides real-time data\n\nThis architecture ensures that MAUI developers don\u0027t need to write Linux-specific accessibility code\u2014the platform handles the AT-SPI2 integration transparently while respecting standard MAUI accessibility properties." + }, + { + "header": "The IAccessible Interface Implementation", + "content": "The \u0060IAccessible\u0060 interface is the cornerstone of accessibility support in OpenMaui Linux applications. It defines the contract that all accessible UI elements must fulfill, serving as the bridge between MAUI controls and the AT-SPI2 accessibility infrastructure.\n\n## Core Interface Design\n\nEvery Skia-based view in OpenMaui implements \u0060IAccessible\u0060, providing consistent accessibility support across all controls:\n\n\u0060\u0060\u0060csharp\npublic interface IAccessible\n{\n // Identity and description\n string? AccessibilityLabel { get; set; }\n string? AccessibilityHint { get; set; }\n \n // Semantic information\n AccessibilityRole Role { get; set; }\n AccessibilityState State { get; }\n \n // Hierarchy navigation\n IAccessible? AccessibilityParent { get; }\n IEnumerable\u003CIAccessible\u003E AccessibilityChildren { get; }\n \n // Value for interactive controls\n string? AccessibilityValue { get; }\n \n // Actions\n bool PerformAccessibilityAction(AccessibilityAction action);\n}\n\u0060\u0060\u0060\n\n## Implementation in SkiaButton\n\nLet\u0027s examine how \u0060SkiaButton\u0060 implements accessibility:\n\n\u0060\u0060\u0060csharp\npublic class SkiaButton : SkiaView, IButton, IAccessible\n{\n private string? _text;\n private ImageSource? _imageSource;\n \n public SkiaButton()\n {\n Role = AccessibilityRole.Button;\n Focusable = true;\n }\n \n public override string? AccessibilityLabel \n {\n get =\u003E base.AccessibilityLabel ?? _text;\n set =\u003E base.AccessibilityLabel = value;\n }\n \n public override AccessibilityState State\n {\n get\n {\n var state = AccessibilityState.None;\n \n if (IsEnabled)\n state |= AccessibilityState.Enabled;\n if (IsFocused)\n state |= AccessibilityState.Focused;\n if (!IsVisible)\n state |= AccessibilityState.Invisible;\n \n return state;\n }\n }\n \n public override bool PerformAccessibilityAction(AccessibilityAction action)\n {\n if (action == AccessibilityAction.Click \u0026\u0026 IsEnabled)\n {\n Command?.Execute(CommandParameter);\n Clicked?.Invoke(this, EventArgs.Empty);\n return true;\n }\n \n return base.PerformAccessibilityAction(action);\n }\n}\n\u0060\u0060\u0060\n\n## Key Implementation Patterns\n\n### 1. Fallback to Visual Content\n\nWhen no explicit \u0060AccessibilityLabel\u0060 is set, controls should fall back to their visual content:\n\n\u0060\u0060\u0060csharp\n// SkiaLabel\npublic override string? AccessibilityLabel\n{\n get =\u003E base.AccessibilityLabel ?? Text;\n set =\u003E base.AccessibilityLabel = value;\n}\n\n// SkiaEntry\npublic override string? AccessibilityLabel\n{\n get =\u003E base.AccessibilityLabel ?? Placeholder;\n set =\u003E base.AccessibilityLabel = value;\n}\n\u0060\u0060\u0060\n\n### 2. Dynamic State Management\n\nAccessibility state should reflect the current visual and interaction state:\n\n\u0060\u0060\u0060csharp\n// SkiaCheckBox\npublic override AccessibilityState State\n{\n get\n {\n var state = base.State;\n \n if (IsChecked)\n state |= AccessibilityState.Checked;\n \n return state;\n }\n}\n\n// SkiaEntry with text selection\npublic override AccessibilityState State\n{\n get\n {\n var state = base.State;\n \n if (IsReadOnly)\n state |= AccessibilityState.ReadOnly;\n if (!string.IsNullOrEmpty(SelectedText))\n state |= AccessibilityState.Selected;\n \n return state;\n }\n}\n\u0060\u0060\u0060\n\n### 3. Hierarchical Relationships\n\nContainer controls must expose their children to build the accessibility tree:\n\n\u0060\u0060\u0060csharp\npublic class SkiaScrollView : SkiaLayoutView\n{\n public override IEnumerable\u003CIAccessible\u003E AccessibilityChildren\n {\n get\n {\n if (Content is IAccessible accessible)\n yield return accessible;\n }\n }\n}\n\npublic class SkiaGrid : SkiaLayoutView\n{\n public override IEnumerable\u003CIAccessible\u003E AccessibilityChildren\n {\n get =\u003E Children.OfType\u003CIAccessible\u003E();\n }\n}\n\u0060\u0060\u0060\n\n## Notifying State Changes\n\nWhen accessibility-relevant properties change, the platform must notify AT-SPI2:\n\n\u0060\u0060\u0060csharp\nprotected void NotifyAccessibilityStateChanged()\n{\n // Platform bridge sends D-Bus signal\n AccessibilityManager.NotifyStateChanged(this);\n}\n\npublic bool IsChecked\n{\n get =\u003E _isChecked;\n set\n {\n if (_isChecked != value)\n {\n _isChecked = value;\n NotifyAccessibilityStateChanged();\n InvalidateVisual();\n }\n }\n}\n\u0060\u0060\u0060\n\nThis implementation pattern ensures that screen readers receive real-time updates when your application\u0027s state changes, providing users with immediate feedback about their interactions." + }, + { + "header": "Role and State Management", + "content": "Proper role and state management is critical for screen readers to understand and communicate the purpose and current condition of UI elements. The OpenMaui platform defines comprehensive enumerations that map to AT-SPI2 roles and states.\n\n## Accessibility Roles\n\nRoles define the semantic purpose of a UI element. OpenMaui supports all standard MAUI accessibility roles:\n\n\u0060\u0060\u0060csharp\npublic enum AccessibilityRole\n{\n None,\n Button,\n CheckBox,\n RadioButton,\n Entry,\n Label,\n Image,\n ProgressBar,\n Slider,\n Switch,\n Tab,\n TabList,\n Menu,\n MenuItem,\n MenuBar,\n Link,\n Header,\n Search,\n ComboBox,\n List,\n ListItem,\n Grid,\n GridCell,\n SpinButton,\n DatePicker,\n TimePicker\n}\n\u0060\u0060\u0060\n\n### Role Assignment Examples\n\nEach control sets its role in the constructor:\n\n\u0060\u0060\u0060csharp\npublic class SkiaButton : SkiaView\n{\n public SkiaButton()\n {\n Role = AccessibilityRole.Button;\n Focusable = true;\n }\n}\n\npublic class SkiaEntry : SkiaView\n{\n public SkiaEntry()\n {\n Role = AccessibilityRole.Entry;\n Focusable = true;\n }\n}\n\npublic class SkiaLabel : SkiaView\n{\n public SkiaLabel()\n {\n Role = AccessibilityRole.Label;\n Focusable = false; // Labels typically aren\u0027t focusable\n }\n}\n\npublic class SkiaSearchBar : SkiaView\n{\n public SkiaSearchBar()\n {\n Role = AccessibilityRole.Search;\n Focusable = true;\n }\n}\n\u0060\u0060\u0060\n\n## Accessibility States\n\nStates describe the current condition of a UI element. Unlike roles (which are static), states change dynamically:\n\n\u0060\u0060\u0060csharp\n[Flags]\npublic enum AccessibilityState\n{\n None = 0,\n Enabled = 1 \u003C\u003C 0,\n Focused = 1 \u003C\u003C 1,\n Checked = 1 \u003C\u003C 2,\n Selected = 1 \u003C\u003C 3,\n Expanded = 1 \u003C\u003C 4,\n Collapsed = 1 \u003C\u003C 5,\n Busy = 1 \u003C\u003C 6,\n ReadOnly = 1 \u003C\u003C 7,\n Required = 1 \u003C\u003C 8,\n Invalid = 1 \u003C\u003C 9,\n Invisible = 1 \u003C\u003C 10,\n Multiline = 1 \u003C\u003C 11,\n Multiselectable = 1 \u003C\u003C 12\n}\n\u0060\u0060\u0060\n\n### Dynamic State Computation\n\nThe base \u0060SkiaView\u0060 class provides default state logic:\n\n\u0060\u0060\u0060csharp\npublic virtual AccessibilityState State\n{\n get\n {\n var state = AccessibilityState.None;\n \n if (IsEnabled)\n state |= AccessibilityState.Enabled;\n \n if (IsFocused)\n state |= AccessibilityState.Focused;\n \n if (!IsVisible)\n state |= AccessibilityState.Invisible;\n \n return state;\n }\n}\n\u0060\u0060\u0060\n\nControls override this to add specific states:\n\n\u0060\u0060\u0060csharp\n// SkiaCheckBox\npublic override AccessibilityState State\n{\n get\n {\n var state = base.State;\n \n if (IsChecked)\n state |= AccessibilityState.Checked;\n \n return state;\n }\n}\n\n// SkiaEditor (multiline text)\npublic override AccessibilityState State\n{\n get\n {\n var state = base.State;\n state |= AccessibilityState.Multiline;\n \n if (IsReadOnly)\n state |= AccessibilityState.ReadOnly;\n \n return state;\n }\n}\n\n// SkiaCollectionView\npublic override AccessibilityState State\n{\n get\n {\n var state = base.State;\n \n if (SelectionMode == SelectionMode.Multiple)\n state |= AccessibilityState.Multiselectable;\n \n return state;\n }\n}\n\n// SkiaActivityIndicator\npublic override AccessibilityState State\n{\n get\n {\n var state = base.State;\n \n if (IsRunning)\n state |= AccessibilityState.Busy;\n \n return state;\n }\n}\n\u0060\u0060\u0060\n\n## Complex State Management: Expandable Controls\n\nControls like accordions, tree views, or combo boxes need to manage expanded/collapsed states:\n\n\u0060\u0060\u0060csharp\npublic class SkiaPicker : SkiaView\n{\n private bool _isOpen;\n \n public SkiaPicker()\n {\n Role = AccessibilityRole.ComboBox;\n Focusable = true;\n }\n \n public override AccessibilityState State\n {\n get\n {\n var state = base.State;\n \n if (_isOpen)\n state |= AccessibilityState.Expanded;\n else\n state |= AccessibilityState.Collapsed;\n \n return state;\n }\n }\n \n private void ToggleDropdown()\n {\n _isOpen = !_isOpen;\n NotifyAccessibilityStateChanged();\n \n // Announce state change to screen reader\n if (_isOpen)\n AnnounceAccessibility(\u0022Dropdown opened\u0022);\n else\n AnnounceAccessibility(\u0022Dropdown closed\u0022);\n }\n}\n\u0060\u0060\u0060\n\n## Form Validation States\n\nFor forms, the \u0060Invalid\u0060 and \u0060Required\u0060 states communicate validation status:\n\n\u0060\u0060\u0060csharp\npublic class SkiaEntry : SkiaView\n{\n private bool _isRequired;\n private bool _isValid = true;\n \n public bool IsRequired\n {\n get =\u003E _isRequired;\n set\n {\n _isRequired = value;\n NotifyAccessibilityStateChanged();\n }\n }\n \n public bool IsValid\n {\n get =\u003E _isValid;\n set\n {\n _isValid = value;\n NotifyAccessibilityStateChanged();\n \n if (!_isValid)\n AnnounceAccessibility(\u0022Invalid input\u0022);\n }\n }\n \n public override AccessibilityState State\n {\n get\n {\n var state = base.State;\n \n if (_isRequired)\n state |= AccessibilityState.Required;\n \n if (!_isValid)\n state |= AccessibilityState.Invalid;\n \n return state;\n }\n }\n}\n\u0060\u0060\u0060\n\n## Best Practices\n\n1. **Set roles in constructors**: Roles should be immutable and set when the control is created\n2. **Compute states dynamically**: Always calculate state from current properties, never cache it\n3. **Notify on changes**: Call \u0060NotifyAccessibilityStateChanged()\u0060 whenever state-affecting properties change\n4. **Use appropriate granularity**: Don\u0027t over-specify states\u2014only include those that are semantically meaningful\n5. **Test state combinations**: Verify that multiple states work correctly together (e.g., Focused \u002B Checked \u002B Enabled)\n\nProper role and state management ensures that screen reader users receive accurate, timely information about your application\u0027s UI, enabling them to navigate and interact effectively." + }, + { + "header": "Keyboard Navigation Patterns", + "content": "Keyboard navigation is essential for accessibility, enabling users who cannot use a mouse\u2014whether due to visual impairments, motor disabilities, or personal preference\u2014to navigate and interact with your application. OpenMaui implements comprehensive keyboard navigation patterns that align with both MAUI standards and Linux desktop conventions.\n\n## Focus Management\n\nThe foundation of keyboard navigation is focus management. The \u0060SkiaView\u0060 base class provides the core focus infrastructure:\n\n\u0060\u0060\u0060csharp\npublic abstract class SkiaView : IAccessible\n{\n private bool _isFocused;\n private bool _focusable = true;\n \n public bool Focusable\n {\n get =\u003E _focusable;\n set =\u003E _focusable = value;\n }\n \n public bool IsFocused\n {\n get =\u003E _isFocused;\n private set\n {\n if (_isFocused != value)\n {\n _isFocused = value;\n OnFocusChanged();\n NotifyAccessibilityStateChanged();\n InvalidateVisual();\n }\n }\n }\n \n public virtual bool Focus()\n {\n if (!Focusable || !IsEnabled || !IsVisible)\n return false;\n \n IsFocused = true;\n Focused?.Invoke(this, new FocusEventArgs(this, true));\n return true;\n }\n \n public virtual void Unfocus()\n {\n if (IsFocused)\n {\n IsFocused = false;\n Unfocused?.Invoke(this, new FocusEventArgs(this, false));\n }\n }\n}\n\u0060\u0060\u0060\n\n## Tab Navigation\n\nThe Tab key moves focus forward through focusable elements, while Shift\u002BTab moves backward. The \u0060FocusManager\u0060 handles tab order:\n\n\u0060\u0060\u0060csharp\npublic class FocusManager\n{\n private readonly List\u003CIAccessible\u003E _focusableElements = new();\n private int _currentIndex = -1;\n \n public void BuildFocusChain(IAccessible root)\n {\n _focusableElements.Clear();\n CollectFocusableElements(root);\n }\n \n private void CollectFocusableElements(IAccessible element)\n {\n if (element is SkiaView view \u0026\u0026 view.Focusable \u0026\u0026 \n view.IsEnabled \u0026\u0026 view.IsVisible)\n {\n _focusableElements.Add(element);\n }\n \n foreach (var child in element.AccessibilityChildren)\n {\n CollectFocusableElements(child);\n }\n }\n \n public bool FocusNext()\n {\n if (_focusableElements.Count == 0)\n return false;\n \n _currentIndex = (_currentIndex \u002B 1) % _focusableElements.Count;\n return FocusCurrent();\n }\n \n public bool FocusPrevious()\n {\n if (_focusableElements.Count == 0)\n return false;\n \n _currentIndex--;\n if (_currentIndex \u003C 0)\n _currentIndex = _focusableElements.Count - 1;\n \n return FocusCurrent();\n }\n \n private bool FocusCurrent()\n {\n if (_currentIndex \u003E= 0 \u0026\u0026 _currentIndex \u003C _focusableElements.Count)\n {\n var element = _focusableElements[_currentIndex];\n if (element is SkiaView view)\n return view.Focus();\n }\n return false;\n }\n}\n\u0060\u0060\u0060\n\n## Arrow Key Navigation\n\nArrow keys provide directional navigation within container controls. The \u0060SkiaCollectionView\u0060 demonstrates this pattern:\n\n\u0060\u0060\u0060csharp\npublic class SkiaCollectionView : SkiaView\n{\n private int _selectedIndex = -1;\n \n protected override void OnKeyDown(KeyEventArgs e)\n {\n switch (e.Key)\n {\n case Key.Down:\n SelectNext();\n e.Handled = true;\n break;\n \n case Key.Up:\n SelectPrevious();\n e.Handled = true;\n break;\n \n case Key.Home:\n SelectFirst();\n e.Handled = true;\n break;\n \n case Key.End:\n SelectLast();\n e.Handled = true;\n break;\n \n case Key.Space:\n case Key.Enter:\n ActivateSelected();\n e.Handled = true;\n break;\n }\n \n base.OnKeyDown(e);\n }\n \n private void SelectNext()\n {\n if (_selectedIndex \u003C ItemsSource.Count - 1)\n {\n SelectedIndex = _selectedIndex \u002B 1;\n ScrollToSelected();\n AnnounceAccessibility($\u0022Item {_selectedIndex \u002B 1} of {ItemsSource.Count}\u0022);\n }\n }\n}\n\u0060\u0060\u0060\n\n## Keyboard Shortcuts and Mnemonics\n\nMenu items and buttons support keyboard shortcuts:\n\n\u0060\u0060\u0060csharp\npublic class SkiaMenuItem : SkiaView\n{\n public KeyGesture? Accelerator { get; set; }\n public char? Mnemonic { get; set; }\n \n public override string? AccessibilityLabel\n {\n get\n {\n var label = base.AccessibilityLabel ?? Text;\n \n if (Accelerator != null)\n label \u002B= $\u0022 ({Accelerator})\u0022; // e.g., \u0022Save (Ctrl\u002BS)\u0022\n \n return label;\n }\n }\n \n protected override void OnKeyDown(KeyEventArgs e)\n {\n if (Accelerator != null \u0026\u0026 Accelerator.Matches(e))\n {\n Command?.Execute(CommandParameter);\n e.Handled = true;\n }\n \n base.OnKeyDown(e);\n }\n}\n\npublic class SkiaMenuBar : SkiaView\n{\n protected override void OnKeyDown(KeyEventArgs e)\n {\n // Alt key activates menu bar\n if (e.Key == Key.Alt \u0026\u0026 !e.IsRepeat)\n {\n ActivateMenuBar();\n e.Handled = true;\n return;\n }\n \n // Alt\u002BLetter activates menu by mnemonic\n if (e.Modifiers.HasFlag(KeyModifiers.Alt) \u0026\u0026 e.Key \u003E= Key.A \u0026\u0026 e.Key \u003C= Key.Z)\n {\n char mnemonic = (char)(\u0027a\u0027 \u002B (e.Key - Key.A));\n ActivateMenuByMnemonic(mnemonic);\n e.Handled = true;\n }\n \n base.OnKeyDown(e);\n }\n}\n\u0060\u0060\u0060\n\n## Text Navigation in Entry Controls\n\n\u0060SkiaEntry\u0060 and \u0060SkiaEditor\u0060 implement comprehensive text navigation:\n\n\u0060\u0060\u0060csharp\npublic class SkiaEntry : SkiaView\n{\n private int _cursorPosition;\n private int _selectionStart;\n private int _selectionLength;\n \n protected override void OnKeyDown(KeyEventArgs e)\n {\n bool shift = e.Modifiers.HasFlag(KeyModifiers.Shift);\n bool ctrl = e.Modifiers.HasFlag(KeyModifiers.Control);\n \n switch (e.Key)\n {\n case Key.Left:\n if (ctrl)\n MoveCursorToPreviousWord(shift);\n else\n MoveCursorLeft(shift);\n e.Handled = true;\n break;\n \n case Key.Right:\n if (ctrl)\n MoveCursorToNextWord(shift);\n else\n MoveCursorRight(shift);\n e.Handled = true;\n break;\n \n case Key.Home:\n MoveCursorToStart(shift);\n e.Handled = true;\n break;\n \n case Key.End:\n MoveCursorToEnd(shift);\n e.Handled = true;\n break;\n \n case Key.A:\n if (ctrl)\n {\n SelectAll();\n AnnounceAccessibility(\u0022All text selected\u0022);\n e.Handled = true;\n }\n break;\n }\n \n base.OnKeyDown(e);\n }\n}\n\u0060\u0060\u0060\n\n## Escape Key Handling\n\nThe Escape key should cancel operations and close dialogs:\n\n\u0060\u0060\u0060csharp\npublic class SkiaAlertDialog : SkiaView\n{\n protected override void OnKeyDown(KeyEventArgs e)\n {\n if (e.Key == Key.Escape)\n {\n CancelButton?.Command?.Execute(CancelButton.CommandParameter);\n Close();\n e.Handled = true;\n }\n \n base.OnKeyDown(e);\n }\n}\n\u0060\u0060\u0060\n\n## Visual Focus Indicators\n\nFocused elements must be visually distinguishable:\n\n\u0060\u0060\u0060csharp\nprotected override void OnPaint(SKCanvas canvas)\n{\n // Draw control content\n DrawContent(canvas);\n \n // Draw focus indicator\n if (IsFocused)\n {\n var focusRect = new SKRect(0, 0, Width, Height);\n focusRect.Inflate(-2, -2);\n \n using var paint = new SKPaint\n {\n Style = SKPaintStyle.Stroke,\n Color = SkiaTheme.Current.FocusColor,\n StrokeWidth = 2,\n PathEffect = SKPathEffect.CreateDash(new[] { 4f, 2f }, 0)\n };\n \n canvas.DrawRect(focusRect, paint);\n }\n}\n\u0060\u0060\u0060\n\n## Best Practices\n\n1. **Make all interactive elements focusable**: Buttons, links, inputs, and custom controls should accept focus\n2. **Provide visible focus indicators**: Users must see which element has focus\n3. **Support standard shortcuts**: Ctrl\u002BC/V/X for copy/paste/cut, Ctrl\u002BA for select all, etc.\n4. **Announce navigation changes**: Use \u0060AnnounceAccessibility()\u0060 when focus moves or selection changes\n5. **Test keyboard-only navigation**: Ensure your entire application is usable without a mouse\n6. **Respect tab order**: Logical tab order follows visual layout (left-to-right, top-to-bottom)\n7. **Trap focus in modals**: Dialogs should keep focus within themselves until closed\n\nProper keyboard navigation ensures that your application is fully accessible to users who rely on keyboard input, whether due to disability, preference, or workflow efficiency." + }, + { + "header": "High Contrast Mode Support", + "content": "High contrast mode is essential for users with low vision or certain types of color blindness. Linux desktop environments provide system-wide high contrast themes, and applications must respect these settings to ensure readability and usability.\n\n## Detecting High Contrast Mode\n\nThe OpenMaui platform integrates with the \u0060SystemThemeService\u0060 to detect high contrast mode across different desktop environments:\n\n\u0060\u0060\u0060csharp\npublic class SystemThemeService\n{\n private bool _isHighContrast;\n \n public bool IsHighContrastMode\n {\n get =\u003E _isHighContrast;\n private set\n {\n if (_isHighContrast != value)\n {\n _isHighContrast = value;\n OnHighContrastChanged();\n }\n }\n }\n \n public event EventHandler? HighContrastChanged;\n \n public void DetectHighContrast()\n {\n // GNOME detection\n if (IsGnomeDesktop())\n {\n var contrastSetting = GetGSettingsValue(\n \u0022org.gnome.desktop.a11y.interface\u0022, \n \u0022high-contrast\u0022);\n IsHighContrastMode = contrastSetting == \u0022true\u0022;\n }\n // KDE Plasma detection\n else if (IsKdeDesktop())\n {\n var theme = GetKdeColorScheme();\n IsHighContrastMode = theme.Contains(\u0022HighContrast\u0022, \n StringComparison.OrdinalIgnoreCase);\n }\n // Fallback: detect by theme name\n else\n {\n var themeName = GetGtkThemeName();\n IsHighContrastMode = themeName.Contains(\u0022HighContrast\u0022, \n StringComparison.OrdinalIgnoreCase);\n }\n }\n}\n\u0060\u0060\u0060\n\n## The SkiaTheme System\n\nThe \u0060SkiaTheme\u0060 class provides centralized theme management with high contrast support:\n\n\u0060\u0060\u0060csharp\npublic class SkiaTheme\n{\n public static SkiaTheme Current { get; private set; } = new();\n \n public bool IsDarkMode { get; set; }\n public bool IsHighContrast { get; set; }\n \n // Color properties with high contrast overrides\n public SKColor ForegroundColor =\u003E IsHighContrast \n ? (IsDarkMode ? SKColors.White : SKColors.Black)\n : (IsDarkMode ? new SKColor(230, 230, 230) : new SKColor(30, 30, 30));\n \n public SKColor BackgroundColor =\u003E IsHighContrast\n ? (IsDarkMode ? SKColors.Black : SKColors.White)\n : (IsDarkMode ? new SKColor(30, 30, 30) : new SKColor(250, 250, 250));\n \n public SKColor AccentColor =\u003E IsHighContrast\n ? (IsDarkMode ? SKColors.Yellow : SKColors.Blue)\n : new SKColor(0, 120, 215); // Default blue\n \n public SKColor BorderColor =\u003E IsHighContrast\n ? ForegroundColor\n : (IsDarkMode ? new SKColor(60, 60, 60) : new SKColor(200, 200, 200));\n \n public SKColor FocusColor =\u003E IsHighContrast\n ? (IsDarkMode ? SKColors.Yellow : SKColors.Blue)\n : new SKColor(0, 120, 215);\n \n public SKColor DisabledForegroundColor =\u003E IsHighContrast\n ? (IsDarkMode ? new SKColor(128, 128, 128) : new SKColor(128, 128, 128))\n : (IsDarkMode ? new SKColor(100, 100, 100) : new SKColor(160, 160, 160));\n \n public float MinimumContrastRatio =\u003E IsHighContrast ? 7.0f : 4.5f;\n \n public static void ApplySystemTheme(bool isDark, bool isHighContrast)\n {\n Current = new SkiaTheme\n {\n IsDarkMode = isDark,\n IsHighContrast = isHighContrast\n };\n \n ThemeChanged?.Invoke(null, EventArgs.Empty);\n }\n \n public static event EventHandler? ThemeChanged;\n}\n\u0060\u0060\u0060\n\n## Applying High Contrast Themes to Controls\n\nControls automatically respond to theme changes:\n\n\u0060\u0060\u0060csharp\npublic class SkiaButton : SkiaView\n{\n public SkiaButton()\n {\n SkiaTheme.ThemeChanged \u002B= OnThemeChanged;\n }\n \n private void OnThemeChanged(object? sender, EventArgs e)\n {\n InvalidateVisual(); // Trigger repaint with new colors\n }\n \n protected override void OnPaint(SKCanvas canvas)\n {\n var theme = SkiaTheme.Current;\n \n // Background\n var bgColor = IsEnabled \n ? (IsPressed ? theme.AccentColor.WithAlpha(200) : theme.AccentColor)\n : theme.BackgroundColor;\n \n using (var paint = new SKPaint { Color = bgColor })\n {\n canvas.DrawRoundRect(Bounds, CornerRadius, CornerRadius, paint);\n }\n \n // Border (always visible in high contrast)\n if (theme.IsHighContrast || IsFocused)\n {\n using var borderPaint = new SKPaint\n {\n Style = SKPaintStyle.Stroke,\n Color = IsFocused ? theme.FocusColor : theme.BorderColor,\n StrokeWidth = theme.IsHighContrast ? 2 : 1\n };\n \n canvas.DrawRoundRect(Bounds, CornerRadius, CornerRadius, borderPaint);\n }\n \n // Text\n var textColor = IsEnabled \n ? (theme.IsHighContrast ? theme.BackgroundColor : SKColors.White)\n : theme.DisabledForegroundColor;\n \n using (var textPaint = new SKPaint\n {\n Color = textColor,\n TextSize = FontSize,\n IsAntialias = true,\n Typeface = GetTypeface()\n })\n {\n canvas.DrawText(Text, TextX, TextY, textPaint);\n }\n }\n}\n\u0060\u0060\u0060\n\n## Ensuring Sufficient Contrast\n\nThe platform includes contrast checking utilities:\n\n\u0060\u0060\u0060csharp\npublic static class ContrastChecker\n{\n public static double CalculateContrastRatio(SKColor foreground, SKColor background)\n {\n double luminance1 = GetRelativeLuminance(foreground);\n double luminance2 = GetRelativeLuminance(background);\n \n double lighter = Math.Max(luminance1, luminance2);\n double darker = Math.Min(luminance1, luminance2);\n \n return (lighter \u002B 0.05) / (darker \u002B 0.05);\n }\n \n private static double GetRelativeLuminance(SKColor color)\n {\n double r = GetLinearValue(color.Red / 255.0);\n double g = GetLinearValue(color.Green / 255.0);\n double b = GetLinearValue(color.Blue / 255.0);\n \n return 0.2126 * r \u002B 0.7152 * g \u002B 0.0722 * b;\n }\n \n private static double GetLinearValue(double value)\n {\n return value \u003C= 0.03928 \n ? value / 12.92 \n : Math.Pow((value \u002B 0.055) / 1.055, 2.4);\n }\n \n public static bool MeetsWCAGAA(SKColor foreground, SKColor background)\n {\n return CalculateContrastRatio(foreground, background) \u003E= 4.5;\n }\n \n public static bool MeetsWCAGAAA(SKColor foreground, SKColor background)\n {\n return CalculateContrastRatio(foreground, background) \u003E= 7.0;\n }\n}\n\u0060\u0060\u0060\n\n## High Contrast Best Practices for Custom Controls\n\nWhen implementing custom controls:\n\n\u0060\u0060\u0060csharp\npublic class CustomControl : SkiaView\n{\n protected override void OnPaint(SKCanvas canvas)\n {\n var theme = SkiaTheme.Current;\n \n // 1. Always draw borders in high contrast mode\n if (theme.IsHighContrast)\n {\n using var borderPaint = new SKPaint\n {\n Style = SKPaintStyle.Stroke,\n Color = theme.BorderColor,\n StrokeWidth = 2\n };\n canvas.DrawRect(Bounds, borderPaint);\n }\n \n // 2. Use solid colors, avoid gradients\n var fillColor = theme.IsHighContrast \n ? theme.BackgroundColor \n : GetGradientColor();\n \n // 3. Increase icon/text sizes slightly\n var fontSize = theme.IsHighContrast \n ? FontSize * 1.1f \n : FontSize;\n \n // 4. Remove transparency\n var color = theme.ForegroundColor;\n if (theme.IsHighContrast)\n color = color.WithAlpha(255);\n \n // 5. Use system colors, don\u0027t hardcode\n using var paint = new SKPaint\n {\n Color = theme.IsHighContrast \n ? theme.ForegroundColor \n : CustomColor\n };\n }\n}\n\u0060\u0060\u0060\n\n## Responding to Runtime Theme Changes\n\nUsers can toggle high contrast mode while your application is running:\n\n\u0060\u0060\u0060csharp\npublic class LinuxApplication\n{\n private readonly SystemThemeService _themeService;\n \n public LinuxApplication()\n {\n _themeService = new SystemThemeService();\n _themeService.HighContrastChanged \u002B= OnHighContrastChanged;\n _themeService.StartMonitoring();\n }\n \n private void OnHighContrastChanged(object? sender, EventArgs e)\n {\n // Update global theme\n SkiaTheme.ApplySystemTheme(\n _themeService.IsDarkMode,\n _themeService.IsHighContrastMode);\n \n // All views automatically repaint due to ThemeChanged event\n \n // Announce to screen reader\n var mode = _themeService.IsHighContrastMode \n ? \u0022High contrast mode enabled\u0022 \n : \u0022High contrast mode disabled\u0022;\n AccessibilityManager.Announce(mode);\n }\n}\n\u0060\u0060\u0060\n\n## Testing High Contrast Mode\n\nTo test your application:\n\n**GNOME:**\n\u0060\u0060\u0060bash\n# Enable high contrast\ngsettings set org.gnome.desktop.a11y.interface high-contrast true\n\n# Disable high contrast\ngsettings set org.gnome.desktop.a11y.interface high-contrast false\n\u0060\u0060\u0060\n\n**KDE Plasma:**\n1. System Settings \u2192 Appearance \u2192 Colors\n2. Select \u0022High Contrast\u0022 scheme\n\n**Manual Testing Checklist:**\n- [ ] All text is readable with sufficient contrast\n- [ ] All interactive elements have visible borders\n- [ ] Focus indicators are clearly visible\n- [ ] Icons and images have text alternatives\n- [ ] No information is conveyed by color alone\n- [ ] Custom colors are replaced with system colors\n- [ ] Application responds to runtime theme changes\n\nHigh contrast support ensures that users with visual impairments can comfortably use your application, meeting both accessibility standards and user expectations on Linux desktop environments." + }, + { + "header": "Testing with Orca Screen Reader", + "content": "Orca is the primary screen reader for Linux desktop environments, providing speech and Braille output for visually impaired users. Testing your application with Orca is essential to ensure that your accessibility implementation works correctly in real-world scenarios.\n\n## Installing and Configuring Orca\n\n**Installation:**\n\n\u0060\u0060\u0060bash\n# Ubuntu/Debian\nsudo apt install orca\n\n# Fedora\nsudo dnf install orca\n\n# Arch Linux\nsudo pacman -S orca\n\u0060\u0060\u0060\n\n**Starting Orca:**\n\n\u0060\u0060\u0060bash\n# Launch Orca\norca\n\n# Launch Orca with preferences dialog\norca --setup\n\n# Enable Orca at login (GNOME)\ngsettings set org.gnome.desktop.a11y.applications screen-reader-enabled true\n\u0060\u0060\u0060\n\n**Basic Orca Commands:**\n\n- **Orca Modifier Key**: Insert (or Caps Lock, configurable)\n- **Toggle Orca on/off**: \u0060Super \u002B Alt \u002B S\u0060 (GNOME)\n- **Orca preferences**: \u0060Orca \u002B Space\u0060\n- **Read current line**: \u0060Orca \u002B Up Arrow\u0060\n- **Read current window**: \u0060Orca \u002B B\u0060\n- **Where am I**: \u0060Orca \u002B Enter\u0060 (announces current focus)\n\n## Testing Your Application\n\n### 1. Launch and Initial Focus\n\nWhen your application starts, Orca should announce the window title and initial focused element:\n\n\u0060\u0060\u0060csharp\npublic class MainWindow : Window\n{\n public MainWindow()\n {\n Title = \u0022My Accessible Application\u0022;\n \n // Set initial focus to meaningful element\n var firstButton = new Button \n { \n Text = \u0022Get Started\u0022,\n AutomationId = \u0022GetStartedButton\u0022\n };\n \n Content = new VerticalStackLayout\n {\n Children = { firstButton }\n };\n \n // Ensure focus is set after window loads\n Loaded \u002B= (s, e) =\u003E firstButton.Focus();\n }\n}\n\u0060\u0060\u0060\n\n**Expected Orca Output:**\n\u0022My Accessible Application window. Get Started button.\u0022\n\n### 2. Navigation Testing\n\nTest tab navigation through your application:\n\n\u0060\u0060\u0060csharp\npublic class FormPage : ContentPage\n{\n public FormPage()\n {\n Content = new VerticalStackLayout\n {\n Children =\n {\n new Label \n { \n Text = \u0022Name:\u0022,\n SemanticProperties.HeadingLevel = SemanticHeadingLevel.Level2\n },\n new Entry \n { \n Placeholder = \u0022Enter your name\u0022,\n AutomationId = \u0022NameEntry\u0022,\n SemanticProperties.Description = \u0022Required field\u0022\n },\n new Label { Text = \u0022Email:\u0022 },\n new Entry \n { \n Placeholder = \u0022Enter your email\u0022,\n Keyboard = Keyboard.Email,\n AutomationId = \u0022EmailEntry\u0022\n },\n new Button \n { \n Text = \u0022Submit\u0022,\n Command = new Command(OnSubmit),\n AutomationId = \u0022SubmitButton\u0022\n }\n }\n };\n }\n}\n\u0060\u0060\u0060\n\n**Testing Steps:**\n1. Press Tab to move through elements\n2. Verify Orca announces each element\u0027s role and label\n3. Verify Orca announces state changes (e.g., \u0022checked\u0022, \u0022expanded\u0022)\n\n**Expected Orca Output:**\n- \u0022Name: heading level 2\u0022\n- \u0022Enter your name entry. Required field.\u0022\n- \u0022Email: heading\u0022\n- \u0022Enter your email entry\u0022\n- \u0022Submit button\u0022\n\n### 3. Testing Interactive Controls\n\n**Buttons:**\n\n\u0060\u0060\u0060csharp\nvar button = new Button\n{\n Text = \u0022Save Document\u0022,\n ImageSource = \u0022save.png\u0022,\n Command = new Command(SaveDocument)\n};\n\n// Orca should announce: \u0022Save Document button\u0022\n// After clicking: \u0022Save Document button pressed\u0022\n\u0060\u0060\u0060\n\n**CheckBoxes:**\n\n\u0060\u0060\u0060csharp\nvar checkbox = new CheckBox\n{\n IsChecked = false,\n SemanticProperties.Description = \u0022Enable notifications\u0022\n};\n\n// Initial: \u0022Enable notifications checkbox not checked\u0022\n// After toggling: \u0022Enable notifications checkbox checked\u0022\n\u0060\u0060\u0060\n\n**Sliders:**\n\n\u0060\u0060\u0060csharp\nvar slider = new Slider\n{\n Minimum = 0,\n Maximum = 100,\n Value = 50,\n SemanticProperties.Description = \u0022Volume\u0022\n};\n\n// Orca should announce: \u0022Volume slider 50\u0022\n// When adjusting: \u0022Volume 75\u0022 (announces value changes)\n\u0060\u0060\u0060\n\n### 4. Testing Collections\n\n\u0060\u0060\u0060csharp\npublic class ContactListPage : ContentPage\n{\n public ContactListPage()\n {\n var collectionView = new CollectionView\n {\n ItemsSource = GetContacts(),\n ItemTemplate = new DataTemplate(() =\u003E\n {\n var label = new Label();\n label.SetBinding(Label.TextProperty, \u0022Name\u0022);\n \n // Set semantic properties for each item\n var grid = new Grid\n {\n Children = { label }\n };\n \n grid.SetBinding(\n SemanticProperties.DescriptionProperty, \n new Binding(\u0022.\u0022, \n stringFormat: \u0022Contact: {0}, {1}\u0022,\n source: new[] { \u0022Name\u0022, \u0022Email\u0022 }));\n \n return grid;\n })\n };\n \n Content = collectionView;\n }\n}\n\u0060\u0060\u0060\n\n**Testing Steps:**\n1. Use arrow keys to navigate list items\n2. Verify Orca announces item content and position\n3. Verify selection state changes are announced\n\n**Expected Orca Output:**\n- \u0022Contact: John Doe, john@example.com. List item 1 of 10.\u0022\n- \u0022Contact: Jane Smith, jane@example.com. List item 2 of 10.\u0022\n\n### 5. Testing Dialogs and Modals\n\n\u0060\u0060\u0060csharp\npublic async Task ShowAccessibleDialog()\n{\n var result = await DisplayAlert(\n \u0022Confirm Delete\u0022,\n \u0022Are you sure you want to delete this item? This action cannot be undone.\u0022,\n \u0022Delete\u0022,\n \u0022Cancel\u0022);\n \n if (result)\n {\n // Announce completion\n SemanticScreenReader.Announce(\u0022Item deleted\u0022);\n }\n}\n\u0060\u0060\u0060\n\n**Expected Orca Output:**\n- \u0022Confirm Delete dialog. Are you sure you want to delete this item? This action cannot be undone.\u0022\n- \u0022Delete button\u0022\n- \u0022Cancel button\u0022\n- After action: \u0022Item deleted\u0022\n\n## Common Issues and Fixes\n\n### Issue 1: Orca Not Announcing Elements\n\n**Problem:** Orca skips over custom controls or announces them incorrectly.\n\n**Solution:** Ensure \u0060IAccessible\u0060 is properly implemented:\n\n\u0060\u0060\u0060csharp\npublic class CustomControl : SkiaView\n{\n public CustomControl()\n {\n // Set appropriate role\n Role = AccessibilityRole.Button;\n \n // Make focusable\n Focusable = true;\n \n // Provide label\n AccessibilityLabel = \u0022Custom Action\u0022;\n }\n \n public override AccessibilityState State\n {\n get\n {\n var state = AccessibilityState.Enabled;\n if (IsFocused)\n state |= AccessibilityState.Focused;\n return state;\n }\n }\n}\n\u0060\u0060\u0060\n\n### Issue 2: Incorrect State Announcements\n\n**Problem:** Orca announces outdated state (e.g., \u0022checked\u0022 when unchecked).\n\n**Solution:** Call \u0060NotifyAccessibilityStateChanged()\u0060 when state changes:\n\n\u0060\u0060\u0060csharp\npublic bool IsChecked\n{\n get =\u003E _isChecked;\n set\n {\n if (_isChecked != value)\n {\n _isChecked = value;\n NotifyAccessibilityStateChanged();\n InvalidateVisual();\n }\n }\n}\n\u0060\u0060\u0060\n\n### Issue 3: Missing Context\n\n**Problem:** Orca announces \u0022button\u0022 without describing what it does.\n\n**Solution:** Use \u0060SemanticProperties\u0060 for additional context:\n\n\u0060\u0060\u0060csharp\nvar deleteButton = new Button\n{\n ImageSource = \u0022trash.png\u0022,\n // Don\u0027t rely on image alone\n SemanticProperties = \n {\n Description = \u0022Delete selected items\u0022,\n Hint = \u0022This action cannot be undone\u0022\n }\n};\n\u0060\u0060\u0060\n\n## Automated Accessibility Testing\n\nWhile manual testing with Orca is essential, you can also write automated tests:\n\n\u0060\u0060\u0060csharp\n[Fact]\npublic void Button_Should_Have_Accessible_Properties()\n{\n // Arrange\n var button = new SkiaButton\n {\n Text = \u0022Click Me\u0022\n };\n \n // Assert\n Assert.Equal(AccessibilityRole.Button, button.Role);\n Assert.Equal(\u0022Click Me\u0022, button.AccessibilityLabel);\n Assert.True(button.Focusable);\n Assert.Contains(AccessibilityState.Enabled, button.State);\n}\n\n[Fact]\npublic void CheckBox_Should_Announce_State_Changes()\n{\n // Arrange\n var checkbox = new SkiaCheckBox();\n bool stateChanged = false;\n \n checkbox.AccessibilityStateChanged \u002B= (s, e) =\u003E stateChanged = true;\n \n // Act\n checkbox.IsChecked = true;\n \n // Assert\n Assert.True(stateChanged);\n Assert.Contains(AccessibilityState.Checked, checkbox.State);\n}\n\u0060\u0060\u0060\n\n## Testing Checklist\n\n- [ ] Application announces window title on launch\n- [ ] Tab navigation moves through all focusable elements\n- [ ] Each element announces its role (button, entry, checkbox, etc.)\n- [ ] Labels and descriptions are meaningful and clear\n- [ ] State changes are announced immediately (checked, expanded, etc.)\n- [ ] Form validation errors are announced\n- [ ] List items announce position (\u0022item 3 of 10\u0022)\n- [ ] Dialogs announce title and content\n- [ ] Focus remains trapped in modal dialogs\n- [ ] Custom controls have appropriate roles and states\n- [ ] Images have text alternatives\n- [ ] Progress indicators announce status updates\n- [ ] Navigation changes are announced\n- [ ] Error messages are announced\n- [ ] Success confirmations are announced\n\n## Debugging with Accerciser\n\nAccerciser is an accessibility inspector for Linux:\n\n\u0060\u0060\u0060bash\n# Install\nsudo apt install accerciser\n\n# Launch\naccerciser\n\u0060\u0060\u0060\n\nUse Accerciser to:\n- Inspect the accessibility tree of your application\n- View roles, states, and properties of each element\n- Verify AT-SPI2 events are being fired\n- Test accessibility actions programmatically\n\nTesting with Orca ensures that your accessibility implementation works correctly for real users, not just in theory. Regular testing throughout development helps catch issues early and ensures a truly accessible application." + }, + { + "header": "Accessibility Best Practices", + "content": "Building accessible applications requires more than just implementing the technical requirements\u2014it demands thoughtful design and consistent attention to user experience. Here are the essential best practices for creating accessible .NET MAUI Linux applications.\n\n## 1. Design for Accessibility from the Start\n\nAccessibility should be a core requirement, not an afterthought.\n\n**Do:**\n\u0060\u0060\u0060csharp\npublic class LoginPage : ContentPage\n{\n public LoginPage()\n {\n // Accessibility considered from the beginning\n Content = new VerticalStackLayout\n {\n Children =\n {\n new Label \n { \n Text = \u0022Email\u0022,\n SemanticProperties.HeadingLevel = SemanticHeadingLevel.Level1\n },\n new Entry \n { \n Placeholder = \u0022Enter email\u0022,\n Keyboard = Keyboard.Email,\n AutomationId = \u0022EmailEntry\u0022,\n SemanticProperties.Description = \u0022Your email address\u0022\n },\n new Label { Text = \u0022Password\u0022 },\n new Entry \n { \n IsPassword = true,\n Placeholder = \u0022Enter password\u0022,\n AutomationId = \u0022PasswordEntry\u0022\n },\n new Button \n { \n Text = \u0022Sign In\u0022,\n Command = new Command(SignIn),\n SemanticProperties.Hint = \u0022Sign in to your account\u0022\n }\n }\n };\n }\n}\n\u0060\u0060\u0060\n\n**Don\u0027t:**\n\u0060\u0060\u0060csharp\n// Missing labels, no semantic information\nContent = new VerticalStackLayout\n{\n Children =\n {\n new Entry { Placeholder = \u0022Email\u0022 },\n new Entry { IsPassword = true },\n new Button { Text = \u0022Go\u0022 }\n }\n};\n\u0060\u0060\u0060\n\n## 2. Provide Meaningful Labels and Descriptions\n\nEvery interactive element must have a clear, descriptive label.\n\n**Good Examples:**\n\n\u0060\u0060\u0060csharp\n// Icon button with descriptive label\nvar deleteButton = new ImageButton\n{\n Source = \u0022trash.png\u0022,\n SemanticProperties.Description = \u0022Delete selected message\u0022\n};\n\n// Entry with context\nvar searchEntry = new Entry\n{\n Placeholder = \u0022Search contacts\u0022,\n SemanticProperties.Description = \u0022Search by name, email, or phone number\u0022\n};\n\n// Checkbox with clear purpose\nvar termsCheckbox = new CheckBox\n{\n SemanticProperties.Description = \u0022I agree to the terms and conditions\u0022\n};\n\u0060\u0060\u0060\n\n**Avoid:**\n- Generic labels: \u0022Button\u0022, \u0022Click here\u0022, \u0022OK\u0022\n- Technical jargon: \u0022Execute query\u0022, \u0022Instantiate object\u0022\n- Ambiguous icons without text alternatives\n\n## 3. Maintain Logical Focus Order\n\nTab order should follow visual layout and logical workflow.\n\n\u0060\u0060\u0060csharp\npublic class CheckoutForm : ContentPage\n{\n public CheckoutForm()\n {\n // Natural top-to-bottom, left-to-right flow\n Content = new VerticalStackLayout\n {\n Children =\n {\n // 1. Shipping information\n new Label { Text = \u0022Shipping Address\u0022 },\n new Entry { Placeholder = \u0022Street Address\u0022, TabIndex = 0 },\n new Entry { Placeholder = \u0022City\u0022, TabIndex = 1 },\n new Entry { Placeholder = \u0022Postal Code\u0022, TabIndex = 2 },\n \n // 2. Payment information\n new Label { Text = \u0022Payment\u0022 },\n new Entry { Placeholder = \u0022Card Number\u0022, TabIndex = 3 },\n new Entry { Placeholder = \u0022Expiry Date\u0022, TabIndex = 4 },\n new Entry { Placeholder = \u0022CVV\u0022, TabIndex = 5 },\n \n // 3. Action buttons\n new HorizontalStackLayout\n {\n Children =\n {\n new Button { Text = \u0022Cancel\u0022, TabIndex = 6 },\n new Button { Text = \u0022Place Order\u0022, TabIndex = 7 }\n }\n }\n }\n };\n }\n}\n\u0060\u0060\u0060\n\n## 4. Provide Clear Visual Focus Indicators\n\nUsers must always know which element has focus.\n\n\u0060\u0060\u0060csharp\nprotected override void OnPaint(SKCanvas canvas)\n{\n // Draw control\n DrawControlContent(canvas);\n \n // Always draw visible focus indicator\n if (IsFocused)\n {\n var focusRect = Bounds;\n focusRect.Inflate(-2, -2);\n \n using var focusPaint = new SKPaint\n {\n Style = SKPaintStyle.Stroke,\n Color = SkiaTheme.Current.FocusColor,\n StrokeWidth = 2,\n IsAntialias = true\n };\n \n // High contrast mode: use solid line\n if (SkiaTheme.Current.IsHighContrast)\n {\n canvas.DrawRect(focusRect, focusPaint);\n }\n // Normal mode: use dashed line\n else\n {\n focusPaint.PathEffect = SKPathEffect.CreateDash(new[] { 4f, 2f }, 0);\n canvas.DrawRect(focusRect, focusPaint);\n }\n }\n}\n\u0060\u0060\u0060\n\n## 5. Ensure Sufficient Color Contrast\n\nAll text must meet WCAG contrast requirements.\n\n\u0060\u0060\u0060csharp\npublic class AccessibleLabel : SkiaLabel\n{\n protected override void OnPaint(SKCanvas canvas)\n {\n var theme = SkiaTheme.Current;\n var backgroundColor = Parent?.BackgroundColor ?? theme.BackgroundColor;\n var foregroundColor = TextColor ?? theme.ForegroundColor;\n \n // Check contrast ratio\n var contrastRatio = ContrastChecker.CalculateContrastRatio(\n foregroundColor, \n backgroundColor);\n \n // Adjust if insufficient\n if (contrastRatio \u003C theme.MinimumContrastRatio)\n {\n foregroundColor = theme.IsHighContrast\n ? theme.ForegroundColor\n : AdjustColorForContrast(foregroundColor, backgroundColor);\n }\n \n // Draw with adjusted color\n using var paint = new SKPaint\n {\n Color = foregroundColor,\n TextSize = FontSize,\n IsAntialias = true\n };\n \n canvas.DrawText(Text, X, Y, paint);\n }\n}\n\u0060\u0060\u0060\n\n## 6. Don\u0027t Rely on Color Alone\n\nUse multiple visual cues to convey information.\n\n**Good:**\n\u0060\u0060\u0060csharp\npublic class ValidationEntry : Entry\n{\n private bool _isValid = true;\n \n public bool IsValid\n {\n get =\u003E _isValid;\n set\n {\n _isValid = value;\n \n // Multiple indicators:\n // 1. Border color\n BorderColor = _isValid ? Colors.Gray : Colors.Red;\n \n // 2. Icon\n var icon = _isValid ? \u0022checkmark.png\u0022 : \u0022error.png\u0022;\n \n // 3. Text message\n var message = _isValid \n ? \u0022Valid input\u0022 \n : \u0022Invalid input: Email format required\u0022;\n \n // 4. Accessible announcement\n SemanticScreenReader.Announce(message);\n }\n }\n}\n\u0060\u0060\u0060\n\n**Bad:**\n\u0060\u0060\u0060csharp\n// Only color changes, no other indication\nentry.BackgroundColor = isValid ? Colors.White : Colors.LightPink;\n\u0060\u0060\u0060\n\n## 7. Provide Text Alternatives for Images\n\nAll images must have meaningful text alternatives.\n\n\u0060\u0060\u0060csharp\n// Informative image\nvar productImage = new Image\n{\n Source = \u0022laptop.jpg\u0022,\n SemanticProperties.Description = \u0022Silver laptop with 15-inch display\u0022\n};\n\n// Decorative image (empty description)\nvar decorativeBorder = new Image\n{\n Source = \u0022border.png\u0022,\n SemanticProperties.Description = string.Empty // Indicates decorative\n};\n\n// Functional image button\nvar saveButton = new ImageButton\n{\n Source = \u0022save.png\u0022,\n SemanticProperties.Description = \u0022Save document\u0022\n};\n\u0060\u0060\u0060\n\n## 8. Handle Errors Accessibly\n\nError messages must be announced and clearly associated with their fields.\n\n\u0060\u0060\u0060csharp\npublic class AccessibleForm : ContentPage\n{\n private Entry _emailEntry;\n private Label _emailError;\n \n private async Task ValidateAndSubmit()\n {\n if (string.IsNullOrEmpty(_emailEntry.Text))\n {\n // 1. Show visual error\n _emailError.Text = \u0022Email is required\u0022;\n _emailError.IsVisible = true;\n _emailEntry.BorderColor = Colors.Red;\n \n // 2. Update accessibility state\n _emailEntry.SemanticProperties.Description = \n \u0022Email address. Error: Email is required\u0022;\n \n // 3. Announce error\n SemanticScreenReader.Announce(\u0022Email is required\u0022);\n \n // 4. Set focus to invalid field\n _emailEntry.Focus();\n \n return;\n }\n \n // Clear errors on success\n _emailError.IsVisible = false;\n _emailEntry.BorderColor = Colors.Gray;\n _emailEntry.SemanticProperties.Description = \u0022Email address\u0022;\n }\n}\n\u0060\u0060\u0060\n\n## 9. Make Dynamic Content Accessible\n\nAnnounce changes that occur without user interaction.\n\n\u0060\u0060\u0060csharp\npublic class NotificationBanner : ContentView\n{\n public void ShowNotification(string message, NotificationType type)\n {\n // Update UI\n Content = new Frame\n {\n BackgroundColor = GetColorForType(type),\n Content = new Label { Text = message }\n };\n \n IsVisible = true;\n \n // Announce to screen reader\n var announcement = type switch\n {\n NotificationType.Error =\u003E $\u0022Error: {message}\u0022,\n NotificationType.Warning =\u003E $\u0022Warning: {message}\u0022,\n NotificationType.Success =\u003E $\u0022Success: {message}\u0022,\n _ =\u003E message\n };\n \n SemanticScreenReader.Announce(announcement);\n \n // Auto-dismiss after delay\n Device.StartTimer(TimeSpan.FromSeconds(5), () =\u003E\n {\n IsVisible = false;\n SemanticScreenReader.Announce(\u0022Notification dismissed\u0022);\n return false;\n });\n }\n}\n\u0060\u0060\u0060\n\n## 10. Test with Real Assistive Technologies\n\nRegularly test with Orca and other tools.\n\n\u0060\u0060\u0060csharp\n// Unit test for accessibility\n[Fact]\npublic void AllButtons_Should_Have_Accessible_Labels()\n{\n var page = new MainPage();\n var buttons = GetAllDescendants\u003CButton\u003E(page);\n \n foreach (var button in buttons)\n {\n var label = button.SemanticProperties.Description \n ?? button.Text;\n \n Assert.False(string.IsNullOrWhiteSpace(label),\n $\u0022Button \u0027{button.AutomationId}\u0027 missing accessible label\u0022);\n \n Assert.True(label.Length \u003E 3,\n $\u0022Button \u0027{button.AutomationId}\u0027 has insufficient label: \u0027{label}\u0027\u0022);\n }\n}\n\u0060\u0060\u0060\n\n## Quick Reference Checklist\n\n**For Every Interactive Element:**\n- [ ] Has a clear, descriptive label\n- [ ] Has an appropriate role\n- [ ] Is keyboard accessible\n- [ ] Has visible focus indicator\n- [ ] Announces state changes\n- [ ] Meets color contrast requirements\n\n**For Every Form:**\n- [ ] Labels are associated with inputs\n- [ ] Required fields are indicated\n- [ ] Errors are announced and clearly described\n- [ ] Tab order follows logical flow\n- [ ] Submit button is clearly labeled\n\n**For Every Page:**\n- [ ] Has a meaningful title\n- [ ] Uses semantic headings\n- [ ] Focus is set appropriately on load\n- [ ] All images have text alternatives\n- [ ] Dynamic content changes are announced\n\n**For Every Custom Control:**\n- [ ] Implements \u0060IAccessible\u0060\n- [ ] Has appropriate role and state\n- [ ] Responds to keyboard input\n- [ ] Tested with Orca\n- [ ] Works in high contrast mode\n\n## Conclusion\n\nAccessibility is not a feature\u2014it\u0027s a fundamental requirement that ensures your application can be used by everyone, regardless of their abilities. By following these best practices and leveraging the OpenMaui platform\u0027s comprehensive accessibility infrastructure, you can build Linux applications that are truly inclusive.\n\nRemember: accessible applications benefit all users, not just those with disabilities. Clear labels, logical navigation, and robust keyboard support create better user experiences for everyone. Start with accessibility in mind, test regularly with assistive technologies, and continuously refine your implementation based on user feedback.\n\nThe AT-SPI2 integration in OpenMaui.Platform.Linux provides the technical foundation, but creating accessible applications requires thoughtful design, consistent implementation, and ongoing commitment to inclusive software development." + } + ], + "generatedAt": 1769750550451 +} \ No newline at end of file diff --git a/.notes/series-1769750791920-5db729.json b/.notes/series-1769750791920-5db729.json new file mode 100644 index 0000000..662d397 --- /dev/null +++ b/.notes/series-1769750791920-5db729.json @@ -0,0 +1,55 @@ +{ + "id": "series-1769750791920-5db729", + "title": "Advanced Text Rendering: IME, Font Fallback, and International Support", + "content": "# Advanced Text Rendering: IME, Font Fallback, and International Support\r\n\r\n*Building a production-ready text rendering system for .NET MAUI on Linux that handles CJK languages, emoji, and complex scripts with grace.*\r\n\r\n## Introduction\r\n\r\nText rendering seems simple until you encounter your first emoji, Chinese character, or right-to-left script. What works perfectly for English ASCII suddenly breaks down when users type \u0022\u0645\u0631\u062D\u0628\u0627\u0022 (Arabic), \u0022\u3053\u3093\u306B\u3061\u306F\u0022 (Japanese), or \u0022\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66\u0022 (family emoji with zero-width joiners).\n\nThe OpenMaui Linux platform implementation tackles these challenges head-on, providing comprehensive international text support for .NET MAUI applications. This isn\u0027t just about displaying characters\u2014it\u0027s about enabling proper text input through Input Method Editors (IMEs), implementing intelligent font fallback for missing glyphs, and leveraging HarfBuzz for complex script shaping.\n\nIn this article, we\u0027ll explore the architecture behind OpenMaui\u0027s text rendering system, examining real implementation patterns that handle everything from IBus integration to emoji rendering. Whether you\u0027re building a text editor, chat application, or international business software, these techniques will help you support users worldwide.\n\n**Key capabilities covered:**\n- IBus and XIM integration for CJK input methods\n- Font fallback strategies for emoji and international scripts\n- HarfBuzz integration for complex text shaping\n- Practical testing approaches for international text\r\n\r\n## Text Rendering Challenges on Linux\r\n\r\nLinux desktop environments present unique text rendering challenges that Windows and macOS developers often don\u0027t encounter. The fragmented ecosystem of input methods, font configurations, and desktop environments requires careful architectural decisions.\n\n## The Complexity Landscape\n\n**Input Method Diversity**: Unlike Windows with its unified IME framework, Linux supports multiple input method frameworks:\n- **IBus** (Intelligent Input Bus) - The most common, used by GNOME and many distributions\n- **Fcitx5** - Popular among advanced users, especially for Chinese input\n- **XIM** (X Input Method) - Legacy protocol still widely used\n\nEach framework has different APIs, event models, and integration requirements. OpenMaui\u0027s \u0060SkiaEntry\u0060 and \u0060SkiaEditor\u0060 controls must support all three to provide consistent text input across distributions.\n\n**Font Fragmentation**: User systems have wildly different font installations. A font that renders perfectly on Ubuntu might be missing on Fedora. Your application needs to:\n- Detect missing glyphs in real-time\n- Fall back to alternative fonts automatically\n- Handle emoji fonts (color vs. monochrome)\n- Support CJK fonts that can be hundreds of megabytes\n\n**Script Complexity**: Different writing systems have different rendering rules:\n- **Latin scripts**: Simple left-to-right, one glyph per character\n- **Arabic/Hebrew**: Right-to-left with contextual letter forms\n- **Indic scripts**: Vowel reordering and complex ligatures\n- **CJK**: Vertical text support, ruby annotations\n- **Emoji**: Multi-codepoint sequences with zero-width joiners\n\n## The SkiaSharp Foundation\n\nOpenMaui uses SkiaSharp as its rendering engine, which provides several advantages:\n\n\u0060\u0060\u0060csharp\n// From SkiaLabel - base text rendering setup\nprivate void DrawText(SKCanvas canvas)\n{\n using var paint = new SKPaint\n {\n Color = textColor.ToSKColor(),\n TextSize = (float)FontSize,\n IsAntialias = true,\n Typeface = GetTypeface(),\n TextAlign = GetTextAlign()\n };\n \n // Font fallback happens here\n canvas.DrawText(text, x, y, paint);\n}\n\u0060\u0060\u0060\n\nSkiaSharp provides consistent rendering across platforms, but it doesn\u0027t solve input method integration or font fallback automatically. That\u0027s where OpenMaui\u0027s platform layer comes in.\n\n## The Three-Layer Architecture\n\nOpenMaui\u0027s text rendering uses a three-layer approach:\n\n1. **Input Layer**: IBus/XIM integration captures text input events\n2. **Shaping Layer**: HarfBuzz converts Unicode to positioned glyphs\n3. **Rendering Layer**: SkiaSharp draws the final output\n\nThis separation allows each layer to be tested independently and swapped out if needed. The \u0060SkiaLabel\u0060 class implements font fallback, while \u0060SkiaEntry\u0060 and \u0060SkiaEditor\u0060 add IME support for text input.\r\n\r\n## IBus and XIM Integration\r\n\r\nInput Method Editors (IMEs) are essential for typing languages that have more characters than keyboard keys. Chinese, Japanese, and Korean users rely on IMEs to convert phonetic input into the correct characters.\n\n## Understanding IME Workflow\n\nWhen a user types Chinese using Pinyin input:\n\n1. User types \u0022nihao\u0022 on the keyboard\n2. IME shows composition text: \u0022\u4F60\u597D\u0022 (\u5019\u9078\u5B57)\n3. User selects the correct characters from candidates\n4. IME commits the final text to the application\n\nDuring composition, the application must display **preedit text** (uncommitted characters) with proper styling, usually with an underline.\n\n## IBus Integration in SkiaEntry\n\nThe \u0060SkiaEntry\u0060 control implements IME support through native IBus integration:\n\n\u0060\u0060\u0060csharp\n// From SkiaEntry - IME composition handling\nprivate string _preeditText = string.Empty;\nprivate int _preeditCursorPos = 0;\n\nprotected override void OnTextInput(TextInputEventArgs e)\n{\n if (e.IsComposing)\n {\n // Handle preedit text during composition\n _preeditText = e.CompositionText;\n _preeditCursorPos = e.CompositionCursorPos;\n Invalidate();\n }\n else\n {\n // Commit final text\n InsertText(e.Text);\n _preeditText = string.Empty;\n }\n}\n\nprotected override void OnDraw(SKCanvas canvas)\n{\n // Draw normal text\n DrawTextContent(canvas);\n \n // Draw preedit text with underline\n if (!string.IsNullOrEmpty(_preeditText))\n {\n DrawPreeditText(canvas, _preeditText, _preeditCursorPos);\n }\n}\n\u0060\u0060\u0060\n\n## XIM Protocol Support\n\nFor compatibility with older systems and applications, OpenMaui also supports the XIM protocol:\n\n\u0060\u0060\u0060csharp\n// XIM event handling pattern\npublic void HandleXIMEvent(XEvent xEvent)\n{\n if (xEvent.type == XEventType.KeyPress)\n {\n // Filter through XIM first\n if (XFilterEvent(ref xEvent, window))\n {\n // Event handled by XIM\n return;\n }\n \n // Handle as normal key event\n HandleKeyPress(xEvent);\n }\n}\n\u0060\u0060\u0060\n\n## Cursor Position Management\n\nIMEs need to know where to display candidate windows. OpenMaui provides cursor position information:\n\n\u0060\u0060\u0060csharp\n// From SkiaEntry - cursor position for IME\npublic Rect GetCursorRect()\n{\n var x = MeasureTextWidth(Text.Substring(0, CursorPosition));\n var y = Padding.Top;\n var height = FontSize * 1.2; // Line height\n \n return new Rect(x, y, 2, height);\n}\n\npublic void UpdateIMECursorPosition()\n{\n if (ibusContext != null)\n {\n var rect = GetCursorRect();\n // Convert to screen coordinates\n var screenRect = TransformToScreen(rect);\n ibusContext.SetCursorLocation(\n (int)screenRect.X,\n (int)screenRect.Y,\n (int)screenRect.Width,\n (int)screenRect.Height\n );\n }\n}\n\u0060\u0060\u0060\n\n## Testing IME Integration\n\nTesting IME functionality requires actual input method frameworks:\n\n\u0060\u0060\u0060bash\n# Install IBus and Chinese input\nsudo apt install ibus ibus-pinyin\n\n# Start IBus daemon\nibus-daemon -drx\n\n# Set environment variables\nexport GTK_IM_MODULE=ibus\nexport QT_IM_MODULE=ibus\nexport XMODIFIERS=@im=ibus\n\u0060\u0060\u0060\n\nIn your application:\n\n\u0060\u0060\u0060csharp\n// Test case for IME composition\n[Fact]\npublic void Entry_HandlesIMEComposition()\n{\n var entry = new SkiaEntry();\n \n // Simulate IME composition\n entry.HandleTextInput(new TextInputEventArgs\n {\n IsComposing = true,\n CompositionText = \u0022\u4F60\u597D\u0022,\n CompositionCursorPos = 2\n });\n \n Assert.Equal(\u0022\u4F60\u597D\u0022, entry.PreeditText);\n Assert.Equal(\u0022\u0022, entry.Text); // Not committed yet\n \n // Simulate commit\n entry.HandleTextInput(new TextInputEventArgs\n {\n IsComposing = false,\n Text = \u0022\u4F60\u597D\u0022\n });\n \n Assert.Equal(\u0022\u4F60\u597D\u0022, entry.Text);\n Assert.Equal(\u0022\u0022, entry.PreeditText);\n}\n\u0060\u0060\u0060\n\n## Fcitx5 Compatibility\n\nWhile IBus is most common, Fcitx5 users expect the same level of support. OpenMaui\u0027s abstraction layer handles both:\n\n\u0060\u0060\u0060csharp\npublic interface IInputMethodContext\n{\n void SetCursorLocation(int x, int y, int width, int height);\n void FocusIn();\n void FocusOut();\n void Reset();\n}\n\n// IBus implementation\npublic class IBusInputContext : IInputMethodContext { ... }\n\n// Fcitx5 implementation\npublic class Fcitx5InputContext : IInputMethodContext { ... }\n\u0060\u0060\u0060\n\nThe application detects which input method framework is active and uses the appropriate implementation.\r\n\r\n## Font Fallback Manager Architecture\r\n\r\nFont fallback is the process of automatically switching to a different font when the current font doesn\u0027t contain a required glyph. Without it, users see empty boxes (\u25A1) or question marks (?) instead of their text.\n\n## The Font Fallback Challenge\n\nConsider this string: \u0022Hello \u4E16\u754C \uD83C\uDF0D\u0022\n\n- \u0022Hello\u0022 might render with Roboto\n- \u0022\u4E16\u754C\u0022 (Chinese) needs a CJK font like Noto Sans CJK\n- \u0022\uD83C\uDF0D\u0022 (emoji) requires a color emoji font like Noto Color Emoji\n\nNo single font contains all these glyphs. The system must detect missing glyphs and switch fonts **per character** or **per run** (consecutive characters from the same script).\n\n## SkiaLabel\u0027s Font Fallback System\n\nThe \u0060SkiaLabel\u0060 control implements intelligent font fallback:\n\n\u0060\u0060\u0060csharp\n// From SkiaLabel - font fallback implementation\npublic class FontFallbackManager\n{\n private readonly List\u003CSKTypeface\u003E _fallbackChain;\n \n public FontFallbackManager()\n {\n _fallbackChain = new List\u003CSKTypeface\u003E\n {\n // Primary font (user\u0027s choice)\n GetPrimaryFont(),\n \n // CJK fonts\n SKTypeface.FromFamilyName(\u0022Noto Sans CJK SC\u0022),\n SKTypeface.FromFamilyName(\u0022Noto Sans CJK JP\u0022),\n SKTypeface.FromFamilyName(\u0022WenQuanYi Micro Hei\u0022),\n \n // Emoji fonts\n SKTypeface.FromFamilyName(\u0022Noto Color Emoji\u0022),\n SKTypeface.FromFamilyName(\u0022Segoe UI Emoji\u0022),\n \n // Fallback for other scripts\n SKTypeface.FromFamilyName(\u0022Noto Sans\u0022),\n SKTypeface.FromFamilyName(\u0022DejaVu Sans\u0022)\n };\n }\n \n public SKTypeface GetTypefaceForCharacter(char c, SKTypeface primary)\n {\n // Check if primary font has this glyph\n if (HasGlyph(primary, c))\n return primary;\n \n // Try fallback fonts\n foreach (var fallback in _fallbackChain)\n {\n if (fallback != null \u0026\u0026 HasGlyph(fallback, c))\n return fallback;\n }\n \n // Last resort: return primary (will show \u25A1)\n return primary;\n }\n \n private bool HasGlyph(SKTypeface typeface, char c)\n {\n var glyphs = new ushort[1];\n var text = c.ToString();\n return typeface.GetGlyphs(text, out glyphs) \u003E 0 \u0026\u0026 glyphs[0] != 0;\n }\n}\n\u0060\u0060\u0060\n\n## Run-Based Rendering\n\nFor efficiency, OpenMaui groups consecutive characters that use the same font into **text runs**:\n\n\u0060\u0060\u0060csharp\n// Text run detection\npublic class TextRun\n{\n public string Text { get; set; }\n public SKTypeface Typeface { get; set; }\n public int StartIndex { get; set; }\n public int Length { get; set; }\n}\n\npublic List\u003CTextRun\u003E SplitIntoRuns(string text, SKTypeface primaryFont)\n{\n var runs = new List\u003CTextRun\u003E();\n var currentRun = new StringBuilder();\n var currentTypeface = primaryFont;\n var startIndex = 0;\n \n for (int i = 0; i \u003C text.Length; i\u002B\u002B)\n {\n var c = text[i];\n var typeface = _fallbackManager.GetTypefaceForCharacter(c, primaryFont);\n \n if (typeface != currentTypeface \u0026\u0026 currentRun.Length \u003E 0)\n {\n // Start new run\n runs.Add(new TextRun\n {\n Text = currentRun.ToString(),\n Typeface = currentTypeface,\n StartIndex = startIndex,\n Length = currentRun.Length\n });\n \n currentRun.Clear();\n startIndex = i;\n currentTypeface = typeface;\n }\n \n currentRun.Append(c);\n }\n \n // Add final run\n if (currentRun.Length \u003E 0)\n {\n runs.Add(new TextRun\n {\n Text = currentRun.ToString(),\n Typeface = currentTypeface,\n StartIndex = startIndex,\n Length = currentRun.Length\n });\n }\n \n return runs;\n}\n\u0060\u0060\u0060\n\n## Drawing with Font Fallback\n\nThe rendering code uses text runs:\n\n\u0060\u0060\u0060csharp\n// From SkiaLabel - rendering with fallback\nprotected override void OnDraw(SKCanvas canvas)\n{\n var runs = SplitIntoRuns(Text, GetPrimaryTypeface());\n float x = (float)Padding.Left;\n float y = (float)(Padding.Top \u002B FontSize);\n \n foreach (var run in runs)\n {\n using var paint = new SKPaint\n {\n Color = TextColor.ToSKColor(),\n TextSize = (float)FontSize,\n Typeface = run.Typeface,\n IsAntialias = true,\n SubpixelText = true\n };\n \n canvas.DrawText(run.Text, x, y, paint);\n x \u002B= paint.MeasureText(run.Text);\n }\n}\n\u0060\u0060\u0060\n\n## Font Discovery on Linux\n\nOpenMaui uses Fontconfig to discover available fonts:\n\n\u0060\u0060\u0060csharp\npublic class LinuxFontDiscovery\n{\n [DllImport(\u0022libfontconfig.so.1\u0022)]\n private static extern IntPtr FcConfigGetCurrent();\n \n [DllImport(\u0022libfontconfig.so.1\u0022)]\n private static extern IntPtr FcPatternCreate();\n \n public List\u003Cstring\u003E GetAvailableFonts()\n {\n var fonts = new List\u003Cstring\u003E();\n var config = FcConfigGetCurrent();\n \n // Query fontconfig for available fonts\n // Implementation details...\n \n return fonts;\n }\n \n public string FindBestFontForScript(UnicodeScript script)\n {\n return script switch\n {\n UnicodeScript.Han =\u003E FindFont(\u0022Noto Sans CJK SC\u0022, \u0022WenQuanYi\u0022),\n UnicodeScript.Hiragana =\u003E FindFont(\u0022Noto Sans CJK JP\u0022),\n UnicodeScript.Arabic =\u003E FindFont(\u0022Noto Sans Arabic\u0022),\n UnicodeScript.Devanagari =\u003E FindFont(\u0022Noto Sans Devanagari\u0022),\n _ =\u003E \u0022Noto Sans\u0022\n };\n }\n}\n\u0060\u0060\u0060\n\n## Emoji Font Handling\n\nEmoji fonts require special handling because they\u0027re often color fonts (CBDT/COLR tables):\n\n\u0060\u0060\u0060csharp\npublic SKTypeface GetEmojiTypeface()\n{\n // Try color emoji fonts first\n var colorEmoji = SKTypeface.FromFamilyName(\u0022Noto Color Emoji\u0022);\n if (colorEmoji != null)\n return colorEmoji;\n \n // Fallback to monochrome emoji\n return SKTypeface.FromFamilyName(\u0022Noto Emoji\u0022);\n}\n\npublic bool IsEmoji(char c)\n{\n // Basic emoji detection\n return (c \u003E= 0x1F300 \u0026\u0026 c \u003C= 0x1F9FF) || // Misc Symbols and Pictographs\n (c \u003E= 0x2600 \u0026\u0026 c \u003C= 0x26FF) || // Misc Symbols\n (c \u003E= 0x2700 \u0026\u0026 c \u003C= 0x27BF); // Dingbats\n}\n\u0060\u0060\u0060\n\n## Performance Optimization\n\nFont fallback can be expensive. OpenMaui caches font lookup results:\n\n\u0060\u0060\u0060csharp\nprivate readonly Dictionary\u003Cchar, SKTypeface\u003E _glyphCache = new();\n\npublic SKTypeface GetTypefaceForCharacterCached(char c, SKTypeface primary)\n{\n if (_glyphCache.TryGetValue(c, out var cached))\n return cached;\n \n var typeface = GetTypefaceForCharacter(c, primary);\n _glyphCache[c] = typeface;\n return typeface;\n}\n\u0060\u0060\u0060\n\nThis reduces font lookups from O(n\u00D7m) to O(n) where n is text length and m is fallback chain length.\r\n\r\n## HarfBuzz for Complex Script Shaping\r\n\r\nHarfBuzz is the industry-standard text shaping engine used by Chrome, Firefox, Android, and most modern text rendering systems. It transforms Unicode text into properly positioned glyphs, handling the complex rules of different writing systems.\n\n## Why Text Shaping Matters\n\nConsider Arabic text \u0022\u0645\u0631\u062D\u0628\u0627\u0022 (hello). The same letter has different forms depending on its position:\n\n- **Isolated form**: \u0645\n- **Initial form**: \u0645\u0640 (when starting a word)\n- **Medial form**: \u0640\u0645\u0640 (in the middle)\n- **Final form**: \u0640\u0645 (at the end)\n\nHarfBuzz applies these contextual substitutions automatically. Without shaping, Arabic text looks disconnected and wrong.\n\n## HarfBuzz Integration\n\nOpenMaui uses HarfBuzzSharp 7.3.0.3 for text shaping:\n\n\u0060\u0060\u0060csharp\nusing HarfBuzzSharp;\n\npublic class TextShaper\n{\n public ShapedGlyphs ShapeText(string text, SKTypeface typeface, float fontSize)\n {\n // Create HarfBuzz font from SKTypeface\n using var blob = typeface.OpenStream(out var ttcIndex).AsHarfBuzzBlob();\n using var face = new Face(blob, ttcIndex);\n using var font = new Font(face);\n font.SetScale((int)fontSize, (int)fontSize);\n \n // Create buffer with text\n using var buffer = new HarfBuzzSharp.Buffer();\n buffer.AddUtf16(text);\n buffer.GuessSegmentProperties();\n \n // Shape the text\n font.Shape(buffer);\n \n // Extract glyph information\n var glyphInfos = buffer.GlyphInfos;\n var glyphPositions = buffer.GlyphPositions;\n \n var result = new ShapedGlyphs();\n float xPos = 0;\n \n for (int i = 0; i \u003C glyphInfos.Length; i\u002B\u002B)\n {\n var info = glyphInfos[i];\n var pos = glyphPositions[i];\n \n result.Glyphs.Add(new ShapedGlyph\n {\n GlyphId = info.Codepoint,\n Cluster = info.Cluster,\n XOffset = xPos \u002B pos.XOffset / 64f,\n YOffset = pos.YOffset / 64f,\n XAdvance = pos.XAdvance / 64f,\n YAdvance = pos.YAdvance / 64f\n });\n \n xPos \u002B= pos.XAdvance / 64f;\n }\n \n return result;\n }\n}\n\npublic class ShapedGlyphs\n{\n public List\u003CShapedGlyph\u003E Glyphs { get; set; } = new();\n}\n\npublic class ShapedGlyph\n{\n public uint GlyphId { get; set; }\n public uint Cluster { get; set; }\n public float XOffset { get; set; }\n public float YOffset { get; set; }\n public float XAdvance { get; set; }\n public float YAdvance { get; set; }\n}\n\u0060\u0060\u0060\n\n## Rendering Shaped Text\n\nOnce text is shaped, we render it using SkiaSharp:\n\n\u0060\u0060\u0060csharp\npublic void DrawShapedText(SKCanvas canvas, ShapedGlyphs shaped, \n SKTypeface typeface, float fontSize, \n SKColor color, float x, float y)\n{\n using var paint = new SKPaint\n {\n Typeface = typeface,\n TextSize = fontSize,\n Color = color,\n IsAntialias = true,\n SubpixelText = true\n };\n \n // Convert glyphs to ushort array\n var glyphIds = shaped.Glyphs.Select(g =\u003E (ushort)g.GlyphId).ToArray();\n var positions = shaped.Glyphs.Select(g =\u003E \n new SKPoint(x \u002B g.XOffset, y \u002B g.YOffset)\n ).ToArray();\n \n // Draw positioned glyphs\n canvas.DrawPositionedText(glyphIds, positions, paint);\n}\n\u0060\u0060\u0060\n\n## Script Detection\n\nHarfBuzz needs to know the script (writing system) to apply correct shaping rules:\n\n\u0060\u0060\u0060csharp\npublic class ScriptDetector\n{\n public static Script DetectScript(string text)\n {\n if (string.IsNullOrEmpty(text))\n return Script.Latin;\n \n var firstChar = text[0];\n \n // Arabic\n if (firstChar \u003E= 0x0600 \u0026\u0026 firstChar \u003C= 0x06FF)\n return Script.Arabic;\n \n // Hebrew\n if (firstChar \u003E= 0x0590 \u0026\u0026 firstChar \u003C= 0x05FF)\n return Script.Hebrew;\n \n // Devanagari\n if (firstChar \u003E= 0x0900 \u0026\u0026 firstChar \u003C= 0x097F)\n return Script.Devanagari;\n \n // Thai\n if (firstChar \u003E= 0x0E00 \u0026\u0026 firstChar \u003C= 0x0E7F)\n return Script.Thai;\n \n // CJK\n if (firstChar \u003E= 0x4E00 \u0026\u0026 firstChar \u003C= 0x9FFF)\n return Script.Han;\n \n return Script.Latin;\n }\n}\n\npublic enum Script\n{\n Latin,\n Arabic,\n Hebrew,\n Devanagari,\n Thai,\n Han,\n Hiragana,\n Katakana\n}\n\u0060\u0060\u0060\n\n## Direction Detection (BiDi)\n\nBidirectional text (mixing left-to-right and right-to-left) requires special handling:\n\n\u0060\u0060\u0060csharp\npublic class BiDiAnalyzer\n{\n public Direction GetTextDirection(string text)\n {\n // Check for RTL characters\n foreach (var c in text)\n {\n if (IsRTL(c))\n return Direction.RightToLeft;\n }\n return Direction.LeftToRight;\n }\n \n private bool IsRTL(char c)\n {\n return (c \u003E= 0x0590 \u0026\u0026 c \u003C= 0x05FF) || // Hebrew\n (c \u003E= 0x0600 \u0026\u0026 c \u003C= 0x06FF) || // Arabic\n (c \u003E= 0x0700 \u0026\u0026 c \u003C= 0x074F); // Syriac\n }\n}\n\npublic enum Direction\n{\n LeftToRight,\n RightToLeft\n}\n\u0060\u0060\u0060\n\n## Ligatures and Contextual Forms\n\nHarfBuzz automatically applies ligatures (like \u0022fi\u0022 \u2192 \u0022\uFB01\u0022) and contextual forms:\n\n\u0060\u0060\u0060csharp\n// Enable/disable OpenType features\npublic ShapedGlyphs ShapeWithFeatures(string text, SKTypeface typeface, \n float fontSize, Feature[] features)\n{\n using var blob = typeface.OpenStream(out var ttcIndex).AsHarfBuzzBlob();\n using var face = new Face(blob, ttcIndex);\n using var font = new Font(face);\n font.SetScale((int)fontSize, (int)fontSize);\n \n using var buffer = new HarfBuzzSharp.Buffer();\n buffer.AddUtf16(text);\n buffer.GuessSegmentProperties();\n \n // Apply features\n font.Shape(buffer, features);\n \n return ExtractGlyphs(buffer);\n}\n\n// Example: Disable ligatures\nvar features = new[]\n{\n new Feature(Tag.Parse(\u0022liga\u0022), 0, 0, uint.MaxValue)\n};\nvar shaped = ShapeWithFeatures(\u0022difficult\u0022, typeface, 16, features);\n\u0060\u0060\u0060\n\n## Emoji Sequences\n\nEmoji sequences (like \uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66) use zero-width joiners. HarfBuzz handles these correctly:\n\n\u0060\u0060\u0060csharp\npublic bool IsEmojiSequence(string text, int index)\n{\n if (index \u002B 1 \u003E= text.Length)\n return false;\n \n // Check for zero-width joiner\n return text[index \u002B 1] == \u0027\\u200D\u0027;\n}\n\n// HarfBuzz automatically shapes emoji sequences\nvar family = \u0022\uD83D\uDC68\\u200D\uD83D\uDC69\\u200D\uD83D\uDC67\\u200D\uD83D\uDC66\u0022;\nvar shaped = ShapeText(family, emojiTypeface, 24);\n// Results in a single glyph for the family emoji\n\u0060\u0060\u0060\n\n## Performance Considerations\n\nText shaping is computationally expensive. OpenMaui caches shaped results:\n\n\u0060\u0060\u0060csharp\npublic class ShapingCache\n{\n private readonly Dictionary\u003CShapingKey, ShapedGlyphs\u003E _cache = new();\n \n public ShapedGlyphs GetOrShape(string text, SKTypeface typeface, float fontSize)\n {\n var key = new ShapingKey(text, typeface, fontSize);\n \n if (_cache.TryGetValue(key, out var cached))\n return cached;\n \n var shaped = _shaper.ShapeText(text, typeface, fontSize);\n _cache[key] = shaped;\n return shaped;\n }\n}\n\nrecord ShapingKey(string Text, SKTypeface Typeface, float FontSize);\n\u0060\u0060\u0060\n\nThis dramatically improves performance for repeated text rendering, especially in lists and scrolling views.\r\n\r\n## Emoji and Unicode Support\r\n\r\nEmoji support is more complex than it appears. Modern emoji use multiple Unicode codepoints, skin tone modifiers, and zero-width joiners to create composite glyphs.\n\n## Unicode Emoji Fundamentals\n\nEmoji come in several forms:\n\n**Simple Emoji**: Single codepoint\n\u0060\u0060\u0060csharp\nvar heart = \u0022\u2764\uFE0F\u0022; // U\u002B2764 \u002B U\u002BFE0F (variation selector)\nvar smile = \u0022\uD83D\uDE00\u0022; // U\u002B1F600\n\u0060\u0060\u0060\n\n**Emoji with Skin Tone Modifiers**:\n\u0060\u0060\u0060csharp\nvar wave = \u0022\uD83D\uDC4B\u0022; // U\u002B1F44B\nvar waveDark = \u0022\uD83D\uDC4B\uD83C\uDFFF\u0022; // U\u002B1F44B \u002B U\u002B1F3FF (dark skin tone)\n\u0060\u0060\u0060\n\n**Zero-Width Joiner (ZWJ) Sequences**:\n\u0060\u0060\u0060csharp\nvar family = \u0022\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66\u0022; // Man \u002B ZWJ \u002B Woman \u002B ZWJ \u002B Girl \u002B ZWJ \u002B Boy\nvar femaleDoctor = \u0022\uD83D\uDC69\u200D\u2695\uFE0F\u0022; // Woman \u002B ZWJ \u002B Medical Symbol\n\u0060\u0060\u0060\n\n## Emoji Detection\n\nOpenMaui implements comprehensive emoji detection:\n\n\u0060\u0060\u0060csharp\npublic class EmojiDetector\n{\n public bool IsEmoji(int codepoint)\n {\n return IsInEmojiRange(codepoint) || \n IsEmojiModifier(codepoint) || \n IsEmojiComponent(codepoint);\n }\n \n private bool IsInEmojiRange(int cp)\n {\n return (cp \u003E= 0x1F600 \u0026\u0026 cp \u003C= 0x1F64F) || // Emoticons\n (cp \u003E= 0x1F300 \u0026\u0026 cp \u003C= 0x1F5FF) || // Misc Symbols and Pictographs\n (cp \u003E= 0x1F680 \u0026\u0026 cp \u003C= 0x1F6FF) || // Transport and Map\n (cp \u003E= 0x1F900 \u0026\u0026 cp \u003C= 0x1F9FF) || // Supplemental Symbols\n (cp \u003E= 0x2600 \u0026\u0026 cp \u003C= 0x26FF) || // Misc symbols\n (cp \u003E= 0x2700 \u0026\u0026 cp \u003C= 0x27BF) || // Dingbats\n (cp \u003E= 0xFE00 \u0026\u0026 cp \u003C= 0xFE0F); // Variation Selectors\n }\n \n private bool IsEmojiModifier(int cp)\n {\n return cp \u003E= 0x1F3FB \u0026\u0026 cp \u003C= 0x1F3FF; // Skin tone modifiers\n }\n \n private bool IsEmojiComponent(int cp)\n {\n return cp == 0x200D || // Zero-width joiner\n cp == 0xFE0F; // Variation Selector-16\n }\n \n public List\u003CEmojiSequence\u003E FindEmojis(string text)\n {\n var emojis = new List\u003CEmojiSequence\u003E();\n int i = 0;\n \n while (i \u003C text.Length)\n {\n if (char.IsHighSurrogate(text[i]))\n {\n var codepoint = char.ConvertToUtf32(text, i);\n if (IsEmoji(codepoint))\n {\n var sequence = ExtractEmojiSequence(text, i);\n emojis.Add(sequence);\n i = sequence.EndIndex;\n continue;\n }\n i \u002B= 2; // Surrogate pair\n }\n else\n {\n i\u002B\u002B;\n }\n }\n \n return emojis;\n }\n \n private EmojiSequence ExtractEmojiSequence(string text, int startIndex)\n {\n int endIndex = startIndex;\n var sequence = new StringBuilder();\n \n // Extract emoji and any following modifiers/joiners\n while (endIndex \u003C text.Length)\n {\n var c = text[endIndex];\n \n if (char.IsHighSurrogate(c) \u0026\u0026 endIndex \u002B 1 \u003C text.Length)\n {\n var cp = char.ConvertToUtf32(text, endIndex);\n if (IsEmoji(cp) || IsEmojiModifier(cp))\n {\n sequence.Append(c);\n sequence.Append(text[endIndex \u002B 1]);\n endIndex \u002B= 2;\n continue;\n }\n }\n \n if (IsEmojiComponent(c))\n {\n sequence.Append(c);\n endIndex\u002B\u002B;\n continue;\n }\n \n break;\n }\n \n return new EmojiSequence\n {\n Text = sequence.ToString(),\n StartIndex = startIndex,\n EndIndex = endIndex\n };\n }\n}\n\npublic class EmojiSequence\n{\n public string Text { get; set; }\n public int StartIndex { get; set; }\n public int EndIndex { get; set; }\n}\n\u0060\u0060\u0060\n\n## Color Emoji Rendering\n\nColor emoji fonts use special tables (CBDT, COLR, SVG):\n\n\u0060\u0060\u0060csharp\npublic class EmojiRenderer\n{\n private SKTypeface _colorEmojiFont;\n \n public EmojiRenderer()\n {\n // Load color emoji font\n _colorEmojiFont = SKTypeface.FromFamilyName(\u0022Noto Color Emoji\u0022) ??\n SKTypeface.FromFamilyName(\u0022Apple Color Emoji\u0022) ??\n SKTypeface.FromFamilyName(\u0022Segoe UI Emoji\u0022);\n }\n \n public void DrawEmoji(SKCanvas canvas, string emoji, float x, float y, float size)\n {\n using var paint = new SKPaint\n {\n Typeface = _colorEmojiFont,\n TextSize = size,\n IsAntialias = true,\n SubpixelText = true\n };\n \n // Color emoji fonts handle color internally\n canvas.DrawText(emoji, x, y, paint);\n }\n}\n\u0060\u0060\u0060\n\n## Emoji Picker Integration\n\nOpenMaui\u0027s text controls can integrate with the system emoji picker:\n\n\u0060\u0060\u0060csharp\n// From SkiaEntry - emoji picker support\npublic void ShowEmojiPicker()\n{\n // On Linux, use IBus emoji picker\n if (HasIBus())\n {\n // Ctrl\u002B. or Ctrl\u002B; triggers IBus emoji picker\n SimulateKeyPress(Key.Period, ModifierKeys.Control);\n }\n else\n {\n // Show custom emoji picker\n var picker = new EmojiPickerDialog();\n picker.EmojiSelected \u002B= (s, emoji) =\u003E\n {\n InsertText(emoji);\n };\n picker.Show();\n }\n}\n\u0060\u0060\u0060\n\n## Emoji Text Measurement\n\nEmoji sequences must be measured as a single unit:\n\n\u0060\u0060\u0060csharp\npublic float MeasureText(string text)\n{\n var detector = new EmojiDetector();\n var emojis = detector.FindEmojis(text);\n \n float totalWidth = 0;\n int lastIndex = 0;\n \n foreach (var emoji in emojis)\n {\n // Measure text before emoji\n if (emoji.StartIndex \u003E lastIndex)\n {\n var beforeText = text.Substring(lastIndex, emoji.StartIndex - lastIndex);\n totalWidth \u002B= MeasureTextSegment(beforeText, _regularFont);\n }\n \n // Measure emoji\n totalWidth \u002B= MeasureTextSegment(emoji.Text, _emojiFont);\n lastIndex = emoji.EndIndex;\n }\n \n // Measure remaining text\n if (lastIndex \u003C text.Length)\n {\n var remainingText = text.Substring(lastIndex);\n totalWidth \u002B= MeasureTextSegment(remainingText, _regularFont);\n }\n \n return totalWidth;\n}\n\nprivate float MeasureTextSegment(string text, SKTypeface font)\n{\n using var paint = new SKPaint\n {\n Typeface = font,\n TextSize = (float)FontSize\n };\n return paint.MeasureText(text);\n}\n\u0060\u0060\u0060\n\n## Unicode Normalization\n\nDifferent Unicode representations can look identical but have different codepoints:\n\n\u0060\u0060\u0060csharp\npublic class UnicodeNormalizer\n{\n public string Normalize(string text)\n {\n // Use NFC (Canonical Decomposition, followed by Canonical Composition)\n return text.Normalize(NormalizationForm.FormC);\n }\n \n public bool AreEquivalent(string text1, string text2)\n {\n return Normalize(text1) == Normalize(text2);\n }\n}\n\n// Example:\nvar e1 = \u0022\u00E9\u0022; // Single codepoint U\u002B00E9\nvar e2 = \u0022\u00E9\u0022; // e (U\u002B0065) \u002B combining acute accent (U\u002B0301)\n// These look identical but text1.Length != text2.Length\n// After normalization, they\u0027re equal\n\u0060\u0060\u0060\n\n## Grapheme Cluster Boundaries\n\nA grapheme cluster is what users perceive as a single character:\n\n\u0060\u0060\u0060csharp\npublic class GraphemeClusterIterator\n{\n public List\u003Cstring\u003E GetGraphemeClusters(string text)\n {\n var clusters = new List\u003Cstring\u003E();\n var enumerator = StringInfo.GetTextElementEnumerator(text);\n \n while (enumerator.MoveNext())\n {\n clusters.Add(enumerator.GetTextElement());\n }\n \n return clusters;\n }\n}\n\n// Example:\nvar text = \u0022\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66Hello\u0022;\nvar clusters = iterator.GetGraphemeClusters(text);\n// clusters[0] = \u0022\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66\u0022 (one grapheme cluster)\n// clusters[1] = \u0022H\u0022\n// clusters[2] = \u0022e\u0022\n// etc.\n\u0060\u0060\u0060\n\nThis is crucial for cursor movement, text selection, and backspace behavior in text editors.\r\n\r\n## CJK Language Input\r\n\r\nChinese, Japanese, and Korean (CJK) languages present unique challenges for text input. With thousands of characters and limited keyboard keys, these languages rely heavily on Input Method Editors (IMEs).\n\n## CJK Input Methods\n\n**Chinese Input Methods:**\n- **Pinyin**: Phonetic input (\u0022nihao\u0022 \u2192 \u4F60\u597D)\n- **Wubi**: Shape-based input for faster typing\n- **Cangjie**: Traditional Chinese input method\n- **Handwriting**: Touch/stylus input\n\n**Japanese Input Methods:**\n- **Romaji**: Type \u0022konnichiwa\u0022 \u2192 \u3053\u3093\u306B\u3061\u306F \u2192 \u4ECA\u65E5\u306F\n- **Kana**: Direct hiragana/katakana input\n- **Flick input**: Mobile touch input\n\n**Korean Input Methods:**\n- **Hangul**: Automatic jamo composition (\u3131 \u002B \u314F \u002B \u3147 \u2192 \uAC15)\n- **2-Set/3-Set**: Different keyboard layouts\n\n## SkiaEntry CJK Support\n\nThe \u0060SkiaEntry\u0060 control provides full CJK input support:\n\n\u0060\u0060\u0060csharp\n// From SkiaEntry - CJK composition handling\npublic class CJKInputHandler\n{\n private string _compositionText = string.Empty;\n private List\u003Cstring\u003E _candidates = new();\n private int _selectedCandidate = 0;\n \n public void HandleCompositionUpdate(string composition, string[] candidates)\n {\n _compositionText = composition;\n _candidates = candidates?.ToList() ?? new();\n _selectedCandidate = 0;\n \n // Show candidate window\n ShowCandidateWindow();\n }\n \n public void HandleCandidateSelection(int index)\n {\n if (index \u003E= 0 \u0026\u0026 index \u003C _candidates.Count)\n {\n _selectedCandidate = index;\n CommitText(_candidates[index]);\n ClearComposition();\n }\n }\n \n private void ShowCandidateWindow()\n {\n if (_candidates.Count == 0)\n return;\n \n var candidateWindow = new CandidateWindow\n {\n Candidates = _candidates,\n SelectedIndex = _selectedCandidate,\n Position = GetCursorScreenPosition()\n };\n \n candidateWindow.Show();\n }\n}\n\u0060\u0060\u0060\n\n## Candidate Window Rendering\n\nCJK input requires displaying candidate characters:\n\n\u0060\u0060\u0060csharp\npublic class CandidateWindow : SkiaView\n{\n public List\u003Cstring\u003E Candidates { get; set; }\n public int SelectedIndex { get; set; }\n \n protected override void OnDraw(SKCanvas canvas)\n {\n // Draw background\n using var bgPaint = new SKPaint\n {\n Color = SKColors.White,\n Style = SKPaintStyle.Fill\n };\n canvas.DrawRoundRect(Bounds.ToSKRect(), 4, 4, bgPaint);\n \n // Draw border\n using var borderPaint = new SKPaint\n {\n Color = SKColors.Gray,\n Style = SKPaintStyle.Stroke,\n StrokeWidth = 1\n };\n canvas.DrawRoundRect(Bounds.ToSKRect(), 4, 4, borderPaint);\n \n // Draw candidates\n float y = 10;\n for (int i = 0; i \u003C Candidates.Count; i\u002B\u002B)\n {\n var isSelected = i == SelectedIndex;\n \n // Highlight selected candidate\n if (isSelected)\n {\n using var highlightPaint = new SKPaint\n {\n Color = new SKColor(200, 220, 255),\n Style = SKPaintStyle.Fill\n };\n var highlightRect = new SKRect(5, y - 2, Width - 5, y \u002B 22);\n canvas.DrawRoundRect(highlightRect, 2, 2, highlightPaint);\n }\n \n // Draw candidate number\n using var numberPaint = new SKPaint\n {\n Color = SKColors.Gray,\n TextSize = 14,\n IsAntialias = true\n };\n canvas.DrawText($\u0022{i \u002B 1}.\u0022, 10, y \u002B 16, numberPaint);\n \n // Draw candidate text\n using var textPaint = new SKPaint\n {\n Color = isSelected ? SKColors.Black : SKColors.DarkGray,\n TextSize = 16,\n IsAntialias = true,\n Typeface = GetCJKTypeface()\n };\n canvas.DrawText(Candidates[i], 35, y \u002B 16, textPaint);\n \n y \u002B= 25;\n }\n }\n \n private SKTypeface GetCJKTypeface()\n {\n return SKTypeface.FromFamilyName(\u0022Noto Sans CJK SC\u0022) ??\n SKTypeface.FromFamilyName(\u0022WenQuanYi Micro Hei\u0022) ??\n SKTypeface.FromFamilyName(\u0022Noto Sans\u0022);\n }\n}\n\u0060\u0060\u0060\n\n## Japanese Kana Conversion\n\nJapanese input requires converting between romaji, hiragana, and kanji:\n\n\u0060\u0060\u0060csharp\npublic class JapaneseInputConverter\n{\n private readonly Dictionary\u003Cstring, string\u003E _romajiToHiragana = new()\n {\n { \u0022a\u0022, \u0022\u3042\u0022 }, { \u0022i\u0022, \u0022\u3044\u0022 }, { \u0022u\u0022, \u0022\u3046\u0022 }, { \u0022e\u0022, \u0022\u3048\u0022 }, { \u0022o\u0022, \u0022\u304A\u0022 },\n { \u0022ka\u0022, \u0022\u304B\u0022 }, { \u0022ki\u0022, \u0022\u304D\u0022 }, { \u0022ku\u0022, \u0022\u304F\u0022 }, { \u0022ke\u0022, \u0022\u3051\u0022 }, { \u0022ko\u0022, \u0022\u3053\u0022 },\n { \u0022sa\u0022, \u0022\u3055\u0022 }, { \u0022shi\u0022, \u0022\u3057\u0022 }, { \u0022su\u0022, \u0022\u3059\u0022 }, { \u0022se\u0022, \u0022\u305B\u0022 }, { \u0022so\u0022, \u0022\u305D\u0022 },\n { \u0022ta\u0022, \u0022\u305F\u0022 }, { \u0022chi\u0022, \u0022\u3061\u0022 }, { \u0022tsu\u0022, \u0022\u3064\u0022 }, { \u0022te\u0022, \u0022\u3066\u0022 }, { \u0022to\u0022, \u0022\u3068\u0022 },\n { \u0022na\u0022, \u0022\u306A\u0022 }, { \u0022ni\u0022, \u0022\u306B\u0022 }, { \u0022nu\u0022, \u0022\u306C\u0022 }, { \u0022ne\u0022, \u0022\u306D\u0022 }, { \u0022no\u0022, \u0022\u306E\u0022 },\n // ... more mappings\n };\n \n public string ConvertRomajiToHiragana(string romaji)\n {\n var result = new StringBuilder();\n var buffer = new StringBuilder();\n \n foreach (var c in romaji)\n {\n buffer.Append(c);\n \n // Try to match longest possible sequence\n var matched = false;\n for (int len = buffer.Length; len \u003E 0; len--)\n {\n var substring = buffer.ToString().Substring(buffer.Length - len);\n if (_romajiToHiragana.TryGetValue(substring, out var hiragana))\n {\n // Keep unmatched prefix\n if (buffer.Length \u003E len)\n {\n result.Append(buffer.ToString().Substring(0, buffer.Length - len));\n }\n result.Append(hiragana);\n buffer.Clear();\n matched = true;\n break;\n }\n }\n }\n \n // Append remaining buffer\n result.Append(buffer);\n return result.ToString();\n }\n}\n\n// Example:\nvar converter = new JapaneseInputConverter();\nvar hiragana = converter.ConvertRomajiToHiragana(\u0022konnichiwa\u0022);\n// Result: \u0022\u3053\u3093\u306B\u3061\u308F\u0022\n\u0060\u0060\u0060\n\n## Korean Hangul Composition\n\nKorean uses jamo (consonants and vowels) that combine into syllables:\n\n\u0060\u0060\u0060csharp\npublic class HangulComposer\n{\n private const int HangulBase = 0xAC00;\n private const int InitialCount = 19;\n private const int MedialCount = 21;\n private const int FinalCount = 28;\n \n public string ComposeHangul(char initial, char medial, char final = \u0027\\0\u0027)\n {\n // Convert jamo to indices\n int initialIndex = GetInitialIndex(initial);\n int medialIndex = GetMedialIndex(medial);\n int finalIndex = final == \u0027\\0\u0027 ? 0 : GetFinalIndex(final) \u002B 1;\n \n if (initialIndex == -1 || medialIndex == -1 || finalIndex == -1)\n return string.Empty;\n \n // Calculate syllable codepoint\n int syllable = HangulBase \u002B \n (initialIndex * MedialCount * FinalCount) \u002B\n (medialIndex * FinalCount) \u002B\n finalIndex;\n \n return char.ConvertFromUtf32(syllable);\n }\n \n private int GetInitialIndex(char c)\n {\n // \u3131 \u3132 \u3134 \u3137 \u3138 \u3139 \u3141 \u3142 \u3143 \u3145 \u3146 \u3147 \u3148 \u3149 \u314A \u314B \u314C \u314D \u314E\n return c switch\n {\n \u0027\u3131\u0027 =\u003E 0, \u0027\u3132\u0027 =\u003E 1, \u0027\u3134\u0027 =\u003E 2, \u0027\u3137\u0027 =\u003E 3, \u0027\u3138\u0027 =\u003E 4,\n \u0027\u3139\u0027 =\u003E 5, \u0027\u3141\u0027 =\u003E 6, \u0027\u3142\u0027 =\u003E 7, \u0027\u3143\u0027 =\u003E 8, \u0027\u3145\u0027 =\u003E 9,\n \u0027\u3146\u0027 =\u003E 10, \u0027\u3147\u0027 =\u003E 11, \u0027\u3148\u0027 =\u003E 12, \u0027\u3149\u0027 =\u003E 13, \u0027\u314A\u0027 =\u003E 14,\n \u0027\u314B\u0027 =\u003E 15, \u0027\u314C\u0027 =\u003E 16, \u0027\u314D\u0027 =\u003E 17, \u0027\u314E\u0027 =\u003E 18,\n _ =\u003E -1\n };\n }\n \n private int GetMedialIndex(char c)\n {\n // \u314F \u3150 \u3151 \u3152 \u3153 \u3154 \u3155 \u3156 \u3157 \u3158 \u3159 \u315A \u315B \u315C \u315D \u315E \u315F \u3160 \u3161 \u3162 \u3163\n return c switch\n {\n \u0027\u314F\u0027 =\u003E 0, \u0027\u3150\u0027 =\u003E 1, \u0027\u3151\u0027 =\u003E 2, \u0027\u3152\u0027 =\u003E 3, \u0027\u3153\u0027 =\u003E 4,\n \u0027\u3154\u0027 =\u003E 5, \u0027\u3155\u0027 =\u003E 6, \u0027\u3156\u0027 =\u003E 7, \u0027\u3157\u0027 =\u003E 8, \u0027\u3158\u0027 =\u003E 9,\n \u0027\u3159\u0027 =\u003E 10, \u0027\u315A\u0027 =\u003E 11, \u0027\u315B\u0027 =\u003E 12, \u0027\u315C\u0027 =\u003E 13, \u0027\u315D\u0027 =\u003E 14,\n \u0027\u315E\u0027 =\u003E 15, \u0027\u315F\u0027 =\u003E 16, \u0027\u3160\u0027 =\u003E 17, \u0027\u3161\u0027 =\u003E 18, \u0027\u3162\u0027 =\u003E 19,\n \u0027\u3163\u0027 =\u003E 20,\n _ =\u003E -1\n };\n }\n \n private int GetFinalIndex(char c)\n {\n // (none) \u3131 \u3132 \u3133 \u3134 \u3135 \u3136 \u3137 \u3139 \u313A \u313B \u313C \u313D \u313E \u313F \u3140 \u3141 \u3142 \u3144 \u3145 \u3146 \u3147 \u3148 \u314A \u314B \u314C \u314D \u314E\n return c switch\n {\n \u0027\u3131\u0027 =\u003E 0, \u0027\u3132\u0027 =\u003E 1, \u0027\u3133\u0027 =\u003E 2, \u0027\u3134\u0027 =\u003E 3, \u0027\u3135\u0027 =\u003E 4,\n \u0027\u3136\u0027 =\u003E 5, \u0027\u3137\u0027 =\u003E 6, \u0027\u3139\u0027 =\u003E 7, \u0027\u313A\u0027 =\u003E 8, \u0027\u313B\u0027 =\u003E 9,\n \u0027\u313C\u0027 =\u003E 10, \u0027\u313D\u0027 =\u003E 11, \u0027\u313E\u0027 =\u003E 12, \u0027\u313F\u0027 =\u003E 13, \u0027\u3140\u0027 =\u003E 14,\n \u0027\u3141\u0027 =\u003E 15, \u0027\u3142\u0027 =\u003E 16, \u0027\u3144\u0027 =\u003E 17, \u0027\u3145\u0027 =\u003E 18, \u0027\u3146\u0027 =\u003E 19,\n \u0027\u3147\u0027 =\u003E 20, \u0027\u3148\u0027 =\u003E 21, \u0027\u314A\u0027 =\u003E 22, \u0027\u314B\u0027 =\u003E 23, \u0027\u314C\u0027 =\u003E 24,\n \u0027\u314D\u0027 =\u003E 25, \u0027\u314E\u0027 =\u003E 26,\n _ =\u003E -1\n };\n }\n}\n\n// Example:\nvar composer = new HangulComposer();\nvar syllable = composer.ComposeHangul(\u0027\u3131\u0027, \u0027\u314F\u0027, \u0027\u3147\u0027);\n// Result: \u0022\uAC15\u0022\n\u0060\u0060\u0060\n\n## CJK Font Selection\n\nDifferent CJK languages prefer different glyph variants:\n\n\u0060\u0060\u0060csharp\npublic class CJKFontSelector\n{\n public SKTypeface GetCJKFont(CJKLanguage language)\n {\n return language switch\n {\n CJKLanguage.SimplifiedChinese =\u003E \n SKTypeface.FromFamilyName(\u0022Noto Sans CJK SC\u0022) ??\n SKTypeface.FromFamilyName(\u0022WenQuanYi Micro Hei\u0022),\n \n CJKLanguage.TraditionalChinese =\u003E\n SKTypeface.FromFamilyName(\u0022Noto Sans CJK TC\u0022) ??\n SKTypeface.FromFamilyName(\u0022WenQuanYi Micro Hei\u0022),\n \n CJKLanguage.Japanese =\u003E\n SKTypeface.FromFamilyName(\u0022Noto Sans CJK JP\u0022) ??\n SKTypeface.FromFamilyName(\u0022IPAGothic\u0022),\n \n CJKLanguage.Korean =\u003E\n SKTypeface.FromFamilyName(\u0022Noto Sans CJK KR\u0022) ??\n SKTypeface.FromFamilyName(\u0022Nanum Gothic\u0022),\n \n _ =\u003E SKTypeface.FromFamilyName(\u0022Noto Sans CJK SC\u0022)\n };\n }\n}\n\npublic enum CJKLanguage\n{\n SimplifiedChinese,\n TraditionalChinese,\n Japanese,\n Korean\n}\n\u0060\u0060\u0060\n\n## Vertical Text Support\n\nCJK languages sometimes use vertical text layout:\n\n\u0060\u0060\u0060csharp\npublic void DrawVerticalText(SKCanvas canvas, string text, float x, float y)\n{\n using var paint = new SKPaint\n {\n Typeface = GetCJKFont(CJKLanguage.Japanese),\n TextSize = 16,\n IsAntialias = true\n };\n \n float currentY = y;\n foreach (var c in text)\n {\n canvas.DrawText(c.ToString(), x, currentY, paint);\n currentY \u002B= paint.TextSize * 1.2f; // Line height\n }\n}\n\u0060\u0060\u0060\r\n\r\n## Testing International Text Input\r\n\r\nTesting international text input requires comprehensive test cases covering different scripts, input methods, and edge cases. OpenMaui\u0027s test suite includes 217 passing tests with extensive coverage for text rendering.\n\n## Unit Testing Text Rendering\n\n\u0060\u0060\u0060csharp\nusing Xunit;\nusing FluentAssertions;\nusing Moq;\n\npublic class TextRenderingTests\n{\n [Fact]\n public void Label_RendersChineseText_Correctly()\n {\n // Arrange\n var label = new SkiaLabel\n {\n Text = \u0022\u4F60\u597D\u4E16\u754C\u0022,\n FontSize = 16\n };\n \n // Act\n var measuredSize = label.Measure(double.PositiveInfinity, double.PositiveInfinity);\n \n // Assert\n measuredSize.Width.Should().BeGreaterThan(0);\n measuredSize.Height.Should().BeGreaterThan(0);\n }\n \n [Theory]\n [InlineData(\u0022Hello \u4E16\u754C \uD83C\uDF0D\u0022)] // Mixed Latin, CJK, Emoji\n [InlineData(\u0022\u0645\u0631\u062D\u0628\u0627\u0022)] // Arabic\n [InlineData(\u0022\u05E9\u05DC\u05D5\u05DD\u0022)] // Hebrew\n [InlineData(\u0022\u3053\u3093\u306B\u3061\u306F\u0022)] // Japanese Hiragana\n [InlineData(\u0022\uC548\uB155\uD558\uC138\uC694\u0022)] // Korean\n public void Label_RendersMixedScripts_WithoutCrashing(string text)\n {\n // Arrange\n var label = new SkiaLabel { Text = text };\n \n // Act\n Action render = () =\u003E label.Measure(100, 100);\n \n // Assert\n render.Should().NotThrow();\n }\n \n [Fact]\n public void FontFallback_SelectsCorrectFont_ForEmoji()\n {\n // Arrange\n var fallbackManager = new FontFallbackManager();\n var primaryFont = SKTypeface.FromFamilyName(\u0022Arial\u0022);\n \n // Act\n var emojiFont = fallbackManager.GetTypefaceForCharacter(\u0027\uD83D\uDE00\u0027, primaryFont);\n \n // Assert\n emojiFont.Should().NotBe(primaryFont);\n emojiFont.FamilyName.Should().Contain(\u0022Emoji\u0022);\n }\n \n [Fact]\n public void TextShaper_HandlesArabicLigatures_Correctly()\n {\n // Arrange\n var shaper = new TextShaper();\n var arabicText = \u0022\u0645\u0631\u062D\u0628\u0627\u0022;\n var font = SKTypeface.FromFamilyName(\u0022Noto Sans Arabic\u0022);\n \n // Act\n var shaped = shaper.ShapeText(arabicText, font, 16);\n \n // Assert\n shaped.Glyphs.Should().NotBeEmpty();\n // Arabic text should have fewer glyphs than characters due to ligatures\n shaped.Glyphs.Count.Should().BeLessThanOrEqualTo(arabicText.Length);\n }\n}\n\u0060\u0060\u0060\n\n## IME Integration Tests\n\n\u0060\u0060\u0060csharp\npublic class IMETests\n{\n [Fact]\n public void Entry_HandlesPreeditText_Correctly()\n {\n // Arrange\n var entry = new SkiaEntry();\n \n // Act - Simulate IME composition\n entry.HandleTextInput(new TextInputEventArgs\n {\n IsComposing = true,\n CompositionText = \u0022nihao\u0022,\n CompositionCursorPos = 5\n });\n \n // Assert\n entry.PreeditText.Should().Be(\u0022nihao\u0022);\n entry.Text.Should().BeEmpty(); // Not committed yet\n }\n \n [Fact]\n public void Entry_CommitsComposedText_OnEnter()\n {\n // Arrange\n var entry = new SkiaEntry();\n entry.HandleTextInput(new TextInputEventArgs\n {\n IsComposing = true,\n CompositionText = \u0022\u4F60\u597D\u0022\n });\n \n // Act - Commit\n entry.HandleTextInput(new TextInputEventArgs\n {\n IsComposing = false,\n Text = \u0022\u4F60\u597D\u0022\n });\n \n // Assert\n entry.Text.Should().Be(\u0022\u4F60\u597D\u0022);\n entry.PreeditText.Should().BeEmpty();\n }\n \n [Fact]\n public void Entry_UpdatesCursorPosition_ForIME()\n {\n // Arrange\n var entry = new SkiaEntry { Text = \u0022Hello\u0022 };\n entry.CursorPosition = 5;\n \n // Act\n var cursorRect = entry.GetCursorRect();\n \n // Assert\n cursorRect.X.Should().BeGreaterThan(0);\n cursorRect.Height.Should().BeGreaterThan(0);\n }\n}\n\u0060\u0060\u0060\n\n## Emoji Detection Tests\n\n\u0060\u0060\u0060csharp\npublic class EmojiTests\n{\n [Theory]\n [InlineData(\u0022\uD83D\uDE00\u0022, true)] // Simple emoji\n [InlineData(\u0022\uD83D\uDC4B\uD83C\uDFFF\u0022, true)] // Emoji with skin tone\n [InlineData(\u0022\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66\u0022, true)] // ZWJ sequence\n [InlineData(\u0022Hello\u0022, false)] // Regular text\n [InlineData(\u0022\u4F60\u597D\u0022, false)] // CJK\n public void EmojiDetector_IdentifiesEmoji_Correctly(string text, bool shouldBeEmoji)\n {\n // Arrange\n var detector = new EmojiDetector();\n \n // Act\n var emojis = detector.FindEmojis(text);\n \n // Assert\n if (shouldBeEmoji)\n {\n emojis.Should().NotBeEmpty();\n }\n else\n {\n emojis.Should().BeEmpty();\n }\n }\n \n [Fact]\n public void EmojiSequence_ExtractsCompleteSequence()\n {\n // Arrange\n var detector = new EmojiDetector();\n var text = \u0022Hello \uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66 World\u0022;\n \n // Act\n var emojis = detector.FindEmojis(text);\n \n // Assert\n emojis.Should().HaveCount(1);\n emojis[0].Text.Should().Be(\u0022\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66\u0022);\n emojis[0].StartIndex.Should().Be(6);\n }\n}\n\u0060\u0060\u0060\n\n## Visual Regression Testing\n\nFor visual testing, OpenMaui can generate reference images:\n\n\u0060\u0060\u0060csharp\npublic class VisualRegressionTests\n{\n [Fact]\n public void Label_RendersChineseText_MatchesReference()\n {\n // Arrange\n var label = new SkiaLabel\n {\n Text = \u0022\u4F60\u597D\u4E16\u754C\u0022,\n FontSize = 24,\n Width = 200,\n Height = 50\n };\n \n // Act\n var bitmap = RenderToBitmap(label);\n \n // Assert\n var referenceImage = LoadReferenceImage(\u0022chinese_text.png\u0022);\n bitmap.Should().MatchImage(referenceImage, tolerance: 0.01);\n }\n \n private SKBitmap RenderToBitmap(SkiaView view)\n {\n var info = new SKImageInfo((int)view.Width, (int)view.Height);\n var bitmap = new SKBitmap(info);\n \n using var canvas = new SKCanvas(bitmap);\n view.Draw(canvas);\n \n return bitmap;\n }\n}\n\u0060\u0060\u0060\n\n## Integration Testing with Real IMEs\n\nFor end-to-end testing, use actual IME frameworks:\n\n\u0060\u0060\u0060bash\n#!/bin/bash\n# Test script for IBus integration\n\n# Start IBus daemon\nibus-daemon -drx\n\n# Set environment\nexport GTK_IM_MODULE=ibus\nexport XMODIFIERS=@im=ibus\n\n# Run tests\ndotnet test --filter \u0022Category=IME\u0022\n\u0060\u0060\u0060\n\n\u0060\u0060\u0060csharp\n[Trait(\u0022Category\u0022, \u0022IME\u0022)]\npublic class IBusIntegrationTests\n{\n [Fact(Skip = \u0022Requires IBus running\u0022)]\n public void Entry_IntegratesWithIBus_ForChineseInput()\n {\n // This test requires a running X11 session with IBus\n var app = new LinuxApplication();\n var window = new GtkHostWindow();\n var entry = new SkiaEntry();\n \n window.Content = entry;\n window.Show();\n \n // Simulate IBus events (requires native integration)\n // This would typically be done through UI automation\n }\n}\n\u0060\u0060\u0060\n\n## Performance Testing\n\n\u0060\u0060\u0060csharp\npublic class PerformanceTests\n{\n [Fact]\n public void TextShaping_HandlesLargeText_Efficiently()\n {\n // Arrange\n var shaper = new TextShaper();\n var largeText = string.Join(\u0022\u0022, Enumerable.Repeat(\u0022\u4F60\u597D\u4E16\u754C\u0022, 1000));\n var font = SKTypeface.FromFamilyName(\u0022Noto Sans CJK SC\u0022);\n \n // Act\n var stopwatch = Stopwatch.StartNew();\n var shaped = shaper.ShapeText(largeText, font, 16);\n stopwatch.Stop();\n \n // Assert\n stopwatch.ElapsedMilliseconds.Should().BeLessThan(100);\n }\n \n [Fact]\n public void FontFallback_CachesResults_ForPerformance()\n {\n // Arrange\n var manager = new FontFallbackManager();\n var primaryFont = SKTypeface.Default;\n \n // Act - First lookup (cache miss)\n var stopwatch1 = Stopwatch.StartNew();\n var font1 = manager.GetTypefaceForCharacterCached(\u0027\u4F60\u0027, primaryFont);\n stopwatch1.Stop();\n \n // Act - Second lookup (cache hit)\n var stopwatch2 = Stopwatch.StartNew();\n var font2 = manager.GetTypefaceForCharacterCached(\u0027\u4F60\u0027, primaryFont);\n stopwatch2.Stop();\n \n // Assert\n font1.Should().Be(font2);\n stopwatch2.ElapsedTicks.Should().BeLessThan(stopwatch1.ElapsedTicks);\n }\n}\n\u0060\u0060\u0060\n\n## Test Data Sets\n\nCreate comprehensive test data covering edge cases:\n\n\u0060\u0060\u0060csharp\npublic static class TestData\n{\n public static IEnumerable\u003Cobject[]\u003E InternationalTextSamples()\n {\n yield return new object[] { \u0022Hello World\u0022, \u0022Latin\u0022 };\n yield return new object[] { \u0022\u4F60\u597D\u4E16\u754C\u0022, \u0022Simplified Chinese\u0022 };\n yield return new object[] { \u0022\u3053\u3093\u306B\u3061\u306F\u4E16\u754C\u0022, \u0022Japanese\u0022 };\n yield return new object[] { \u0022\uC548\uB155\uD558\uC138\uC694 \uC138\uACC4\u0022, \u0022Korean\u0022 };\n yield return new object[] { \u0022\u0645\u0631\u062D\u0628\u0627 \u0628\u0627\u0644\u0639\u0627\u0644\u0645\u0022, \u0022Arabic\u0022 };\n yield return new object[] { \u0022\u05E9\u05DC\u05D5\u05DD \u05E2\u05D5\u05DC\u05DD\u0022, \u0022Hebrew\u0022 };\n yield return new object[] { \u0022\u041F\u0440\u0438\u0432\u0435\u0442 \u043C\u0438\u0440\u0022, \u0022Cyrillic\u0022 };\n yield return new object[] { \u0022\u0E2A\u0E27\u0E31\u0E2A\u0E14\u0E35\u0E0A\u0E32\u0E27\u0E42\u0E25\u0E01\u0022, \u0022Thai\u0022 };\n yield return new object[] { \u0022Hello \u4E16\u754C \uD83C\uDF0D\u0022, \u0022Mixed\u0022 };\n yield return new object[] { \u0022\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66\uD83D\uDC4B\uD83C\uDFFF\uD83C\uDF0D\u0022, \u0022Emoji\u0022 };\n }\n}\n\n[Theory]\n[MemberData(nameof(TestData.InternationalTextSamples), MemberType = typeof(TestData))]\npublic void Label_RendersInternationalText_Correctly(string text, string script)\n{\n var label = new SkiaLabel { Text = text };\n var size = label.Measure(double.PositiveInfinity, double.PositiveInfinity);\n size.Width.Should().BeGreaterThan(0, $\u0022Failed to render {script} text\u0022);\n}\n\u0060\u0060\u0060\n\n## Continuous Integration\n\nRun tests in CI with proper font installation:\n\n\u0060\u0060\u0060yaml\n# .github/workflows/test.yml\nname: Tests\n\non: [push, pull_request]\n\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v2\n \n - name: Install fonts\n run: |\n sudo apt-get update\n sudo apt-get install -y \\\n fonts-noto-cjk \\\n fonts-noto-color-emoji \\\n fonts-noto-core \\\n fonts-liberation\n \n - name: Setup .NET\n uses: actions/setup-dotnet@v1\n with:\n dotnet-version: \u00278.0.x\u0027\n \n - name: Run tests\n run: dotnet test --logger \u0022trx;LogFileName=test-results.trx\u0022\n \n - name: Publish test results\n uses: dorny/test-reporter@v1\n if: always()\n with:\n name: Test Results\n path: \u0027**/test-results.trx\u0027\n reporter: dotnet-trx\n\u0060\u0060\u0060\n\nThis comprehensive testing approach ensures OpenMaui\u0027s text rendering works correctly across all supported languages and scripts, providing a robust foundation for international applications.\r\n\r\n---\r\n\r\n\u003E Font fallback isn\u0027t just a nice-to-have feature\u2014it\u0027s the difference between displaying \u0027\u4F60\u597D\u0027 correctly and showing empty boxes to millions of users.\r\n\r\n\u003E The IBus integration enables seamless input for over 3 billion people who use CJK languages, making your application truly international.\r\n\r\n\u003E HarfBuzz transforms Unicode text into properly shaped glyphs, handling the complex rules of Arabic ligatures, Indic vowel reordering, and emoji sequences.", + "createdAt": 1769750791920, + "updatedAt": 1769750791920, + "tags": [ + "series", + "generated", + "Building Linux Apps with .NET MAUI" + ], + "isArticle": true, + "seriesGroup": "Building Linux Apps with .NET MAUI", + "subtitle": "Building a production-ready text rendering system for .NET MAUI on Linux that handles CJK languages, emoji, and complex scripts with grace.", + "pullQuotes": [ + "Font fallback isn\u0027t just a nice-to-have feature\u2014it\u0027s the difference between displaying \u0027\u4F60\u597D\u0027 correctly and showing empty boxes to millions of users.", + "The IBus integration enables seamless input for over 3 billion people who use CJK languages, making your application truly international.", + "HarfBuzz transforms Unicode text into properly shaped glyphs, handling the complex rules of Arabic ligatures, Indic vowel reordering, and emoji sequences." + ], + "sections": [ + { + "header": "Introduction", + "content": "Text rendering seems simple until you encounter your first emoji, Chinese character, or right-to-left script. What works perfectly for English ASCII suddenly breaks down when users type \u0022\u0645\u0631\u062D\u0628\u0627\u0022 (Arabic), \u0022\u3053\u3093\u306B\u3061\u306F\u0022 (Japanese), or \u0022\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66\u0022 (family emoji with zero-width joiners).\n\nThe OpenMaui Linux platform implementation tackles these challenges head-on, providing comprehensive international text support for .NET MAUI applications. This isn\u0027t just about displaying characters\u2014it\u0027s about enabling proper text input through Input Method Editors (IMEs), implementing intelligent font fallback for missing glyphs, and leveraging HarfBuzz for complex script shaping.\n\nIn this article, we\u0027ll explore the architecture behind OpenMaui\u0027s text rendering system, examining real implementation patterns that handle everything from IBus integration to emoji rendering. Whether you\u0027re building a text editor, chat application, or international business software, these techniques will help you support users worldwide.\n\n**Key capabilities covered:**\n- IBus and XIM integration for CJK input methods\n- Font fallback strategies for emoji and international scripts\n- HarfBuzz integration for complex text shaping\n- Practical testing approaches for international text" + }, + { + "header": "Text Rendering Challenges on Linux", + "content": "Linux desktop environments present unique text rendering challenges that Windows and macOS developers often don\u0027t encounter. The fragmented ecosystem of input methods, font configurations, and desktop environments requires careful architectural decisions.\n\n## The Complexity Landscape\n\n**Input Method Diversity**: Unlike Windows with its unified IME framework, Linux supports multiple input method frameworks:\n- **IBus** (Intelligent Input Bus) - The most common, used by GNOME and many distributions\n- **Fcitx5** - Popular among advanced users, especially for Chinese input\n- **XIM** (X Input Method) - Legacy protocol still widely used\n\nEach framework has different APIs, event models, and integration requirements. OpenMaui\u0027s \u0060SkiaEntry\u0060 and \u0060SkiaEditor\u0060 controls must support all three to provide consistent text input across distributions.\n\n**Font Fragmentation**: User systems have wildly different font installations. A font that renders perfectly on Ubuntu might be missing on Fedora. Your application needs to:\n- Detect missing glyphs in real-time\n- Fall back to alternative fonts automatically\n- Handle emoji fonts (color vs. monochrome)\n- Support CJK fonts that can be hundreds of megabytes\n\n**Script Complexity**: Different writing systems have different rendering rules:\n- **Latin scripts**: Simple left-to-right, one glyph per character\n- **Arabic/Hebrew**: Right-to-left with contextual letter forms\n- **Indic scripts**: Vowel reordering and complex ligatures\n- **CJK**: Vertical text support, ruby annotations\n- **Emoji**: Multi-codepoint sequences with zero-width joiners\n\n## The SkiaSharp Foundation\n\nOpenMaui uses SkiaSharp as its rendering engine, which provides several advantages:\n\n\u0060\u0060\u0060csharp\n// From SkiaLabel - base text rendering setup\nprivate void DrawText(SKCanvas canvas)\n{\n using var paint = new SKPaint\n {\n Color = textColor.ToSKColor(),\n TextSize = (float)FontSize,\n IsAntialias = true,\n Typeface = GetTypeface(),\n TextAlign = GetTextAlign()\n };\n \n // Font fallback happens here\n canvas.DrawText(text, x, y, paint);\n}\n\u0060\u0060\u0060\n\nSkiaSharp provides consistent rendering across platforms, but it doesn\u0027t solve input method integration or font fallback automatically. That\u0027s where OpenMaui\u0027s platform layer comes in.\n\n## The Three-Layer Architecture\n\nOpenMaui\u0027s text rendering uses a three-layer approach:\n\n1. **Input Layer**: IBus/XIM integration captures text input events\n2. **Shaping Layer**: HarfBuzz converts Unicode to positioned glyphs\n3. **Rendering Layer**: SkiaSharp draws the final output\n\nThis separation allows each layer to be tested independently and swapped out if needed. The \u0060SkiaLabel\u0060 class implements font fallback, while \u0060SkiaEntry\u0060 and \u0060SkiaEditor\u0060 add IME support for text input." + }, + { + "header": "IBus and XIM Integration", + "content": "Input Method Editors (IMEs) are essential for typing languages that have more characters than keyboard keys. Chinese, Japanese, and Korean users rely on IMEs to convert phonetic input into the correct characters.\n\n## Understanding IME Workflow\n\nWhen a user types Chinese using Pinyin input:\n\n1. User types \u0022nihao\u0022 on the keyboard\n2. IME shows composition text: \u0022\u4F60\u597D\u0022 (\u5019\u9078\u5B57)\n3. User selects the correct characters from candidates\n4. IME commits the final text to the application\n\nDuring composition, the application must display **preedit text** (uncommitted characters) with proper styling, usually with an underline.\n\n## IBus Integration in SkiaEntry\n\nThe \u0060SkiaEntry\u0060 control implements IME support through native IBus integration:\n\n\u0060\u0060\u0060csharp\n// From SkiaEntry - IME composition handling\nprivate string _preeditText = string.Empty;\nprivate int _preeditCursorPos = 0;\n\nprotected override void OnTextInput(TextInputEventArgs e)\n{\n if (e.IsComposing)\n {\n // Handle preedit text during composition\n _preeditText = e.CompositionText;\n _preeditCursorPos = e.CompositionCursorPos;\n Invalidate();\n }\n else\n {\n // Commit final text\n InsertText(e.Text);\n _preeditText = string.Empty;\n }\n}\n\nprotected override void OnDraw(SKCanvas canvas)\n{\n // Draw normal text\n DrawTextContent(canvas);\n \n // Draw preedit text with underline\n if (!string.IsNullOrEmpty(_preeditText))\n {\n DrawPreeditText(canvas, _preeditText, _preeditCursorPos);\n }\n}\n\u0060\u0060\u0060\n\n## XIM Protocol Support\n\nFor compatibility with older systems and applications, OpenMaui also supports the XIM protocol:\n\n\u0060\u0060\u0060csharp\n// XIM event handling pattern\npublic void HandleXIMEvent(XEvent xEvent)\n{\n if (xEvent.type == XEventType.KeyPress)\n {\n // Filter through XIM first\n if (XFilterEvent(ref xEvent, window))\n {\n // Event handled by XIM\n return;\n }\n \n // Handle as normal key event\n HandleKeyPress(xEvent);\n }\n}\n\u0060\u0060\u0060\n\n## Cursor Position Management\n\nIMEs need to know where to display candidate windows. OpenMaui provides cursor position information:\n\n\u0060\u0060\u0060csharp\n// From SkiaEntry - cursor position for IME\npublic Rect GetCursorRect()\n{\n var x = MeasureTextWidth(Text.Substring(0, CursorPosition));\n var y = Padding.Top;\n var height = FontSize * 1.2; // Line height\n \n return new Rect(x, y, 2, height);\n}\n\npublic void UpdateIMECursorPosition()\n{\n if (ibusContext != null)\n {\n var rect = GetCursorRect();\n // Convert to screen coordinates\n var screenRect = TransformToScreen(rect);\n ibusContext.SetCursorLocation(\n (int)screenRect.X,\n (int)screenRect.Y,\n (int)screenRect.Width,\n (int)screenRect.Height\n );\n }\n}\n\u0060\u0060\u0060\n\n## Testing IME Integration\n\nTesting IME functionality requires actual input method frameworks:\n\n\u0060\u0060\u0060bash\n# Install IBus and Chinese input\nsudo apt install ibus ibus-pinyin\n\n# Start IBus daemon\nibus-daemon -drx\n\n# Set environment variables\nexport GTK_IM_MODULE=ibus\nexport QT_IM_MODULE=ibus\nexport XMODIFIERS=@im=ibus\n\u0060\u0060\u0060\n\nIn your application:\n\n\u0060\u0060\u0060csharp\n// Test case for IME composition\n[Fact]\npublic void Entry_HandlesIMEComposition()\n{\n var entry = new SkiaEntry();\n \n // Simulate IME composition\n entry.HandleTextInput(new TextInputEventArgs\n {\n IsComposing = true,\n CompositionText = \u0022\u4F60\u597D\u0022,\n CompositionCursorPos = 2\n });\n \n Assert.Equal(\u0022\u4F60\u597D\u0022, entry.PreeditText);\n Assert.Equal(\u0022\u0022, entry.Text); // Not committed yet\n \n // Simulate commit\n entry.HandleTextInput(new TextInputEventArgs\n {\n IsComposing = false,\n Text = \u0022\u4F60\u597D\u0022\n });\n \n Assert.Equal(\u0022\u4F60\u597D\u0022, entry.Text);\n Assert.Equal(\u0022\u0022, entry.PreeditText);\n}\n\u0060\u0060\u0060\n\n## Fcitx5 Compatibility\n\nWhile IBus is most common, Fcitx5 users expect the same level of support. OpenMaui\u0027s abstraction layer handles both:\n\n\u0060\u0060\u0060csharp\npublic interface IInputMethodContext\n{\n void SetCursorLocation(int x, int y, int width, int height);\n void FocusIn();\n void FocusOut();\n void Reset();\n}\n\n// IBus implementation\npublic class IBusInputContext : IInputMethodContext { ... }\n\n// Fcitx5 implementation\npublic class Fcitx5InputContext : IInputMethodContext { ... }\n\u0060\u0060\u0060\n\nThe application detects which input method framework is active and uses the appropriate implementation." + }, + { + "header": "Font Fallback Manager Architecture", + "content": "Font fallback is the process of automatically switching to a different font when the current font doesn\u0027t contain a required glyph. Without it, users see empty boxes (\u25A1) or question marks (?) instead of their text.\n\n## The Font Fallback Challenge\n\nConsider this string: \u0022Hello \u4E16\u754C \uD83C\uDF0D\u0022\n\n- \u0022Hello\u0022 might render with Roboto\n- \u0022\u4E16\u754C\u0022 (Chinese) needs a CJK font like Noto Sans CJK\n- \u0022\uD83C\uDF0D\u0022 (emoji) requires a color emoji font like Noto Color Emoji\n\nNo single font contains all these glyphs. The system must detect missing glyphs and switch fonts **per character** or **per run** (consecutive characters from the same script).\n\n## SkiaLabel\u0027s Font Fallback System\n\nThe \u0060SkiaLabel\u0060 control implements intelligent font fallback:\n\n\u0060\u0060\u0060csharp\n// From SkiaLabel - font fallback implementation\npublic class FontFallbackManager\n{\n private readonly List\u003CSKTypeface\u003E _fallbackChain;\n \n public FontFallbackManager()\n {\n _fallbackChain = new List\u003CSKTypeface\u003E\n {\n // Primary font (user\u0027s choice)\n GetPrimaryFont(),\n \n // CJK fonts\n SKTypeface.FromFamilyName(\u0022Noto Sans CJK SC\u0022),\n SKTypeface.FromFamilyName(\u0022Noto Sans CJK JP\u0022),\n SKTypeface.FromFamilyName(\u0022WenQuanYi Micro Hei\u0022),\n \n // Emoji fonts\n SKTypeface.FromFamilyName(\u0022Noto Color Emoji\u0022),\n SKTypeface.FromFamilyName(\u0022Segoe UI Emoji\u0022),\n \n // Fallback for other scripts\n SKTypeface.FromFamilyName(\u0022Noto Sans\u0022),\n SKTypeface.FromFamilyName(\u0022DejaVu Sans\u0022)\n };\n }\n \n public SKTypeface GetTypefaceForCharacter(char c, SKTypeface primary)\n {\n // Check if primary font has this glyph\n if (HasGlyph(primary, c))\n return primary;\n \n // Try fallback fonts\n foreach (var fallback in _fallbackChain)\n {\n if (fallback != null \u0026\u0026 HasGlyph(fallback, c))\n return fallback;\n }\n \n // Last resort: return primary (will show \u25A1)\n return primary;\n }\n \n private bool HasGlyph(SKTypeface typeface, char c)\n {\n var glyphs = new ushort[1];\n var text = c.ToString();\n return typeface.GetGlyphs(text, out glyphs) \u003E 0 \u0026\u0026 glyphs[0] != 0;\n }\n}\n\u0060\u0060\u0060\n\n## Run-Based Rendering\n\nFor efficiency, OpenMaui groups consecutive characters that use the same font into **text runs**:\n\n\u0060\u0060\u0060csharp\n// Text run detection\npublic class TextRun\n{\n public string Text { get; set; }\n public SKTypeface Typeface { get; set; }\n public int StartIndex { get; set; }\n public int Length { get; set; }\n}\n\npublic List\u003CTextRun\u003E SplitIntoRuns(string text, SKTypeface primaryFont)\n{\n var runs = new List\u003CTextRun\u003E();\n var currentRun = new StringBuilder();\n var currentTypeface = primaryFont;\n var startIndex = 0;\n \n for (int i = 0; i \u003C text.Length; i\u002B\u002B)\n {\n var c = text[i];\n var typeface = _fallbackManager.GetTypefaceForCharacter(c, primaryFont);\n \n if (typeface != currentTypeface \u0026\u0026 currentRun.Length \u003E 0)\n {\n // Start new run\n runs.Add(new TextRun\n {\n Text = currentRun.ToString(),\n Typeface = currentTypeface,\n StartIndex = startIndex,\n Length = currentRun.Length\n });\n \n currentRun.Clear();\n startIndex = i;\n currentTypeface = typeface;\n }\n \n currentRun.Append(c);\n }\n \n // Add final run\n if (currentRun.Length \u003E 0)\n {\n runs.Add(new TextRun\n {\n Text = currentRun.ToString(),\n Typeface = currentTypeface,\n StartIndex = startIndex,\n Length = currentRun.Length\n });\n }\n \n return runs;\n}\n\u0060\u0060\u0060\n\n## Drawing with Font Fallback\n\nThe rendering code uses text runs:\n\n\u0060\u0060\u0060csharp\n// From SkiaLabel - rendering with fallback\nprotected override void OnDraw(SKCanvas canvas)\n{\n var runs = SplitIntoRuns(Text, GetPrimaryTypeface());\n float x = (float)Padding.Left;\n float y = (float)(Padding.Top \u002B FontSize);\n \n foreach (var run in runs)\n {\n using var paint = new SKPaint\n {\n Color = TextColor.ToSKColor(),\n TextSize = (float)FontSize,\n Typeface = run.Typeface,\n IsAntialias = true,\n SubpixelText = true\n };\n \n canvas.DrawText(run.Text, x, y, paint);\n x \u002B= paint.MeasureText(run.Text);\n }\n}\n\u0060\u0060\u0060\n\n## Font Discovery on Linux\n\nOpenMaui uses Fontconfig to discover available fonts:\n\n\u0060\u0060\u0060csharp\npublic class LinuxFontDiscovery\n{\n [DllImport(\u0022libfontconfig.so.1\u0022)]\n private static extern IntPtr FcConfigGetCurrent();\n \n [DllImport(\u0022libfontconfig.so.1\u0022)]\n private static extern IntPtr FcPatternCreate();\n \n public List\u003Cstring\u003E GetAvailableFonts()\n {\n var fonts = new List\u003Cstring\u003E();\n var config = FcConfigGetCurrent();\n \n // Query fontconfig for available fonts\n // Implementation details...\n \n return fonts;\n }\n \n public string FindBestFontForScript(UnicodeScript script)\n {\n return script switch\n {\n UnicodeScript.Han =\u003E FindFont(\u0022Noto Sans CJK SC\u0022, \u0022WenQuanYi\u0022),\n UnicodeScript.Hiragana =\u003E FindFont(\u0022Noto Sans CJK JP\u0022),\n UnicodeScript.Arabic =\u003E FindFont(\u0022Noto Sans Arabic\u0022),\n UnicodeScript.Devanagari =\u003E FindFont(\u0022Noto Sans Devanagari\u0022),\n _ =\u003E \u0022Noto Sans\u0022\n };\n }\n}\n\u0060\u0060\u0060\n\n## Emoji Font Handling\n\nEmoji fonts require special handling because they\u0027re often color fonts (CBDT/COLR tables):\n\n\u0060\u0060\u0060csharp\npublic SKTypeface GetEmojiTypeface()\n{\n // Try color emoji fonts first\n var colorEmoji = SKTypeface.FromFamilyName(\u0022Noto Color Emoji\u0022);\n if (colorEmoji != null)\n return colorEmoji;\n \n // Fallback to monochrome emoji\n return SKTypeface.FromFamilyName(\u0022Noto Emoji\u0022);\n}\n\npublic bool IsEmoji(char c)\n{\n // Basic emoji detection\n return (c \u003E= 0x1F300 \u0026\u0026 c \u003C= 0x1F9FF) || // Misc Symbols and Pictographs\n (c \u003E= 0x2600 \u0026\u0026 c \u003C= 0x26FF) || // Misc Symbols\n (c \u003E= 0x2700 \u0026\u0026 c \u003C= 0x27BF); // Dingbats\n}\n\u0060\u0060\u0060\n\n## Performance Optimization\n\nFont fallback can be expensive. OpenMaui caches font lookup results:\n\n\u0060\u0060\u0060csharp\nprivate readonly Dictionary\u003Cchar, SKTypeface\u003E _glyphCache = new();\n\npublic SKTypeface GetTypefaceForCharacterCached(char c, SKTypeface primary)\n{\n if (_glyphCache.TryGetValue(c, out var cached))\n return cached;\n \n var typeface = GetTypefaceForCharacter(c, primary);\n _glyphCache[c] = typeface;\n return typeface;\n}\n\u0060\u0060\u0060\n\nThis reduces font lookups from O(n\u00D7m) to O(n) where n is text length and m is fallback chain length." + }, + { + "header": "HarfBuzz for Complex Script Shaping", + "content": "HarfBuzz is the industry-standard text shaping engine used by Chrome, Firefox, Android, and most modern text rendering systems. It transforms Unicode text into properly positioned glyphs, handling the complex rules of different writing systems.\n\n## Why Text Shaping Matters\n\nConsider Arabic text \u0022\u0645\u0631\u062D\u0628\u0627\u0022 (hello). The same letter has different forms depending on its position:\n\n- **Isolated form**: \u0645\n- **Initial form**: \u0645\u0640 (when starting a word)\n- **Medial form**: \u0640\u0645\u0640 (in the middle)\n- **Final form**: \u0640\u0645 (at the end)\n\nHarfBuzz applies these contextual substitutions automatically. Without shaping, Arabic text looks disconnected and wrong.\n\n## HarfBuzz Integration\n\nOpenMaui uses HarfBuzzSharp 7.3.0.3 for text shaping:\n\n\u0060\u0060\u0060csharp\nusing HarfBuzzSharp;\n\npublic class TextShaper\n{\n public ShapedGlyphs ShapeText(string text, SKTypeface typeface, float fontSize)\n {\n // Create HarfBuzz font from SKTypeface\n using var blob = typeface.OpenStream(out var ttcIndex).AsHarfBuzzBlob();\n using var face = new Face(blob, ttcIndex);\n using var font = new Font(face);\n font.SetScale((int)fontSize, (int)fontSize);\n \n // Create buffer with text\n using var buffer = new HarfBuzzSharp.Buffer();\n buffer.AddUtf16(text);\n buffer.GuessSegmentProperties();\n \n // Shape the text\n font.Shape(buffer);\n \n // Extract glyph information\n var glyphInfos = buffer.GlyphInfos;\n var glyphPositions = buffer.GlyphPositions;\n \n var result = new ShapedGlyphs();\n float xPos = 0;\n \n for (int i = 0; i \u003C glyphInfos.Length; i\u002B\u002B)\n {\n var info = glyphInfos[i];\n var pos = glyphPositions[i];\n \n result.Glyphs.Add(new ShapedGlyph\n {\n GlyphId = info.Codepoint,\n Cluster = info.Cluster,\n XOffset = xPos \u002B pos.XOffset / 64f,\n YOffset = pos.YOffset / 64f,\n XAdvance = pos.XAdvance / 64f,\n YAdvance = pos.YAdvance / 64f\n });\n \n xPos \u002B= pos.XAdvance / 64f;\n }\n \n return result;\n }\n}\n\npublic class ShapedGlyphs\n{\n public List\u003CShapedGlyph\u003E Glyphs { get; set; } = new();\n}\n\npublic class ShapedGlyph\n{\n public uint GlyphId { get; set; }\n public uint Cluster { get; set; }\n public float XOffset { get; set; }\n public float YOffset { get; set; }\n public float XAdvance { get; set; }\n public float YAdvance { get; set; }\n}\n\u0060\u0060\u0060\n\n## Rendering Shaped Text\n\nOnce text is shaped, we render it using SkiaSharp:\n\n\u0060\u0060\u0060csharp\npublic void DrawShapedText(SKCanvas canvas, ShapedGlyphs shaped, \n SKTypeface typeface, float fontSize, \n SKColor color, float x, float y)\n{\n using var paint = new SKPaint\n {\n Typeface = typeface,\n TextSize = fontSize,\n Color = color,\n IsAntialias = true,\n SubpixelText = true\n };\n \n // Convert glyphs to ushort array\n var glyphIds = shaped.Glyphs.Select(g =\u003E (ushort)g.GlyphId).ToArray();\n var positions = shaped.Glyphs.Select(g =\u003E \n new SKPoint(x \u002B g.XOffset, y \u002B g.YOffset)\n ).ToArray();\n \n // Draw positioned glyphs\n canvas.DrawPositionedText(glyphIds, positions, paint);\n}\n\u0060\u0060\u0060\n\n## Script Detection\n\nHarfBuzz needs to know the script (writing system) to apply correct shaping rules:\n\n\u0060\u0060\u0060csharp\npublic class ScriptDetector\n{\n public static Script DetectScript(string text)\n {\n if (string.IsNullOrEmpty(text))\n return Script.Latin;\n \n var firstChar = text[0];\n \n // Arabic\n if (firstChar \u003E= 0x0600 \u0026\u0026 firstChar \u003C= 0x06FF)\n return Script.Arabic;\n \n // Hebrew\n if (firstChar \u003E= 0x0590 \u0026\u0026 firstChar \u003C= 0x05FF)\n return Script.Hebrew;\n \n // Devanagari\n if (firstChar \u003E= 0x0900 \u0026\u0026 firstChar \u003C= 0x097F)\n return Script.Devanagari;\n \n // Thai\n if (firstChar \u003E= 0x0E00 \u0026\u0026 firstChar \u003C= 0x0E7F)\n return Script.Thai;\n \n // CJK\n if (firstChar \u003E= 0x4E00 \u0026\u0026 firstChar \u003C= 0x9FFF)\n return Script.Han;\n \n return Script.Latin;\n }\n}\n\npublic enum Script\n{\n Latin,\n Arabic,\n Hebrew,\n Devanagari,\n Thai,\n Han,\n Hiragana,\n Katakana\n}\n\u0060\u0060\u0060\n\n## Direction Detection (BiDi)\n\nBidirectional text (mixing left-to-right and right-to-left) requires special handling:\n\n\u0060\u0060\u0060csharp\npublic class BiDiAnalyzer\n{\n public Direction GetTextDirection(string text)\n {\n // Check for RTL characters\n foreach (var c in text)\n {\n if (IsRTL(c))\n return Direction.RightToLeft;\n }\n return Direction.LeftToRight;\n }\n \n private bool IsRTL(char c)\n {\n return (c \u003E= 0x0590 \u0026\u0026 c \u003C= 0x05FF) || // Hebrew\n (c \u003E= 0x0600 \u0026\u0026 c \u003C= 0x06FF) || // Arabic\n (c \u003E= 0x0700 \u0026\u0026 c \u003C= 0x074F); // Syriac\n }\n}\n\npublic enum Direction\n{\n LeftToRight,\n RightToLeft\n}\n\u0060\u0060\u0060\n\n## Ligatures and Contextual Forms\n\nHarfBuzz automatically applies ligatures (like \u0022fi\u0022 \u2192 \u0022\uFB01\u0022) and contextual forms:\n\n\u0060\u0060\u0060csharp\n// Enable/disable OpenType features\npublic ShapedGlyphs ShapeWithFeatures(string text, SKTypeface typeface, \n float fontSize, Feature[] features)\n{\n using var blob = typeface.OpenStream(out var ttcIndex).AsHarfBuzzBlob();\n using var face = new Face(blob, ttcIndex);\n using var font = new Font(face);\n font.SetScale((int)fontSize, (int)fontSize);\n \n using var buffer = new HarfBuzzSharp.Buffer();\n buffer.AddUtf16(text);\n buffer.GuessSegmentProperties();\n \n // Apply features\n font.Shape(buffer, features);\n \n return ExtractGlyphs(buffer);\n}\n\n// Example: Disable ligatures\nvar features = new[]\n{\n new Feature(Tag.Parse(\u0022liga\u0022), 0, 0, uint.MaxValue)\n};\nvar shaped = ShapeWithFeatures(\u0022difficult\u0022, typeface, 16, features);\n\u0060\u0060\u0060\n\n## Emoji Sequences\n\nEmoji sequences (like \uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66) use zero-width joiners. HarfBuzz handles these correctly:\n\n\u0060\u0060\u0060csharp\npublic bool IsEmojiSequence(string text, int index)\n{\n if (index \u002B 1 \u003E= text.Length)\n return false;\n \n // Check for zero-width joiner\n return text[index \u002B 1] == \u0027\\u200D\u0027;\n}\n\n// HarfBuzz automatically shapes emoji sequences\nvar family = \u0022\uD83D\uDC68\\u200D\uD83D\uDC69\\u200D\uD83D\uDC67\\u200D\uD83D\uDC66\u0022;\nvar shaped = ShapeText(family, emojiTypeface, 24);\n// Results in a single glyph for the family emoji\n\u0060\u0060\u0060\n\n## Performance Considerations\n\nText shaping is computationally expensive. OpenMaui caches shaped results:\n\n\u0060\u0060\u0060csharp\npublic class ShapingCache\n{\n private readonly Dictionary\u003CShapingKey, ShapedGlyphs\u003E _cache = new();\n \n public ShapedGlyphs GetOrShape(string text, SKTypeface typeface, float fontSize)\n {\n var key = new ShapingKey(text, typeface, fontSize);\n \n if (_cache.TryGetValue(key, out var cached))\n return cached;\n \n var shaped = _shaper.ShapeText(text, typeface, fontSize);\n _cache[key] = shaped;\n return shaped;\n }\n}\n\nrecord ShapingKey(string Text, SKTypeface Typeface, float FontSize);\n\u0060\u0060\u0060\n\nThis dramatically improves performance for repeated text rendering, especially in lists and scrolling views." + }, + { + "header": "Emoji and Unicode Support", + "content": "Emoji support is more complex than it appears. Modern emoji use multiple Unicode codepoints, skin tone modifiers, and zero-width joiners to create composite glyphs.\n\n## Unicode Emoji Fundamentals\n\nEmoji come in several forms:\n\n**Simple Emoji**: Single codepoint\n\u0060\u0060\u0060csharp\nvar heart = \u0022\u2764\uFE0F\u0022; // U\u002B2764 \u002B U\u002BFE0F (variation selector)\nvar smile = \u0022\uD83D\uDE00\u0022; // U\u002B1F600\n\u0060\u0060\u0060\n\n**Emoji with Skin Tone Modifiers**:\n\u0060\u0060\u0060csharp\nvar wave = \u0022\uD83D\uDC4B\u0022; // U\u002B1F44B\nvar waveDark = \u0022\uD83D\uDC4B\uD83C\uDFFF\u0022; // U\u002B1F44B \u002B U\u002B1F3FF (dark skin tone)\n\u0060\u0060\u0060\n\n**Zero-Width Joiner (ZWJ) Sequences**:\n\u0060\u0060\u0060csharp\nvar family = \u0022\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66\u0022; // Man \u002B ZWJ \u002B Woman \u002B ZWJ \u002B Girl \u002B ZWJ \u002B Boy\nvar femaleDoctor = \u0022\uD83D\uDC69\u200D\u2695\uFE0F\u0022; // Woman \u002B ZWJ \u002B Medical Symbol\n\u0060\u0060\u0060\n\n## Emoji Detection\n\nOpenMaui implements comprehensive emoji detection:\n\n\u0060\u0060\u0060csharp\npublic class EmojiDetector\n{\n public bool IsEmoji(int codepoint)\n {\n return IsInEmojiRange(codepoint) || \n IsEmojiModifier(codepoint) || \n IsEmojiComponent(codepoint);\n }\n \n private bool IsInEmojiRange(int cp)\n {\n return (cp \u003E= 0x1F600 \u0026\u0026 cp \u003C= 0x1F64F) || // Emoticons\n (cp \u003E= 0x1F300 \u0026\u0026 cp \u003C= 0x1F5FF) || // Misc Symbols and Pictographs\n (cp \u003E= 0x1F680 \u0026\u0026 cp \u003C= 0x1F6FF) || // Transport and Map\n (cp \u003E= 0x1F900 \u0026\u0026 cp \u003C= 0x1F9FF) || // Supplemental Symbols\n (cp \u003E= 0x2600 \u0026\u0026 cp \u003C= 0x26FF) || // Misc symbols\n (cp \u003E= 0x2700 \u0026\u0026 cp \u003C= 0x27BF) || // Dingbats\n (cp \u003E= 0xFE00 \u0026\u0026 cp \u003C= 0xFE0F); // Variation Selectors\n }\n \n private bool IsEmojiModifier(int cp)\n {\n return cp \u003E= 0x1F3FB \u0026\u0026 cp \u003C= 0x1F3FF; // Skin tone modifiers\n }\n \n private bool IsEmojiComponent(int cp)\n {\n return cp == 0x200D || // Zero-width joiner\n cp == 0xFE0F; // Variation Selector-16\n }\n \n public List\u003CEmojiSequence\u003E FindEmojis(string text)\n {\n var emojis = new List\u003CEmojiSequence\u003E();\n int i = 0;\n \n while (i \u003C text.Length)\n {\n if (char.IsHighSurrogate(text[i]))\n {\n var codepoint = char.ConvertToUtf32(text, i);\n if (IsEmoji(codepoint))\n {\n var sequence = ExtractEmojiSequence(text, i);\n emojis.Add(sequence);\n i = sequence.EndIndex;\n continue;\n }\n i \u002B= 2; // Surrogate pair\n }\n else\n {\n i\u002B\u002B;\n }\n }\n \n return emojis;\n }\n \n private EmojiSequence ExtractEmojiSequence(string text, int startIndex)\n {\n int endIndex = startIndex;\n var sequence = new StringBuilder();\n \n // Extract emoji and any following modifiers/joiners\n while (endIndex \u003C text.Length)\n {\n var c = text[endIndex];\n \n if (char.IsHighSurrogate(c) \u0026\u0026 endIndex \u002B 1 \u003C text.Length)\n {\n var cp = char.ConvertToUtf32(text, endIndex);\n if (IsEmoji(cp) || IsEmojiModifier(cp))\n {\n sequence.Append(c);\n sequence.Append(text[endIndex \u002B 1]);\n endIndex \u002B= 2;\n continue;\n }\n }\n \n if (IsEmojiComponent(c))\n {\n sequence.Append(c);\n endIndex\u002B\u002B;\n continue;\n }\n \n break;\n }\n \n return new EmojiSequence\n {\n Text = sequence.ToString(),\n StartIndex = startIndex,\n EndIndex = endIndex\n };\n }\n}\n\npublic class EmojiSequence\n{\n public string Text { get; set; }\n public int StartIndex { get; set; }\n public int EndIndex { get; set; }\n}\n\u0060\u0060\u0060\n\n## Color Emoji Rendering\n\nColor emoji fonts use special tables (CBDT, COLR, SVG):\n\n\u0060\u0060\u0060csharp\npublic class EmojiRenderer\n{\n private SKTypeface _colorEmojiFont;\n \n public EmojiRenderer()\n {\n // Load color emoji font\n _colorEmojiFont = SKTypeface.FromFamilyName(\u0022Noto Color Emoji\u0022) ??\n SKTypeface.FromFamilyName(\u0022Apple Color Emoji\u0022) ??\n SKTypeface.FromFamilyName(\u0022Segoe UI Emoji\u0022);\n }\n \n public void DrawEmoji(SKCanvas canvas, string emoji, float x, float y, float size)\n {\n using var paint = new SKPaint\n {\n Typeface = _colorEmojiFont,\n TextSize = size,\n IsAntialias = true,\n SubpixelText = true\n };\n \n // Color emoji fonts handle color internally\n canvas.DrawText(emoji, x, y, paint);\n }\n}\n\u0060\u0060\u0060\n\n## Emoji Picker Integration\n\nOpenMaui\u0027s text controls can integrate with the system emoji picker:\n\n\u0060\u0060\u0060csharp\n// From SkiaEntry - emoji picker support\npublic void ShowEmojiPicker()\n{\n // On Linux, use IBus emoji picker\n if (HasIBus())\n {\n // Ctrl\u002B. or Ctrl\u002B; triggers IBus emoji picker\n SimulateKeyPress(Key.Period, ModifierKeys.Control);\n }\n else\n {\n // Show custom emoji picker\n var picker = new EmojiPickerDialog();\n picker.EmojiSelected \u002B= (s, emoji) =\u003E\n {\n InsertText(emoji);\n };\n picker.Show();\n }\n}\n\u0060\u0060\u0060\n\n## Emoji Text Measurement\n\nEmoji sequences must be measured as a single unit:\n\n\u0060\u0060\u0060csharp\npublic float MeasureText(string text)\n{\n var detector = new EmojiDetector();\n var emojis = detector.FindEmojis(text);\n \n float totalWidth = 0;\n int lastIndex = 0;\n \n foreach (var emoji in emojis)\n {\n // Measure text before emoji\n if (emoji.StartIndex \u003E lastIndex)\n {\n var beforeText = text.Substring(lastIndex, emoji.StartIndex - lastIndex);\n totalWidth \u002B= MeasureTextSegment(beforeText, _regularFont);\n }\n \n // Measure emoji\n totalWidth \u002B= MeasureTextSegment(emoji.Text, _emojiFont);\n lastIndex = emoji.EndIndex;\n }\n \n // Measure remaining text\n if (lastIndex \u003C text.Length)\n {\n var remainingText = text.Substring(lastIndex);\n totalWidth \u002B= MeasureTextSegment(remainingText, _regularFont);\n }\n \n return totalWidth;\n}\n\nprivate float MeasureTextSegment(string text, SKTypeface font)\n{\n using var paint = new SKPaint\n {\n Typeface = font,\n TextSize = (float)FontSize\n };\n return paint.MeasureText(text);\n}\n\u0060\u0060\u0060\n\n## Unicode Normalization\n\nDifferent Unicode representations can look identical but have different codepoints:\n\n\u0060\u0060\u0060csharp\npublic class UnicodeNormalizer\n{\n public string Normalize(string text)\n {\n // Use NFC (Canonical Decomposition, followed by Canonical Composition)\n return text.Normalize(NormalizationForm.FormC);\n }\n \n public bool AreEquivalent(string text1, string text2)\n {\n return Normalize(text1) == Normalize(text2);\n }\n}\n\n// Example:\nvar e1 = \u0022\u00E9\u0022; // Single codepoint U\u002B00E9\nvar e2 = \u0022\u00E9\u0022; // e (U\u002B0065) \u002B combining acute accent (U\u002B0301)\n// These look identical but text1.Length != text2.Length\n// After normalization, they\u0027re equal\n\u0060\u0060\u0060\n\n## Grapheme Cluster Boundaries\n\nA grapheme cluster is what users perceive as a single character:\n\n\u0060\u0060\u0060csharp\npublic class GraphemeClusterIterator\n{\n public List\u003Cstring\u003E GetGraphemeClusters(string text)\n {\n var clusters = new List\u003Cstring\u003E();\n var enumerator = StringInfo.GetTextElementEnumerator(text);\n \n while (enumerator.MoveNext())\n {\n clusters.Add(enumerator.GetTextElement());\n }\n \n return clusters;\n }\n}\n\n// Example:\nvar text = \u0022\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66Hello\u0022;\nvar clusters = iterator.GetGraphemeClusters(text);\n// clusters[0] = \u0022\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66\u0022 (one grapheme cluster)\n// clusters[1] = \u0022H\u0022\n// clusters[2] = \u0022e\u0022\n// etc.\n\u0060\u0060\u0060\n\nThis is crucial for cursor movement, text selection, and backspace behavior in text editors." + }, + { + "header": "CJK Language Input", + "content": "Chinese, Japanese, and Korean (CJK) languages present unique challenges for text input. With thousands of characters and limited keyboard keys, these languages rely heavily on Input Method Editors (IMEs).\n\n## CJK Input Methods\n\n**Chinese Input Methods:**\n- **Pinyin**: Phonetic input (\u0022nihao\u0022 \u2192 \u4F60\u597D)\n- **Wubi**: Shape-based input for faster typing\n- **Cangjie**: Traditional Chinese input method\n- **Handwriting**: Touch/stylus input\n\n**Japanese Input Methods:**\n- **Romaji**: Type \u0022konnichiwa\u0022 \u2192 \u3053\u3093\u306B\u3061\u306F \u2192 \u4ECA\u65E5\u306F\n- **Kana**: Direct hiragana/katakana input\n- **Flick input**: Mobile touch input\n\n**Korean Input Methods:**\n- **Hangul**: Automatic jamo composition (\u3131 \u002B \u314F \u002B \u3147 \u2192 \uAC15)\n- **2-Set/3-Set**: Different keyboard layouts\n\n## SkiaEntry CJK Support\n\nThe \u0060SkiaEntry\u0060 control provides full CJK input support:\n\n\u0060\u0060\u0060csharp\n// From SkiaEntry - CJK composition handling\npublic class CJKInputHandler\n{\n private string _compositionText = string.Empty;\n private List\u003Cstring\u003E _candidates = new();\n private int _selectedCandidate = 0;\n \n public void HandleCompositionUpdate(string composition, string[] candidates)\n {\n _compositionText = composition;\n _candidates = candidates?.ToList() ?? new();\n _selectedCandidate = 0;\n \n // Show candidate window\n ShowCandidateWindow();\n }\n \n public void HandleCandidateSelection(int index)\n {\n if (index \u003E= 0 \u0026\u0026 index \u003C _candidates.Count)\n {\n _selectedCandidate = index;\n CommitText(_candidates[index]);\n ClearComposition();\n }\n }\n \n private void ShowCandidateWindow()\n {\n if (_candidates.Count == 0)\n return;\n \n var candidateWindow = new CandidateWindow\n {\n Candidates = _candidates,\n SelectedIndex = _selectedCandidate,\n Position = GetCursorScreenPosition()\n };\n \n candidateWindow.Show();\n }\n}\n\u0060\u0060\u0060\n\n## Candidate Window Rendering\n\nCJK input requires displaying candidate characters:\n\n\u0060\u0060\u0060csharp\npublic class CandidateWindow : SkiaView\n{\n public List\u003Cstring\u003E Candidates { get; set; }\n public int SelectedIndex { get; set; }\n \n protected override void OnDraw(SKCanvas canvas)\n {\n // Draw background\n using var bgPaint = new SKPaint\n {\n Color = SKColors.White,\n Style = SKPaintStyle.Fill\n };\n canvas.DrawRoundRect(Bounds.ToSKRect(), 4, 4, bgPaint);\n \n // Draw border\n using var borderPaint = new SKPaint\n {\n Color = SKColors.Gray,\n Style = SKPaintStyle.Stroke,\n StrokeWidth = 1\n };\n canvas.DrawRoundRect(Bounds.ToSKRect(), 4, 4, borderPaint);\n \n // Draw candidates\n float y = 10;\n for (int i = 0; i \u003C Candidates.Count; i\u002B\u002B)\n {\n var isSelected = i == SelectedIndex;\n \n // Highlight selected candidate\n if (isSelected)\n {\n using var highlightPaint = new SKPaint\n {\n Color = new SKColor(200, 220, 255),\n Style = SKPaintStyle.Fill\n };\n var highlightRect = new SKRect(5, y - 2, Width - 5, y \u002B 22);\n canvas.DrawRoundRect(highlightRect, 2, 2, highlightPaint);\n }\n \n // Draw candidate number\n using var numberPaint = new SKPaint\n {\n Color = SKColors.Gray,\n TextSize = 14,\n IsAntialias = true\n };\n canvas.DrawText($\u0022{i \u002B 1}.\u0022, 10, y \u002B 16, numberPaint);\n \n // Draw candidate text\n using var textPaint = new SKPaint\n {\n Color = isSelected ? SKColors.Black : SKColors.DarkGray,\n TextSize = 16,\n IsAntialias = true,\n Typeface = GetCJKTypeface()\n };\n canvas.DrawText(Candidates[i], 35, y \u002B 16, textPaint);\n \n y \u002B= 25;\n }\n }\n \n private SKTypeface GetCJKTypeface()\n {\n return SKTypeface.FromFamilyName(\u0022Noto Sans CJK SC\u0022) ??\n SKTypeface.FromFamilyName(\u0022WenQuanYi Micro Hei\u0022) ??\n SKTypeface.FromFamilyName(\u0022Noto Sans\u0022);\n }\n}\n\u0060\u0060\u0060\n\n## Japanese Kana Conversion\n\nJapanese input requires converting between romaji, hiragana, and kanji:\n\n\u0060\u0060\u0060csharp\npublic class JapaneseInputConverter\n{\n private readonly Dictionary\u003Cstring, string\u003E _romajiToHiragana = new()\n {\n { \u0022a\u0022, \u0022\u3042\u0022 }, { \u0022i\u0022, \u0022\u3044\u0022 }, { \u0022u\u0022, \u0022\u3046\u0022 }, { \u0022e\u0022, \u0022\u3048\u0022 }, { \u0022o\u0022, \u0022\u304A\u0022 },\n { \u0022ka\u0022, \u0022\u304B\u0022 }, { \u0022ki\u0022, \u0022\u304D\u0022 }, { \u0022ku\u0022, \u0022\u304F\u0022 }, { \u0022ke\u0022, \u0022\u3051\u0022 }, { \u0022ko\u0022, \u0022\u3053\u0022 },\n { \u0022sa\u0022, \u0022\u3055\u0022 }, { \u0022shi\u0022, \u0022\u3057\u0022 }, { \u0022su\u0022, \u0022\u3059\u0022 }, { \u0022se\u0022, \u0022\u305B\u0022 }, { \u0022so\u0022, \u0022\u305D\u0022 },\n { \u0022ta\u0022, \u0022\u305F\u0022 }, { \u0022chi\u0022, \u0022\u3061\u0022 }, { \u0022tsu\u0022, \u0022\u3064\u0022 }, { \u0022te\u0022, \u0022\u3066\u0022 }, { \u0022to\u0022, \u0022\u3068\u0022 },\n { \u0022na\u0022, \u0022\u306A\u0022 }, { \u0022ni\u0022, \u0022\u306B\u0022 }, { \u0022nu\u0022, \u0022\u306C\u0022 }, { \u0022ne\u0022, \u0022\u306D\u0022 }, { \u0022no\u0022, \u0022\u306E\u0022 },\n // ... more mappings\n };\n \n public string ConvertRomajiToHiragana(string romaji)\n {\n var result = new StringBuilder();\n var buffer = new StringBuilder();\n \n foreach (var c in romaji)\n {\n buffer.Append(c);\n \n // Try to match longest possible sequence\n var matched = false;\n for (int len = buffer.Length; len \u003E 0; len--)\n {\n var substring = buffer.ToString().Substring(buffer.Length - len);\n if (_romajiToHiragana.TryGetValue(substring, out var hiragana))\n {\n // Keep unmatched prefix\n if (buffer.Length \u003E len)\n {\n result.Append(buffer.ToString().Substring(0, buffer.Length - len));\n }\n result.Append(hiragana);\n buffer.Clear();\n matched = true;\n break;\n }\n }\n }\n \n // Append remaining buffer\n result.Append(buffer);\n return result.ToString();\n }\n}\n\n// Example:\nvar converter = new JapaneseInputConverter();\nvar hiragana = converter.ConvertRomajiToHiragana(\u0022konnichiwa\u0022);\n// Result: \u0022\u3053\u3093\u306B\u3061\u308F\u0022\n\u0060\u0060\u0060\n\n## Korean Hangul Composition\n\nKorean uses jamo (consonants and vowels) that combine into syllables:\n\n\u0060\u0060\u0060csharp\npublic class HangulComposer\n{\n private const int HangulBase = 0xAC00;\n private const int InitialCount = 19;\n private const int MedialCount = 21;\n private const int FinalCount = 28;\n \n public string ComposeHangul(char initial, char medial, char final = \u0027\\0\u0027)\n {\n // Convert jamo to indices\n int initialIndex = GetInitialIndex(initial);\n int medialIndex = GetMedialIndex(medial);\n int finalIndex = final == \u0027\\0\u0027 ? 0 : GetFinalIndex(final) \u002B 1;\n \n if (initialIndex == -1 || medialIndex == -1 || finalIndex == -1)\n return string.Empty;\n \n // Calculate syllable codepoint\n int syllable = HangulBase \u002B \n (initialIndex * MedialCount * FinalCount) \u002B\n (medialIndex * FinalCount) \u002B\n finalIndex;\n \n return char.ConvertFromUtf32(syllable);\n }\n \n private int GetInitialIndex(char c)\n {\n // \u3131 \u3132 \u3134 \u3137 \u3138 \u3139 \u3141 \u3142 \u3143 \u3145 \u3146 \u3147 \u3148 \u3149 \u314A \u314B \u314C \u314D \u314E\n return c switch\n {\n \u0027\u3131\u0027 =\u003E 0, \u0027\u3132\u0027 =\u003E 1, \u0027\u3134\u0027 =\u003E 2, \u0027\u3137\u0027 =\u003E 3, \u0027\u3138\u0027 =\u003E 4,\n \u0027\u3139\u0027 =\u003E 5, \u0027\u3141\u0027 =\u003E 6, \u0027\u3142\u0027 =\u003E 7, \u0027\u3143\u0027 =\u003E 8, \u0027\u3145\u0027 =\u003E 9,\n \u0027\u3146\u0027 =\u003E 10, \u0027\u3147\u0027 =\u003E 11, \u0027\u3148\u0027 =\u003E 12, \u0027\u3149\u0027 =\u003E 13, \u0027\u314A\u0027 =\u003E 14,\n \u0027\u314B\u0027 =\u003E 15, \u0027\u314C\u0027 =\u003E 16, \u0027\u314D\u0027 =\u003E 17, \u0027\u314E\u0027 =\u003E 18,\n _ =\u003E -1\n };\n }\n \n private int GetMedialIndex(char c)\n {\n // \u314F \u3150 \u3151 \u3152 \u3153 \u3154 \u3155 \u3156 \u3157 \u3158 \u3159 \u315A \u315B \u315C \u315D \u315E \u315F \u3160 \u3161 \u3162 \u3163\n return c switch\n {\n \u0027\u314F\u0027 =\u003E 0, \u0027\u3150\u0027 =\u003E 1, \u0027\u3151\u0027 =\u003E 2, \u0027\u3152\u0027 =\u003E 3, \u0027\u3153\u0027 =\u003E 4,\n \u0027\u3154\u0027 =\u003E 5, \u0027\u3155\u0027 =\u003E 6, \u0027\u3156\u0027 =\u003E 7, \u0027\u3157\u0027 =\u003E 8, \u0027\u3158\u0027 =\u003E 9,\n \u0027\u3159\u0027 =\u003E 10, \u0027\u315A\u0027 =\u003E 11, \u0027\u315B\u0027 =\u003E 12, \u0027\u315C\u0027 =\u003E 13, \u0027\u315D\u0027 =\u003E 14,\n \u0027\u315E\u0027 =\u003E 15, \u0027\u315F\u0027 =\u003E 16, \u0027\u3160\u0027 =\u003E 17, \u0027\u3161\u0027 =\u003E 18, \u0027\u3162\u0027 =\u003E 19,\n \u0027\u3163\u0027 =\u003E 20,\n _ =\u003E -1\n };\n }\n \n private int GetFinalIndex(char c)\n {\n // (none) \u3131 \u3132 \u3133 \u3134 \u3135 \u3136 \u3137 \u3139 \u313A \u313B \u313C \u313D \u313E \u313F \u3140 \u3141 \u3142 \u3144 \u3145 \u3146 \u3147 \u3148 \u314A \u314B \u314C \u314D \u314E\n return c switch\n {\n \u0027\u3131\u0027 =\u003E 0, \u0027\u3132\u0027 =\u003E 1, \u0027\u3133\u0027 =\u003E 2, \u0027\u3134\u0027 =\u003E 3, \u0027\u3135\u0027 =\u003E 4,\n \u0027\u3136\u0027 =\u003E 5, \u0027\u3137\u0027 =\u003E 6, \u0027\u3139\u0027 =\u003E 7, \u0027\u313A\u0027 =\u003E 8, \u0027\u313B\u0027 =\u003E 9,\n \u0027\u313C\u0027 =\u003E 10, \u0027\u313D\u0027 =\u003E 11, \u0027\u313E\u0027 =\u003E 12, \u0027\u313F\u0027 =\u003E 13, \u0027\u3140\u0027 =\u003E 14,\n \u0027\u3141\u0027 =\u003E 15, \u0027\u3142\u0027 =\u003E 16, \u0027\u3144\u0027 =\u003E 17, \u0027\u3145\u0027 =\u003E 18, \u0027\u3146\u0027 =\u003E 19,\n \u0027\u3147\u0027 =\u003E 20, \u0027\u3148\u0027 =\u003E 21, \u0027\u314A\u0027 =\u003E 22, \u0027\u314B\u0027 =\u003E 23, \u0027\u314C\u0027 =\u003E 24,\n \u0027\u314D\u0027 =\u003E 25, \u0027\u314E\u0027 =\u003E 26,\n _ =\u003E -1\n };\n }\n}\n\n// Example:\nvar composer = new HangulComposer();\nvar syllable = composer.ComposeHangul(\u0027\u3131\u0027, \u0027\u314F\u0027, \u0027\u3147\u0027);\n// Result: \u0022\uAC15\u0022\n\u0060\u0060\u0060\n\n## CJK Font Selection\n\nDifferent CJK languages prefer different glyph variants:\n\n\u0060\u0060\u0060csharp\npublic class CJKFontSelector\n{\n public SKTypeface GetCJKFont(CJKLanguage language)\n {\n return language switch\n {\n CJKLanguage.SimplifiedChinese =\u003E \n SKTypeface.FromFamilyName(\u0022Noto Sans CJK SC\u0022) ??\n SKTypeface.FromFamilyName(\u0022WenQuanYi Micro Hei\u0022),\n \n CJKLanguage.TraditionalChinese =\u003E\n SKTypeface.FromFamilyName(\u0022Noto Sans CJK TC\u0022) ??\n SKTypeface.FromFamilyName(\u0022WenQuanYi Micro Hei\u0022),\n \n CJKLanguage.Japanese =\u003E\n SKTypeface.FromFamilyName(\u0022Noto Sans CJK JP\u0022) ??\n SKTypeface.FromFamilyName(\u0022IPAGothic\u0022),\n \n CJKLanguage.Korean =\u003E\n SKTypeface.FromFamilyName(\u0022Noto Sans CJK KR\u0022) ??\n SKTypeface.FromFamilyName(\u0022Nanum Gothic\u0022),\n \n _ =\u003E SKTypeface.FromFamilyName(\u0022Noto Sans CJK SC\u0022)\n };\n }\n}\n\npublic enum CJKLanguage\n{\n SimplifiedChinese,\n TraditionalChinese,\n Japanese,\n Korean\n}\n\u0060\u0060\u0060\n\n## Vertical Text Support\n\nCJK languages sometimes use vertical text layout:\n\n\u0060\u0060\u0060csharp\npublic void DrawVerticalText(SKCanvas canvas, string text, float x, float y)\n{\n using var paint = new SKPaint\n {\n Typeface = GetCJKFont(CJKLanguage.Japanese),\n TextSize = 16,\n IsAntialias = true\n };\n \n float currentY = y;\n foreach (var c in text)\n {\n canvas.DrawText(c.ToString(), x, currentY, paint);\n currentY \u002B= paint.TextSize * 1.2f; // Line height\n }\n}\n\u0060\u0060\u0060" + }, + { + "header": "Testing International Text Input", + "content": "Testing international text input requires comprehensive test cases covering different scripts, input methods, and edge cases. OpenMaui\u0027s test suite includes 217 passing tests with extensive coverage for text rendering.\n\n## Unit Testing Text Rendering\n\n\u0060\u0060\u0060csharp\nusing Xunit;\nusing FluentAssertions;\nusing Moq;\n\npublic class TextRenderingTests\n{\n [Fact]\n public void Label_RendersChineseText_Correctly()\n {\n // Arrange\n var label = new SkiaLabel\n {\n Text = \u0022\u4F60\u597D\u4E16\u754C\u0022,\n FontSize = 16\n };\n \n // Act\n var measuredSize = label.Measure(double.PositiveInfinity, double.PositiveInfinity);\n \n // Assert\n measuredSize.Width.Should().BeGreaterThan(0);\n measuredSize.Height.Should().BeGreaterThan(0);\n }\n \n [Theory]\n [InlineData(\u0022Hello \u4E16\u754C \uD83C\uDF0D\u0022)] // Mixed Latin, CJK, Emoji\n [InlineData(\u0022\u0645\u0631\u062D\u0628\u0627\u0022)] // Arabic\n [InlineData(\u0022\u05E9\u05DC\u05D5\u05DD\u0022)] // Hebrew\n [InlineData(\u0022\u3053\u3093\u306B\u3061\u306F\u0022)] // Japanese Hiragana\n [InlineData(\u0022\uC548\uB155\uD558\uC138\uC694\u0022)] // Korean\n public void Label_RendersMixedScripts_WithoutCrashing(string text)\n {\n // Arrange\n var label = new SkiaLabel { Text = text };\n \n // Act\n Action render = () =\u003E label.Measure(100, 100);\n \n // Assert\n render.Should().NotThrow();\n }\n \n [Fact]\n public void FontFallback_SelectsCorrectFont_ForEmoji()\n {\n // Arrange\n var fallbackManager = new FontFallbackManager();\n var primaryFont = SKTypeface.FromFamilyName(\u0022Arial\u0022);\n \n // Act\n var emojiFont = fallbackManager.GetTypefaceForCharacter(\u0027\uD83D\uDE00\u0027, primaryFont);\n \n // Assert\n emojiFont.Should().NotBe(primaryFont);\n emojiFont.FamilyName.Should().Contain(\u0022Emoji\u0022);\n }\n \n [Fact]\n public void TextShaper_HandlesArabicLigatures_Correctly()\n {\n // Arrange\n var shaper = new TextShaper();\n var arabicText = \u0022\u0645\u0631\u062D\u0628\u0627\u0022;\n var font = SKTypeface.FromFamilyName(\u0022Noto Sans Arabic\u0022);\n \n // Act\n var shaped = shaper.ShapeText(arabicText, font, 16);\n \n // Assert\n shaped.Glyphs.Should().NotBeEmpty();\n // Arabic text should have fewer glyphs than characters due to ligatures\n shaped.Glyphs.Count.Should().BeLessThanOrEqualTo(arabicText.Length);\n }\n}\n\u0060\u0060\u0060\n\n## IME Integration Tests\n\n\u0060\u0060\u0060csharp\npublic class IMETests\n{\n [Fact]\n public void Entry_HandlesPreeditText_Correctly()\n {\n // Arrange\n var entry = new SkiaEntry();\n \n // Act - Simulate IME composition\n entry.HandleTextInput(new TextInputEventArgs\n {\n IsComposing = true,\n CompositionText = \u0022nihao\u0022,\n CompositionCursorPos = 5\n });\n \n // Assert\n entry.PreeditText.Should().Be(\u0022nihao\u0022);\n entry.Text.Should().BeEmpty(); // Not committed yet\n }\n \n [Fact]\n public void Entry_CommitsComposedText_OnEnter()\n {\n // Arrange\n var entry = new SkiaEntry();\n entry.HandleTextInput(new TextInputEventArgs\n {\n IsComposing = true,\n CompositionText = \u0022\u4F60\u597D\u0022\n });\n \n // Act - Commit\n entry.HandleTextInput(new TextInputEventArgs\n {\n IsComposing = false,\n Text = \u0022\u4F60\u597D\u0022\n });\n \n // Assert\n entry.Text.Should().Be(\u0022\u4F60\u597D\u0022);\n entry.PreeditText.Should().BeEmpty();\n }\n \n [Fact]\n public void Entry_UpdatesCursorPosition_ForIME()\n {\n // Arrange\n var entry = new SkiaEntry { Text = \u0022Hello\u0022 };\n entry.CursorPosition = 5;\n \n // Act\n var cursorRect = entry.GetCursorRect();\n \n // Assert\n cursorRect.X.Should().BeGreaterThan(0);\n cursorRect.Height.Should().BeGreaterThan(0);\n }\n}\n\u0060\u0060\u0060\n\n## Emoji Detection Tests\n\n\u0060\u0060\u0060csharp\npublic class EmojiTests\n{\n [Theory]\n [InlineData(\u0022\uD83D\uDE00\u0022, true)] // Simple emoji\n [InlineData(\u0022\uD83D\uDC4B\uD83C\uDFFF\u0022, true)] // Emoji with skin tone\n [InlineData(\u0022\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66\u0022, true)] // ZWJ sequence\n [InlineData(\u0022Hello\u0022, false)] // Regular text\n [InlineData(\u0022\u4F60\u597D\u0022, false)] // CJK\n public void EmojiDetector_IdentifiesEmoji_Correctly(string text, bool shouldBeEmoji)\n {\n // Arrange\n var detector = new EmojiDetector();\n \n // Act\n var emojis = detector.FindEmojis(text);\n \n // Assert\n if (shouldBeEmoji)\n {\n emojis.Should().NotBeEmpty();\n }\n else\n {\n emojis.Should().BeEmpty();\n }\n }\n \n [Fact]\n public void EmojiSequence_ExtractsCompleteSequence()\n {\n // Arrange\n var detector = new EmojiDetector();\n var text = \u0022Hello \uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66 World\u0022;\n \n // Act\n var emojis = detector.FindEmojis(text);\n \n // Assert\n emojis.Should().HaveCount(1);\n emojis[0].Text.Should().Be(\u0022\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66\u0022);\n emojis[0].StartIndex.Should().Be(6);\n }\n}\n\u0060\u0060\u0060\n\n## Visual Regression Testing\n\nFor visual testing, OpenMaui can generate reference images:\n\n\u0060\u0060\u0060csharp\npublic class VisualRegressionTests\n{\n [Fact]\n public void Label_RendersChineseText_MatchesReference()\n {\n // Arrange\n var label = new SkiaLabel\n {\n Text = \u0022\u4F60\u597D\u4E16\u754C\u0022,\n FontSize = 24,\n Width = 200,\n Height = 50\n };\n \n // Act\n var bitmap = RenderToBitmap(label);\n \n // Assert\n var referenceImage = LoadReferenceImage(\u0022chinese_text.png\u0022);\n bitmap.Should().MatchImage(referenceImage, tolerance: 0.01);\n }\n \n private SKBitmap RenderToBitmap(SkiaView view)\n {\n var info = new SKImageInfo((int)view.Width, (int)view.Height);\n var bitmap = new SKBitmap(info);\n \n using var canvas = new SKCanvas(bitmap);\n view.Draw(canvas);\n \n return bitmap;\n }\n}\n\u0060\u0060\u0060\n\n## Integration Testing with Real IMEs\n\nFor end-to-end testing, use actual IME frameworks:\n\n\u0060\u0060\u0060bash\n#!/bin/bash\n# Test script for IBus integration\n\n# Start IBus daemon\nibus-daemon -drx\n\n# Set environment\nexport GTK_IM_MODULE=ibus\nexport XMODIFIERS=@im=ibus\n\n# Run tests\ndotnet test --filter \u0022Category=IME\u0022\n\u0060\u0060\u0060\n\n\u0060\u0060\u0060csharp\n[Trait(\u0022Category\u0022, \u0022IME\u0022)]\npublic class IBusIntegrationTests\n{\n [Fact(Skip = \u0022Requires IBus running\u0022)]\n public void Entry_IntegratesWithIBus_ForChineseInput()\n {\n // This test requires a running X11 session with IBus\n var app = new LinuxApplication();\n var window = new GtkHostWindow();\n var entry = new SkiaEntry();\n \n window.Content = entry;\n window.Show();\n \n // Simulate IBus events (requires native integration)\n // This would typically be done through UI automation\n }\n}\n\u0060\u0060\u0060\n\n## Performance Testing\n\n\u0060\u0060\u0060csharp\npublic class PerformanceTests\n{\n [Fact]\n public void TextShaping_HandlesLargeText_Efficiently()\n {\n // Arrange\n var shaper = new TextShaper();\n var largeText = string.Join(\u0022\u0022, Enumerable.Repeat(\u0022\u4F60\u597D\u4E16\u754C\u0022, 1000));\n var font = SKTypeface.FromFamilyName(\u0022Noto Sans CJK SC\u0022);\n \n // Act\n var stopwatch = Stopwatch.StartNew();\n var shaped = shaper.ShapeText(largeText, font, 16);\n stopwatch.Stop();\n \n // Assert\n stopwatch.ElapsedMilliseconds.Should().BeLessThan(100);\n }\n \n [Fact]\n public void FontFallback_CachesResults_ForPerformance()\n {\n // Arrange\n var manager = new FontFallbackManager();\n var primaryFont = SKTypeface.Default;\n \n // Act - First lookup (cache miss)\n var stopwatch1 = Stopwatch.StartNew();\n var font1 = manager.GetTypefaceForCharacterCached(\u0027\u4F60\u0027, primaryFont);\n stopwatch1.Stop();\n \n // Act - Second lookup (cache hit)\n var stopwatch2 = Stopwatch.StartNew();\n var font2 = manager.GetTypefaceForCharacterCached(\u0027\u4F60\u0027, primaryFont);\n stopwatch2.Stop();\n \n // Assert\n font1.Should().Be(font2);\n stopwatch2.ElapsedTicks.Should().BeLessThan(stopwatch1.ElapsedTicks);\n }\n}\n\u0060\u0060\u0060\n\n## Test Data Sets\n\nCreate comprehensive test data covering edge cases:\n\n\u0060\u0060\u0060csharp\npublic static class TestData\n{\n public static IEnumerable\u003Cobject[]\u003E InternationalTextSamples()\n {\n yield return new object[] { \u0022Hello World\u0022, \u0022Latin\u0022 };\n yield return new object[] { \u0022\u4F60\u597D\u4E16\u754C\u0022, \u0022Simplified Chinese\u0022 };\n yield return new object[] { \u0022\u3053\u3093\u306B\u3061\u306F\u4E16\u754C\u0022, \u0022Japanese\u0022 };\n yield return new object[] { \u0022\uC548\uB155\uD558\uC138\uC694 \uC138\uACC4\u0022, \u0022Korean\u0022 };\n yield return new object[] { \u0022\u0645\u0631\u062D\u0628\u0627 \u0628\u0627\u0644\u0639\u0627\u0644\u0645\u0022, \u0022Arabic\u0022 };\n yield return new object[] { \u0022\u05E9\u05DC\u05D5\u05DD \u05E2\u05D5\u05DC\u05DD\u0022, \u0022Hebrew\u0022 };\n yield return new object[] { \u0022\u041F\u0440\u0438\u0432\u0435\u0442 \u043C\u0438\u0440\u0022, \u0022Cyrillic\u0022 };\n yield return new object[] { \u0022\u0E2A\u0E27\u0E31\u0E2A\u0E14\u0E35\u0E0A\u0E32\u0E27\u0E42\u0E25\u0E01\u0022, \u0022Thai\u0022 };\n yield return new object[] { \u0022Hello \u4E16\u754C \uD83C\uDF0D\u0022, \u0022Mixed\u0022 };\n yield return new object[] { \u0022\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66\uD83D\uDC4B\uD83C\uDFFF\uD83C\uDF0D\u0022, \u0022Emoji\u0022 };\n }\n}\n\n[Theory]\n[MemberData(nameof(TestData.InternationalTextSamples), MemberType = typeof(TestData))]\npublic void Label_RendersInternationalText_Correctly(string text, string script)\n{\n var label = new SkiaLabel { Text = text };\n var size = label.Measure(double.PositiveInfinity, double.PositiveInfinity);\n size.Width.Should().BeGreaterThan(0, $\u0022Failed to render {script} text\u0022);\n}\n\u0060\u0060\u0060\n\n## Continuous Integration\n\nRun tests in CI with proper font installation:\n\n\u0060\u0060\u0060yaml\n# .github/workflows/test.yml\nname: Tests\n\non: [push, pull_request]\n\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v2\n \n - name: Install fonts\n run: |\n sudo apt-get update\n sudo apt-get install -y \\\n fonts-noto-cjk \\\n fonts-noto-color-emoji \\\n fonts-noto-core \\\n fonts-liberation\n \n - name: Setup .NET\n uses: actions/setup-dotnet@v1\n with:\n dotnet-version: \u00278.0.x\u0027\n \n - name: Run tests\n run: dotnet test --logger \u0022trx;LogFileName=test-results.trx\u0022\n \n - name: Publish test results\n uses: dorny/test-reporter@v1\n if: always()\n with:\n name: Test Results\n path: \u0027**/test-results.trx\u0027\n reporter: dotnet-trx\n\u0060\u0060\u0060\n\nThis comprehensive testing approach ensures OpenMaui\u0027s text rendering works correctly across all supported languages and scripts, providing a robust foundation for international applications." + } + ], + "generatedAt": 1769750791920 +} \ No newline at end of file diff --git a/.notes/series-1769751056430-116f9a.json b/.notes/series-1769751056430-116f9a.json new file mode 100644 index 0000000..ab11a82 --- /dev/null +++ b/.notes/series-1769751056430-116f9a.json @@ -0,0 +1,55 @@ +{ + "id": "series-1769751056430-116f9a", + "title": "Performance Optimization: Rendering, Caching, and Memory Management", + "content": "# Performance Optimization: Rendering, Caching, and Memory Management\r\n\r\n*Unlock blazing-fast UI performance in OpenMaui applications with advanced techniques for render caching, dirty region management, GPU acceleration, and native resource lifecycle control.*\r\n\r\n## Introduction\r\n\r\nPerformance optimization in modern UI frameworks is a multifaceted challenge that requires understanding the entire rendering pipeline\u2014from how pixels are drawn to the screen to how native resources are managed in memory. OpenMaui\u0027s Linux implementation, built on SkiaSharp and native windowing systems, provides a rich set of optimization opportunities that can dramatically improve application responsiveness and reduce resource consumption.\n\nThis article dives deep into the performance optimization techniques available in OpenMaui applications running on Linux. We\u0027ll explore how the \u0060SkiaRenderingEngine\u0060 manages dirty regions to minimize redraws, how render caching can eliminate redundant drawing operations, and when to choose GPU acceleration over software rendering. We\u0027ll also examine critical memory management patterns for P/Invoke interop and advanced rendering strategies for complex UIs.\n\nWhether you\u0027re building a lightweight utility or a complex enterprise application, these techniques will help you achieve smooth 60 FPS performance while keeping memory usage under control. The strategies discussed here are based on real implementation patterns from the OpenMaui codebase, which successfully runs full .NET MAUI applications on Linux with 100% API compliance.\r\n\r\n## Performance Profiling Tools\r\n\r\nBefore optimizing anything, you need accurate measurements. OpenMaui applications benefit from multiple profiling approaches that reveal different aspects of performance bottlenecks.\n\n## Built-in Frame Timing\n\nThe \u0060SkiaRenderingEngine\u0060 includes frame timing diagnostics that can be enabled during development:\n\n\u0060\u0060\u0060csharp\npublic class PerformanceMonitor\n{\n private readonly Stopwatch _frameTimer = new();\n private readonly Queue\u003Cdouble\u003E _frameTimes = new(60);\n \n public void OnFrameStart()\n {\n _frameTimer.Restart();\n }\n \n public void OnFrameEnd()\n {\n var elapsed = _frameTimer.Elapsed.TotalMilliseconds;\n _frameTimes.Enqueue(elapsed);\n \n if (_frameTimes.Count \u003E 60)\n _frameTimes.Dequeue();\n \n var avgFrameTime = _frameTimes.Average();\n var fps = 1000.0 / avgFrameTime;\n \n Debug.WriteLine($\u0022Frame: {elapsed:F2}ms | Avg FPS: {fps:F1}\u0022);\n }\n}\n\u0060\u0060\u0060\n\n## Skia Performance Flags\n\nSkiaSharp provides performance tracking through \u0060SKPaint\u0060 and \u0060SKCanvas\u0060 profiling. Enable these during development:\n\n\u0060\u0060\u0060csharp\npublic void MeasureDrawOperation(SKCanvas canvas, Action\u003CSKCanvas\u003E drawAction)\n{\n var sw = Stopwatch.StartNew();\n drawAction(canvas);\n sw.Stop();\n \n if (sw.ElapsedMilliseconds \u003E 16) // Exceeds 60 FPS budget\n {\n Console.WriteLine($\u0022\u26A0\uFE0F Slow draw operation: {sw.ElapsedMilliseconds}ms\u0022);\n }\n}\n\u0060\u0060\u0060\n\n## Native Memory Profiling\n\nFor P/Invoke-heavy code, use \u0060dotnet-counters\u0060 to monitor native allocations:\n\n\u0060\u0060\u0060bash\n# Monitor GC and native allocations\ndotnet-counters monitor --process-id \u003CPID\u003E \\\n System.Runtime[gen-0-gc-count,gen-1-gc-count,gen-2-gc-count] \\\n System.Runtime[alloc-rate,gc-heap-size]\n\u0060\u0060\u0060\n\n## X11/GTK Event Loop Profiling\n\nThe \u0060LinuxDispatcher\u0060 uses GLib\u0027s main loop. Profile event processing overhead:\n\n\u0060\u0060\u0060csharp\npublic class DispatcherProfiler\n{\n public void ProfileEventProcessing()\n {\n var before = GC.GetTotalMemory(false);\n var sw = Stopwatch.StartNew();\n \n // Process pending events\n while (gtk_events_pending())\n {\n gtk_main_iteration();\n }\n \n sw.Stop();\n var after = GC.GetTotalMemory(false);\n var allocated = after - before;\n \n Console.WriteLine($\u0022Event loop: {sw.ElapsedMilliseconds}ms, \u0022 \u002B\n $\u0022Allocated: {allocated / 1024}KB\u0022);\n }\n}\n\u0060\u0060\u0060\n\n## Identifying Bottlenecks\n\nCommon performance bottlenecks in OpenMaui applications:\n\n1. **Excessive redraws**: Check if dirty region management is working correctly\n2. **Font shaping overhead**: HarfBuzz text shaping can be expensive for complex scripts\n3. **Image decoding**: Large images decoded on the UI thread\n4. **Collection rendering**: \u0060SkiaCollectionView\u0060 with non-virtualized items\n5. **P/Invoke marshaling**: Frequent string conversions between managed and native code\n\nUse these profiling tools to establish baselines before applying optimizations, then measure again to validate improvements.\r\n\r\n## Render Cache and TextRenderCache\r\n\r\nCaching rendered content is one of the most effective optimization techniques available. OpenMaui\u0027s Skia-based rendering system provides multiple caching strategies to eliminate redundant drawing operations.\n\n## Understanding Render Caching\n\nEvery time a view is drawn, SkiaSharp executes a series of drawing commands\u2014filling rectangles, stroking paths, rendering text with font shaping. For static or infrequently changing content, this work is wasteful. Render caching stores the output of these operations in an \u0060SKBitmap\u0060 or \u0060SKImage\u0060 that can be quickly blitted to the canvas.\n\n## Implementing View-Level Caching\n\nThe \u0060SkiaView\u0060 base class supports render caching through a simple pattern:\n\n\u0060\u0060\u0060csharp\npublic class CachedSkiaView : SkiaView\n{\n private SKBitmap? _renderCache;\n private bool _cacheInvalidated = true;\n \n protected override void OnPropertyChanged(string propertyName = null)\n {\n base.OnPropertyChanged(propertyName);\n \n // Invalidate cache when visual properties change\n if (propertyName == nameof(BackgroundColor) ||\n propertyName == nameof(Width) ||\n propertyName == nameof(Height))\n {\n InvalidateCache();\n }\n }\n \n public void InvalidateCache()\n {\n _cacheInvalidated = true;\n InvalidateSurface();\n }\n \n protected override void OnDraw(SKCanvas canvas, int width, int height)\n {\n if (_cacheInvalidated || _renderCache == null)\n {\n // Recreate cache\n _renderCache?.Dispose();\n _renderCache = new SKBitmap(width, height);\n \n using var cacheCanvas = new SKCanvas(_renderCache);\n DrawContent(cacheCanvas, width, height);\n \n _cacheInvalidated = false;\n }\n \n // Fast blit from cache\n canvas.DrawBitmap(_renderCache, 0, 0);\n }\n \n protected virtual void DrawContent(SKCanvas canvas, int width, int height)\n {\n // Expensive drawing operations here\n }\n \n protected override void Dispose(bool disposing)\n {\n if (disposing)\n {\n _renderCache?.Dispose();\n }\n base.Dispose(disposing);\n }\n}\n\u0060\u0060\u0060\n\n## Text Render Caching\n\nText rendering with HarfBuzz shaping is particularly expensive for complex scripts (Arabic, Devanagari, CJK). The \u0060SkiaLabel\u0060 implementation benefits significantly from text caching:\n\n\u0060\u0060\u0060csharp\npublic class TextRenderCache\n{\n private readonly Dictionary\u003CTextCacheKey, SKTextBlob\u003E _cache = new();\n private const int MaxCacheSize = 1000;\n \n private record TextCacheKey(\n string Text,\n string FontFamily,\n float FontSize,\n SKFontStyleWeight Weight,\n SKFontStyleSlant Slant\n );\n \n public SKTextBlob GetOrCreateTextBlob(\n string text,\n SKFont font,\n SKPaint paint)\n {\n var key = new TextCacheKey(\n text,\n font.Typeface.FamilyName,\n font.Size,\n font.Typeface.FontWeight,\n font.Typeface.FontSlant\n );\n \n if (_cache.TryGetValue(key, out var cached))\n {\n return cached;\n }\n \n // Perform expensive text shaping\n var blob = SKTextBlob.Create(text, font);\n \n // Evict old entries if cache is full\n if (_cache.Count \u003E= MaxCacheSize)\n {\n var oldest = _cache.Keys.First();\n _cache[oldest].Dispose();\n _cache.Remove(oldest);\n }\n \n _cache[key] = blob;\n return blob;\n }\n \n public void Clear()\n {\n foreach (var blob in _cache.Values)\n {\n blob.Dispose();\n }\n _cache.Clear();\n }\n}\n\u0060\u0060\u0060\n\nIntegrate this into \u0060SkiaLabel\u0060:\n\n\u0060\u0060\u0060csharp\npublic class OptimizedSkiaLabel : SkiaLabel\n{\n private static readonly TextRenderCache _textCache = new();\n \n protected override void DrawText(SKCanvas canvas, SKPaint paint)\n {\n using var font = paint.ToFont();\n var textBlob = _textCache.GetOrCreateTextBlob(Text, font, paint);\n \n canvas.DrawText(textBlob, (float)X, (float)Y, paint);\n }\n}\n\u0060\u0060\u0060\n\n## Image Caching Strategy\n\nThe \u0060SkiaImage\u0060 control implements multi-level caching:\n\n\u0060\u0060\u0060csharp\npublic class ImageCacheManager\n{\n private readonly MemoryCache _memoryCache;\n private readonly string _diskCacheDir;\n \n public ImageCacheManager()\n {\n _memoryCache = new MemoryCache(new MemoryCacheOptions\n {\n SizeLimit = 100 * 1024 * 1024 // 100 MB\n });\n \n _diskCacheDir = Path.Combine(\n Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),\n \u0022OpenMaui\u0022, \u0022ImageCache\u0022\n );\n Directory.CreateDirectory(_diskCacheDir);\n }\n \n public async Task\u003CSKBitmap?\u003E GetOrLoadImageAsync(\n string source,\n CancellationToken ct = default)\n {\n var cacheKey = ComputeHash(source);\n \n // Level 1: Memory cache\n if (_memoryCache.TryGetValue\u003CSKBitmap\u003E(cacheKey, out var cached))\n {\n return cached;\n }\n \n // Level 2: Disk cache\n var diskPath = Path.Combine(_diskCacheDir, cacheKey \u002B \u0022.skia\u0022);\n if (File.Exists(diskPath))\n {\n var bitmap = SKBitmap.Decode(diskPath);\n CacheInMemory(cacheKey, bitmap);\n return bitmap;\n }\n \n // Level 3: Load from source\n var loaded = await LoadImageAsync(source, ct);\n if (loaded != null)\n {\n await SaveToDiskAsync(diskPath, loaded);\n CacheInMemory(cacheKey, loaded);\n }\n \n return loaded;\n }\n \n private void CacheInMemory(string key, SKBitmap bitmap)\n {\n var size = bitmap.ByteCount;\n _memoryCache.Set(key, bitmap, new MemoryCacheEntryOptions\n {\n Size = size,\n SlidingExpiration = TimeSpan.FromMinutes(10)\n });\n }\n}\n\u0060\u0060\u0060\n\n## When to Use Caching\n\n**Cache aggressively:**\n- Static labels and icons\n- Complex vector graphics (SVG)\n- Formatted text with multiple styles\n- Background gradients and patterns\n\n**Cache selectively:**\n- List item templates in \u0060SkiaCollectionView\u0060\n- Navigation bar backgrounds\n- Custom control chrome (borders, shadows)\n\n**Don\u0027t cache:**\n- Animated content\n- Real-time data visualizations\n- User input controls (Entry, Editor)\n- Content that changes every frame\n\nProper caching can reduce CPU usage by 50-70% in typical business applications with mostly static content.\r\n\r\n## DirtyRectManager Optimization\r\n\r\nThe \u0060DirtyRectManager\u0060 is the cornerstone of efficient rendering in OpenMaui. Instead of redrawing the entire window on every frame, dirty region tracking identifies the minimal set of rectangles that need repainting.\n\n## How Dirty Region Tracking Works\n\nWhen a view\u0027s visual properties change (color, size, position), it marks its bounding rectangle as \u0022dirty.\u0022 The rendering engine accumulates these dirty regions and, during the next frame, only redraws the affected areas.\n\n\u0060\u0060\u0060csharp\npublic class DirtyRectManager\n{\n private readonly List\u003CSKRect\u003E _dirtyRegions = new();\n private readonly object _lock = new();\n \n public void MarkDirty(SKRect rect)\n {\n lock (_lock)\n {\n _dirtyRegions.Add(rect);\n }\n }\n \n public void MarkDirty(double x, double y, double width, double height)\n {\n MarkDirty(new SKRect(\n (float)x,\n (float)y,\n (float)(x \u002B width),\n (float)(y \u002B height)\n ));\n }\n \n public SKRect[] GetDirtyRegions()\n {\n lock (_lock)\n {\n if (_dirtyRegions.Count == 0)\n return Array.Empty\u003CSKRect\u003E();\n \n // Optimize: merge overlapping rectangles\n var optimized = OptimizeDirtyRegions(_dirtyRegions);\n _dirtyRegions.Clear();\n \n return optimized;\n }\n }\n \n private SKRect[] OptimizeDirtyRegions(List\u003CSKRect\u003E regions)\n {\n if (regions.Count \u003C= 1)\n return regions.ToArray();\n \n var merged = new List\u003CSKRect\u003E();\n var sorted = regions.OrderBy(r =\u003E r.Left).ThenBy(r =\u003E r.Top).ToList();\n \n var current = sorted[0];\n \n for (int i = 1; i \u003C sorted.Count; i\u002B\u002B)\n {\n var next = sorted[i];\n \n // Check if rectangles overlap or are adjacent\n if (current.IntersectsWith(next) || AreAdjacent(current, next))\n {\n // Merge rectangles\n current = SKRect.Union(current, next);\n }\n else\n {\n merged.Add(current);\n current = next;\n }\n }\n \n merged.Add(current);\n return merged.ToArray();\n }\n \n private bool AreAdjacent(SKRect a, SKRect b)\n {\n const float threshold = 10f; // Pixels\n \n return Math.Abs(a.Right - b.Left) \u003C threshold ||\n Math.Abs(b.Right - a.Left) \u003C threshold ||\n Math.Abs(a.Bottom - b.Top) \u003C threshold ||\n Math.Abs(b.Bottom - a.Top) \u003C threshold;\n }\n \n public bool ShouldRenderFullFrame(SKRect[] dirtyRegions, SKSize windowSize)\n {\n if (dirtyRegions.Length == 0)\n return false;\n \n var totalDirtyArea = dirtyRegions.Sum(r =\u003E r.Width * r.Height);\n var windowArea = windowSize.Width * windowSize.Height;\n \n // If dirty area exceeds 60% of window, just redraw everything\n return (totalDirtyArea / windowArea) \u003E 0.6f;\n }\n}\n\u0060\u0060\u0060\n\n## Integrating with SkiaRenderingEngine\n\nThe rendering engine uses dirty regions to clip drawing operations:\n\n\u0060\u0060\u0060csharp\npublic class SkiaRenderingEngine\n{\n private readonly DirtyRectManager _dirtyRectManager = new();\n \n public void Render(SKCanvas canvas, SkiaView rootView)\n {\n var dirtyRegions = _dirtyRectManager.GetDirtyRegions();\n \n if (dirtyRegions.Length == 0)\n {\n // Nothing to render\n return;\n }\n \n var windowSize = new SKSize((float)rootView.Width, (float)rootView.Height);\n \n if (_dirtyRectManager.ShouldRenderFullFrame(dirtyRegions, windowSize))\n {\n // Full frame render\n RenderView(canvas, rootView, null);\n }\n else\n {\n // Partial render with clipping\n foreach (var dirtyRect in dirtyRegions)\n {\n canvas.Save();\n canvas.ClipRect(dirtyRect);\n \n RenderView(canvas, rootView, dirtyRect);\n \n canvas.Restore();\n }\n }\n }\n \n private void RenderView(SKCanvas canvas, SkiaView view, SKRect? clipRect)\n {\n if (!view.IsVisible)\n return;\n \n var viewBounds = new SKRect(\n (float)view.X,\n (float)view.Y,\n (float)(view.X \u002B view.Width),\n (float)(view.Y \u002B view.Height)\n );\n \n // Skip views outside dirty region\n if (clipRect.HasValue \u0026\u0026 !clipRect.Value.IntersectsWith(viewBounds))\n return;\n \n canvas.Save();\n canvas.Translate((float)view.X, (float)view.Y);\n \n view.Draw(canvas, (int)view.Width, (int)view.Height);\n \n // Recursively render children\n if (view is SkiaLayoutView layout)\n {\n foreach (var child in layout.Children.OfType\u003CSkiaView\u003E())\n {\n RenderView(canvas, child, clipRect);\n }\n }\n \n canvas.Restore();\n }\n}\n\u0060\u0060\u0060\n\n## View-Level Dirty Tracking\n\nViews should mark themselves dirty when visual properties change:\n\n\u0060\u0060\u0060csharp\npublic abstract class SkiaView : IView\n{\n private DirtyRectManager? _dirtyRectManager;\n \n public void AttachDirtyRectManager(DirtyRectManager manager)\n {\n _dirtyRectManager = manager;\n }\n \n protected void MarkDirty()\n {\n _dirtyRectManager?.MarkDirty(X, Y, Width, Height);\n }\n \n public Color BackgroundColor\n {\n get =\u003E _backgroundColor;\n set\n {\n if (_backgroundColor != value)\n {\n _backgroundColor = value;\n MarkDirty();\n OnPropertyChanged();\n }\n }\n }\n}\n\u0060\u0060\u0060\n\n## Layout Optimization\n\nLayouts should propagate dirty regions from children:\n\n\u0060\u0060\u0060csharp\npublic class SkiaStackLayout : SkiaLayoutView\n{\n protected override void OnChildPropertyChanged(SkiaView child, string propertyName)\n {\n base.OnChildPropertyChanged(child, propertyName);\n \n if (propertyName == nameof(child.Width) ||\n propertyName == nameof(child.Height))\n {\n // Child size changed, might affect layout\n InvalidateLayout();\n MarkDirty(); // Mark entire layout dirty\n }\n else\n {\n // Only child appearance changed\n _dirtyRectManager?.MarkDirty(\n child.X,\n child.Y,\n child.Width,\n child.Height\n );\n }\n }\n}\n\u0060\u0060\u0060\n\n## Performance Impact\n\nDirty region optimization provides dramatic performance improvements:\n\n- **Small UI updates** (single button click): 80-90% reduction in rendering time\n- **List scrolling**: 50-60% reduction by only rendering visible items\n- **Animations**: 30-40% reduction by clipping to animated region\n\n## Best Practices\n\n1. **Granular invalidation**: Only mark the minimal affected area dirty\n2. **Batch updates**: Accumulate property changes before invalidating\n3. **Avoid full-window invalidation**: Reserve for theme changes or window resizes\n4. **Profile dirty regions**: Log region counts and sizes to identify excessive invalidation\n5. **Optimize merging**: Adjacent dirty regions should be merged to reduce draw calls\r\n\r\n## GPU vs Software Rendering Trade-offs\r\n\r\nOpenMaui supports both GPU-accelerated rendering via OpenGL and software rendering via CPU-based rasterization. Choosing the right rendering mode is critical for optimal performance across different hardware configurations.\n\n## Understanding the Rendering Backends\n\n### GPU Rendering with OpenGL\n\nThe \u0060GpuRenderingEngine\u0060 uses SkiaSharp\u0027s OpenGL backend to leverage hardware acceleration:\n\n\u0060\u0060\u0060csharp\npublic class GpuRenderingEngine : IDisposable\n{\n private GRContext? _grContext;\n private SKSurface? _surface;\n \n public bool Initialize(IntPtr glContext, int width, int height)\n {\n try\n {\n var glInterface = GRGlInterface.Create();\n if (glInterface == null)\n {\n Console.WriteLine(\u0022Failed to create GL interface\u0022);\n return false;\n }\n \n _grContext = GRContext.CreateGl(glInterface);\n \n var frameBufferInfo = new GRGlFramebufferInfo(\n fboId: 0, // Default framebuffer\n format: SKColorType.Rgba8888.ToGlSizedFormat()\n );\n \n var backendRenderTarget = new GRBackendRenderTarget(\n width,\n height,\n sampleCount: 0,\n stencilBits: 8,\n frameBufferInfo\n );\n \n _surface = SKSurface.Create(\n _grContext,\n backendRenderTarget,\n GRSurfaceOrigin.BottomLeft,\n SKColorType.Rgba8888\n );\n \n return _surface != null;\n }\n catch (Exception ex)\n {\n Console.WriteLine($\u0022GPU initialization failed: {ex.Message}\u0022);\n return false;\n }\n }\n \n public void Render(Action\u003CSKCanvas\u003E drawAction)\n {\n if (_surface == null || _grContext == null)\n return;\n \n var canvas = _surface.Canvas;\n canvas.Clear(SKColors.White);\n \n drawAction(canvas);\n \n canvas.Flush();\n _grContext.Flush();\n }\n \n public void Dispose()\n {\n _surface?.Dispose();\n _grContext?.Dispose();\n }\n}\n\u0060\u0060\u0060\n\n### Software Rendering\n\nSoftware rendering uses CPU-based rasterization, which is more predictable but slower:\n\n\u0060\u0060\u0060csharp\npublic class SoftwareRenderingEngine : IDisposable\n{\n private SKBitmap? _bitmap;\n private SKCanvas? _canvas;\n \n public void Initialize(int width, int height)\n {\n _bitmap = new SKBitmap(width, height, SKColorType.Rgba8888, SKAlphaType.Premul);\n _canvas = new SKCanvas(_bitmap);\n }\n \n public void Render(Action\u003CSKCanvas\u003E drawAction)\n {\n if (_canvas == null)\n return;\n \n _canvas.Clear(SKColors.White);\n drawAction(_canvas);\n }\n \n public IntPtr GetPixelData()\n {\n return _bitmap?.GetPixels() ?? IntPtr.Zero;\n }\n \n public void Dispose()\n {\n _canvas?.Dispose();\n _bitmap?.Dispose();\n }\n}\n\u0060\u0060\u0060\n\n## Performance Characteristics\n\n### GPU Rendering Advantages\n\n- **Complex vector graphics**: 5-10x faster for paths, gradients, and effects\n- **Large canvases**: Scales better with resolution (4K displays)\n- **Animations**: Smooth 60 FPS even with many moving elements\n- **Filters and effects**: Blur, shadows, and image filters are hardware-accelerated\n\n### Software Rendering Advantages\n\n- **Predictable performance**: No driver issues or GPU quirks\n- **Lower latency**: No GPU context switching overhead\n- **Better for simple UIs**: Faster for basic rectangles and text\n- **Compatibility**: Works on all systems, including virtual machines\n\n## Adaptive Rendering Strategy\n\nImplement runtime selection based on content complexity:\n\n\u0060\u0060\u0060csharp\npublic class AdaptiveRenderingEngine\n{\n private readonly GpuRenderingEngine _gpuEngine;\n private readonly SoftwareRenderingEngine _softwareEngine;\n private bool _useGpu;\n private int _complexityScore;\n \n public AdaptiveRenderingEngine()\n {\n _gpuEngine = new GpuRenderingEngine();\n _softwareEngine = new SoftwareRenderingEngine();\n _useGpu = TryInitializeGpu();\n }\n \n private bool TryInitializeGpu()\n {\n try\n {\n return _gpuEngine.Initialize(GetGLContext(), 1920, 1080);\n }\n catch\n {\n return false;\n }\n }\n \n public void Render(SkiaView rootView, Action\u003CSKCanvas\u003E drawAction)\n {\n // Analyze scene complexity\n _complexityScore = CalculateComplexity(rootView);\n \n // Use GPU for complex scenes, software for simple ones\n var shouldUseGpu = _useGpu \u0026\u0026 _complexityScore \u003E 100;\n \n if (shouldUseGpu)\n {\n _gpuEngine.Render(drawAction);\n }\n else\n {\n _softwareEngine.Render(drawAction);\n }\n }\n \n private int CalculateComplexity(SkiaView view)\n {\n int score = 0;\n \n // Complex rendering operations increase score\n if (view is SkiaImage) score \u002B= 20;\n if (view is SkiaGraphicsView) score \u002B= 50;\n if (view.HasShadow) score \u002B= 10;\n if (view.HasGradient) score \u002B= 15;\n \n if (view is SkiaLayoutView layout)\n {\n foreach (var child in layout.Children.OfType\u003CSkiaView\u003E())\n {\n score \u002B= CalculateComplexity(child);\n }\n }\n \n return score;\n }\n}\n\u0060\u0060\u0060\n\n## GPU Acceleration Tuning\n\nOptimize GPU rendering with these techniques:\n\n\u0060\u0060\u0060csharp\npublic class GpuOptimizationSettings\n{\n // Enable MSAA for smoother edges (costs performance)\n public int SampleCount { get; set; } = 4;\n \n // Cache GPU resources between frames\n public bool EnableResourceCaching { get; set; } = true;\n \n // Use GPU texture atlas for images\n public bool UseTextureAtlas { get; set; } = true;\n \n public void ApplyToContext(GRContext context)\n {\n // Set resource cache limits\n if (EnableResourceCaching)\n {\n context.SetResourceCacheLimit(256 * 1024 * 1024); // 256 MB\n }\n \n // Purge unused resources periodically\n context.PurgeUnlockedResources();\n }\n}\n\u0060\u0060\u0060\n\n## Detecting GPU Capabilities\n\nQuery GPU capabilities to make informed decisions:\n\n\u0060\u0060\u0060csharp\npublic class GpuCapabilities\n{\n public static GpuInfo DetectCapabilities()\n {\n var glInterface = GRGlInterface.Create();\n if (glInterface == null)\n {\n return new GpuInfo { Available = false };\n }\n \n using var context = GRContext.CreateGl(glInterface);\n \n return new GpuInfo\n {\n Available = true,\n MaxTextureSize = context.GetMaxTextureSize(),\n Vendor = GetGLString(GL_VENDOR),\n Renderer = GetGLString(GL_RENDERER),\n Version = GetGLString(GL_VERSION)\n };\n }\n \n [DllImport(\u0022libGL.so.1\u0022)]\n private static extern IntPtr glGetString(uint name);\n \n private static string GetGLString(uint name)\n {\n var ptr = glGetString(name);\n return Marshal.PtrToStringAnsi(ptr) ?? \u0022Unknown\u0022;\n }\n \n private const uint GL_VENDOR = 0x1F00;\n private const uint GL_RENDERER = 0x1F01;\n private const uint GL_VERSION = 0x1F02;\n}\n\npublic class GpuInfo\n{\n public bool Available { get; set; }\n public int MaxTextureSize { get; set; }\n public string Vendor { get; set; } = \u0022\u0022;\n public string Renderer { get; set; } = \u0022\u0022;\n public string Version { get; set; } = \u0022\u0022;\n \n public bool IsIntelIntegrated =\u003E \n Vendor.Contains(\u0022Intel\u0022, StringComparison.OrdinalIgnoreCase);\n \n public bool IsNvidia =\u003E \n Vendor.Contains(\u0022NVIDIA\u0022, StringComparison.OrdinalIgnoreCase);\n \n public bool IsAMD =\u003E \n Vendor.Contains(\u0022AMD\u0022, StringComparison.OrdinalIgnoreCase) ||\n Vendor.Contains(\u0022ATI\u0022, StringComparison.OrdinalIgnoreCase);\n}\n\u0060\u0060\u0060\n\n## Decision Matrix\n\n| Scenario | Recommended Backend | Reason |\n|----------|-------------------|--------|\n| Simple business forms | Software | Lower overhead for basic shapes |\n| Image-heavy applications | GPU | Hardware-accelerated image compositing |\n| Data visualization | GPU | Complex paths and gradients |\n| Text-heavy UIs | Software | Font rendering is fast on CPU |\n| Animations | GPU | Smooth 60 FPS with hardware acceleration |\n| Virtual machines | Software | GPU passthrough often unavailable |\n| Low-end hardware | Software | Integrated GPUs may be slower |\n\n## Fallback Strategy\n\nAlways implement graceful fallback:\n\n\u0060\u0060\u0060csharp\npublic class RenderingEngineFactory\n{\n public static IRenderingEngine Create()\n {\n // Try GPU first\n var gpuEngine = new GpuRenderingEngine();\n if (gpuEngine.Initialize(GetGLContext(), 1920, 1080))\n {\n Console.WriteLine(\u0022\u2713 Using GPU rendering\u0022);\n return gpuEngine;\n }\n \n // Fallback to software\n Console.WriteLine(\u0022\u26A0 GPU unavailable, using software rendering\u0022);\n var softwareEngine = new SoftwareRenderingEngine();\n softwareEngine.Initialize(1920, 1080);\n return softwareEngine;\n }\n}\n\u0060\u0060\u0060\n\nThe key is measuring real-world performance on your target hardware and choosing the backend that delivers the best user experience.\r\n\r\n## Memory Management with P/Invoke\r\n\r\nOpenMaui\u0027s Linux implementation relies heavily on P/Invoke for native interop with GTK, X11, and Wayland. Improper memory management in these scenarios leads to leaks, crashes, and degraded performance. This section covers essential patterns for safe native resource handling.\n\n## Understanding Native Memory Lifecycle\n\nWhen you call native functions, you\u0027re crossing the managed/unmanaged boundary. The .NET garbage collector doesn\u0027t track native allocations, so you must manually manage their lifecycle.\n\n## SafeHandle Pattern for Native Resources\n\nUse \u0060SafeHandle\u0060 for automatic cleanup of native resources:\n\n\u0060\u0060\u0060csharp\npublic class GtkWidgetHandle : SafeHandle\n{\n public GtkWidgetHandle() : base(IntPtr.Zero, true)\n {\n }\n \n public GtkWidgetHandle(IntPtr handle) : base(IntPtr.Zero, true)\n {\n SetHandle(handle);\n }\n \n public override bool IsInvalid =\u003E handle == IntPtr.Zero;\n \n protected override bool ReleaseHandle()\n {\n if (!IsInvalid)\n {\n // Properly unreference GTK widget\n gtk_widget_destroy(handle);\n g_object_unref(handle);\n }\n return true;\n }\n \n [DllImport(\u0022libgtk-3.so.0\u0022)]\n private static extern void gtk_widget_destroy(IntPtr widget);\n \n [DllImport(\u0022libgobject-2.0.so.0\u0022)]\n private static extern void g_object_unref(IntPtr obj);\n}\n\u0060\u0060\u0060\n\nUse it in your code:\n\n\u0060\u0060\u0060csharp\npublic class GtkButton : IDisposable\n{\n private readonly GtkWidgetHandle _handle;\n \n public GtkButton(string label)\n {\n var labelPtr = Marshal.StringToHGlobalAnsi(label);\n try\n {\n var widget = gtk_button_new_with_label(labelPtr);\n _handle = new GtkWidgetHandle(widget);\n }\n finally\n {\n Marshal.FreeHGlobal(labelPtr);\n }\n }\n \n public void Dispose()\n {\n _handle?.Dispose();\n }\n \n [DllImport(\u0022libgtk-3.so.0\u0022)]\n private static extern IntPtr gtk_button_new_with_label(IntPtr label);\n}\n\u0060\u0060\u0060\n\n## String Marshaling\n\nString conversions are a common source of memory leaks:\n\n\u0060\u0060\u0060csharp\npublic static class StringMarshalHelper\n{\n // Allocate UTF-8 string for native code\n public static IntPtr StringToUtf8(string str)\n {\n if (str == null)\n return IntPtr.Zero;\n \n var bytes = Encoding.UTF8.GetByteCount(str);\n var ptr = Marshal.AllocHGlobal(bytes \u002B 1);\n \n unsafe\n {\n fixed (char* chars = str)\n {\n var bytePtr = (byte*)ptr;\n Encoding.UTF8.GetBytes(chars, str.Length, bytePtr, bytes);\n bytePtr[bytes] = 0; // Null terminator\n }\n }\n \n return ptr;\n }\n \n // Convert native UTF-8 string to managed\n public static string? Utf8ToString(IntPtr ptr)\n {\n if (ptr == IntPtr.Zero)\n return null;\n \n var length = 0;\n unsafe\n {\n var bytePtr = (byte*)ptr;\n while (bytePtr[length] != 0)\n length\u002B\u002B;\n }\n \n var bytes = new byte[length];\n Marshal.Copy(ptr, bytes, 0, length);\n \n return Encoding.UTF8.GetString(bytes);\n }\n \n // Use for temporary strings (auto-freed)\n public static void WithUtf8String(string str, Action\u003CIntPtr\u003E action)\n {\n var ptr = StringToUtf8(str);\n try\n {\n action(ptr);\n }\n finally\n {\n Marshal.FreeHGlobal(ptr);\n }\n }\n}\n\u0060\u0060\u0060\n\nUsage:\n\n\u0060\u0060\u0060csharp\npublic void SetWindowTitle(string title)\n{\n StringMarshalHelper.WithUtf8String(title, titlePtr =\u003E\n {\n gtk_window_set_title(_windowHandle, titlePtr);\n });\n}\n\u0060\u0060\u0060\n\n## Structure Marshaling\n\nFor complex structures, use explicit layout:\n\n\u0060\u0060\u0060csharp\n[StructLayout(LayoutKind.Sequential)]\npublic struct XEvent\n{\n public int type;\n public IntPtr serial;\n public int send_event;\n public IntPtr display;\n public IntPtr window;\n // ... more fields\n}\n\npublic static class X11Interop\n{\n public static XEvent ReadEvent(IntPtr display)\n {\n XEvent evt;\n XNextEvent(display, out evt);\n return evt;\n }\n \n [DllImport(\u0022libX11.so.6\u0022)]\n private static extern void XNextEvent(IntPtr display, out XEvent evt);\n}\n\u0060\u0060\u0060\n\n## Callback Memory Management\n\nCallbacks from native code require special handling:\n\n\u0060\u0060\u0060csharp\npublic class GtkEventHandler\n{\n // Keep delegate alive to prevent GC collection\n private readonly GtkCallback _callback;\n private GCHandle _callbackHandle;\n \n public GtkEventHandler()\n {\n _callback = OnGtkEvent;\n _callbackHandle = GCHandle.Alloc(_callback);\n }\n \n public void Connect(IntPtr widget, string signal)\n {\n StringMarshalHelper.WithUtf8String(signal, signalPtr =\u003E\n {\n var callbackPtr = Marshal.GetFunctionPointerForDelegate(_callback);\n g_signal_connect_data(\n widget,\n signalPtr,\n callbackPtr,\n IntPtr.Zero,\n IntPtr.Zero,\n 0\n );\n });\n }\n \n private void OnGtkEvent(IntPtr widget, IntPtr userData)\n {\n // Handle event\n }\n \n public void Dispose()\n {\n if (_callbackHandle.IsAllocated)\n {\n _callbackHandle.Free();\n }\n }\n \n [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n private delegate void GtkCallback(IntPtr widget, IntPtr userData);\n \n [DllImport(\u0022libgobject-2.0.so.0\u0022)]\n private static extern ulong g_signal_connect_data(\n IntPtr instance,\n IntPtr signal,\n IntPtr callback,\n IntPtr data,\n IntPtr destroyData,\n int connectFlags\n );\n}\n\u0060\u0060\u0060\n\n## Memory Pool for Frequent Allocations\n\nReduce allocation overhead with pooling:\n\n\u0060\u0060\u0060csharp\npublic class NativeMemoryPool\n{\n private readonly ConcurrentBag\u003CIntPtr\u003E _pool = new();\n private readonly int _blockSize;\n private readonly int _maxPoolSize;\n \n public NativeMemoryPool(int blockSize, int maxPoolSize = 100)\n {\n _blockSize = blockSize;\n _maxPoolSize = maxPoolSize;\n }\n \n public IntPtr Rent()\n {\n if (_pool.TryTake(out var ptr))\n {\n // Reuse pooled memory\n return ptr;\n }\n \n // Allocate new block\n return Marshal.AllocHGlobal(_blockSize);\n }\n \n public void Return(IntPtr ptr)\n {\n if (_pool.Count \u003C _maxPoolSize)\n {\n // Return to pool\n _pool.Add(ptr);\n }\n else\n {\n // Pool full, free immediately\n Marshal.FreeHGlobal(ptr);\n }\n }\n \n public void Clear()\n {\n while (_pool.TryTake(out var ptr))\n {\n Marshal.FreeHGlobal(ptr);\n }\n }\n}\n\u0060\u0060\u0060\n\n## Detecting Memory Leaks\n\nImplement diagnostics to track native allocations:\n\n\u0060\u0060\u0060csharp\npublic static class NativeMemoryTracker\n{\n private static readonly ConcurrentDictionary\u003CIntPtr, AllocationInfo\u003E _allocations = new();\n \n private record AllocationInfo(int Size, string StackTrace, DateTime Timestamp);\n \n public static IntPtr Allocate(int size, [CallerMemberName] string caller = \u0022\u0022)\n {\n var ptr = Marshal.AllocHGlobal(size);\n \n _allocations[ptr] = new AllocationInfo(\n size,\n Environment.StackTrace,\n DateTime.UtcNow\n );\n \n return ptr;\n }\n \n public static void Free(IntPtr ptr)\n {\n _allocations.TryRemove(ptr, out _);\n Marshal.FreeHGlobal(ptr);\n }\n \n public static void ReportLeaks()\n {\n var leaks = _allocations.ToArray();\n \n if (leaks.Length == 0)\n {\n Console.WriteLine(\u0022\u2713 No memory leaks detected\u0022);\n return;\n }\n \n Console.WriteLine($\u0022\u26A0 {leaks.Length} potential leaks detected:\u0022);\n \n foreach (var (ptr, info) in leaks)\n {\n var age = DateTime.UtcNow - info.Timestamp;\n Console.WriteLine($\u0022 Ptr: {ptr:X}, Size: {info.Size} bytes, Age: {age.TotalSeconds:F1}s\u0022);\n Console.WriteLine($\u0022 Allocated at:\\n{info.StackTrace}\u0022);\n }\n }\n}\n\u0060\u0060\u0060\n\n## Best Practices\n\n1. **Always use SafeHandle** for native resources that need cleanup\n2. **Free strings immediately** after passing to native code\n3. **Pin delegates** passed to native callbacks to prevent GC collection\n4. **Use \u0060using\u0060 statements** for IDisposable wrappers\n5. **Implement finalizers** as a safety net (but don\u0027t rely on them)\n6. **Profile native allocations** regularly during development\n7. **Document ownership** - who is responsible for freeing each allocation\n\n## Example: Complete Native Resource Wrapper\n\n\u0060\u0060\u0060csharp\npublic class X11Window : IDisposable\n{\n private readonly SafeX11DisplayHandle _display;\n private readonly SafeX11WindowHandle _window;\n private bool _disposed;\n \n public X11Window(string title, int width, int height)\n {\n _display = SafeX11DisplayHandle.Open();\n _window = CreateWindow(_display, title, width, height);\n }\n \n private SafeX11WindowHandle CreateWindow(\n SafeX11DisplayHandle display,\n string title,\n int width,\n int height)\n {\n var window = XCreateSimpleWindow(\n display.DangerousGetHandle(),\n XDefaultRootWindow(display.DangerousGetHandle()),\n 0, 0, width, height, 0, 0, 0\n );\n \n StringMarshalHelper.WithUtf8String(title, titlePtr =\u003E\n {\n XStoreName(display.DangerousGetHandle(), window, titlePtr);\n });\n \n return new SafeX11WindowHandle(display, window);\n }\n \n public void Dispose()\n {\n if (_disposed)\n return;\n \n _window?.Dispose();\n _display?.Dispose();\n \n _disposed = true;\n GC.SuppressFinalize(this);\n }\n \n ~X11Window()\n {\n Dispose();\n }\n}\n\u0060\u0060\u0060\n\nProper P/Invoke memory management is non-negotiable for production applications. The patterns shown here prevent the silent memory leaks that plague many native interop scenarios.\r\n\r\n## LayeredRenderer for Complex UIs\r\n\r\nComplex user interfaces with overlapping elements, transparency, and z-ordering require sophisticated rendering strategies. The \u0060LayeredRenderer\u0060 pattern separates UI elements into distinct layers, enabling efficient compositing and advanced visual effects.\n\n## Understanding Layer-Based Rendering\n\nTraditional immediate-mode rendering draws all UI elements in a single pass. Layer-based rendering separates elements into independent surfaces that can be:\n\n- Rendered independently at different rates\n- Cached and reused across frames\n- Composited with blend modes and opacity\n- Reordered without redrawing\n\n## Implementing LayeredRenderer\n\n\u0060\u0060\u0060csharp\npublic class LayeredRenderer : IDisposable\n{\n private readonly List\u003CRenderLayer\u003E _layers = new();\n private readonly SKPaint _compositePaint = new();\n \n public class RenderLayer : IDisposable\n {\n public string Name { get; set; } = \u0022\u0022;\n public int ZIndex { get; set; }\n public SKBitmap? Surface { get; set; }\n public SKCanvas? Canvas { get; set; }\n public float Opacity { get; set; } = 1.0f;\n public SKBlendMode BlendMode { get; set; } = SKBlendMode.SrcOver;\n public bool IsDirty { get; set; } = true;\n public bool IsVisible { get; set; } = true;\n public Action\u003CSKCanvas, int, int\u003E? DrawAction { get; set; }\n \n public void Initialize(int width, int height)\n {\n Surface?.Dispose();\n Canvas?.Dispose();\n \n Surface = new SKBitmap(width, height, SKColorType.Rgba8888, SKAlphaType.Premul);\n Canvas = new SKCanvas(Surface);\n }\n \n public void Render(int width, int height)\n {\n if (!IsDirty || Canvas == null || DrawAction == null)\n return;\n \n Canvas.Clear(SKColors.Transparent);\n DrawAction(Canvas, width, height);\n IsDirty = false;\n }\n \n public void Dispose()\n {\n Canvas?.Dispose();\n Surface?.Dispose();\n }\n }\n \n public RenderLayer AddLayer(string name, int zIndex)\n {\n var layer = new RenderLayer\n {\n Name = name,\n ZIndex = zIndex\n };\n \n _layers.Add(layer);\n _layers.Sort((a, b) =\u003E a.ZIndex.CompareTo(b.ZIndex));\n \n return layer;\n }\n \n public void RemoveLayer(string name)\n {\n var layer = _layers.FirstOrDefault(l =\u003E l.Name == name);\n if (layer != null)\n {\n layer.Dispose();\n _layers.Remove(layer);\n }\n }\n \n public RenderLayer? GetLayer(string name)\n {\n return _layers.FirstOrDefault(l =\u003E l.Name == name);\n }\n \n public void InvalidateLayer(string name)\n {\n var layer = GetLayer(name);\n if (layer != null)\n {\n layer.IsDirty = true;\n }\n }\n \n public void Resize(int width, int height)\n {\n foreach (var layer in _layers)\n {\n layer.Initialize(width, height);\n layer.IsDirty = true;\n }\n }\n \n public void Render(SKCanvas targetCanvas, int width, int height)\n {\n // Render each dirty layer\n foreach (var layer in _layers)\n {\n if (layer.IsDirty)\n {\n layer.Render(width, height);\n }\n }\n \n // Composite layers onto target canvas\n targetCanvas.Clear(SKColors.White);\n \n foreach (var layer in _layers)\n {\n if (!layer.IsVisible || layer.Surface == null)\n continue;\n \n _compositePaint.Color = new SKColor(255, 255, 255, (byte)(layer.Opacity * 255));\n _compositePaint.BlendMode = layer.BlendMode;\n \n targetCanvas.DrawBitmap(layer.Surface, 0, 0, _compositePaint);\n }\n }\n \n public void Dispose()\n {\n foreach (var layer in _layers)\n {\n layer.Dispose();\n }\n _layers.Clear();\n _compositePaint.Dispose();\n }\n}\n\u0060\u0060\u0060\n\n## Practical Usage: Complex UI Application\n\n\u0060\u0060\u0060csharp\npublic class ComplexUIRenderer\n{\n private readonly LayeredRenderer _renderer = new();\n private LayeredRenderer.RenderLayer _backgroundLayer;\n private LayeredRenderer.RenderLayer _contentLayer;\n private LayeredRenderer.RenderLayer _overlayLayer;\n private LayeredRenderer.RenderLayer _cursorLayer;\n \n public void Initialize(int width, int height)\n {\n // Background layer (static, rarely changes)\n _backgroundLayer = _renderer.AddLayer(\u0022background\u0022, 0);\n _backgroundLayer.DrawAction = DrawBackground;\n _backgroundLayer.Initialize(width, height);\n \n // Content layer (main UI, moderate updates)\n _contentLayer = _renderer.AddLayer(\u0022content\u0022, 10);\n _contentLayer.DrawAction = DrawContent;\n _contentLayer.Initialize(width, height);\n \n // Overlay layer (dialogs, tooltips)\n _overlayLayer = _renderer.AddLayer(\u0022overlay\u0022, 20);\n _overlayLayer.DrawAction = DrawOverlay;\n _overlayLayer.Opacity = 0.95f;\n _overlayLayer.Initialize(width, height);\n \n // Cursor layer (updates every frame)\n _cursorLayer = _renderer.AddLayer(\u0022cursor\u0022, 30);\n _cursorLayer.DrawAction = DrawCursor;\n _cursorLayer.Initialize(width, height);\n }\n \n private void DrawBackground(SKCanvas canvas, int width, int height)\n {\n // Draw gradient background\n using var paint = new SKPaint\n {\n Shader = SKShader.CreateLinearGradient(\n new SKPoint(0, 0),\n new SKPoint(0, height),\n new[] { new SKColor(240, 240, 245), new SKColor(220, 220, 230) },\n SKShaderTileMode.Clamp\n )\n };\n \n canvas.DrawRect(0, 0, width, height, paint);\n }\n \n private void DrawContent(SKCanvas canvas, int width, int height)\n {\n // Draw main UI content (buttons, labels, etc.)\n // This layer invalidates when UI state changes\n }\n \n private void DrawOverlay(SKCanvas canvas, int width, int height)\n {\n // Draw modal dialogs, context menus\n if (_showDialog)\n {\n DrawDialog(canvas, width, height);\n }\n }\n \n private void DrawCursor(SKCanvas canvas, int width, int height)\n {\n // Draw custom cursor\n using var paint = new SKPaint\n {\n Color = SKColors.Black,\n IsAntialias = true\n };\n \n canvas.DrawCircle(_cursorX, _cursorY, 5, paint);\n }\n \n public void OnMouseMove(float x, float y)\n {\n _cursorX = x;\n _cursorY = y;\n \n // Only cursor layer needs redraw\n _renderer.InvalidateLayer(\u0022cursor\u0022);\n }\n \n public void OnContentChanged()\n {\n // Content changed, but background and cursor remain valid\n _renderer.InvalidateLayer(\u0022content\u0022);\n }\n \n public void ShowDialog()\n {\n _showDialog = true;\n _renderer.InvalidateLayer(\u0022overlay\u0022);\n }\n}\n\u0060\u0060\u0060\n\n## Advanced: Animated Layers\n\nImplement smooth animations with layer opacity and transforms:\n\n\u0060\u0060\u0060csharp\npublic class AnimatedLayerController\n{\n private readonly LayeredRenderer _renderer;\n private readonly Dictionary\u003Cstring, Animation\u003E _animations = new();\n \n public class Animation\n {\n public float StartOpacity { get; set; }\n public float EndOpacity { get; set; }\n public TimeSpan Duration { get; set; }\n public DateTime StartTime { get; set; }\n public Action? OnComplete { get; set; }\n }\n \n public AnimatedLayerController(LayeredRenderer renderer)\n {\n _renderer = renderer;\n }\n \n public void FadeIn(string layerName, TimeSpan duration, Action? onComplete = null)\n {\n var layer = _renderer.GetLayer(layerName);\n if (layer == null) return;\n \n layer.Opacity = 0;\n layer.IsVisible = true;\n \n _animations[layerName] = new Animation\n {\n StartOpacity = 0,\n EndOpacity = 1,\n Duration = duration,\n StartTime = DateTime.UtcNow,\n OnComplete = onComplete\n };\n }\n \n public void FadeOut(string layerName, TimeSpan duration, Action? onComplete = null)\n {\n var layer = _renderer.GetLayer(layerName);\n if (layer == null) return;\n \n _animations[layerName] = new Animation\n {\n StartOpacity = layer.Opacity,\n EndOpacity = 0,\n Duration = duration,\n StartTime = DateTime.UtcNow,\n OnComplete = () =\u003E\n {\n layer.IsVisible = false;\n onComplete?.Invoke();\n }\n };\n }\n \n public void Update()\n {\n var now = DateTime.UtcNow;\n var completed = new List\u003Cstring\u003E();\n \n foreach (var (layerName, animation) in _animations)\n {\n var layer = _renderer.GetLayer(layerName);\n if (layer == null)\n {\n completed.Add(layerName);\n continue;\n }\n \n var elapsed = now - animation.StartTime;\n var progress = Math.Min(1.0, elapsed.TotalSeconds / animation.Duration.TotalSeconds);\n \n // Ease-out cubic\n var eased = 1 - Math.Pow(1 - progress, 3);\n \n layer.Opacity = (float)(animation.StartOpacity \u002B \n (animation.EndOpacity - animation.StartOpacity) * eased);\n \n if (progress \u003E= 1.0)\n {\n animation.OnComplete?.Invoke();\n completed.Add(layerName);\n }\n }\n \n foreach (var layerName in completed)\n {\n _animations.Remove(layerName);\n }\n }\n}\n\u0060\u0060\u0060\n\n## Performance Benefits\n\nLayered rendering provides substantial performance improvements:\n\n- **Selective rendering**: Only dirty layers are redrawn (50-80% reduction in draw calls)\n- **Caching**: Static layers rendered once and reused (90% reduction for backgrounds)\n- **Parallelization**: Layers can be rendered on separate threads\n- **Compositing effects**: Hardware-accelerated blending and opacity\n\n## Use Cases\n\n1. **Gaming UIs**: Separate background, game world, UI overlay, and cursor\n2. **Image editors**: Background, canvas, tools, selection overlay\n3. **Data dashboards**: Static chrome, updating charts, overlay alerts\n4. **Video players**: Video layer, controls layer, subtitle layer\n\n## Best Practices\n\n1. **Minimize layer count**: Each layer has memory overhead\n2. **Group related elements**: Don\u0027t create a layer for every control\n3. **Invalidate granularly**: Only mark changed layers as dirty\n4. **Use appropriate z-index spacing**: Leave room for insertion (0, 10, 20, 30...)\n5. **Profile memory usage**: Each layer allocates width \u00D7 height \u00D7 4 bytes\n6. **Dispose properly**: Layers hold unmanaged bitmap resources\n\nLayered rendering transforms complex UI scenarios from performance nightmares into smooth, efficient experiences.\r\n\r\n## Benchmarking and Best Practices\r\n\r\nEffective performance optimization requires rigorous benchmarking and adherence to proven best practices. This section provides a comprehensive framework for measuring and improving OpenMaui application performance.\n\n## Comprehensive Benchmarking Framework\n\n\u0060\u0060\u0060csharp\npublic class PerformanceBenchmark\n{\n private readonly Dictionary\u003Cstring, List\u003Cdouble\u003E\u003E _measurements = new();\n private readonly Stopwatch _stopwatch = new();\n \n public IDisposable Measure(string operation)\n {\n return new MeasurementScope(this, operation);\n }\n \n private class MeasurementScope : IDisposable\n {\n private readonly PerformanceBenchmark _benchmark;\n private readonly string _operation;\n private readonly Stopwatch _sw;\n \n public MeasurementScope(PerformanceBenchmark benchmark, string operation)\n {\n _benchmark = benchmark;\n _operation = operation;\n _sw = Stopwatch.StartNew();\n }\n \n public void Dispose()\n {\n _sw.Stop();\n _benchmark.RecordMeasurement(_operation, _sw.Elapsed.TotalMilliseconds);\n }\n }\n \n private void RecordMeasurement(string operation, double milliseconds)\n {\n if (!_measurements.ContainsKey(operation))\n {\n _measurements[operation] = new List\u003Cdouble\u003E();\n }\n \n _measurements[operation].Add(milliseconds);\n }\n \n public BenchmarkReport GenerateReport()\n {\n var report = new BenchmarkReport();\n \n foreach (var (operation, measurements) in _measurements)\n {\n var stats = new BenchmarkStats\n {\n Operation = operation,\n Count = measurements.Count,\n Min = measurements.Min(),\n Max = measurements.Max(),\n Average = measurements.Average(),\n Median = CalculateMedian(measurements),\n P95 = CalculatePercentile(measurements, 0.95),\n P99 = CalculatePercentile(measurements, 0.99)\n };\n \n report.Stats.Add(stats);\n }\n \n return report;\n }\n \n private double CalculateMedian(List\u003Cdouble\u003E values)\n {\n var sorted = values.OrderBy(v =\u003E v).ToList();\n var mid = sorted.Count / 2;\n \n if (sorted.Count % 2 == 0)\n return (sorted[mid - 1] \u002B sorted[mid]) / 2.0;\n else\n return sorted[mid];\n }\n \n private double CalculatePercentile(List\u003Cdouble\u003E values, double percentile)\n {\n var sorted = values.OrderBy(v =\u003E v).ToList();\n var index = (int)Math.Ceiling(percentile * sorted.Count) - 1;\n return sorted[Math.Max(0, index)];\n }\n}\n\npublic class BenchmarkReport\n{\n public List\u003CBenchmarkStats\u003E Stats { get; } = new();\n \n public void Print()\n {\n Console.WriteLine(\u0022\\n=== Performance Benchmark Report ===\u0022);\n Console.WriteLine($\u0022{{\u0022Operation\u0022,-30}} {{\u0022Count\u0022,8}} {{\u0022Min\u0022,10}} {{\u0022Avg\u0022,10}} {{\u0022Median\u0022,10}} {{\u0022P95\u0022,10}} {{\u0022P99\u0022,10}} {{\u0022Max\u0022,10}}\u0022);\n Console.WriteLine(new string(\u0027-\u0027, 110));\n \n foreach (var stat in Stats.OrderByDescending(s =\u003E s.Average))\n {\n Console.WriteLine(\n $\u0022{stat.Operation,-30} {stat.Count,8} {stat.Min,10:F2} {stat.Average,10:F2} \u0022 \u002B\n $\u0022{stat.Median,10:F2} {stat.P95,10:F2} {stat.P99,10:F2} {stat.Max,10:F2}\u0022\n );\n }\n }\n}\n\npublic class BenchmarkStats\n{\n public string Operation { get; set; } = \u0022\u0022;\n public int Count { get; set; }\n public double Min { get; set; }\n public double Max { get; set; }\n public double Average { get; set; }\n public double Median { get; set; }\n public double P95 { get; set; }\n public double P99 { get; set; }\n}\n\u0060\u0060\u0060\n\n## Real-World Benchmark Suite\n\n\u0060\u0060\u0060csharp\npublic class OpenMauiBenchmarkSuite\n{\n private readonly PerformanceBenchmark _benchmark = new();\n \n public void RunAllBenchmarks()\n {\n BenchmarkRendering();\n BenchmarkLayout();\n BenchmarkTextShaping();\n BenchmarkImageLoading();\n BenchmarkCollectionView();\n \n _benchmark.GenerateReport().Print();\n }\n \n private void BenchmarkRendering()\n {\n var view = new SkiaButton { Text = \u0022Test Button\u0022, Width = 200, Height = 50 };\n var surface = SKSurface.Create(new SKImageInfo(200, 50));\n var canvas = surface.Canvas;\n \n // Warmup\n for (int i = 0; i \u003C 10; i\u002B\u002B)\n view.Draw(canvas, 200, 50);\n \n // Benchmark\n for (int i = 0; i \u003C 1000; i\u002B\u002B)\n {\n using (_benchmark.Measure(\u0022Button Render\u0022))\n {\n view.Draw(canvas, 200, 50);\n }\n }\n }\n \n private void BenchmarkLayout()\n {\n var grid = new SkiaGrid\n {\n RowDefinitions = new RowDefinitionCollection\n {\n new RowDefinition { Height = GridLength.Auto },\n new RowDefinition { Height = GridLength.Star },\n new RowDefinition { Height = new GridLength(100) }\n },\n ColumnDefinitions = new ColumnDefinitionCollection\n {\n new ColumnDefinition { Width = new GridLength(200) },\n new ColumnDefinition { Width = GridLength.Star }\n }\n };\n \n // Add children\n for (int i = 0; i \u003C 6; i\u002B\u002B)\n {\n grid.Children.Add(new SkiaLabel { Text = $\u0022Label {i}\u0022 });\n }\n \n // Benchmark layout\n for (int i = 0; i \u003C 1000; i\u002B\u002B)\n {\n using (_benchmark.Measure(\u0022Grid Layout\u0022))\n {\n grid.Measure(800, 600);\n grid.Arrange(new Rect(0, 0, 800, 600));\n }\n }\n }\n \n private void BenchmarkTextShaping()\n {\n var texts = new[]\n {\n \u0022Simple ASCII text\u0022,\n \u0022Unicode: \u00E9mojis \uD83C\uDF89\uD83D\uDE80\uD83D\uDCBB\u0022,\n \u0022CJK: \u65E5\u672C\u8A9E\u306E\u30C6\u30AD\u30B9\u30C8\u0022,\n \u0022Arabic: \u0627\u0644\u0646\u0635 \u0627\u0644\u0639\u0631\u0628\u064A\u0022\n };\n \n using var paint = new SKPaint\n {\n Typeface = SKTypeface.Default,\n TextSize = 14\n };\n \n foreach (var text in texts)\n {\n for (int i = 0; i \u003C 500; i\u002B\u002B)\n {\n using (_benchmark.Measure($\u0022Text Shape: {text.Substring(0, Math.Min(10, text.Length))}\u0022))\n {\n var blob = SKTextBlob.Create(text, paint.ToFont());\n blob.Dispose();\n }\n }\n }\n }\n \n private void BenchmarkImageLoading()\n {\n var testImagePath = \u0022test_image.png\u0022;\n \n for (int i = 0; i \u003C 100; i\u002B\u002B)\n {\n using (_benchmark.Measure(\u0022Image Decode\u0022))\n {\n using var bitmap = SKBitmap.Decode(testImagePath);\n }\n }\n }\n \n private void BenchmarkCollectionView()\n {\n var collectionView = new SkiaCollectionView\n {\n Width = 400,\n Height = 600\n };\n \n var items = Enumerable.Range(0, 1000)\n .Select(i =\u003E new { Text = $\u0022Item {i}\u0022, Value = i })\n .ToList();\n \n using (_benchmark.Measure(\u0022CollectionView Bind\u0022))\n {\n collectionView.ItemsSource = items;\n }\n \n for (int i = 0; i \u003C 100; i\u002B\u002B)\n {\n using (_benchmark.Measure(\u0022CollectionView Scroll\u0022))\n {\n collectionView.ScrollTo(i * 10);\n }\n }\n }\n}\n\u0060\u0060\u0060\n\n## Memory Profiling\n\n\u0060\u0060\u0060csharp\npublic class MemoryProfiler\n{\n public static MemorySnapshot TakeSnapshot(string label)\n {\n GC.Collect();\n GC.WaitForPendingFinalizers();\n GC.Collect();\n \n return new MemorySnapshot\n {\n Label = label,\n Timestamp = DateTime.UtcNow,\n TotalMemory = GC.GetTotalMemory(false),\n Gen0Collections = GC.CollectionCount(0),\n Gen1Collections = GC.CollectionCount(1),\n Gen2Collections = GC.CollectionCount(2)\n };\n }\n \n public static void CompareSnapshots(MemorySnapshot before, MemorySnapshot after)\n {\n var memoryDiff = after.TotalMemory - before.TotalMemory;\n var gen0Diff = after.Gen0Collections - before.Gen0Collections;\n var gen1Diff = after.Gen1Collections - before.Gen1Collections;\n var gen2Diff = after.Gen2Collections - before.Gen2Collections;\n \n Console.WriteLine(\u0022\\n=== Memory Profile Comparison ===\u0022);\n Console.WriteLine($\u0022Before: {before.Label} at {before.Timestamp}\u0022);\n Console.WriteLine($\u0022After: {after.Label} at {after.Timestamp}\u0022);\n Console.WriteLine($\u0022Memory Delta: {memoryDiff / 1024.0:F2} KB\u0022);\n Console.WriteLine($\u0022GC Gen0: {gen0Diff}, Gen1: {gen1Diff}, Gen2: {gen2Diff}\u0022);\n \n if (memoryDiff \u003E 1024 * 1024) // \u003E 1 MB\n {\n Console.WriteLine(\u0022\u26A0\uFE0F WARNING: Significant memory increase detected!\u0022);\n }\n }\n}\n\npublic class MemorySnapshot\n{\n public string Label { get; set; } = \u0022\u0022;\n public DateTime Timestamp { get; set; }\n public long TotalMemory { get; set; }\n public int Gen0Collections { get; set; }\n public int Gen1Collections { get; set; }\n public int Gen2Collections { get; set; }\n}\n\u0060\u0060\u0060\n\n## Best Practices Checklist\n\n### Rendering Optimization\n\n- \u2705 **Use dirty region tracking** for all custom views\n- \u2705 **Cache static content** (backgrounds, icons, formatted text)\n- \u2705 **Implement render layers** for complex UIs\n- \u2705 **Clip drawing operations** to visible regions\n- \u2705 **Batch draw calls** when possible\n- \u2705 **Use GPU rendering** for complex graphics\n- \u2705 **Profile frame times** and target 60 FPS (16.67ms per frame)\n\n### Memory Management\n\n- \u2705 **Use SafeHandle** for all native resources\n- \u2705 **Implement IDisposable** on classes holding unmanaged resources\n- \u2705 **Free P/Invoke allocations** immediately after use\n- \u2705 **Pool frequently allocated objects**\n- \u2705 **Avoid string allocations** in hot paths\n- \u2705 **Monitor GC collections** and minimize Gen2 collections\n- \u2705 **Use weak references** for caches\n\n### Layout Optimization\n\n- \u2705 **Minimize layout passes** by caching measurements\n- \u2705 **Use absolute layout** for static positioning\n- \u2705 **Avoid nested layouts** when possible\n- \u2705 **Virtualize long lists** with CollectionView\n- \u2705 **Defer offscreen layout** until needed\n\n### Text and Fonts\n\n- \u2705 **Cache shaped text** (SKTextBlob)\n- \u2705 **Limit font variations** to reduce shaping overhead\n- \u2705 **Use system fonts** when possible\n- \u2705 **Preload fonts** at startup\n- \u2705 **Measure text once** and cache results\n\n### Images\n\n- \u2705 **Decode images asynchronously** off the UI thread\n- \u2705 **Implement multi-level caching** (memory \u002B disk)\n- \u2705 **Resize images** to display size\n- \u2705 **Use appropriate formats** (PNG for UI, JPEG for photos)\n- \u2705 **Lazy load images** in lists\n\n## Continuous Performance Monitoring\n\n\u0060\u0060\u0060csharp\npublic class PerformanceMonitor\n{\n private static readonly PerformanceMonitor _instance = new();\n public static PerformanceMonitor Instance =\u003E _instance;\n \n private readonly ConcurrentQueue\u003CPerformanceEvent\u003E _events = new();\n private Timer? _reportTimer;\n \n public void Start()\n {\n _reportTimer = new Timer(_ =\u003E GenerateReport(), null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1));\n }\n \n public void RecordEvent(string category, string operation, double milliseconds)\n {\n _events.Enqueue(new PerformanceEvent\n {\n Category = category,\n Operation = operation,\n Milliseconds = milliseconds,\n Timestamp = DateTime.UtcNow\n });\n \n // Keep last 10,000 events\n while (_events.Count \u003E 10000)\n {\n _events.TryDequeue(out _);\n }\n }\n \n private void GenerateReport()\n {\n var events = _events.ToArray();\n var slowEvents = events.Where(e =\u003E e.Milliseconds \u003E 16.67).ToArray();\n \n if (slowEvents.Length \u003E 0)\n {\n Console.WriteLine($\u0022\\n\u26A0\uFE0F {slowEvents.Length} slow operations detected in last minute:\u0022);\n \n foreach (var evt in slowEvents.Take(10))\n {\n Console.WriteLine($\u0022 {evt.Category}/{evt.Operation}: {evt.Milliseconds:F2}ms\u0022);\n }\n }\n }\n}\n\npublic class PerformanceEvent\n{\n public string Category { get; set; } = \u0022\u0022;\n public string Operation { get; set; } = \u0022\u0022;\n public double Milliseconds { get; set; }\n public DateTime Timestamp { get; set; }\n}\n\u0060\u0060\u0060\n\n## Final Recommendations\n\n1. **Establish baselines**: Measure performance before optimizing\n2. **Profile in production**: Development builds don\u0027t reflect real-world performance\n3. **Test on target hardware**: Low-end devices reveal bottlenecks\n4. **Automate benchmarks**: Run performance tests in CI/CD\n5. **Monitor in production**: Track frame times and memory usage\n6. **Optimize iteratively**: Focus on the biggest bottlenecks first\n7. **Document optimizations**: Explain why specific patterns were chosen\n\nPerformance optimization is an ongoing process. Regular benchmarking and adherence to best practices ensure your OpenMaui applications deliver smooth, responsive experiences across all Linux desktop environments.\r\n\r\n---\r\n\r\n\u003E The difference between a smooth 60 FPS application and a sluggish one often comes down to understanding when to render, what to cache, and how to leverage hardware acceleration effectively.\r\n\r\n\u003E Dirty region optimization can reduce rendering overhead by 80-90% in scenarios where only small portions of the UI change between frames.\r\n\r\n\u003E Memory leaks in P/Invoke scenarios are silent killers\u2014proper native resource lifecycle management is not optional, it\u0027s essential for production applications.", + "createdAt": 1769751056430, + "updatedAt": 1769751056430, + "tags": [ + "series", + "generated", + "Building Linux Apps with .NET MAUI" + ], + "isArticle": true, + "seriesGroup": "Building Linux Apps with .NET MAUI", + "subtitle": "Unlock blazing-fast UI performance in OpenMaui applications with advanced techniques for render caching, dirty region management, GPU acceleration, and native resource lifecycle control.", + "pullQuotes": [ + "The difference between a smooth 60 FPS application and a sluggish one often comes down to understanding when to render, what to cache, and how to leverage hardware acceleration effectively.", + "Dirty region optimization can reduce rendering overhead by 80-90% in scenarios where only small portions of the UI change between frames.", + "Memory leaks in P/Invoke scenarios are silent killers\u2014proper native resource lifecycle management is not optional, it\u0027s essential for production applications." + ], + "sections": [ + { + "header": "Introduction", + "content": "Performance optimization in modern UI frameworks is a multifaceted challenge that requires understanding the entire rendering pipeline\u2014from how pixels are drawn to the screen to how native resources are managed in memory. OpenMaui\u0027s Linux implementation, built on SkiaSharp and native windowing systems, provides a rich set of optimization opportunities that can dramatically improve application responsiveness and reduce resource consumption.\n\nThis article dives deep into the performance optimization techniques available in OpenMaui applications running on Linux. We\u0027ll explore how the \u0060SkiaRenderingEngine\u0060 manages dirty regions to minimize redraws, how render caching can eliminate redundant drawing operations, and when to choose GPU acceleration over software rendering. We\u0027ll also examine critical memory management patterns for P/Invoke interop and advanced rendering strategies for complex UIs.\n\nWhether you\u0027re building a lightweight utility or a complex enterprise application, these techniques will help you achieve smooth 60 FPS performance while keeping memory usage under control. The strategies discussed here are based on real implementation patterns from the OpenMaui codebase, which successfully runs full .NET MAUI applications on Linux with 100% API compliance." + }, + { + "header": "Performance Profiling Tools", + "content": "Before optimizing anything, you need accurate measurements. OpenMaui applications benefit from multiple profiling approaches that reveal different aspects of performance bottlenecks.\n\n## Built-in Frame Timing\n\nThe \u0060SkiaRenderingEngine\u0060 includes frame timing diagnostics that can be enabled during development:\n\n\u0060\u0060\u0060csharp\npublic class PerformanceMonitor\n{\n private readonly Stopwatch _frameTimer = new();\n private readonly Queue\u003Cdouble\u003E _frameTimes = new(60);\n \n public void OnFrameStart()\n {\n _frameTimer.Restart();\n }\n \n public void OnFrameEnd()\n {\n var elapsed = _frameTimer.Elapsed.TotalMilliseconds;\n _frameTimes.Enqueue(elapsed);\n \n if (_frameTimes.Count \u003E 60)\n _frameTimes.Dequeue();\n \n var avgFrameTime = _frameTimes.Average();\n var fps = 1000.0 / avgFrameTime;\n \n Debug.WriteLine($\u0022Frame: {elapsed:F2}ms | Avg FPS: {fps:F1}\u0022);\n }\n}\n\u0060\u0060\u0060\n\n## Skia Performance Flags\n\nSkiaSharp provides performance tracking through \u0060SKPaint\u0060 and \u0060SKCanvas\u0060 profiling. Enable these during development:\n\n\u0060\u0060\u0060csharp\npublic void MeasureDrawOperation(SKCanvas canvas, Action\u003CSKCanvas\u003E drawAction)\n{\n var sw = Stopwatch.StartNew();\n drawAction(canvas);\n sw.Stop();\n \n if (sw.ElapsedMilliseconds \u003E 16) // Exceeds 60 FPS budget\n {\n Console.WriteLine($\u0022\u26A0\uFE0F Slow draw operation: {sw.ElapsedMilliseconds}ms\u0022);\n }\n}\n\u0060\u0060\u0060\n\n## Native Memory Profiling\n\nFor P/Invoke-heavy code, use \u0060dotnet-counters\u0060 to monitor native allocations:\n\n\u0060\u0060\u0060bash\n# Monitor GC and native allocations\ndotnet-counters monitor --process-id \u003CPID\u003E \\\n System.Runtime[gen-0-gc-count,gen-1-gc-count,gen-2-gc-count] \\\n System.Runtime[alloc-rate,gc-heap-size]\n\u0060\u0060\u0060\n\n## X11/GTK Event Loop Profiling\n\nThe \u0060LinuxDispatcher\u0060 uses GLib\u0027s main loop. Profile event processing overhead:\n\n\u0060\u0060\u0060csharp\npublic class DispatcherProfiler\n{\n public void ProfileEventProcessing()\n {\n var before = GC.GetTotalMemory(false);\n var sw = Stopwatch.StartNew();\n \n // Process pending events\n while (gtk_events_pending())\n {\n gtk_main_iteration();\n }\n \n sw.Stop();\n var after = GC.GetTotalMemory(false);\n var allocated = after - before;\n \n Console.WriteLine($\u0022Event loop: {sw.ElapsedMilliseconds}ms, \u0022 \u002B\n $\u0022Allocated: {allocated / 1024}KB\u0022);\n }\n}\n\u0060\u0060\u0060\n\n## Identifying Bottlenecks\n\nCommon performance bottlenecks in OpenMaui applications:\n\n1. **Excessive redraws**: Check if dirty region management is working correctly\n2. **Font shaping overhead**: HarfBuzz text shaping can be expensive for complex scripts\n3. **Image decoding**: Large images decoded on the UI thread\n4. **Collection rendering**: \u0060SkiaCollectionView\u0060 with non-virtualized items\n5. **P/Invoke marshaling**: Frequent string conversions between managed and native code\n\nUse these profiling tools to establish baselines before applying optimizations, then measure again to validate improvements." + }, + { + "header": "Render Cache and TextRenderCache", + "content": "Caching rendered content is one of the most effective optimization techniques available. OpenMaui\u0027s Skia-based rendering system provides multiple caching strategies to eliminate redundant drawing operations.\n\n## Understanding Render Caching\n\nEvery time a view is drawn, SkiaSharp executes a series of drawing commands\u2014filling rectangles, stroking paths, rendering text with font shaping. For static or infrequently changing content, this work is wasteful. Render caching stores the output of these operations in an \u0060SKBitmap\u0060 or \u0060SKImage\u0060 that can be quickly blitted to the canvas.\n\n## Implementing View-Level Caching\n\nThe \u0060SkiaView\u0060 base class supports render caching through a simple pattern:\n\n\u0060\u0060\u0060csharp\npublic class CachedSkiaView : SkiaView\n{\n private SKBitmap? _renderCache;\n private bool _cacheInvalidated = true;\n \n protected override void OnPropertyChanged(string propertyName = null)\n {\n base.OnPropertyChanged(propertyName);\n \n // Invalidate cache when visual properties change\n if (propertyName == nameof(BackgroundColor) ||\n propertyName == nameof(Width) ||\n propertyName == nameof(Height))\n {\n InvalidateCache();\n }\n }\n \n public void InvalidateCache()\n {\n _cacheInvalidated = true;\n InvalidateSurface();\n }\n \n protected override void OnDraw(SKCanvas canvas, int width, int height)\n {\n if (_cacheInvalidated || _renderCache == null)\n {\n // Recreate cache\n _renderCache?.Dispose();\n _renderCache = new SKBitmap(width, height);\n \n using var cacheCanvas = new SKCanvas(_renderCache);\n DrawContent(cacheCanvas, width, height);\n \n _cacheInvalidated = false;\n }\n \n // Fast blit from cache\n canvas.DrawBitmap(_renderCache, 0, 0);\n }\n \n protected virtual void DrawContent(SKCanvas canvas, int width, int height)\n {\n // Expensive drawing operations here\n }\n \n protected override void Dispose(bool disposing)\n {\n if (disposing)\n {\n _renderCache?.Dispose();\n }\n base.Dispose(disposing);\n }\n}\n\u0060\u0060\u0060\n\n## Text Render Caching\n\nText rendering with HarfBuzz shaping is particularly expensive for complex scripts (Arabic, Devanagari, CJK). The \u0060SkiaLabel\u0060 implementation benefits significantly from text caching:\n\n\u0060\u0060\u0060csharp\npublic class TextRenderCache\n{\n private readonly Dictionary\u003CTextCacheKey, SKTextBlob\u003E _cache = new();\n private const int MaxCacheSize = 1000;\n \n private record TextCacheKey(\n string Text,\n string FontFamily,\n float FontSize,\n SKFontStyleWeight Weight,\n SKFontStyleSlant Slant\n );\n \n public SKTextBlob GetOrCreateTextBlob(\n string text,\n SKFont font,\n SKPaint paint)\n {\n var key = new TextCacheKey(\n text,\n font.Typeface.FamilyName,\n font.Size,\n font.Typeface.FontWeight,\n font.Typeface.FontSlant\n );\n \n if (_cache.TryGetValue(key, out var cached))\n {\n return cached;\n }\n \n // Perform expensive text shaping\n var blob = SKTextBlob.Create(text, font);\n \n // Evict old entries if cache is full\n if (_cache.Count \u003E= MaxCacheSize)\n {\n var oldest = _cache.Keys.First();\n _cache[oldest].Dispose();\n _cache.Remove(oldest);\n }\n \n _cache[key] = blob;\n return blob;\n }\n \n public void Clear()\n {\n foreach (var blob in _cache.Values)\n {\n blob.Dispose();\n }\n _cache.Clear();\n }\n}\n\u0060\u0060\u0060\n\nIntegrate this into \u0060SkiaLabel\u0060:\n\n\u0060\u0060\u0060csharp\npublic class OptimizedSkiaLabel : SkiaLabel\n{\n private static readonly TextRenderCache _textCache = new();\n \n protected override void DrawText(SKCanvas canvas, SKPaint paint)\n {\n using var font = paint.ToFont();\n var textBlob = _textCache.GetOrCreateTextBlob(Text, font, paint);\n \n canvas.DrawText(textBlob, (float)X, (float)Y, paint);\n }\n}\n\u0060\u0060\u0060\n\n## Image Caching Strategy\n\nThe \u0060SkiaImage\u0060 control implements multi-level caching:\n\n\u0060\u0060\u0060csharp\npublic class ImageCacheManager\n{\n private readonly MemoryCache _memoryCache;\n private readonly string _diskCacheDir;\n \n public ImageCacheManager()\n {\n _memoryCache = new MemoryCache(new MemoryCacheOptions\n {\n SizeLimit = 100 * 1024 * 1024 // 100 MB\n });\n \n _diskCacheDir = Path.Combine(\n Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),\n \u0022OpenMaui\u0022, \u0022ImageCache\u0022\n );\n Directory.CreateDirectory(_diskCacheDir);\n }\n \n public async Task\u003CSKBitmap?\u003E GetOrLoadImageAsync(\n string source,\n CancellationToken ct = default)\n {\n var cacheKey = ComputeHash(source);\n \n // Level 1: Memory cache\n if (_memoryCache.TryGetValue\u003CSKBitmap\u003E(cacheKey, out var cached))\n {\n return cached;\n }\n \n // Level 2: Disk cache\n var diskPath = Path.Combine(_diskCacheDir, cacheKey \u002B \u0022.skia\u0022);\n if (File.Exists(diskPath))\n {\n var bitmap = SKBitmap.Decode(diskPath);\n CacheInMemory(cacheKey, bitmap);\n return bitmap;\n }\n \n // Level 3: Load from source\n var loaded = await LoadImageAsync(source, ct);\n if (loaded != null)\n {\n await SaveToDiskAsync(diskPath, loaded);\n CacheInMemory(cacheKey, loaded);\n }\n \n return loaded;\n }\n \n private void CacheInMemory(string key, SKBitmap bitmap)\n {\n var size = bitmap.ByteCount;\n _memoryCache.Set(key, bitmap, new MemoryCacheEntryOptions\n {\n Size = size,\n SlidingExpiration = TimeSpan.FromMinutes(10)\n });\n }\n}\n\u0060\u0060\u0060\n\n## When to Use Caching\n\n**Cache aggressively:**\n- Static labels and icons\n- Complex vector graphics (SVG)\n- Formatted text with multiple styles\n- Background gradients and patterns\n\n**Cache selectively:**\n- List item templates in \u0060SkiaCollectionView\u0060\n- Navigation bar backgrounds\n- Custom control chrome (borders, shadows)\n\n**Don\u0027t cache:**\n- Animated content\n- Real-time data visualizations\n- User input controls (Entry, Editor)\n- Content that changes every frame\n\nProper caching can reduce CPU usage by 50-70% in typical business applications with mostly static content." + }, + { + "header": "DirtyRectManager Optimization", + "content": "The \u0060DirtyRectManager\u0060 is the cornerstone of efficient rendering in OpenMaui. Instead of redrawing the entire window on every frame, dirty region tracking identifies the minimal set of rectangles that need repainting.\n\n## How Dirty Region Tracking Works\n\nWhen a view\u0027s visual properties change (color, size, position), it marks its bounding rectangle as \u0022dirty.\u0022 The rendering engine accumulates these dirty regions and, during the next frame, only redraws the affected areas.\n\n\u0060\u0060\u0060csharp\npublic class DirtyRectManager\n{\n private readonly List\u003CSKRect\u003E _dirtyRegions = new();\n private readonly object _lock = new();\n \n public void MarkDirty(SKRect rect)\n {\n lock (_lock)\n {\n _dirtyRegions.Add(rect);\n }\n }\n \n public void MarkDirty(double x, double y, double width, double height)\n {\n MarkDirty(new SKRect(\n (float)x,\n (float)y,\n (float)(x \u002B width),\n (float)(y \u002B height)\n ));\n }\n \n public SKRect[] GetDirtyRegions()\n {\n lock (_lock)\n {\n if (_dirtyRegions.Count == 0)\n return Array.Empty\u003CSKRect\u003E();\n \n // Optimize: merge overlapping rectangles\n var optimized = OptimizeDirtyRegions(_dirtyRegions);\n _dirtyRegions.Clear();\n \n return optimized;\n }\n }\n \n private SKRect[] OptimizeDirtyRegions(List\u003CSKRect\u003E regions)\n {\n if (regions.Count \u003C= 1)\n return regions.ToArray();\n \n var merged = new List\u003CSKRect\u003E();\n var sorted = regions.OrderBy(r =\u003E r.Left).ThenBy(r =\u003E r.Top).ToList();\n \n var current = sorted[0];\n \n for (int i = 1; i \u003C sorted.Count; i\u002B\u002B)\n {\n var next = sorted[i];\n \n // Check if rectangles overlap or are adjacent\n if (current.IntersectsWith(next) || AreAdjacent(current, next))\n {\n // Merge rectangles\n current = SKRect.Union(current, next);\n }\n else\n {\n merged.Add(current);\n current = next;\n }\n }\n \n merged.Add(current);\n return merged.ToArray();\n }\n \n private bool AreAdjacent(SKRect a, SKRect b)\n {\n const float threshold = 10f; // Pixels\n \n return Math.Abs(a.Right - b.Left) \u003C threshold ||\n Math.Abs(b.Right - a.Left) \u003C threshold ||\n Math.Abs(a.Bottom - b.Top) \u003C threshold ||\n Math.Abs(b.Bottom - a.Top) \u003C threshold;\n }\n \n public bool ShouldRenderFullFrame(SKRect[] dirtyRegions, SKSize windowSize)\n {\n if (dirtyRegions.Length == 0)\n return false;\n \n var totalDirtyArea = dirtyRegions.Sum(r =\u003E r.Width * r.Height);\n var windowArea = windowSize.Width * windowSize.Height;\n \n // If dirty area exceeds 60% of window, just redraw everything\n return (totalDirtyArea / windowArea) \u003E 0.6f;\n }\n}\n\u0060\u0060\u0060\n\n## Integrating with SkiaRenderingEngine\n\nThe rendering engine uses dirty regions to clip drawing operations:\n\n\u0060\u0060\u0060csharp\npublic class SkiaRenderingEngine\n{\n private readonly DirtyRectManager _dirtyRectManager = new();\n \n public void Render(SKCanvas canvas, SkiaView rootView)\n {\n var dirtyRegions = _dirtyRectManager.GetDirtyRegions();\n \n if (dirtyRegions.Length == 0)\n {\n // Nothing to render\n return;\n }\n \n var windowSize = new SKSize((float)rootView.Width, (float)rootView.Height);\n \n if (_dirtyRectManager.ShouldRenderFullFrame(dirtyRegions, windowSize))\n {\n // Full frame render\n RenderView(canvas, rootView, null);\n }\n else\n {\n // Partial render with clipping\n foreach (var dirtyRect in dirtyRegions)\n {\n canvas.Save();\n canvas.ClipRect(dirtyRect);\n \n RenderView(canvas, rootView, dirtyRect);\n \n canvas.Restore();\n }\n }\n }\n \n private void RenderView(SKCanvas canvas, SkiaView view, SKRect? clipRect)\n {\n if (!view.IsVisible)\n return;\n \n var viewBounds = new SKRect(\n (float)view.X,\n (float)view.Y,\n (float)(view.X \u002B view.Width),\n (float)(view.Y \u002B view.Height)\n );\n \n // Skip views outside dirty region\n if (clipRect.HasValue \u0026\u0026 !clipRect.Value.IntersectsWith(viewBounds))\n return;\n \n canvas.Save();\n canvas.Translate((float)view.X, (float)view.Y);\n \n view.Draw(canvas, (int)view.Width, (int)view.Height);\n \n // Recursively render children\n if (view is SkiaLayoutView layout)\n {\n foreach (var child in layout.Children.OfType\u003CSkiaView\u003E())\n {\n RenderView(canvas, child, clipRect);\n }\n }\n \n canvas.Restore();\n }\n}\n\u0060\u0060\u0060\n\n## View-Level Dirty Tracking\n\nViews should mark themselves dirty when visual properties change:\n\n\u0060\u0060\u0060csharp\npublic abstract class SkiaView : IView\n{\n private DirtyRectManager? _dirtyRectManager;\n \n public void AttachDirtyRectManager(DirtyRectManager manager)\n {\n _dirtyRectManager = manager;\n }\n \n protected void MarkDirty()\n {\n _dirtyRectManager?.MarkDirty(X, Y, Width, Height);\n }\n \n public Color BackgroundColor\n {\n get =\u003E _backgroundColor;\n set\n {\n if (_backgroundColor != value)\n {\n _backgroundColor = value;\n MarkDirty();\n OnPropertyChanged();\n }\n }\n }\n}\n\u0060\u0060\u0060\n\n## Layout Optimization\n\nLayouts should propagate dirty regions from children:\n\n\u0060\u0060\u0060csharp\npublic class SkiaStackLayout : SkiaLayoutView\n{\n protected override void OnChildPropertyChanged(SkiaView child, string propertyName)\n {\n base.OnChildPropertyChanged(child, propertyName);\n \n if (propertyName == nameof(child.Width) ||\n propertyName == nameof(child.Height))\n {\n // Child size changed, might affect layout\n InvalidateLayout();\n MarkDirty(); // Mark entire layout dirty\n }\n else\n {\n // Only child appearance changed\n _dirtyRectManager?.MarkDirty(\n child.X,\n child.Y,\n child.Width,\n child.Height\n );\n }\n }\n}\n\u0060\u0060\u0060\n\n## Performance Impact\n\nDirty region optimization provides dramatic performance improvements:\n\n- **Small UI updates** (single button click): 80-90% reduction in rendering time\n- **List scrolling**: 50-60% reduction by only rendering visible items\n- **Animations**: 30-40% reduction by clipping to animated region\n\n## Best Practices\n\n1. **Granular invalidation**: Only mark the minimal affected area dirty\n2. **Batch updates**: Accumulate property changes before invalidating\n3. **Avoid full-window invalidation**: Reserve for theme changes or window resizes\n4. **Profile dirty regions**: Log region counts and sizes to identify excessive invalidation\n5. **Optimize merging**: Adjacent dirty regions should be merged to reduce draw calls" + }, + { + "header": "GPU vs Software Rendering Trade-offs", + "content": "OpenMaui supports both GPU-accelerated rendering via OpenGL and software rendering via CPU-based rasterization. Choosing the right rendering mode is critical for optimal performance across different hardware configurations.\n\n## Understanding the Rendering Backends\n\n### GPU Rendering with OpenGL\n\nThe \u0060GpuRenderingEngine\u0060 uses SkiaSharp\u0027s OpenGL backend to leverage hardware acceleration:\n\n\u0060\u0060\u0060csharp\npublic class GpuRenderingEngine : IDisposable\n{\n private GRContext? _grContext;\n private SKSurface? _surface;\n \n public bool Initialize(IntPtr glContext, int width, int height)\n {\n try\n {\n var glInterface = GRGlInterface.Create();\n if (glInterface == null)\n {\n Console.WriteLine(\u0022Failed to create GL interface\u0022);\n return false;\n }\n \n _grContext = GRContext.CreateGl(glInterface);\n \n var frameBufferInfo = new GRGlFramebufferInfo(\n fboId: 0, // Default framebuffer\n format: SKColorType.Rgba8888.ToGlSizedFormat()\n );\n \n var backendRenderTarget = new GRBackendRenderTarget(\n width,\n height,\n sampleCount: 0,\n stencilBits: 8,\n frameBufferInfo\n );\n \n _surface = SKSurface.Create(\n _grContext,\n backendRenderTarget,\n GRSurfaceOrigin.BottomLeft,\n SKColorType.Rgba8888\n );\n \n return _surface != null;\n }\n catch (Exception ex)\n {\n Console.WriteLine($\u0022GPU initialization failed: {ex.Message}\u0022);\n return false;\n }\n }\n \n public void Render(Action\u003CSKCanvas\u003E drawAction)\n {\n if (_surface == null || _grContext == null)\n return;\n \n var canvas = _surface.Canvas;\n canvas.Clear(SKColors.White);\n \n drawAction(canvas);\n \n canvas.Flush();\n _grContext.Flush();\n }\n \n public void Dispose()\n {\n _surface?.Dispose();\n _grContext?.Dispose();\n }\n}\n\u0060\u0060\u0060\n\n### Software Rendering\n\nSoftware rendering uses CPU-based rasterization, which is more predictable but slower:\n\n\u0060\u0060\u0060csharp\npublic class SoftwareRenderingEngine : IDisposable\n{\n private SKBitmap? _bitmap;\n private SKCanvas? _canvas;\n \n public void Initialize(int width, int height)\n {\n _bitmap = new SKBitmap(width, height, SKColorType.Rgba8888, SKAlphaType.Premul);\n _canvas = new SKCanvas(_bitmap);\n }\n \n public void Render(Action\u003CSKCanvas\u003E drawAction)\n {\n if (_canvas == null)\n return;\n \n _canvas.Clear(SKColors.White);\n drawAction(_canvas);\n }\n \n public IntPtr GetPixelData()\n {\n return _bitmap?.GetPixels() ?? IntPtr.Zero;\n }\n \n public void Dispose()\n {\n _canvas?.Dispose();\n _bitmap?.Dispose();\n }\n}\n\u0060\u0060\u0060\n\n## Performance Characteristics\n\n### GPU Rendering Advantages\n\n- **Complex vector graphics**: 5-10x faster for paths, gradients, and effects\n- **Large canvases**: Scales better with resolution (4K displays)\n- **Animations**: Smooth 60 FPS even with many moving elements\n- **Filters and effects**: Blur, shadows, and image filters are hardware-accelerated\n\n### Software Rendering Advantages\n\n- **Predictable performance**: No driver issues or GPU quirks\n- **Lower latency**: No GPU context switching overhead\n- **Better for simple UIs**: Faster for basic rectangles and text\n- **Compatibility**: Works on all systems, including virtual machines\n\n## Adaptive Rendering Strategy\n\nImplement runtime selection based on content complexity:\n\n\u0060\u0060\u0060csharp\npublic class AdaptiveRenderingEngine\n{\n private readonly GpuRenderingEngine _gpuEngine;\n private readonly SoftwareRenderingEngine _softwareEngine;\n private bool _useGpu;\n private int _complexityScore;\n \n public AdaptiveRenderingEngine()\n {\n _gpuEngine = new GpuRenderingEngine();\n _softwareEngine = new SoftwareRenderingEngine();\n _useGpu = TryInitializeGpu();\n }\n \n private bool TryInitializeGpu()\n {\n try\n {\n return _gpuEngine.Initialize(GetGLContext(), 1920, 1080);\n }\n catch\n {\n return false;\n }\n }\n \n public void Render(SkiaView rootView, Action\u003CSKCanvas\u003E drawAction)\n {\n // Analyze scene complexity\n _complexityScore = CalculateComplexity(rootView);\n \n // Use GPU for complex scenes, software for simple ones\n var shouldUseGpu = _useGpu \u0026\u0026 _complexityScore \u003E 100;\n \n if (shouldUseGpu)\n {\n _gpuEngine.Render(drawAction);\n }\n else\n {\n _softwareEngine.Render(drawAction);\n }\n }\n \n private int CalculateComplexity(SkiaView view)\n {\n int score = 0;\n \n // Complex rendering operations increase score\n if (view is SkiaImage) score \u002B= 20;\n if (view is SkiaGraphicsView) score \u002B= 50;\n if (view.HasShadow) score \u002B= 10;\n if (view.HasGradient) score \u002B= 15;\n \n if (view is SkiaLayoutView layout)\n {\n foreach (var child in layout.Children.OfType\u003CSkiaView\u003E())\n {\n score \u002B= CalculateComplexity(child);\n }\n }\n \n return score;\n }\n}\n\u0060\u0060\u0060\n\n## GPU Acceleration Tuning\n\nOptimize GPU rendering with these techniques:\n\n\u0060\u0060\u0060csharp\npublic class GpuOptimizationSettings\n{\n // Enable MSAA for smoother edges (costs performance)\n public int SampleCount { get; set; } = 4;\n \n // Cache GPU resources between frames\n public bool EnableResourceCaching { get; set; } = true;\n \n // Use GPU texture atlas for images\n public bool UseTextureAtlas { get; set; } = true;\n \n public void ApplyToContext(GRContext context)\n {\n // Set resource cache limits\n if (EnableResourceCaching)\n {\n context.SetResourceCacheLimit(256 * 1024 * 1024); // 256 MB\n }\n \n // Purge unused resources periodically\n context.PurgeUnlockedResources();\n }\n}\n\u0060\u0060\u0060\n\n## Detecting GPU Capabilities\n\nQuery GPU capabilities to make informed decisions:\n\n\u0060\u0060\u0060csharp\npublic class GpuCapabilities\n{\n public static GpuInfo DetectCapabilities()\n {\n var glInterface = GRGlInterface.Create();\n if (glInterface == null)\n {\n return new GpuInfo { Available = false };\n }\n \n using var context = GRContext.CreateGl(glInterface);\n \n return new GpuInfo\n {\n Available = true,\n MaxTextureSize = context.GetMaxTextureSize(),\n Vendor = GetGLString(GL_VENDOR),\n Renderer = GetGLString(GL_RENDERER),\n Version = GetGLString(GL_VERSION)\n };\n }\n \n [DllImport(\u0022libGL.so.1\u0022)]\n private static extern IntPtr glGetString(uint name);\n \n private static string GetGLString(uint name)\n {\n var ptr = glGetString(name);\n return Marshal.PtrToStringAnsi(ptr) ?? \u0022Unknown\u0022;\n }\n \n private const uint GL_VENDOR = 0x1F00;\n private const uint GL_RENDERER = 0x1F01;\n private const uint GL_VERSION = 0x1F02;\n}\n\npublic class GpuInfo\n{\n public bool Available { get; set; }\n public int MaxTextureSize { get; set; }\n public string Vendor { get; set; } = \u0022\u0022;\n public string Renderer { get; set; } = \u0022\u0022;\n public string Version { get; set; } = \u0022\u0022;\n \n public bool IsIntelIntegrated =\u003E \n Vendor.Contains(\u0022Intel\u0022, StringComparison.OrdinalIgnoreCase);\n \n public bool IsNvidia =\u003E \n Vendor.Contains(\u0022NVIDIA\u0022, StringComparison.OrdinalIgnoreCase);\n \n public bool IsAMD =\u003E \n Vendor.Contains(\u0022AMD\u0022, StringComparison.OrdinalIgnoreCase) ||\n Vendor.Contains(\u0022ATI\u0022, StringComparison.OrdinalIgnoreCase);\n}\n\u0060\u0060\u0060\n\n## Decision Matrix\n\n| Scenario | Recommended Backend | Reason |\n|----------|-------------------|--------|\n| Simple business forms | Software | Lower overhead for basic shapes |\n| Image-heavy applications | GPU | Hardware-accelerated image compositing |\n| Data visualization | GPU | Complex paths and gradients |\n| Text-heavy UIs | Software | Font rendering is fast on CPU |\n| Animations | GPU | Smooth 60 FPS with hardware acceleration |\n| Virtual machines | Software | GPU passthrough often unavailable |\n| Low-end hardware | Software | Integrated GPUs may be slower |\n\n## Fallback Strategy\n\nAlways implement graceful fallback:\n\n\u0060\u0060\u0060csharp\npublic class RenderingEngineFactory\n{\n public static IRenderingEngine Create()\n {\n // Try GPU first\n var gpuEngine = new GpuRenderingEngine();\n if (gpuEngine.Initialize(GetGLContext(), 1920, 1080))\n {\n Console.WriteLine(\u0022\u2713 Using GPU rendering\u0022);\n return gpuEngine;\n }\n \n // Fallback to software\n Console.WriteLine(\u0022\u26A0 GPU unavailable, using software rendering\u0022);\n var softwareEngine = new SoftwareRenderingEngine();\n softwareEngine.Initialize(1920, 1080);\n return softwareEngine;\n }\n}\n\u0060\u0060\u0060\n\nThe key is measuring real-world performance on your target hardware and choosing the backend that delivers the best user experience." + }, + { + "header": "Memory Management with P/Invoke", + "content": "OpenMaui\u0027s Linux implementation relies heavily on P/Invoke for native interop with GTK, X11, and Wayland. Improper memory management in these scenarios leads to leaks, crashes, and degraded performance. This section covers essential patterns for safe native resource handling.\n\n## Understanding Native Memory Lifecycle\n\nWhen you call native functions, you\u0027re crossing the managed/unmanaged boundary. The .NET garbage collector doesn\u0027t track native allocations, so you must manually manage their lifecycle.\n\n## SafeHandle Pattern for Native Resources\n\nUse \u0060SafeHandle\u0060 for automatic cleanup of native resources:\n\n\u0060\u0060\u0060csharp\npublic class GtkWidgetHandle : SafeHandle\n{\n public GtkWidgetHandle() : base(IntPtr.Zero, true)\n {\n }\n \n public GtkWidgetHandle(IntPtr handle) : base(IntPtr.Zero, true)\n {\n SetHandle(handle);\n }\n \n public override bool IsInvalid =\u003E handle == IntPtr.Zero;\n \n protected override bool ReleaseHandle()\n {\n if (!IsInvalid)\n {\n // Properly unreference GTK widget\n gtk_widget_destroy(handle);\n g_object_unref(handle);\n }\n return true;\n }\n \n [DllImport(\u0022libgtk-3.so.0\u0022)]\n private static extern void gtk_widget_destroy(IntPtr widget);\n \n [DllImport(\u0022libgobject-2.0.so.0\u0022)]\n private static extern void g_object_unref(IntPtr obj);\n}\n\u0060\u0060\u0060\n\nUse it in your code:\n\n\u0060\u0060\u0060csharp\npublic class GtkButton : IDisposable\n{\n private readonly GtkWidgetHandle _handle;\n \n public GtkButton(string label)\n {\n var labelPtr = Marshal.StringToHGlobalAnsi(label);\n try\n {\n var widget = gtk_button_new_with_label(labelPtr);\n _handle = new GtkWidgetHandle(widget);\n }\n finally\n {\n Marshal.FreeHGlobal(labelPtr);\n }\n }\n \n public void Dispose()\n {\n _handle?.Dispose();\n }\n \n [DllImport(\u0022libgtk-3.so.0\u0022)]\n private static extern IntPtr gtk_button_new_with_label(IntPtr label);\n}\n\u0060\u0060\u0060\n\n## String Marshaling\n\nString conversions are a common source of memory leaks:\n\n\u0060\u0060\u0060csharp\npublic static class StringMarshalHelper\n{\n // Allocate UTF-8 string for native code\n public static IntPtr StringToUtf8(string str)\n {\n if (str == null)\n return IntPtr.Zero;\n \n var bytes = Encoding.UTF8.GetByteCount(str);\n var ptr = Marshal.AllocHGlobal(bytes \u002B 1);\n \n unsafe\n {\n fixed (char* chars = str)\n {\n var bytePtr = (byte*)ptr;\n Encoding.UTF8.GetBytes(chars, str.Length, bytePtr, bytes);\n bytePtr[bytes] = 0; // Null terminator\n }\n }\n \n return ptr;\n }\n \n // Convert native UTF-8 string to managed\n public static string? Utf8ToString(IntPtr ptr)\n {\n if (ptr == IntPtr.Zero)\n return null;\n \n var length = 0;\n unsafe\n {\n var bytePtr = (byte*)ptr;\n while (bytePtr[length] != 0)\n length\u002B\u002B;\n }\n \n var bytes = new byte[length];\n Marshal.Copy(ptr, bytes, 0, length);\n \n return Encoding.UTF8.GetString(bytes);\n }\n \n // Use for temporary strings (auto-freed)\n public static void WithUtf8String(string str, Action\u003CIntPtr\u003E action)\n {\n var ptr = StringToUtf8(str);\n try\n {\n action(ptr);\n }\n finally\n {\n Marshal.FreeHGlobal(ptr);\n }\n }\n}\n\u0060\u0060\u0060\n\nUsage:\n\n\u0060\u0060\u0060csharp\npublic void SetWindowTitle(string title)\n{\n StringMarshalHelper.WithUtf8String(title, titlePtr =\u003E\n {\n gtk_window_set_title(_windowHandle, titlePtr);\n });\n}\n\u0060\u0060\u0060\n\n## Structure Marshaling\n\nFor complex structures, use explicit layout:\n\n\u0060\u0060\u0060csharp\n[StructLayout(LayoutKind.Sequential)]\npublic struct XEvent\n{\n public int type;\n public IntPtr serial;\n public int send_event;\n public IntPtr display;\n public IntPtr window;\n // ... more fields\n}\n\npublic static class X11Interop\n{\n public static XEvent ReadEvent(IntPtr display)\n {\n XEvent evt;\n XNextEvent(display, out evt);\n return evt;\n }\n \n [DllImport(\u0022libX11.so.6\u0022)]\n private static extern void XNextEvent(IntPtr display, out XEvent evt);\n}\n\u0060\u0060\u0060\n\n## Callback Memory Management\n\nCallbacks from native code require special handling:\n\n\u0060\u0060\u0060csharp\npublic class GtkEventHandler\n{\n // Keep delegate alive to prevent GC collection\n private readonly GtkCallback _callback;\n private GCHandle _callbackHandle;\n \n public GtkEventHandler()\n {\n _callback = OnGtkEvent;\n _callbackHandle = GCHandle.Alloc(_callback);\n }\n \n public void Connect(IntPtr widget, string signal)\n {\n StringMarshalHelper.WithUtf8String(signal, signalPtr =\u003E\n {\n var callbackPtr = Marshal.GetFunctionPointerForDelegate(_callback);\n g_signal_connect_data(\n widget,\n signalPtr,\n callbackPtr,\n IntPtr.Zero,\n IntPtr.Zero,\n 0\n );\n });\n }\n \n private void OnGtkEvent(IntPtr widget, IntPtr userData)\n {\n // Handle event\n }\n \n public void Dispose()\n {\n if (_callbackHandle.IsAllocated)\n {\n _callbackHandle.Free();\n }\n }\n \n [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n private delegate void GtkCallback(IntPtr widget, IntPtr userData);\n \n [DllImport(\u0022libgobject-2.0.so.0\u0022)]\n private static extern ulong g_signal_connect_data(\n IntPtr instance,\n IntPtr signal,\n IntPtr callback,\n IntPtr data,\n IntPtr destroyData,\n int connectFlags\n );\n}\n\u0060\u0060\u0060\n\n## Memory Pool for Frequent Allocations\n\nReduce allocation overhead with pooling:\n\n\u0060\u0060\u0060csharp\npublic class NativeMemoryPool\n{\n private readonly ConcurrentBag\u003CIntPtr\u003E _pool = new();\n private readonly int _blockSize;\n private readonly int _maxPoolSize;\n \n public NativeMemoryPool(int blockSize, int maxPoolSize = 100)\n {\n _blockSize = blockSize;\n _maxPoolSize = maxPoolSize;\n }\n \n public IntPtr Rent()\n {\n if (_pool.TryTake(out var ptr))\n {\n // Reuse pooled memory\n return ptr;\n }\n \n // Allocate new block\n return Marshal.AllocHGlobal(_blockSize);\n }\n \n public void Return(IntPtr ptr)\n {\n if (_pool.Count \u003C _maxPoolSize)\n {\n // Return to pool\n _pool.Add(ptr);\n }\n else\n {\n // Pool full, free immediately\n Marshal.FreeHGlobal(ptr);\n }\n }\n \n public void Clear()\n {\n while (_pool.TryTake(out var ptr))\n {\n Marshal.FreeHGlobal(ptr);\n }\n }\n}\n\u0060\u0060\u0060\n\n## Detecting Memory Leaks\n\nImplement diagnostics to track native allocations:\n\n\u0060\u0060\u0060csharp\npublic static class NativeMemoryTracker\n{\n private static readonly ConcurrentDictionary\u003CIntPtr, AllocationInfo\u003E _allocations = new();\n \n private record AllocationInfo(int Size, string StackTrace, DateTime Timestamp);\n \n public static IntPtr Allocate(int size, [CallerMemberName] string caller = \u0022\u0022)\n {\n var ptr = Marshal.AllocHGlobal(size);\n \n _allocations[ptr] = new AllocationInfo(\n size,\n Environment.StackTrace,\n DateTime.UtcNow\n );\n \n return ptr;\n }\n \n public static void Free(IntPtr ptr)\n {\n _allocations.TryRemove(ptr, out _);\n Marshal.FreeHGlobal(ptr);\n }\n \n public static void ReportLeaks()\n {\n var leaks = _allocations.ToArray();\n \n if (leaks.Length == 0)\n {\n Console.WriteLine(\u0022\u2713 No memory leaks detected\u0022);\n return;\n }\n \n Console.WriteLine($\u0022\u26A0 {leaks.Length} potential leaks detected:\u0022);\n \n foreach (var (ptr, info) in leaks)\n {\n var age = DateTime.UtcNow - info.Timestamp;\n Console.WriteLine($\u0022 Ptr: {ptr:X}, Size: {info.Size} bytes, Age: {age.TotalSeconds:F1}s\u0022);\n Console.WriteLine($\u0022 Allocated at:\\n{info.StackTrace}\u0022);\n }\n }\n}\n\u0060\u0060\u0060\n\n## Best Practices\n\n1. **Always use SafeHandle** for native resources that need cleanup\n2. **Free strings immediately** after passing to native code\n3. **Pin delegates** passed to native callbacks to prevent GC collection\n4. **Use \u0060using\u0060 statements** for IDisposable wrappers\n5. **Implement finalizers** as a safety net (but don\u0027t rely on them)\n6. **Profile native allocations** regularly during development\n7. **Document ownership** - who is responsible for freeing each allocation\n\n## Example: Complete Native Resource Wrapper\n\n\u0060\u0060\u0060csharp\npublic class X11Window : IDisposable\n{\n private readonly SafeX11DisplayHandle _display;\n private readonly SafeX11WindowHandle _window;\n private bool _disposed;\n \n public X11Window(string title, int width, int height)\n {\n _display = SafeX11DisplayHandle.Open();\n _window = CreateWindow(_display, title, width, height);\n }\n \n private SafeX11WindowHandle CreateWindow(\n SafeX11DisplayHandle display,\n string title,\n int width,\n int height)\n {\n var window = XCreateSimpleWindow(\n display.DangerousGetHandle(),\n XDefaultRootWindow(display.DangerousGetHandle()),\n 0, 0, width, height, 0, 0, 0\n );\n \n StringMarshalHelper.WithUtf8String(title, titlePtr =\u003E\n {\n XStoreName(display.DangerousGetHandle(), window, titlePtr);\n });\n \n return new SafeX11WindowHandle(display, window);\n }\n \n public void Dispose()\n {\n if (_disposed)\n return;\n \n _window?.Dispose();\n _display?.Dispose();\n \n _disposed = true;\n GC.SuppressFinalize(this);\n }\n \n ~X11Window()\n {\n Dispose();\n }\n}\n\u0060\u0060\u0060\n\nProper P/Invoke memory management is non-negotiable for production applications. The patterns shown here prevent the silent memory leaks that plague many native interop scenarios." + }, + { + "header": "LayeredRenderer for Complex UIs", + "content": "Complex user interfaces with overlapping elements, transparency, and z-ordering require sophisticated rendering strategies. The \u0060LayeredRenderer\u0060 pattern separates UI elements into distinct layers, enabling efficient compositing and advanced visual effects.\n\n## Understanding Layer-Based Rendering\n\nTraditional immediate-mode rendering draws all UI elements in a single pass. Layer-based rendering separates elements into independent surfaces that can be:\n\n- Rendered independently at different rates\n- Cached and reused across frames\n- Composited with blend modes and opacity\n- Reordered without redrawing\n\n## Implementing LayeredRenderer\n\n\u0060\u0060\u0060csharp\npublic class LayeredRenderer : IDisposable\n{\n private readonly List\u003CRenderLayer\u003E _layers = new();\n private readonly SKPaint _compositePaint = new();\n \n public class RenderLayer : IDisposable\n {\n public string Name { get; set; } = \u0022\u0022;\n public int ZIndex { get; set; }\n public SKBitmap? Surface { get; set; }\n public SKCanvas? Canvas { get; set; }\n public float Opacity { get; set; } = 1.0f;\n public SKBlendMode BlendMode { get; set; } = SKBlendMode.SrcOver;\n public bool IsDirty { get; set; } = true;\n public bool IsVisible { get; set; } = true;\n public Action\u003CSKCanvas, int, int\u003E? DrawAction { get; set; }\n \n public void Initialize(int width, int height)\n {\n Surface?.Dispose();\n Canvas?.Dispose();\n \n Surface = new SKBitmap(width, height, SKColorType.Rgba8888, SKAlphaType.Premul);\n Canvas = new SKCanvas(Surface);\n }\n \n public void Render(int width, int height)\n {\n if (!IsDirty || Canvas == null || DrawAction == null)\n return;\n \n Canvas.Clear(SKColors.Transparent);\n DrawAction(Canvas, width, height);\n IsDirty = false;\n }\n \n public void Dispose()\n {\n Canvas?.Dispose();\n Surface?.Dispose();\n }\n }\n \n public RenderLayer AddLayer(string name, int zIndex)\n {\n var layer = new RenderLayer\n {\n Name = name,\n ZIndex = zIndex\n };\n \n _layers.Add(layer);\n _layers.Sort((a, b) =\u003E a.ZIndex.CompareTo(b.ZIndex));\n \n return layer;\n }\n \n public void RemoveLayer(string name)\n {\n var layer = _layers.FirstOrDefault(l =\u003E l.Name == name);\n if (layer != null)\n {\n layer.Dispose();\n _layers.Remove(layer);\n }\n }\n \n public RenderLayer? GetLayer(string name)\n {\n return _layers.FirstOrDefault(l =\u003E l.Name == name);\n }\n \n public void InvalidateLayer(string name)\n {\n var layer = GetLayer(name);\n if (layer != null)\n {\n layer.IsDirty = true;\n }\n }\n \n public void Resize(int width, int height)\n {\n foreach (var layer in _layers)\n {\n layer.Initialize(width, height);\n layer.IsDirty = true;\n }\n }\n \n public void Render(SKCanvas targetCanvas, int width, int height)\n {\n // Render each dirty layer\n foreach (var layer in _layers)\n {\n if (layer.IsDirty)\n {\n layer.Render(width, height);\n }\n }\n \n // Composite layers onto target canvas\n targetCanvas.Clear(SKColors.White);\n \n foreach (var layer in _layers)\n {\n if (!layer.IsVisible || layer.Surface == null)\n continue;\n \n _compositePaint.Color = new SKColor(255, 255, 255, (byte)(layer.Opacity * 255));\n _compositePaint.BlendMode = layer.BlendMode;\n \n targetCanvas.DrawBitmap(layer.Surface, 0, 0, _compositePaint);\n }\n }\n \n public void Dispose()\n {\n foreach (var layer in _layers)\n {\n layer.Dispose();\n }\n _layers.Clear();\n _compositePaint.Dispose();\n }\n}\n\u0060\u0060\u0060\n\n## Practical Usage: Complex UI Application\n\n\u0060\u0060\u0060csharp\npublic class ComplexUIRenderer\n{\n private readonly LayeredRenderer _renderer = new();\n private LayeredRenderer.RenderLayer _backgroundLayer;\n private LayeredRenderer.RenderLayer _contentLayer;\n private LayeredRenderer.RenderLayer _overlayLayer;\n private LayeredRenderer.RenderLayer _cursorLayer;\n \n public void Initialize(int width, int height)\n {\n // Background layer (static, rarely changes)\n _backgroundLayer = _renderer.AddLayer(\u0022background\u0022, 0);\n _backgroundLayer.DrawAction = DrawBackground;\n _backgroundLayer.Initialize(width, height);\n \n // Content layer (main UI, moderate updates)\n _contentLayer = _renderer.AddLayer(\u0022content\u0022, 10);\n _contentLayer.DrawAction = DrawContent;\n _contentLayer.Initialize(width, height);\n \n // Overlay layer (dialogs, tooltips)\n _overlayLayer = _renderer.AddLayer(\u0022overlay\u0022, 20);\n _overlayLayer.DrawAction = DrawOverlay;\n _overlayLayer.Opacity = 0.95f;\n _overlayLayer.Initialize(width, height);\n \n // Cursor layer (updates every frame)\n _cursorLayer = _renderer.AddLayer(\u0022cursor\u0022, 30);\n _cursorLayer.DrawAction = DrawCursor;\n _cursorLayer.Initialize(width, height);\n }\n \n private void DrawBackground(SKCanvas canvas, int width, int height)\n {\n // Draw gradient background\n using var paint = new SKPaint\n {\n Shader = SKShader.CreateLinearGradient(\n new SKPoint(0, 0),\n new SKPoint(0, height),\n new[] { new SKColor(240, 240, 245), new SKColor(220, 220, 230) },\n SKShaderTileMode.Clamp\n )\n };\n \n canvas.DrawRect(0, 0, width, height, paint);\n }\n \n private void DrawContent(SKCanvas canvas, int width, int height)\n {\n // Draw main UI content (buttons, labels, etc.)\n // This layer invalidates when UI state changes\n }\n \n private void DrawOverlay(SKCanvas canvas, int width, int height)\n {\n // Draw modal dialogs, context menus\n if (_showDialog)\n {\n DrawDialog(canvas, width, height);\n }\n }\n \n private void DrawCursor(SKCanvas canvas, int width, int height)\n {\n // Draw custom cursor\n using var paint = new SKPaint\n {\n Color = SKColors.Black,\n IsAntialias = true\n };\n \n canvas.DrawCircle(_cursorX, _cursorY, 5, paint);\n }\n \n public void OnMouseMove(float x, float y)\n {\n _cursorX = x;\n _cursorY = y;\n \n // Only cursor layer needs redraw\n _renderer.InvalidateLayer(\u0022cursor\u0022);\n }\n \n public void OnContentChanged()\n {\n // Content changed, but background and cursor remain valid\n _renderer.InvalidateLayer(\u0022content\u0022);\n }\n \n public void ShowDialog()\n {\n _showDialog = true;\n _renderer.InvalidateLayer(\u0022overlay\u0022);\n }\n}\n\u0060\u0060\u0060\n\n## Advanced: Animated Layers\n\nImplement smooth animations with layer opacity and transforms:\n\n\u0060\u0060\u0060csharp\npublic class AnimatedLayerController\n{\n private readonly LayeredRenderer _renderer;\n private readonly Dictionary\u003Cstring, Animation\u003E _animations = new();\n \n public class Animation\n {\n public float StartOpacity { get; set; }\n public float EndOpacity { get; set; }\n public TimeSpan Duration { get; set; }\n public DateTime StartTime { get; set; }\n public Action? OnComplete { get; set; }\n }\n \n public AnimatedLayerController(LayeredRenderer renderer)\n {\n _renderer = renderer;\n }\n \n public void FadeIn(string layerName, TimeSpan duration, Action? onComplete = null)\n {\n var layer = _renderer.GetLayer(layerName);\n if (layer == null) return;\n \n layer.Opacity = 0;\n layer.IsVisible = true;\n \n _animations[layerName] = new Animation\n {\n StartOpacity = 0,\n EndOpacity = 1,\n Duration = duration,\n StartTime = DateTime.UtcNow,\n OnComplete = onComplete\n };\n }\n \n public void FadeOut(string layerName, TimeSpan duration, Action? onComplete = null)\n {\n var layer = _renderer.GetLayer(layerName);\n if (layer == null) return;\n \n _animations[layerName] = new Animation\n {\n StartOpacity = layer.Opacity,\n EndOpacity = 0,\n Duration = duration,\n StartTime = DateTime.UtcNow,\n OnComplete = () =\u003E\n {\n layer.IsVisible = false;\n onComplete?.Invoke();\n }\n };\n }\n \n public void Update()\n {\n var now = DateTime.UtcNow;\n var completed = new List\u003Cstring\u003E();\n \n foreach (var (layerName, animation) in _animations)\n {\n var layer = _renderer.GetLayer(layerName);\n if (layer == null)\n {\n completed.Add(layerName);\n continue;\n }\n \n var elapsed = now - animation.StartTime;\n var progress = Math.Min(1.0, elapsed.TotalSeconds / animation.Duration.TotalSeconds);\n \n // Ease-out cubic\n var eased = 1 - Math.Pow(1 - progress, 3);\n \n layer.Opacity = (float)(animation.StartOpacity \u002B \n (animation.EndOpacity - animation.StartOpacity) * eased);\n \n if (progress \u003E= 1.0)\n {\n animation.OnComplete?.Invoke();\n completed.Add(layerName);\n }\n }\n \n foreach (var layerName in completed)\n {\n _animations.Remove(layerName);\n }\n }\n}\n\u0060\u0060\u0060\n\n## Performance Benefits\n\nLayered rendering provides substantial performance improvements:\n\n- **Selective rendering**: Only dirty layers are redrawn (50-80% reduction in draw calls)\n- **Caching**: Static layers rendered once and reused (90% reduction for backgrounds)\n- **Parallelization**: Layers can be rendered on separate threads\n- **Compositing effects**: Hardware-accelerated blending and opacity\n\n## Use Cases\n\n1. **Gaming UIs**: Separate background, game world, UI overlay, and cursor\n2. **Image editors**: Background, canvas, tools, selection overlay\n3. **Data dashboards**: Static chrome, updating charts, overlay alerts\n4. **Video players**: Video layer, controls layer, subtitle layer\n\n## Best Practices\n\n1. **Minimize layer count**: Each layer has memory overhead\n2. **Group related elements**: Don\u0027t create a layer for every control\n3. **Invalidate granularly**: Only mark changed layers as dirty\n4. **Use appropriate z-index spacing**: Leave room for insertion (0, 10, 20, 30...)\n5. **Profile memory usage**: Each layer allocates width \u00D7 height \u00D7 4 bytes\n6. **Dispose properly**: Layers hold unmanaged bitmap resources\n\nLayered rendering transforms complex UI scenarios from performance nightmares into smooth, efficient experiences." + }, + { + "header": "Benchmarking and Best Practices", + "content": "Effective performance optimization requires rigorous benchmarking and adherence to proven best practices. This section provides a comprehensive framework for measuring and improving OpenMaui application performance.\n\n## Comprehensive Benchmarking Framework\n\n\u0060\u0060\u0060csharp\npublic class PerformanceBenchmark\n{\n private readonly Dictionary\u003Cstring, List\u003Cdouble\u003E\u003E _measurements = new();\n private readonly Stopwatch _stopwatch = new();\n \n public IDisposable Measure(string operation)\n {\n return new MeasurementScope(this, operation);\n }\n \n private class MeasurementScope : IDisposable\n {\n private readonly PerformanceBenchmark _benchmark;\n private readonly string _operation;\n private readonly Stopwatch _sw;\n \n public MeasurementScope(PerformanceBenchmark benchmark, string operation)\n {\n _benchmark = benchmark;\n _operation = operation;\n _sw = Stopwatch.StartNew();\n }\n \n public void Dispose()\n {\n _sw.Stop();\n _benchmark.RecordMeasurement(_operation, _sw.Elapsed.TotalMilliseconds);\n }\n }\n \n private void RecordMeasurement(string operation, double milliseconds)\n {\n if (!_measurements.ContainsKey(operation))\n {\n _measurements[operation] = new List\u003Cdouble\u003E();\n }\n \n _measurements[operation].Add(milliseconds);\n }\n \n public BenchmarkReport GenerateReport()\n {\n var report = new BenchmarkReport();\n \n foreach (var (operation, measurements) in _measurements)\n {\n var stats = new BenchmarkStats\n {\n Operation = operation,\n Count = measurements.Count,\n Min = measurements.Min(),\n Max = measurements.Max(),\n Average = measurements.Average(),\n Median = CalculateMedian(measurements),\n P95 = CalculatePercentile(measurements, 0.95),\n P99 = CalculatePercentile(measurements, 0.99)\n };\n \n report.Stats.Add(stats);\n }\n \n return report;\n }\n \n private double CalculateMedian(List\u003Cdouble\u003E values)\n {\n var sorted = values.OrderBy(v =\u003E v).ToList();\n var mid = sorted.Count / 2;\n \n if (sorted.Count % 2 == 0)\n return (sorted[mid - 1] \u002B sorted[mid]) / 2.0;\n else\n return sorted[mid];\n }\n \n private double CalculatePercentile(List\u003Cdouble\u003E values, double percentile)\n {\n var sorted = values.OrderBy(v =\u003E v).ToList();\n var index = (int)Math.Ceiling(percentile * sorted.Count) - 1;\n return sorted[Math.Max(0, index)];\n }\n}\n\npublic class BenchmarkReport\n{\n public List\u003CBenchmarkStats\u003E Stats { get; } = new();\n \n public void Print()\n {\n Console.WriteLine(\u0022\\n=== Performance Benchmark Report ===\u0022);\n Console.WriteLine($\u0022{{\u0022Operation\u0022,-30}} {{\u0022Count\u0022,8}} {{\u0022Min\u0022,10}} {{\u0022Avg\u0022,10}} {{\u0022Median\u0022,10}} {{\u0022P95\u0022,10}} {{\u0022P99\u0022,10}} {{\u0022Max\u0022,10}}\u0022);\n Console.WriteLine(new string(\u0027-\u0027, 110));\n \n foreach (var stat in Stats.OrderByDescending(s =\u003E s.Average))\n {\n Console.WriteLine(\n $\u0022{stat.Operation,-30} {stat.Count,8} {stat.Min,10:F2} {stat.Average,10:F2} \u0022 \u002B\n $\u0022{stat.Median,10:F2} {stat.P95,10:F2} {stat.P99,10:F2} {stat.Max,10:F2}\u0022\n );\n }\n }\n}\n\npublic class BenchmarkStats\n{\n public string Operation { get; set; } = \u0022\u0022;\n public int Count { get; set; }\n public double Min { get; set; }\n public double Max { get; set; }\n public double Average { get; set; }\n public double Median { get; set; }\n public double P95 { get; set; }\n public double P99 { get; set; }\n}\n\u0060\u0060\u0060\n\n## Real-World Benchmark Suite\n\n\u0060\u0060\u0060csharp\npublic class OpenMauiBenchmarkSuite\n{\n private readonly PerformanceBenchmark _benchmark = new();\n \n public void RunAllBenchmarks()\n {\n BenchmarkRendering();\n BenchmarkLayout();\n BenchmarkTextShaping();\n BenchmarkImageLoading();\n BenchmarkCollectionView();\n \n _benchmark.GenerateReport().Print();\n }\n \n private void BenchmarkRendering()\n {\n var view = new SkiaButton { Text = \u0022Test Button\u0022, Width = 200, Height = 50 };\n var surface = SKSurface.Create(new SKImageInfo(200, 50));\n var canvas = surface.Canvas;\n \n // Warmup\n for (int i = 0; i \u003C 10; i\u002B\u002B)\n view.Draw(canvas, 200, 50);\n \n // Benchmark\n for (int i = 0; i \u003C 1000; i\u002B\u002B)\n {\n using (_benchmark.Measure(\u0022Button Render\u0022))\n {\n view.Draw(canvas, 200, 50);\n }\n }\n }\n \n private void BenchmarkLayout()\n {\n var grid = new SkiaGrid\n {\n RowDefinitions = new RowDefinitionCollection\n {\n new RowDefinition { Height = GridLength.Auto },\n new RowDefinition { Height = GridLength.Star },\n new RowDefinition { Height = new GridLength(100) }\n },\n ColumnDefinitions = new ColumnDefinitionCollection\n {\n new ColumnDefinition { Width = new GridLength(200) },\n new ColumnDefinition { Width = GridLength.Star }\n }\n };\n \n // Add children\n for (int i = 0; i \u003C 6; i\u002B\u002B)\n {\n grid.Children.Add(new SkiaLabel { Text = $\u0022Label {i}\u0022 });\n }\n \n // Benchmark layout\n for (int i = 0; i \u003C 1000; i\u002B\u002B)\n {\n using (_benchmark.Measure(\u0022Grid Layout\u0022))\n {\n grid.Measure(800, 600);\n grid.Arrange(new Rect(0, 0, 800, 600));\n }\n }\n }\n \n private void BenchmarkTextShaping()\n {\n var texts = new[]\n {\n \u0022Simple ASCII text\u0022,\n \u0022Unicode: \u00E9mojis \uD83C\uDF89\uD83D\uDE80\uD83D\uDCBB\u0022,\n \u0022CJK: \u65E5\u672C\u8A9E\u306E\u30C6\u30AD\u30B9\u30C8\u0022,\n \u0022Arabic: \u0627\u0644\u0646\u0635 \u0627\u0644\u0639\u0631\u0628\u064A\u0022\n };\n \n using var paint = new SKPaint\n {\n Typeface = SKTypeface.Default,\n TextSize = 14\n };\n \n foreach (var text in texts)\n {\n for (int i = 0; i \u003C 500; i\u002B\u002B)\n {\n using (_benchmark.Measure($\u0022Text Shape: {text.Substring(0, Math.Min(10, text.Length))}\u0022))\n {\n var blob = SKTextBlob.Create(text, paint.ToFont());\n blob.Dispose();\n }\n }\n }\n }\n \n private void BenchmarkImageLoading()\n {\n var testImagePath = \u0022test_image.png\u0022;\n \n for (int i = 0; i \u003C 100; i\u002B\u002B)\n {\n using (_benchmark.Measure(\u0022Image Decode\u0022))\n {\n using var bitmap = SKBitmap.Decode(testImagePath);\n }\n }\n }\n \n private void BenchmarkCollectionView()\n {\n var collectionView = new SkiaCollectionView\n {\n Width = 400,\n Height = 600\n };\n \n var items = Enumerable.Range(0, 1000)\n .Select(i =\u003E new { Text = $\u0022Item {i}\u0022, Value = i })\n .ToList();\n \n using (_benchmark.Measure(\u0022CollectionView Bind\u0022))\n {\n collectionView.ItemsSource = items;\n }\n \n for (int i = 0; i \u003C 100; i\u002B\u002B)\n {\n using (_benchmark.Measure(\u0022CollectionView Scroll\u0022))\n {\n collectionView.ScrollTo(i * 10);\n }\n }\n }\n}\n\u0060\u0060\u0060\n\n## Memory Profiling\n\n\u0060\u0060\u0060csharp\npublic class MemoryProfiler\n{\n public static MemorySnapshot TakeSnapshot(string label)\n {\n GC.Collect();\n GC.WaitForPendingFinalizers();\n GC.Collect();\n \n return new MemorySnapshot\n {\n Label = label,\n Timestamp = DateTime.UtcNow,\n TotalMemory = GC.GetTotalMemory(false),\n Gen0Collections = GC.CollectionCount(0),\n Gen1Collections = GC.CollectionCount(1),\n Gen2Collections = GC.CollectionCount(2)\n };\n }\n \n public static void CompareSnapshots(MemorySnapshot before, MemorySnapshot after)\n {\n var memoryDiff = after.TotalMemory - before.TotalMemory;\n var gen0Diff = after.Gen0Collections - before.Gen0Collections;\n var gen1Diff = after.Gen1Collections - before.Gen1Collections;\n var gen2Diff = after.Gen2Collections - before.Gen2Collections;\n \n Console.WriteLine(\u0022\\n=== Memory Profile Comparison ===\u0022);\n Console.WriteLine($\u0022Before: {before.Label} at {before.Timestamp}\u0022);\n Console.WriteLine($\u0022After: {after.Label} at {after.Timestamp}\u0022);\n Console.WriteLine($\u0022Memory Delta: {memoryDiff / 1024.0:F2} KB\u0022);\n Console.WriteLine($\u0022GC Gen0: {gen0Diff}, Gen1: {gen1Diff}, Gen2: {gen2Diff}\u0022);\n \n if (memoryDiff \u003E 1024 * 1024) // \u003E 1 MB\n {\n Console.WriteLine(\u0022\u26A0\uFE0F WARNING: Significant memory increase detected!\u0022);\n }\n }\n}\n\npublic class MemorySnapshot\n{\n public string Label { get; set; } = \u0022\u0022;\n public DateTime Timestamp { get; set; }\n public long TotalMemory { get; set; }\n public int Gen0Collections { get; set; }\n public int Gen1Collections { get; set; }\n public int Gen2Collections { get; set; }\n}\n\u0060\u0060\u0060\n\n## Best Practices Checklist\n\n### Rendering Optimization\n\n- \u2705 **Use dirty region tracking** for all custom views\n- \u2705 **Cache static content** (backgrounds, icons, formatted text)\n- \u2705 **Implement render layers** for complex UIs\n- \u2705 **Clip drawing operations** to visible regions\n- \u2705 **Batch draw calls** when possible\n- \u2705 **Use GPU rendering** for complex graphics\n- \u2705 **Profile frame times** and target 60 FPS (16.67ms per frame)\n\n### Memory Management\n\n- \u2705 **Use SafeHandle** for all native resources\n- \u2705 **Implement IDisposable** on classes holding unmanaged resources\n- \u2705 **Free P/Invoke allocations** immediately after use\n- \u2705 **Pool frequently allocated objects**\n- \u2705 **Avoid string allocations** in hot paths\n- \u2705 **Monitor GC collections** and minimize Gen2 collections\n- \u2705 **Use weak references** for caches\n\n### Layout Optimization\n\n- \u2705 **Minimize layout passes** by caching measurements\n- \u2705 **Use absolute layout** for static positioning\n- \u2705 **Avoid nested layouts** when possible\n- \u2705 **Virtualize long lists** with CollectionView\n- \u2705 **Defer offscreen layout** until needed\n\n### Text and Fonts\n\n- \u2705 **Cache shaped text** (SKTextBlob)\n- \u2705 **Limit font variations** to reduce shaping overhead\n- \u2705 **Use system fonts** when possible\n- \u2705 **Preload fonts** at startup\n- \u2705 **Measure text once** and cache results\n\n### Images\n\n- \u2705 **Decode images asynchronously** off the UI thread\n- \u2705 **Implement multi-level caching** (memory \u002B disk)\n- \u2705 **Resize images** to display size\n- \u2705 **Use appropriate formats** (PNG for UI, JPEG for photos)\n- \u2705 **Lazy load images** in lists\n\n## Continuous Performance Monitoring\n\n\u0060\u0060\u0060csharp\npublic class PerformanceMonitor\n{\n private static readonly PerformanceMonitor _instance = new();\n public static PerformanceMonitor Instance =\u003E _instance;\n \n private readonly ConcurrentQueue\u003CPerformanceEvent\u003E _events = new();\n private Timer? _reportTimer;\n \n public void Start()\n {\n _reportTimer = new Timer(_ =\u003E GenerateReport(), null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1));\n }\n \n public void RecordEvent(string category, string operation, double milliseconds)\n {\n _events.Enqueue(new PerformanceEvent\n {\n Category = category,\n Operation = operation,\n Milliseconds = milliseconds,\n Timestamp = DateTime.UtcNow\n });\n \n // Keep last 10,000 events\n while (_events.Count \u003E 10000)\n {\n _events.TryDequeue(out _);\n }\n }\n \n private void GenerateReport()\n {\n var events = _events.ToArray();\n var slowEvents = events.Where(e =\u003E e.Milliseconds \u003E 16.67).ToArray();\n \n if (slowEvents.Length \u003E 0)\n {\n Console.WriteLine($\u0022\\n\u26A0\uFE0F {slowEvents.Length} slow operations detected in last minute:\u0022);\n \n foreach (var evt in slowEvents.Take(10))\n {\n Console.WriteLine($\u0022 {evt.Category}/{evt.Operation}: {evt.Milliseconds:F2}ms\u0022);\n }\n }\n }\n}\n\npublic class PerformanceEvent\n{\n public string Category { get; set; } = \u0022\u0022;\n public string Operation { get; set; } = \u0022\u0022;\n public double Milliseconds { get; set; }\n public DateTime Timestamp { get; set; }\n}\n\u0060\u0060\u0060\n\n## Final Recommendations\n\n1. **Establish baselines**: Measure performance before optimizing\n2. **Profile in production**: Development builds don\u0027t reflect real-world performance\n3. **Test on target hardware**: Low-end devices reveal bottlenecks\n4. **Automate benchmarks**: Run performance tests in CI/CD\n5. **Monitor in production**: Track frame times and memory usage\n6. **Optimize iteratively**: Focus on the biggest bottlenecks first\n7. **Document optimizations**: Explain why specific patterns were chosen\n\nPerformance optimization is an ongoing process. Regular benchmarking and adherence to best practices ensure your OpenMaui applications deliver smooth, responsive experiences across all Linux desktop environments." + } + ], + "generatedAt": 1769751056430 +} \ No newline at end of file diff --git a/.notes/series-1769751324711-9007af.json b/.notes/series-1769751324711-9007af.json new file mode 100644 index 0000000..bcb5d33 --- /dev/null +++ b/.notes/series-1769751324711-9007af.json @@ -0,0 +1,59 @@ +{ + "id": "series-1769751324711-9007af", + "title": "Building Production-Ready Apps: Testing, Packaging, and Distribution", + "content": "# Building Production-Ready Apps: Testing, Packaging, and Distribution\r\n\r\n*Take your OpenMaui Linux applications from development to production with comprehensive testing strategies, packaging for multiple distributions, and deployment best practices.*\r\n\r\n## Introduction\r\n\r\nDeveloping a .NET MAUI application for Linux is just the beginning. To deliver a professional, production-ready application, you need robust testing, proper packaging for different Linux distributions, and a streamlined deployment pipeline. OpenMaui\u0027s comprehensive Linux platform implementation demonstrates how to achieve all of this while maintaining 100% MAUI API compliance.\n\nThe journey from \u0060dotnet run\u0060 to a polished application available in distribution repositories involves several critical steps: unit and integration testing of your Skia-based views, packaging for different distribution formats (.deb, .rpm, Flatpak, Snap, AppImage), setting up continuous integration pipelines, and establishing update mechanisms. Each step presents unique challenges in the Linux ecosystem, where fragmentation across distributions, display servers (X11 vs Wayland), and desktop environments (GNOME, KDE, XFCE) requires careful consideration.\n\nThis guide walks through the complete production pipeline for OpenMaui applications, drawing from real-world implementation experience with SkiaSharp rendering, GTK integration, and cross-distribution compatibility. Whether you\u0027re targeting a single distribution or aiming for universal Linux support, these strategies will help you deliver a reliable, maintainable application.\r\n\r\n## Unit Testing Skia Views and Handlers\r\n\r\nOpenMaui\u0027s architecture separates rendering logic from platform integration, making unit testing straightforward. With 217 passing unit tests using xUnit, Moq, and FluentAssertions, the project demonstrates comprehensive test coverage for Skia-based views and handlers.\n\n## Testing View Rendering Logic\n\nSkia views like \u0060SkiaButton\u0060, \u0060SkiaLabel\u0060, and \u0060SkiaEntry\u0060 contain rendering logic that should be tested independently of the windowing system. Focus your tests on:\n\n**Property mapping and updates:**\n\u0060\u0060\u0060csharp\n[Fact]\npublic void SkiaButton_TextProperty_UpdatesRendering()\n{\n var button = new SkiaButton();\n button.Text = \u0022Click Me\u0022;\n \n Assert.Equal(\u0022Click Me\u0022, button.Text);\n Assert.True(button.IsDirty); // Dirty flag triggers redraw\n}\n\u0060\u0060\u0060\n\n**Visual state management:**\n\u0060\u0060\u0060csharp\n[Fact]\npublic void SkiaButton_PressedState_ChangesAppearance()\n{\n var button = new SkiaButton\n {\n BackgroundColor = Colors.Blue\n };\n \n button.SetVisualState(VisualStates.Pressed);\n \n // Verify visual state affects rendering\n Assert.NotEqual(Colors.Blue, button.CurrentBackgroundColor);\n}\n\u0060\u0060\u0060\n\n**Layout calculations:**\n\u0060\u0060\u0060csharp\n[Fact]\npublic void SkiaGrid_MeasureAndArrange_CalculatesCorrectBounds()\n{\n var grid = new SkiaGrid\n {\n RowDefinitions = new RowDefinitionCollection\n {\n new RowDefinition { Height = GridLength.Auto },\n new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }\n }\n };\n \n var size = grid.Measure(new Size(400, 600));\n grid.Arrange(new Rect(0, 0, 400, 600));\n \n Assert.Equal(400, grid.Width);\n Assert.Equal(600, grid.Height);\n}\n\u0060\u0060\u0060\n\n## Testing Handler Implementations\n\nHandlers bridge MAUI controls to platform views. Test that property mappers correctly translate MAUI properties to Skia view properties:\n\n\u0060\u0060\u0060csharp\n[Fact]\npublic void ButtonHandler_TextProperty_MapsToSkiaButton()\n{\n var mauiButton = new Button { Text = \u0022Test\u0022 };\n var handler = new ButtonHandler();\n handler.SetVirtualView(mauiButton);\n \n var skiaButton = handler.PlatformView as SkiaButton;\n \n Assert.NotNull(skiaButton);\n Assert.Equal(\u0022Test\u0022, skiaButton.Text);\n}\n\n[Fact]\npublic void EntryHandler_TextChangedEvent_FiresCorrectly()\n{\n var mauiEntry = new Entry();\n var handler = new EntryHandler();\n handler.SetVirtualView(mauiEntry);\n \n string changedText = null;\n mauiEntry.TextChanged \u002B= (s, e) =\u003E changedText = e.NewTextValue;\n \n var skiaEntry = handler.PlatformView as SkiaEntry;\n skiaEntry.Text = \u0022New Text\u0022;\n skiaEntry.RaiseTextChanged();\n \n Assert.Equal(\u0022New Text\u0022, changedText);\n}\n\u0060\u0060\u0060\n\n## Mocking Platform Services\n\nUse Moq to isolate platform services during testing:\n\n\u0060\u0060\u0060csharp\n[Fact]\npublic async Task FilePicker_PickAsync_ReturnsSelectedFile()\n{\n var mockDialogService = new Mock\u003CILinuxDialogService\u003E();\n mockDialogService\n .Setup(x =\u003E x.ShowFilePickerAsync(It.IsAny\u003CFilePickerOptions\u003E()))\n .ReturnsAsync(\u0022/home/user/document.pdf\u0022);\n \n var filePicker = new LinuxFilePicker(mockDialogService.Object);\n var result = await filePicker.PickAsync();\n \n Assert.NotNull(result);\n Assert.Equal(\u0022document.pdf\u0022, result.FileName);\n}\n\u0060\u0060\u0060\n\n## Test Organization\n\nOrganize tests by component type:\n- \u0060Tests/Views/\u0060 - Skia view rendering and behavior\n- \u0060Tests/Handlers/\u0060 - Handler property mapping and events\n- \u0060Tests/Services/\u0060 - Platform service implementations\n- \u0060Tests/Layout/\u0060 - Layout container measure and arrange logic\n\nWith 217 passing unit tests covering views, services, and handlers, OpenMaui demonstrates that comprehensive testing is achievable even for complex platform implementations.\r\n\r\n## Integration Testing with Display Servers\r\n\r\nUnit tests verify logic, but integration tests ensure your application works correctly with X11, Wayland, and different desktop environments. These tests require actual display servers and window managers.\n\n## Setting Up Test Environments\n\n**Xvfb for headless X11 testing:**\n\u0060\u0060\u0060bash\n# Install Xvfb (X Virtual Frame Buffer)\nsudo apt-get install xvfb\n\n# Run tests with virtual display\nxvfb-run -a dotnet test\n\n# With specific resolution and color depth\nxvfb-run -a -s \u0022-screen 0 1920x1080x24\u0022 dotnet test\n\u0060\u0060\u0060\n\n**Wayland testing with weston:**\n\u0060\u0060\u0060bash\n# Install weston compositor\nsudo apt-get install weston\n\n# Run tests in weston environment\nweston --backend=headless-backend.so \u0026\nexport WAYLAND_DISPLAY=wayland-0\ndotnet test\n\u0060\u0060\u0060\n\n## Window Lifecycle Tests\n\nTest window creation, showing, hiding, and destruction:\n\n\u0060\u0060\u0060csharp\n[Fact]\npublic void LinuxWindow_CreateAndShow_DisplaysCorrectly()\n{\n var app = new LinuxApplication();\n var window = new LinuxWindow(app)\n {\n Title = \u0022Test Window\u0022,\n Width = 800,\n Height = 600\n };\n \n window.Show();\n app.ProcessEvents(); // Process pending X11/Wayland events\n \n Assert.True(window.IsVisible);\n Assert.Equal(800, window.Width);\n Assert.Equal(600, window.Height);\n \n window.Close();\n}\n\u0060\u0060\u0060\n\n## Rendering Pipeline Tests\n\nVerify the SkiaSharp rendering pipeline works with actual GPU contexts:\n\n\u0060\u0060\u0060csharp\n[Fact]\npublic void GpuRenderingEngine_Initialize_CreatesOpenGLContext()\n{\n var window = new GtkHostWindow();\n window.Show();\n \n var renderEngine = new GpuRenderingEngine(window);\n var initialized = renderEngine.Initialize();\n \n Assert.True(initialized);\n Assert.NotNull(renderEngine.GRContext);\n Assert.True(renderEngine.SupportsHardwareAcceleration);\n \n window.Close();\n}\n\n[Fact]\npublic void SkiaRenderingEngine_DirtyRegion_OptimizesRedraws()\n{\n var surface = CreateTestSurface(800, 600);\n var engine = new SkiaRenderingEngine(surface);\n \n var button = new SkiaButton\n {\n Bounds = new Rect(10, 10, 100, 40)\n };\n \n engine.AddView(button);\n engine.Render(); // Initial full render\n \n button.Text = \u0022Updated\u0022;\n var dirtyRect = engine.GetDirtyRegion();\n \n // Only button region should be dirty\n Assert.Equal(new Rect(10, 10, 100, 40), dirtyRect);\n}\n\u0060\u0060\u0060\n\n## Multi-Monitor Tests\n\nTest HiDPI scaling and multi-monitor configurations:\n\n\u0060\u0060\u0060csharp\n[Fact]\npublic void LinuxWindow_HiDPIDisplay_ScalesCorrectly()\n{\n var window = new LinuxWindow(new LinuxApplication());\n var scaleFactor = window.GetScaleFactor();\n \n window.Width = 800;\n window.Height = 600;\n \n // Physical pixels should account for scale factor\n Assert.Equal(800 * scaleFactor, window.PhysicalWidth);\n Assert.Equal(600 * scaleFactor, window.PhysicalHeight);\n}\n\u0060\u0060\u0060\n\n## Input Method Editor (IME) Tests\n\nTest text input with IBus, Fcitx5, and XIM:\n\n\u0060\u0060\u0060csharp\n[Fact]\npublic void SkiaEntry_IMEInput_HandlesComposition()\n{\n var entry = new SkiaEntry();\n var imeContext = new LinuxIMEContext(entry);\n \n // Simulate IME composition\n imeContext.SetCompositionText(\u0022\u3053\u3093\u306B\u3061\u306F\u0022);\n Assert.Equal(\u0022\u3053\u3093\u306B\u3061\u306F\u0022, entry.CompositionText);\n \n // Commit composition\n imeContext.CommitComposition();\n Assert.Equal(\u0022\u3053\u3093\u306B\u3061\u306F\u0022, entry.Text);\n Assert.Null(entry.CompositionText);\n}\n\u0060\u0060\u0060\n\n## Theme Detection Tests\n\nVerify system theme detection across desktop environments:\n\n\u0060\u0060\u0060csharp\n[Theory]\n[InlineData(\u0022GNOME\u0022, \u0022Adwaita-dark\u0022)]\n[InlineData(\u0022KDE\u0022, \u0022Breeze-Dark\u0022)]\npublic void SystemThemeService_DetectDarkMode_ReturnsCorrectTheme(\n string desktopEnv, string themeName)\n{\n Environment.SetEnvironmentVariable(\u0022XDG_CURRENT_DESKTOP\u0022, desktopEnv);\n Environment.SetEnvironmentVariable(\u0022GTK_THEME\u0022, themeName);\n \n var themeService = new SystemThemeService();\n var isDark = themeService.IsDarkMode();\n \n Assert.True(isDark);\n}\n\u0060\u0060\u0060\n\n## Continuous Integration Setup\n\nConfigure GitHub Actions for automated integration testing:\n\n\u0060\u0060\u0060yaml\nname: Integration Tests\n\non: [push, pull_request]\n\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v3\n \n - name: Setup .NET\n uses: actions/setup-dotnet@v3\n with:\n dotnet-version: \u00278.0.x\u0027\n \n - name: Install dependencies\n run: |\n sudo apt-get update\n sudo apt-get install -y xvfb libgtk-3-0 libwebkit2gtk-4.0-37\n \n - name: Run integration tests\n run: xvfb-run -a dotnet test --filter Category=Integration\n\u0060\u0060\u0060\n\nIntegration tests catch issues that unit tests miss: display server compatibility, font rendering with HarfBuzz, gesture recognition accuracy, and theme application consistency.\r\n\r\n## Packaging as .deb and .rpm\r\n\r\nDebian (.deb) and RPM packages are the foundation of Linux distribution, used by Ubuntu/Debian and Fedora/RHEL families respectively. Proper packaging ensures your application integrates seamlessly with system package managers.\n\n## Debian Package Structure\n\nCreate a \u0060debian/\u0060 directory in your project root:\n\n\u0060\u0060\u0060\ndebian/\n\u251C\u2500\u2500 changelog\n\u251C\u2500\u2500 control\n\u251C\u2500\u2500 copyright\n\u251C\u2500\u2500 rules\n\u251C\u2500\u2500 openmaui-app.install\n\u2514\u2500\u2500 openmaui-app.desktop\n\u0060\u0060\u0060\n\n**debian/control** - Package metadata:\n\u0060\u0060\u0060\nSource: openmaui-app\nSection: misc\nPriority: optional\nMaintainer: Your Name \u003Cyour.email@example.com\u003E\nBuild-Depends: debhelper (\u003E= 12), dotnet-sdk-8.0\nStandards-Version: 4.5.0\n\nPackage: openmaui-app\nArchitecture: amd64\nDepends: ${shlibs:Depends}, ${misc:Depends},\n libgtk-3-0 (\u003E= 3.24),\n libwebkit2gtk-4.0-37,\n libskia,\n libharfbuzz0b,\n aspnetcore-runtime-8.0\nDescription: OpenMaui Linux Application\n A cross-platform .NET MAUI application running natively on Linux\n with full Skia rendering and GTK integration.\n\u0060\u0060\u0060\n\n**debian/rules** - Build instructions:\n\u0060\u0060\u0060makefile\n#!/usr/bin/make -f\n\n%:\n\tdh $@\n\noverride_dh_auto_build:\n\tdotnet publish -c Release -r linux-x64 \\\n\t\t--self-contained false \\\n\t\t-p:PublishSingleFile=false \\\n\t\t-o debian/tmp/usr/lib/openmaui-app\n\noverride_dh_auto_install:\n\tdh_auto_install\n\tinstall -D -m 644 debian/openmaui-app.desktop \\\n\t\tdebian/openmaui-app/usr/share/applications/openmaui-app.desktop\n\tinstall -D -m 644 Resources/appicon.png \\\n\t\tdebian/openmaui-app/usr/share/icons/hicolor/256x256/apps/openmaui-app.png\n\u0060\u0060\u0060\n\n**debian/openmaui-app.install** - File installation paths:\n\u0060\u0060\u0060\nusr/lib/openmaui-app/*\nusr/share/applications/openmaui-app.desktop\nusr/share/icons/hicolor/256x256/apps/openmaui-app.png\n\u0060\u0060\u0060\n\n**debian/changelog** - Version history:\n\u0060\u0060\u0060\nopenmaui-app (1.0.0-1) unstable; urgency=medium\n\n * Initial release\n * Full MAUI API compliance\n * X11 and Wayland support\n\n -- Your Name \u003Cyour.email@example.com\u003E Mon, 20 Jan 2025 10:00:00 \u002B0000\n\u0060\u0060\u0060\n\n## Building the .deb Package\n\n\u0060\u0060\u0060bash\n# Install build tools\nsudo apt-get install debhelper devscripts\n\n# Build package\ndpkg-buildpackage -us -uc -b\n\n# Result: ../openmaui-app_1.0.0-1_amd64.deb\n\n# Test installation\nsudo dpkg -i ../openmaui-app_1.0.0-1_amd64.deb\nsudo apt-get install -f # Fix dependencies if needed\n\u0060\u0060\u0060\n\n## RPM Package Structure\n\nCreate an RPM spec file \u0060openmaui-app.spec\u0060:\n\n\u0060\u0060\u0060spec\nName: openmaui-app\nVersion: 1.0.0\nRelease: 1%{?dist}\nSummary: OpenMaui Linux Application\n\nLicense: MIT\nURL: https://github.com/yourusername/openmaui-app\nSource0: %{name}-%{version}.tar.gz\n\nBuildRequires: dotnet-sdk-8.0\nRequires: gtk3 \u003E= 3.24\nRequires: webkit2gtk3\nRequires: aspnetcore-runtime-8.0\nRequires: libSkiaSharp\nRequires: harfbuzz\n\n%description\nA cross-platform .NET MAUI application running natively on Linux\nwith full Skia rendering and GTK integration.\n\n%prep\n%setup -q\n\n%build\ndotnet publish -c Release -r linux-x64 \\\n --self-contained false \\\n -p:PublishSingleFile=false \\\n -o %{buildroot}/usr/lib/%{name}\n\n%install\nrm -rf %{buildroot}\nmkdir -p %{buildroot}/usr/lib/%{name}\nmkdir -p %{buildroot}/usr/share/applications\nmkdir -p %{buildroot}/usr/share/icons/hicolor/256x256/apps\n\ncp -r bin/Release/net8.0/linux-x64/publish/* %{buildroot}/usr/lib/%{name}/\ninstall -m 644 openmaui-app.desktop %{buildroot}/usr/share/applications/\ninstall -m 644 Resources/appicon.png %{buildroot}/usr/share/icons/hicolor/256x256/apps/%{name}.png\n\n%files\n/usr/lib/%{name}/\n/usr/share/applications/%{name}.desktop\n/usr/share/icons/hicolor/256x256/apps/%{name}.png\n\n%changelog\n* Mon Jan 20 2025 Your Name \u003Cyour.email@example.com\u003E - 1.0.0-1\n- Initial release\n\u0060\u0060\u0060\n\n## Building the RPM Package\n\n\u0060\u0060\u0060bash\n# Install build tools (Fedora/RHEL)\nsudo dnf install rpm-build rpmdevtools\n\n# Setup RPM build environment\nrpmdev-setuptree\n\n# Copy spec and source\ncp openmaui-app.spec ~/rpmbuild/SPECS/\ntar czf ~/rpmbuild/SOURCES/openmaui-app-1.0.0.tar.gz .\n\n# Build RPM\nrpmbuild -ba ~/rpmbuild/SPECS/openmaui-app.spec\n\n# Result: ~/rpmbuild/RPMS/x86_64/openmaui-app-1.0.0-1.x86_64.rpm\n\n# Test installation\nsudo dnf install ~/rpmbuild/RPMS/x86_64/openmaui-app-1.0.0-1.x86_64.rpm\n\u0060\u0060\u0060\n\n## Desktop Entry File\n\nCreate \u0060openmaui-app.desktop\u0060 for both package types:\n\n\u0060\u0060\u0060ini\n[Desktop Entry]\nVersion=1.0\nType=Application\nName=OpenMaui App\nComment=Cross-platform .NET MAUI application\nExec=/usr/lib/openmaui-app/OpenMauiApp\nIcon=openmaui-app\nTerminal=false\nCategories=Utility;Development;\nKeywords=maui;dotnet;cross-platform;\nStartupWMClass=OpenMauiApp\n\u0060\u0060\u0060\n\n## Dependency Management\n\nOpenMaui applications require specific runtime dependencies:\n\n**Core dependencies:**\n- \u0060aspnetcore-runtime-8.0\u0060 - .NET runtime\n- \u0060libgtk-3-0\u0060 / \u0060gtk3\u0060 - GTK windowing\n- \u0060libwebkit2gtk-4.0-37\u0060 / \u0060webkit2gtk3\u0060 - WebView support\n- \u0060libskia\u0060 - SkiaSharp native libraries\n- \u0060libharfbuzz0b\u0060 / \u0060harfbuzz\u0060 - Text shaping\n\n**Optional dependencies:**\n- \u0060libva2\u0060 - Hardware video acceleration\n- \u0060at-spi2-core\u0060 - Accessibility support\n- \u0060ibus\u0060 / \u0060fcitx5\u0060 - Input method support\n\nTest your packages on clean VMs to verify all dependencies are correctly specified.\r\n\r\n## Flatpak and Snap Distribution\r\n\r\nFlatpak and Snap provide sandboxed, distribution-agnostic packaging with automatic updates. They\u0027re ideal for reaching users across all Linux distributions without maintaining separate .deb and .rpm packages.\n\n## Flatpak Packaging\n\nFlatpak uses manifest files to define your application and its dependencies.\n\n**com.example.OpenMauiApp.yml** - Flatpak manifest:\n\u0060\u0060\u0060yaml\napp-id: com.example.OpenMauiApp\nruntime: org.freedesktop.Platform\nruntime-version: \u002723.08\u0027\nsdk: org.freedesktop.Sdk\nsdk-extensions:\n - org.freedesktop.Sdk.Extension.dotnet8\ncommand: openmaui-app\n\nfinish-args:\n # Wayland and X11 access\n - --socket=wayland\n - --socket=fallback-x11\n - --share=ipc\n \n # GPU acceleration\n - --device=dri\n \n # File system access\n - --filesystem=home\n - --filesystem=xdg-documents\n \n # Network access (for WebView)\n - --share=network\n \n # Desktop integration\n - --talk-name=org.freedesktop.Notifications\n - --talk-name=org.freedesktop.secrets\n - --talk-name=org.gtk.vfs.*\n \n # Theme access\n - --filesystem=xdg-config/gtk-3.0:ro\n - --env=GTK_THEME=Adwaita:dark\n\nbuild-options:\n append-path: /usr/lib/sdk/dotnet8/bin\n env:\n PKG_CONFIG_PATH: /app/lib/pkgconfig:/app/share/pkgconfig:/usr/lib/pkgconfig:/usr/share/pkgconfig\n\nmodules:\n # SkiaSharp native dependencies\n - name: skia\n buildsystem: simple\n build-commands:\n - install -Dm755 libSkiaSharp.so /app/lib/libSkiaSharp.so\n sources:\n - type: file\n url: https://www.nuget.org/api/v2/package/SkiaSharp.NativeAssets.Linux/2.88.9\n sha256: \u003Chash\u003E\n \n # HarfBuzz for text shaping\n - name: harfbuzz\n buildsystem: meson\n config-opts:\n - -Dglib=enabled\n - -Dfreetype=enabled\n sources:\n - type: archive\n url: https://github.com/harfbuzz/harfbuzz/releases/download/7.3.0/harfbuzz-7.3.0.tar.xz\n sha256: \u003Chash\u003E\n \n # Main application\n - name: openmaui-app\n buildsystem: simple\n build-commands:\n - dotnet publish -c Release -r linux-x64 --self-contained false -o /app/bin\n - install -Dm644 com.example.OpenMauiApp.desktop /app/share/applications/com.example.OpenMauiApp.desktop\n - install -Dm644 Resources/appicon.png /app/share/icons/hicolor/256x256/apps/com.example.OpenMauiApp.png\n - install -Dm644 com.example.OpenMauiApp.metainfo.xml /app/share/metainfo/com.example.OpenMauiApp.metainfo.xml\n sources:\n - type: dir\n path: .\n\u0060\u0060\u0060\n\n**Building and testing Flatpak:**\n\u0060\u0060\u0060bash\n# Install flatpak-builder\nsudo apt-get install flatpak-builder\n\n# Add Flathub repository\nflatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo\n\n# Install required runtimes\nflatpak install flathub org.freedesktop.Platform//23.08\nflatpak install flathub org.freedesktop.Sdk//23.08\nflatpak install flathub org.freedesktop.Sdk.Extension.dotnet8//23.08\n\n# Build Flatpak\nflatpak-builder --force-clean build-dir com.example.OpenMauiApp.yml\n\n# Test locally\nflatpak-builder --run build-dir com.example.OpenMauiApp.yml openmaui-app\n\n# Create single-file bundle for distribution\nflatpak-builder --repo=repo --force-clean build-dir com.example.OpenMauiApp.yml\nflatpak build-bundle repo openmaui-app.flatpak com.example.OpenMauiApp\n\n# Install bundle\nflatpak install openmaui-app.flatpak\n\u0060\u0060\u0060\n\n**AppStream metadata** (com.example.OpenMauiApp.metainfo.xml):\n\u0060\u0060\u0060xml\n\u003C?xml version=\u00221.0\u0022 encoding=\u0022UTF-8\u0022?\u003E\n\u003Ccomponent type=\u0022desktop-application\u0022\u003E\n \u003Cid\u003Ecom.example.OpenMauiApp\u003C/id\u003E\n \u003Cmetadata_license\u003ECC0-1.0\u003C/metadata_license\u003E\n \u003Cproject_license\u003EMIT\u003C/project_license\u003E\n \u003Cname\u003EOpenMaui App\u003C/name\u003E\n \u003Csummary\u003ECross-platform .NET MAUI application\u003C/summary\u003E\n \u003Cdescription\u003E\n \u003Cp\u003E\n A production-ready .NET MAUI application running natively on Linux\n with full Skia rendering, GTK integration, and cross-platform compatibility.\n \u003C/p\u003E\n \u003C/description\u003E\n \u003Cscreenshots\u003E\n \u003Cscreenshot type=\u0022default\u0022\u003E\n \u003Cimage\u003Ehttps://example.com/screenshots/main.png\u003C/image\u003E\n \u003C/screenshot\u003E\n \u003C/screenshots\u003E\n \u003Curl type=\u0022homepage\u0022\u003Ehttps://example.com\u003C/url\u003E\n \u003Creleases\u003E\n \u003Crelease version=\u00221.0.0\u0022 date=\u00222025-01-20\u0022/\u003E\n \u003C/releases\u003E\n \u003Ccontent_rating type=\u0022oars-1.1\u0022/\u003E\n\u003C/component\u003E\n\u0060\u0060\u0060\n\n## Snap Packaging\n\nSnap uses a \u0060snapcraft.yaml\u0060 file for configuration.\n\n**snapcraft.yaml:**\n\u0060\u0060\u0060yaml\nname: openmaui-app\nbase: core22\nversion: \u00271.0.0\u0027\nsummary: OpenMaui Linux Application\ndescription: |\n A cross-platform .NET MAUI application running natively on Linux\n with full Skia rendering and GTK integration.\n\ngrade: stable\nconfinement: strict\n\napps:\n openmaui-app:\n command: bin/openmaui-app\n extensions: [gnome]\n plugs:\n - home\n - network\n - desktop\n - desktop-legacy\n - wayland\n - x11\n - opengl\n - audio-playback\n - removable-media\n\nparts:\n dotnet-runtime:\n plugin: nil\n build-packages:\n - wget\n override-build: |\n wget https://dot.net/v1/dotnet-install.sh\n chmod \u002Bx dotnet-install.sh\n ./dotnet-install.sh --channel 8.0 --runtime aspnetcore --install-dir $SNAPCRAFT_PART_INSTALL/dotnet\n stage:\n - dotnet/*\n\n openmaui-app:\n plugin: dotnet\n source: .\n dotnet-build-configuration: Release\n dotnet-self-contained-runtime-identifier: linux-x64\n build-packages:\n - dotnet-sdk-8.0\n stage-packages:\n - libgtk-3-0\n - libwebkit2gtk-4.0-37\n - libharfbuzz0b\n - libfontconfig1\n - libfreetype6\n override-build: |\n dotnet publish -c Release -r linux-x64 \\\n --self-contained false \\\n -o $SNAPCRAFT_PART_INSTALL/bin\n\u0060\u0060\u0060\n\n**Building and publishing Snap:**\n\u0060\u0060\u0060bash\n# Install snapcraft\nsudo snap install snapcraft --classic\n\n# Build snap\nsnapcraft\n\n# Test locally\nsudo snap install openmaui-app_1.0.0_amd64.snap --dangerous\n\n# Run application\nopenmaui-app\n\n# Publish to Snap Store (requires account)\nsnapcraft login\nsnapcraft upload openmaui-app_1.0.0_amd64.snap --release=stable\n\u0060\u0060\u0060\n\n## Portal Permissions\n\nBoth Flatpak and Snap use portals for secure file access:\n\n\u0060\u0060\u0060csharp\n// OpenMaui handles portals automatically\npublic class LinuxFilePicker : IFilePicker\n{\n public async Task\u003CFileResult\u003E PickAsync(PickOptions options)\n {\n // Detects if running in sandbox\n if (IsRunningInFlatpak() || IsRunningInSnap())\n {\n // Use xdg-desktop-portal for sandboxed file access\n return await PickViaPortalAsync(options);\n }\n else\n {\n // Direct file picker (zenity/kdialog)\n return await PickDirectAsync(options);\n }\n }\n \n private bool IsRunningInFlatpak()\n {\n return File.Exists(\u0022/.flatpak-info\u0022);\n }\n \n private bool IsRunningInSnap()\n {\n return !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(\u0022SNAP\u0022));\n }\n}\n\u0060\u0060\u0060\n\n## Comparison: Flatpak vs Snap\n\n**Flatpak advantages:**\n- Better desktop integration\n- More granular permissions\n- Preferred by traditional Linux distributions\n- Excellent for GTK applications\n\n**Snap advantages:**\n- Simpler manifest syntax\n- Better CLI tool support\n- Strong Ubuntu/Canonical ecosystem\n- Automatic delta updates\n\nFor OpenMaui applications, **Flatpak is recommended** due to superior GTK integration and broader community adoption.\r\n\r\n## AppImage for Universal Compatibility\r\n\r\nAppImage provides the simplest distribution method: a single executable file that runs on any Linux distribution without installation. The beauty of AppImage is its simplicity: bundle everything your application needs into a single executable file that runs on virtually any Linux distribution without installation.\n\n## Creating an AppImage\n\nAppImages use the AppDir structure with all dependencies bundled:\n\n\u0060\u0060\u0060bash\n# Install appimagetool\nwget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage\nchmod \u002Bx appimagetool-x86_64.AppImage\n\n# Create AppDir structure\nmkdir -p OpenMauiApp.AppDir/usr/bin\nmkdir -p OpenMauiApp.AppDir/usr/lib\nmkdir -p OpenMauiApp.AppDir/usr/share/applications\nmkdir -p OpenMauiApp.AppDir/usr/share/icons/hicolor/256x256/apps\n\u0060\u0060\u0060\n\n## Publishing Self-Contained Build\n\nPublish your application with all dependencies:\n\n\u0060\u0060\u0060bash\ndotnet publish -c Release -r linux-x64 \\\n --self-contained true \\\n -p:PublishSingleFile=false \\\n -p:PublishTrimmed=false \\\n -o publish/\n\n# Copy to AppDir\ncp -r publish/* OpenMauiApp.AppDir/usr/bin/\n\u0060\u0060\u0060\n\n## Bundling Native Dependencies\n\nCopy required native libraries:\n\n\u0060\u0060\u0060bash\n#!/bin/bash\n# bundle-dependencies.sh\n\nAPPDIR=\u0022OpenMauiApp.AppDir\u0022\nLIBDIR=\u0022$APPDIR/usr/lib\u0022\n\n# Copy GTK and dependencies\ncp /usr/lib/x86_64-linux-gnu/libgtk-3.so.0 $LIBDIR/\ncp /usr/lib/x86_64-linux-gnu/libgdk-3.so.0 $LIBDIR/\ncp /usr/lib/x86_64-linux-gnu/libwebkit2gtk-4.0.so.37 $LIBDIR/\n\n# Copy SkiaSharp native\ncp ~/.nuget/packages/skiasharp.nativeassets.linux/2.88.9/runtimes/linux-x64/native/libSkiaSharp.so $LIBDIR/\n\n# Copy HarfBuzz\ncp /usr/lib/x86_64-linux-gnu/libharfbuzz.so.0 $LIBDIR/\n\n# Copy their dependencies\nfor lib in $LIBDIR/*.so*; do\n ldd $lib | grep \u0022=\u003E /\u0022 | awk \u0027{print $3}\u0027 | while read dep; do\n if [ ! -f \u0022$LIBDIR/$(basename $dep)\u0022 ]; then\n cp $dep $LIBDIR/\n fi\n done\ndone\n\u0060\u0060\u0060\n\n## AppRun Script\n\nCreate the launcher script \u0060OpenMauiApp.AppDir/AppRun\u0060:\n\n\u0060\u0060\u0060bash\n#!/bin/bash\n\n# Get the directory of this script\nHERE=\u0022$(dirname \u0022$(readlink -f \u0022${0}\u0022)\u0022)\u0022\n\n# Set library path to include bundled libraries\nexport LD_LIBRARY_PATH=\u0022$HERE/usr/lib:$LD_LIBRARY_PATH\u0022\n\n# Set GTK theme path\nexport GTK_PATH=\u0022$HERE/usr/lib/gtk-3.0\u0022\nexport GTK_DATA_PREFIX=\u0022$HERE/usr\u0022\n\n# Set GDK backend (prefer Wayland, fallback to X11)\nexport GDK_BACKEND=wayland,x11\n\n# Disable AppImage desktop integration (optional)\nexport APPIMAGE_DISABLE_INTEGRATION=1\n\n# Run the application\nexec \u0022$HERE/usr/bin/OpenMauiApp\u0022 \u0022$@\u0022\n\u0060\u0060\u0060\n\nMake it executable:\n\u0060\u0060\u0060bash\nchmod \u002Bx OpenMauiApp.AppDir/AppRun\n\u0060\u0060\u0060\n\n## Desktop Entry and Icon\n\n**OpenMauiApp.AppDir/openmaui-app.desktop:**\n\u0060\u0060\u0060ini\n[Desktop Entry]\nType=Application\nName=OpenMaui App\nExec=openmaui-app\nIcon=openmaui-app\nCategories=Utility;Development;\nTerminal=false\n\u0060\u0060\u0060\n\nCopy icon and create symlinks:\n\u0060\u0060\u0060bash\ncp Resources/appicon.png OpenMauiApp.AppDir/usr/share/icons/hicolor/256x256/apps/openmaui-app.png\ncp Resources/appicon.png OpenMauiApp.AppDir/openmaui-app.png\ncp Resources/appicon.png OpenMauiApp.AppDir/.DirIcon\n\nln -s usr/share/applications/openmaui-app.desktop OpenMauiApp.AppDir/openmaui-app.desktop\n\u0060\u0060\u0060\n\n## Building the AppImage\n\n\u0060\u0060\u0060bash\n# Build AppImage\n./appimagetool-x86_64.AppImage OpenMauiApp.AppDir OpenMauiApp-1.0.0-x86_64.AppImage\n\n# Make executable\nchmod \u002Bx OpenMauiApp-1.0.0-x86_64.AppImage\n\n# Test\n./OpenMauiApp-1.0.0-x86_64.AppImage\n\u0060\u0060\u0060\n\n## AppImage Updates\n\nImplement AppImageUpdate support by embedding update information:\n\n\u0060\u0060\u0060bash\n# Add update information to AppImage\n./appimagetool-x86_64.AppImage OpenMauiApp.AppDir \\\n OpenMauiApp-1.0.0-x86_64.AppImage \\\n -u \u0022gh-releases-zsync|yourusername|openmaui-app|latest|OpenMauiApp-*-x86_64.AppImage.zsync\u0022\n\u0060\u0060\u0060\n\nGenerate zsync file for delta updates:\n\u0060\u0060\u0060bash\nzsyncmake OpenMauiApp-1.0.0-x86_64.AppImage\n\u0060\u0060\u0060\n\n## Runtime Update Checks\n\nImplement update checking in your application:\n\n\u0060\u0060\u0060csharp\npublic class AppImageUpdateService\n{\n public async Task\u003Cbool\u003E CheckForUpdatesAsync()\n {\n var appImagePath = Environment.GetEnvironmentVariable(\u0022APPIMAGE\u0022);\n if (string.IsNullOrEmpty(appImagePath))\n return false; // Not running as AppImage\n \n var process = new Process\n {\n StartInfo = new ProcessStartInfo\n {\n FileName = \u0022appimageupdatetool\u0022,\n Arguments = $\u0022-j {appImagePath}\u0022,\n RedirectStandardOutput = true,\n UseShellExecute = false\n }\n };\n \n process.Start();\n var output = await process.StandardOutput.ReadToEndAsync();\n await process.WaitForExitAsync();\n \n var updateInfo = JsonSerializer.Deserialize\u003CUpdateInfo\u003E(output);\n return updateInfo?.UpdateAvailable ?? false;\n }\n \n public async Task ApplyUpdateAsync()\n {\n var appImagePath = Environment.GetEnvironmentVariable(\u0022APPIMAGE\u0022);\n var process = new Process\n {\n StartInfo = new ProcessStartInfo\n {\n FileName = \u0022appimageupdatetool\u0022,\n Arguments = appImagePath,\n UseShellExecute = true\n }\n };\n \n process.Start();\n await process.WaitForExitAsync();\n \n // Restart application\n Process.Start(appImagePath);\n Environment.Exit(0);\n }\n}\n\u0060\u0060\u0060\n\n## Automated AppImage Build Script\n\nCreate \u0060build-appimage.sh\u0060:\n\n\u0060\u0060\u0060bash\n#!/bin/bash\nset -e\n\nVERSION=\u00221.0.0\u0022\nAPPNAME=\u0022OpenMauiApp\u0022\nAPPDIR=\u0022$APPNAME.AppDir\u0022\n\necho \u0022Building $APPNAME v$VERSION AppImage...\u0022\n\n# Clean previous build\nrm -rf $APPDIR\nrm -f $APPNAME-*.AppImage\n\n# Publish application\ndotnet publish -c Release -r linux-x64 --self-contained true -o publish/\n\n# Create AppDir structure\nmkdir -p $APPDIR/usr/bin\nmkdir -p $APPDIR/usr/lib\nmkdir -p $APPDIR/usr/share/applications\nmkdir -p $APPDIR/usr/share/icons/hicolor/256x256/apps\n\n# Copy application\ncp -r publish/* $APPDIR/usr/bin/\n\n# Bundle dependencies\n./bundle-dependencies.sh\n\n# Copy desktop entry and icon\ncp openmaui-app.desktop $APPDIR/usr/share/applications/\ncp Resources/appicon.png $APPDIR/usr/share/icons/hicolor/256x256/apps/openmaui-app.png\ncp Resources/appicon.png $APPDIR/openmaui-app.png\ncp Resources/appicon.png $APPDIR/.DirIcon\nln -s usr/share/applications/openmaui-app.desktop $APPDIR/openmaui-app.desktop\n\n# Create AppRun\ncat \u003E $APPDIR/AppRun \u003C\u003C \u0027EOF\u0027\n#!/bin/bash\nHERE=\u0022$(dirname \u0022$(readlink -f \u0022${0}\u0022)\u0022)\u0022 \nexport LD_LIBRARY_PATH=\u0022$HERE/usr/lib:$LD_LIBRARY_PATH\u0022\nexport GTK_PATH=\u0022$HERE/usr/lib/gtk-3.0\u0022\nexec \u0022$HERE/usr/bin/OpenMauiApp\u0022 \u0022$@\u0022\nEOF\nchmod \u002Bx $APPDIR/AppRun\n\n# Build AppImage\n./appimagetool-x86_64.AppImage $APPDIR $APPNAME-$VERSION-x86_64.AppImage\n\necho \u0022AppImage created: $APPNAME-$VERSION-x86_64.AppImage\u0022\necho \u0022Size: $(du -h $APPNAME-$VERSION-x86_64.AppImage | cut -f1)\u0022\n\u0060\u0060\u0060\n\nAppImages are perfect for rapid distribution, beta testing, and users who prefer not to use package managers.\r\n\r\n## CI/CD Pipeline Setup\r\n\r\nAutomated build, test, and release pipelines ensure consistent quality and streamline distribution across multiple package formats. Here\u0027s how to set up comprehensive CI/CD for OpenMaui Linux applications.\n\n## GitHub Actions Workflow\n\nCreate \u0060.github/workflows/release.yml\u0060:\n\n\u0060\u0060\u0060yaml\nname: Build and Release\n\non:\n push:\n branches: [main]\n tags:\n - \u0027v*\u0027\n pull_request:\n branches: [main]\n\nenv:\n DOTNET_VERSION: \u00278.0.x\u0027\n PROJECT_NAME: OpenMauiApp\n\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v3\n \n - name: Setup .NET\n uses: actions/setup-dotnet@v3\n with:\n dotnet-version: ${{ env.DOTNET_VERSION }}\n \n - name: Install dependencies\n run: |\n sudo apt-get update\n sudo apt-get install -y xvfb libgtk-3-0 libwebkit2gtk-4.0-37 \\\n libharfbuzz0b libfontconfig1 libfreetype6\n \n - name: Restore dependencies\n run: dotnet restore\n \n - name: Build\n run: dotnet build -c Release --no-restore\n \n - name: Run unit tests\n run: dotnet test -c Release --no-build --verbosity normal \\\n --filter Category=Unit\n \n - name: Run integration tests\n run: xvfb-run -a dotnet test -c Release --no-build --verbosity normal \\\n --filter Category=Integration\n\n build-deb:\n needs: test\n runs-on: ubuntu-latest\n if: startsWith(github.ref, \u0027refs/tags/v\u0027)\n steps:\n - uses: actions/checkout@v3\n \n - name: Setup .NET\n uses: actions/setup-dotnet@v3\n with:\n dotnet-version: ${{ env.DOTNET_VERSION }}\n \n - name: Install build tools\n run: sudo apt-get install -y debhelper devscripts\n \n - name: Build .deb package\n run: |\n dpkg-buildpackage -us -uc -b\n mv ../*.deb .\n \n - name: Upload .deb artifact\n uses: actions/upload-artifact@v3\n with:\n name: deb-package\n path: \u0027*.deb\u0027\n\n build-rpm:\n needs: test\n runs-on: ubuntu-latest\n if: startsWith(github.ref, \u0027refs/tags/v\u0027)\n container:\n image: fedora:latest\n steps:\n - uses: actions/checkout@v3\n \n - name: Install dependencies\n run: |\n dnf install -y dotnet-sdk-8.0 rpm-build rpmdevtools\n \n - name: Setup RPM build tree\n run: rpmdev-setuptree\n \n - name: Build RPM\n run: |\n cp openmaui-app.spec ~/rpmbuild/SPECS/\n tar czf ~/rpmbuild/SOURCES/${{ env.PROJECT_NAME }}-${GITHUB_REF#refs/tags/v}.tar.gz .\n rpmbuild -ba ~/rpmbuild/SPECS/openmaui-app.spec\n cp ~/rpmbuild/RPMS/x86_64/*.rpm .\n \n - name: Upload RPM artifact\n uses: actions/upload-artifact@v3\n with:\n name: rpm-package\n path: \u0027*.rpm\u0027\n\n build-flatpak:\n needs: test\n runs-on: ubuntu-latest\n if: startsWith(github.ref, \u0027refs/tags/v\u0027)\n steps:\n - uses: actions/checkout@v3\n \n - name: Install Flatpak and flatpak-builder\n run: |\n sudo apt-get install -y flatpak flatpak-builder\n flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo\n flatpak install -y flathub org.freedesktop.Platform//23.08\n flatpak install -y flathub org.freedesktop.Sdk//23.08\n flatpak install -y flathub org.freedesktop.Sdk.Extension.dotnet8//23.08\n \n - name: Build Flatpak\n run: |\n flatpak-builder --repo=repo --force-clean build-dir \\\n com.example.OpenMauiApp.yml\n flatpak build-bundle repo ${{ env.PROJECT_NAME }}.flatpak \\\n com.example.OpenMauiApp\n \n - name: Upload Flatpak artifact\n uses: actions/upload-artifact@v3\n with:\n name: flatpak-package\n path: \u0027*.flatpak\u0027\n\n build-appimage:\n needs: test\n runs-on: ubuntu-latest\n if: startsWith(github.ref, \u0027refs/tags/v\u0027)\n steps:\n - uses: actions/checkout@v3\n \n - name: Setup .NET\n uses: actions/setup-dotnet@v3\n with:\n dotnet-version: ${{ env.DOTNET_VERSION }}\n \n - name: Install dependencies\n run: |\n sudo apt-get install -y libgtk-3-0 libwebkit2gtk-4.0-37 \\\n libharfbuzz0b zsync\n wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage\n chmod \u002Bx appimagetool-x86_64.AppImage\n \n - name: Build AppImage\n run: |\n chmod \u002Bx build-appimage.sh\n ./build-appimage.sh\n zsyncmake ${{ env.PROJECT_NAME }}-*.AppImage\n \n - name: Upload AppImage artifact\n uses: actions/upload-artifact@v3\n with:\n name: appimage-package\n path: |\n *.AppImage\n *.AppImage.zsync\n\n release:\n needs: [build-deb, build-rpm, build-flatpak, build-appimage]\n runs-on: ubuntu-latest\n if: startsWith(github.ref, \u0027refs/tags/v\u0027)\n steps:\n - name: Download all artifacts\n uses: actions/download-artifact@v3\n \n - name: Create Release\n uses: softprops/action-gh-release@v1\n with:\n files: |\n deb-package/*.deb\n rpm-package/*.rpm\n flatpak-package/*.flatpak\n appimage-package/*.AppImage\n appimage-package/*.AppImage.zsync\n draft: false\n prerelease: false\n env:\n GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\u0060\u0060\u0060\n\n## GitLab CI/CD Pipeline\n\nFor GitLab, create \u0060.gitlab-ci.yml\u0060:\n\n\u0060\u0060\u0060yaml\nstages:\n - test\n - build\n - release\n\nvariables:\n DOTNET_VERSION: \u00228.0\u0022\n PROJECT_NAME: \u0022OpenMauiApp\u0022\n\ntest:unit:\n stage: test\n image: mcr.microsoft.com/dotnet/sdk:8.0\n before_script:\n - apt-get update\n - apt-get install -y libgtk-3-0 libwebkit2gtk-4.0-37\n script:\n - dotnet restore\n - dotnet build -c Release\n - dotnet test -c Release --filter Category=Unit\n\ntest:integration:\n stage: test\n image: mcr.microsoft.com/dotnet/sdk:8.0\n before_script:\n - apt-get update\n - apt-get install -y xvfb libgtk-3-0 libwebkit2gtk-4.0-37\n script:\n - xvfb-run -a dotnet test -c Release --filter Category=Integration\n\nbuild:deb:\n stage: build\n image: ubuntu:22.04\n only:\n - tags\n script:\n - apt-get update\n - apt-get install -y debhelper devscripts dotnet-sdk-8.0\n - dpkg-buildpackage -us -uc -b\n - mv ../*.deb .\n artifacts:\n paths:\n - \u0022*.deb\u0022\n\nbuild:appimage:\n stage: build\n image: ubuntu:22.04\n only:\n - tags\n script:\n - apt-get update\n - apt-get install -y dotnet-sdk-8.0 libgtk-3-0 libwebkit2gtk-4.0-37 wget\n - wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage\n - chmod \u002Bx appimagetool-x86_64.AppImage build-appimage.sh\n - ./build-appimage.sh\n artifacts:\n paths:\n - \u0022*.AppImage\u0022\n\nrelease:\n stage: release\n image: registry.gitlab.com/gitlab-org/release-cli:latest\n only:\n - tags\n script:\n - echo \u0022Creating release $CI_COMMIT_TAG\u0022\n release:\n tag_name: $CI_COMMIT_TAG\n description: \u0022Release $CI_COMMIT_TAG\u0022\n assets:\n links:\n - name: \u0022Debian Package\u0022\n url: \u0022${CI_PROJECT_URL}/-/jobs/artifacts/${CI_COMMIT_TAG}/raw/*.deb?job=build:deb\u0022\n - name: \u0022AppImage\u0022\n url: \u0022${CI_PROJECT_URL}/-/jobs/artifacts/${CI_COMMIT_TAG}/raw/*.AppImage?job=build:appimage\u0022\n\u0060\u0060\u0060\n\n## Version Management\n\nAutomate version bumping in \u0060.csproj\u0060:\n\n\u0060\u0060\u0060xml\n\u003CProject Sdk=\u0022Microsoft.NET.Sdk\u0022\u003E\n \u003CPropertyGroup\u003E\n \u003CTargetFramework\u003Enet8.0\u003C/TargetFramework\u003E\n \u003CVersion\u003E1.0.0\u003C/Version\u003E\n \u003CAssemblyVersion\u003E1.0.0.0\u003C/AssemblyVersion\u003E\n \u003CFileVersion\u003E1.0.0.0\u003C/FileVersion\u003E\n \n \u003C!-- Auto-increment build number in CI --\u003E\n \u003CVersion Condition=\u0022\u0027$(BUILD_NUMBER)\u0027 != \u0027\u0027\u0022\u003E1.0.$(BUILD_NUMBER)\u003C/Version\u003E\n \u003C/PropertyGroup\u003E\n\u003C/Project\u003E\n\u0060\u0060\u0060\n\n## Automated Testing Matrix\n\nTest across multiple distributions:\n\n\u0060\u0060\u0060yaml\ntest-matrix:\n strategy:\n matrix:\n os:\n - ubuntu-22.04\n - ubuntu-20.04\n display-server:\n - x11\n - wayland\n runs-on: ${{ matrix.os }}\n steps:\n - name: Setup display server\n run: |\n if [ \u0022${{ matrix.display-server }}\u0022 = \u0022wayland\u0022 ]; then\n sudo apt-get install -y weston\n weston --backend=headless-backend.so \u0026\n export WAYLAND_DISPLAY=wayland-0\n else\n xvfb-run -a dotnet test\n fi\n\u0060\u0060\u0060\n\n## Code Signing\n\nSign packages for distribution:\n\n\u0060\u0060\u0060bash\n# GPG signing for .deb packages\ndpkg-sig --sign builder openmaui-app_1.0.0-1_amd64.deb\n\n# RPM signing\nrpmsign --addsign openmaui-app-1.0.0-1.x86_64.rpm\n\n# AppImage signing\ngpg --detach-sign --armor OpenMauiApp-1.0.0-x86_64.AppImage\n\u0060\u0060\u0060\n\nStore signing keys in CI secrets and automate signing in the pipeline.\r\n\r\n## Debugging in WSL vs Native Linux\r\n\r\nDeveloping on Windows Subsystem for Linux (WSL) offers convenience, but debugging production issues often requires native Linux environments. Understanding the differences helps you debug effectively in both contexts.\n\n## WSL Development Setup\n\nWSL 2 with WSLg provides GUI support for Linux applications:\n\n\u0060\u0060\u0060bash\n# Check WSL version\nwsl --version\n\n# Ensure WSL 2 is enabled\nwsl --set-default-version 2\n\n# Install Ubuntu\nwsl --install -d Ubuntu-22.04\n\u0060\u0060\u0060\n\n**Install development dependencies:**\n\u0060\u0060\u0060bash\nsudo apt-get update\nsudo apt-get install -y dotnet-sdk-8.0 libgtk-3-0 libwebkit2gtk-4.0-37 \\\n libharfbuzz0b libfontconfig1 libfreetype6\n\u0060\u0060\u0060\n\n## Visual Studio Code Remote Development\n\nConfigure VS Code for WSL debugging:\n\n**.vscode/launch.json:**\n\u0060\u0060\u0060json\n{\n \u0022version\u0022: \u00220.2.0\u0022,\n \u0022configurations\u0022: [\n {\n \u0022name\u0022: \u0022Launch OpenMaui (WSL)\u0022,\n \u0022type\u0022: \u0022coreclr\u0022,\n \u0022request\u0022: \u0022launch\u0022,\n \u0022preLaunchTask\u0022: \u0022build\u0022,\n \u0022program\u0022: \u0022${workspaceFolder}/bin/Debug/net8.0/OpenMauiApp.dll\u0022,\n \u0022args\u0022: [],\n \u0022cwd\u0022: \u0022${workspaceFolder}\u0022,\n \u0022console\u0022: \u0022internalConsole\u0022,\n \u0022stopAtEntry\u0022: false,\n \u0022env\u0022: {\n \u0022DISPLAY\u0022: \u0022:0\u0022,\n \u0022WAYLAND_DISPLAY\u0022: \u0022wayland-0\u0022\n }\n },\n {\n \u0022name\u0022: \u0022Attach to Process (WSL)\u0022,\n \u0022type\u0022: \u0022coreclr\u0022,\n \u0022request\u0022: \u0022attach\u0022,\n \u0022processId\u0022: \u0022${command:pickProcess}\u0022\n }\n ]\n}\n\u0060\u0060\u0060\n\n## WSL-Specific Issues\n\n**Display server differences:**\nWSLg uses Weston as the Wayland compositor, which may behave differently from GNOME or KDE:\n\n\u0060\u0060\u0060csharp\npublic class DisplayServerDetector\n{\n public static bool IsRunningInWSL()\n {\n try\n {\n var version = File.ReadAllText(\u0022/proc/version\u0022);\n return version.Contains(\u0022microsoft\u0022, StringComparison.OrdinalIgnoreCase);\n }\n catch\n {\n return false;\n }\n }\n \n public static void LogEnvironment()\n {\n Console.WriteLine($\u0022WSL: {IsRunningInWSL()}\u0022);\n Console.WriteLine($\u0022DISPLAY: {Environment.GetEnvironmentVariable(\u0022DISPLAY\u0022)}\u0022);\n Console.WriteLine($\u0022WAYLAND_DISPLAY: {Environment.GetEnvironmentVariable(\u0022WAYLAND_DISPLAY\u0022)}\u0022);\n Console.WriteLine($\u0022XDG_SESSION_TYPE: {Environment.GetEnvironmentVariable(\u0022XDG_SESSION_TYPE\u0022)}\u0022);\n }\n}\n\u0060\u0060\u0060\n\n**Font rendering:**\nWSL may have limited font fallback support. Test with emoji and CJK characters:\n\n\u0060\u0060\u0060csharp\n[Fact]\npublic void SkiaLabel_ComplexText_RendersCorrectly()\n{\n var label = new SkiaLabel\n {\n Text = \u0022Hello \u4E16\u754C \uD83C\uDF0D \u0645\u0631\u062D\u0628\u0627\u0022\n };\n \n var surface = CreateTestSurface(400, 100);\n label.Measure(new Size(400, 100));\n label.Arrange(new Rect(0, 0, 400, 100));\n label.Draw(surface.Canvas);\n \n // Verify text was rendered (not just boxes)\n var bitmap = surface.PeekPixels();\n Assert.True(HasNonWhitePixels(bitmap));\n}\n\u0060\u0060\u0060\n\n**Hardware acceleration:**\nWSLg provides limited GPU access. Detect and fallback gracefully:\n\n\u0060\u0060\u0060csharp\npublic class RenderEngineFactory\n{\n public static ISkiaRenderingEngine Create(IWindow window)\n {\n if (DisplayServerDetector.IsRunningInWSL())\n {\n // WSL: Prefer software rendering\n Console.WriteLine(\u0022WSL detected, using software rendering\u0022);\n return new SkiaRenderingEngine(window);\n }\n \n // Native Linux: Try GPU first\n try\n {\n var gpuEngine = new GpuRenderingEngine(window);\n if (gpuEngine.Initialize())\n {\n Console.WriteLine(\u0022Using GPU-accelerated rendering\u0022);\n return gpuEngine;\n }\n }\n catch (Exception ex)\n {\n Console.WriteLine($\u0022GPU initialization failed: {ex.Message}\u0022);\n }\n \n Console.WriteLine(\u0022Falling back to software rendering\u0022);\n return new SkiaRenderingEngine(window);\n }\n}\n\u0060\u0060\u0060\n\n## Native Linux Debugging\n\nTesting in WSL provides convenience during development, but native Linux testing is essential for catching display server quirks, font rendering issues, and hardware acceleration problems.\n\n**SSH remote debugging:**\n\u0060\u0060\u0060bash\n# On Linux machine, install SSH server\nsudo apt-get install openssh-server\nsudo systemctl start ssh\n\n# From Windows, connect via VS Code Remote-SSH\n# Or use dotnet trace for profiling\nssh user@linux-machine\ndotnet trace collect --process-id $(pgrep OpenMauiApp)\n\u0060\u0060\u0060\n\n**Remote debugging with lldb:**\n\u0060\u0060\u0060bash\n# Install lldb\nsudo apt-get install lldb\n\n# Attach to running process\nsudo lldb -p $(pgrep OpenMauiApp)\n\n# Set breakpoint\n(lldb) breakpoint set --name SkiaButton.OnDraw\n(lldb) continue\n\u0060\u0060\u0060\n\n## Logging and Diagnostics\n\nImplement comprehensive logging for debugging:\n\n\u0060\u0060\u0060csharp\npublic static class DiagnosticLogger\n{\n private static readonly string LogPath = Path.Combine(\n Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),\n \u0022OpenMauiApp\u0022,\n \u0022logs\u0022,\n $\u0022app-{DateTime.Now:yyyyMMdd-HHmmss}.log\u0022\n );\n \n static DiagnosticLogger()\n {\n Directory.CreateDirectory(Path.GetDirectoryName(LogPath));\n }\n \n public static void LogEnvironment()\n {\n var sb = new StringBuilder();\n sb.AppendLine(\u0022=== Environment Information ===\u0022);\n sb.AppendLine($\u0022OS: {Environment.OSVersion}\u0022);\n sb.AppendLine($\u0022Runtime: {RuntimeInformation.FrameworkDescription}\u0022);\n sb.AppendLine($\u0022WSL: {DisplayServerDetector.IsRunningInWSL()}\u0022);\n sb.AppendLine($\u0022Display: {Environment.GetEnvironmentVariable(\u0022DISPLAY\u0022)}\u0022);\n sb.AppendLine($\u0022Session Type: {Environment.GetEnvironmentVariable(\u0022XDG_SESSION_TYPE\u0022)}\u0022);\n sb.AppendLine($\u0022Desktop: {Environment.GetEnvironmentVariable(\u0022XDG_CURRENT_DESKTOP\u0022)}\u0022);\n sb.AppendLine($\u0022Theme: {Environment.GetEnvironmentVariable(\u0022GTK_THEME\u0022)}\u0022);\n \n File.AppendAllText(LogPath, sb.ToString());\n }\n \n public static void LogException(Exception ex, string context)\n {\n var sb = new StringBuilder();\n sb.AppendLine($\u0022[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] ERROR in {context}\u0022);\n sb.AppendLine($\u0022Message: {ex.Message}\u0022);\n sb.AppendLine($\u0022Stack Trace:\\n{ex.StackTrace}\u0022);\n \n if (ex.InnerException != null)\n {\n sb.AppendLine($\u0022Inner Exception: {ex.InnerException.Message}\u0022);\n }\n \n File.AppendAllText(LogPath, sb.ToString());\n }\n}\n\u0060\u0060\u0060\n\n## Performance Profiling\n\nCompare performance between WSL and native Linux:\n\n\u0060\u0060\u0060csharp\npublic class PerformanceBenchmark\n{\n [Benchmark]\n public void RenderComplexLayout()\n {\n var grid = CreateComplexGrid();\n var surface = CreateTestSurface(1920, 1080);\n \n var stopwatch = Stopwatch.StartNew();\n grid.Measure(new Size(1920, 1080));\n grid.Arrange(new Rect(0, 0, 1920, 1080));\n grid.Draw(surface.Canvas);\n stopwatch.Stop();\n \n Console.WriteLine($\u0022Render time: {stopwatch.ElapsedMilliseconds}ms\u0022);\n Console.WriteLine($\u0022Environment: {(DisplayServerDetector.IsRunningInWSL() ? \u0022WSL\u0022 : \u0022Native\u0022)}\u0022);\n }\n}\n\u0060\u0060\u0060\n\n## Testing Checklist\n\n**WSL testing:**\n- \u2713 Basic functionality\n- \u2713 UI layout and rendering\n- \u2713 Unit and integration tests\n- \u2713 Rapid iteration during development\n\n**Native Linux testing:**\n- \u2713 Multiple distributions (Ubuntu, Fedora, Arch)\n- \u2713 Different desktop environments (GNOME, KDE, XFCE)\n- \u2713 X11 and Wayland display servers\n- \u2713 Hardware acceleration\n- \u2713 Font rendering with complex scripts\n- \u2713 System theme integration\n- \u2713 Multi-monitor configurations\n- \u2713 HiDPI scaling\n- \u2713 Performance profiling\n\nUse WSL for day-to-day development, but always validate on native Linux before release.\r\n\r\n## Distribution and Update Strategies\r\n\r\nGetting your application into users\u0027 hands is just the beginning. A robust distribution and update strategy ensures users stay on the latest version with minimal friction.\n\n## Distribution Channels\n\n### Official Distribution Repositories\n\n**Ubuntu PPA (Personal Package Archive):**\n\u0060\u0060\u0060bash\n# Create Launchpad account and PPA\n# Upload source package\ndput ppa:yourusername/openmaui-app openmaui-app_1.0.0-1_source.changes\n\n# Users install via:\nsudo add-apt-repository ppa:yourusername/openmaui-app\nsudo apt-get update\nsudo apt-get install openmaui-app\n\u0060\u0060\u0060\n\n**Fedora COPR (Cool Other Package Repo):**\n\u0060\u0060\u0060bash\n# Create COPR project at copr.fedorainfracloud.org\ncopr-cli create openmaui-app --chroot fedora-39-x86_64\n\n# Build package\ncopr-cli build openmaui-app openmaui-app-1.0.0-1.src.rpm\n\n# Users install via:\nsudo dnf copr enable yourusername/openmaui-app\nsudo dnf install openmaui-app\n\u0060\u0060\u0060\n\n**Arch User Repository (AUR):**\n\nCreate \u0060PKGBUILD\u0060:\n\u0060\u0060\u0060bash\n# Maintainer: Your Name \u003Cyour.email@example.com\u003E\npkgname=openmaui-app\npkgver=1.0.0\npkgrel=1\npkgdesc=\u0022OpenMaui Linux Application\u0022\narch=(\u0027x86_64\u0027)\nurl=\u0022https://github.com/yourusername/openmaui-app\u0022\nlicense=(\u0027MIT\u0027)\ndepends=(\u0027dotnet-runtime\u0027 \u0027gtk3\u0027 \u0027webkit2gtk\u0027 \u0027harfbuzz\u0027)\nmakdepends=(\u0027dotnet-sdk\u0027)\nsource=(\u0022$pkgname-$pkgver.tar.gz::$url/archive/v$pkgver.tar.gz\u0022)\nsha256sums=(\u0027SKIP\u0027)\n\nbuild() {\n cd \u0022$pkgname-$pkgver\u0022\n dotnet publish -c Release -r linux-x64 --self-contained false -o out\n}\n\npackage() {\n cd \u0022$pkgname-$pkgver\u0022\n install -dm755 \u0022$pkgdir/usr/lib/$pkgname\u0022\n cp -r out/* \u0022$pkgdir/usr/lib/$pkgname/\u0022\n \n install -Dm644 \u0022openmaui-app.desktop\u0022 \u0022$pkgdir/usr/share/applications/$pkgname.desktop\u0022\n install -Dm644 \u0022Resources/appicon.png\u0022 \u0022$pkgdir/usr/share/icons/hicolor/256x256/apps/$pkgname.png\u0022\n}\n\u0060\u0060\u0060\n\n### Flathub Submission\n\nSubmit your Flatpak to Flathub for centralized distribution:\n\n\u0060\u0060\u0060bash\n# Fork flathub/flathub repository\ngit clone https://github.com/flathub/flathub.git\ncd flathub\n\n# Create application directory\nmkdir com.example.OpenMauiApp\ncd com.example.OpenMauiApp\n\n# Add manifest and metadata\ncp ~/openmaui-app/com.example.OpenMauiApp.yml .\ncp ~/openmaui-app/com.example.OpenMauiApp.metainfo.xml .\n\n# Submit pull request\ngit add .\ngit commit -m \u0022Add OpenMaui App\u0022\ngit push origin main\n\u0060\u0060\u0060\n\n## Automatic Update Mechanisms\n\n### In-App Update Checker\n\nImplement update checking for AppImage and standalone installations:\n\n\u0060\u0060\u0060csharp\npublic class UpdateService\n{\n private const string UpdateCheckUrl = \u0022https://api.github.com/repos/yourusername/openmaui-app/releases/latest\u0022;\n private readonly HttpClient _httpClient;\n \n public UpdateService()\n {\n _httpClient = new HttpClient();\n _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(\u0022OpenMauiApp/1.0\u0022);\n }\n \n public async Task\u003CUpdateInfo\u003E CheckForUpdatesAsync()\n {\n try\n {\n var response = await _httpClient.GetStringAsync(UpdateCheckUrl);\n var release = JsonSerializer.Deserialize\u003CGitHubRelease\u003E(response);\n \n var currentVersion = Assembly.GetExecutingAssembly().GetName().Version;\n var latestVersion = Version.Parse(release.TagName.TrimStart(\u0027v\u0027));\n \n return new UpdateInfo\n {\n UpdateAvailable = latestVersion \u003E currentVersion,\n CurrentVersion = currentVersion.ToString(),\n LatestVersion = latestVersion.ToString(),\n ReleaseNotes = release.Body,\n DownloadUrl = GetDownloadUrl(release)\n };\n }\n catch (Exception ex)\n {\n Console.WriteLine($\u0022Update check failed: {ex.Message}\u0022);\n return new UpdateInfo { UpdateAvailable = false };\n }\n }\n \n private string GetDownloadUrl(GitHubRelease release)\n {\n // Detect installation type and return appropriate download URL\n if (IsAppImage())\n return release.Assets.FirstOrDefault(a =\u003E a.Name.EndsWith(\u0022.AppImage\u0022))?.BrowserDownloadUrl;\n else if (IsDebian())\n return release.Assets.FirstOrDefault(a =\u003E a.Name.EndsWith(\u0022.deb\u0022))?.BrowserDownloadUrl;\n else if (IsFedora())\n return release.Assets.FirstOrDefault(a =\u003E a.Name.EndsWith(\u0022.rpm\u0022))?.BrowserDownloadUrl;\n \n return release.Assets.FirstOrDefault()?.BrowserDownloadUrl;\n }\n \n private bool IsAppImage() =\u003E !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(\u0022APPIMAGE\u0022));\n private bool IsDebian() =\u003E File.Exists(\u0022/etc/debian_version\u0022);\n private bool IsFedora() =\u003E File.Exists(\u0022/etc/fedora-release\u0022);\n}\n\npublic class UpdateInfo\n{\n public bool UpdateAvailable { get; set; }\n public string CurrentVersion { get; set; }\n public string LatestVersion { get; set; }\n public string ReleaseNotes { get; set; }\n public string DownloadUrl { get; set; }\n}\n\npublic class GitHubRelease\n{\n [JsonPropertyName(\u0022tag_name\u0022)]\n public string TagName { get; set; }\n \n [JsonPropertyName(\u0022body\u0022)]\n public string Body { get; set; }\n \n [JsonPropertyName(\u0022assets\u0022)]\n public List\u003CGitHubAsset\u003E Assets { get; set; }\n}\n\npublic class GitHubAsset\n{\n [JsonPropertyName(\u0022name\u0022)]\n public string Name { get; set; }\n \n [JsonPropertyName(\u0022browser_download_url\u0022)]\n public string BrowserDownloadUrl { get; set; }\n}\n\u0060\u0060\u0060\n\n### Update UI Integration\n\nShow update notifications in your application:\n\n\u0060\u0060\u0060csharp\npublic class MainPage : ContentPage\n{\n private readonly UpdateService _updateService;\n \n public MainPage()\n {\n InitializeComponent();\n _updateService = new UpdateService();\n \n // Check for updates on startup (after short delay)\n Dispatcher.DispatchDelayed(TimeSpan.FromSeconds(5), async () =\u003E\n {\n await CheckForUpdatesAsync();\n });\n }\n \n private async Task CheckForUpdatesAsync()\n {\n var updateInfo = await _updateService.CheckForUpdatesAsync();\n \n if (updateInfo.UpdateAvailable)\n {\n var result = await DisplayAlert(\n \u0022Update Available\u0022,\n $\u0022Version {updateInfo.LatestVersion} is available. \u0022 \u002B\n $\u0022You are currently running version {updateInfo.CurrentVersion}.\\n\\n\u0022 \u002B\n $\u0022Release Notes:\\n{updateInfo.ReleaseNotes}\\n\\n\u0022 \u002B\n \u0022Would you like to download the update?\u0022,\n \u0022Download\u0022,\n \u0022Later\u0022\n );\n \n if (result)\n {\n await Launcher.OpenAsync(updateInfo.DownloadUrl);\n }\n }\n }\n}\n\u0060\u0060\u0060\n\n### Flatpak/Snap Automatic Updates\n\nFlatpak and Snap handle updates automatically:\n\n**Flatpak:**\n\u0060\u0060\u0060bash\n# Users can manually update\nflatpak update com.example.OpenMauiApp\n\n# Or enable automatic updates\nflatpak update --assumeyes\n\n# Configure systemd timer for automatic updates\nsystemctl --user enable flatpak-update.timer\n\u0060\u0060\u0060\n\n**Snap:**\n\u0060\u0060\u0060bash\n# Snap updates automatically by default\n# Users can manually trigger\nsudo snap refresh openmaui-app\n\n# Check update status\nsnap refresh --list\n\u0060\u0060\u0060\n\n## Analytics and Telemetry\n\nTrack version adoption (with user consent):\n\n\u0060\u0060\u0060csharp\npublic class TelemetryService\n{\n private const string TelemetryEndpoint = \u0022https://telemetry.example.com/api/events\u0022;\n \n public async Task ReportAppStartAsync()\n {\n if (!HasUserConsent())\n return;\n \n var telemetry = new\n {\n EventType = \u0022app_start\u0022,\n Version = Assembly.GetExecutingAssembly().GetName().Version.ToString(),\n OS = Environment.OSVersion.ToString(),\n Distribution = GetLinuxDistribution(),\n DesktopEnvironment = Environment.GetEnvironmentVariable(\u0022XDG_CURRENT_DESKTOP\u0022),\n DisplayServer = Environment.GetEnvironmentVariable(\u0022XDG_SESSION_TYPE\u0022),\n InstallationType = GetInstallationType(),\n Timestamp = DateTime.UtcNow\n };\n \n try\n {\n var json = JsonSerializer.Serialize(telemetry);\n var content = new StringContent(json, Encoding.UTF8, \u0022application/json\u0022);\n await new HttpClient().PostAsync(TelemetryEndpoint, content);\n }\n catch\n {\n // Silently fail - telemetry should never break the app\n }\n }\n \n private string GetInstallationType()\n {\n if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(\u0022APPIMAGE\u0022)))\n return \u0022AppImage\u0022;\n if (File.Exists(\u0022/.flatpak-info\u0022))\n return \u0022Flatpak\u0022;\n if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(\u0022SNAP\u0022)))\n return \u0022Snap\u0022;\n if (File.Exists(\u0022/var/lib/dpkg/status\u0022))\n return \u0022Deb\u0022;\n if (File.Exists(\u0022/var/lib/rpm\u0022))\n return \u0022RPM\u0022;\n \n return \u0022Unknown\u0022;\n }\n \n private string GetLinuxDistribution()\n {\n try\n {\n var osRelease = File.ReadAllLines(\u0022/etc/os-release\u0022)\n .Select(line =\u003E line.Split(\u0027=\u0027))\n .Where(parts =\u003E parts.Length == 2)\n .ToDictionary(parts =\u003E parts[0], parts =\u003E parts[1].Trim(\u0027\u0022\u0027));\n \n return osRelease.GetValueOrDefault(\u0022PRETTY_NAME\u0022, \u0022Unknown\u0022);\n }\n catch\n {\n return \u0022Unknown\u0022;\n }\n }\n \n private bool HasUserConsent()\n {\n // Check if user has consented to telemetry\n return Preferences.Get(\u0022telemetry_enabled\u0022, false);\n }\n}\n\u0060\u0060\u0060\n\n## Release Checklist\n\nBefore each release:\n\n- \u2705 All tests passing (unit, integration, manual)\n- \u2705 Version numbers updated (.csproj, changelog, manifests)\n- \u2705 Release notes written\n- \u2705 Packages built for all formats (.deb, .rpm, Flatpak, AppImage)\n- \u2705 Packages tested on clean VMs\n- \u2705 Code signing completed\n- \u2705 Git tag created and pushed\n- \u2705 GitHub/GitLab release created with artifacts\n- \u2705 Distribution repositories updated (PPA, COPR, AUR)\n- \u2705 Flathub pull request submitted (if applicable)\n- \u2705 Documentation updated\n- \u2705 Social media/blog announcement prepared\n\nA well-executed distribution strategy ensures your OpenMaui application reaches users across the diverse Linux ecosystem while maintaining quality and reliability.\r\n\r\n---\r\n\r\n\u003E With 217 passing unit tests covering views, services, and handlers, OpenMaui demonstrates that comprehensive testing is achievable even for complex platform implementations.\r\n\r\n\u003E The beauty of AppImage is its simplicity: bundle everything your application needs into a single executable file that runs on virtually any Linux distribution without installation.\r\n\r\n\u003E Testing in WSL provides convenience during development, but native Linux testing is essential for catching display server quirks, font rendering issues, and hardware acceleration problems.", + "createdAt": 1769751324711, + "updatedAt": 1769751324711, + "tags": [ + "series", + "generated", + "Building Linux Apps with .NET MAUI" + ], + "isArticle": true, + "seriesGroup": "Building Linux Apps with .NET MAUI", + "subtitle": "Take your OpenMaui Linux applications from development to production with comprehensive testing strategies, packaging for multiple distributions, and deployment best practices.", + "pullQuotes": [ + "With 217 passing unit tests covering views, services, and handlers, OpenMaui demonstrates that comprehensive testing is achievable even for complex platform implementations.", + "The beauty of AppImage is its simplicity: bundle everything your application needs into a single executable file that runs on virtually any Linux distribution without installation.", + "Testing in WSL provides convenience during development, but native Linux testing is essential for catching display server quirks, font rendering issues, and hardware acceleration problems." + ], + "sections": [ + { + "header": "Introduction", + "content": "Developing a .NET MAUI application for Linux is just the beginning. To deliver a professional, production-ready application, you need robust testing, proper packaging for different Linux distributions, and a streamlined deployment pipeline. OpenMaui\u0027s comprehensive Linux platform implementation demonstrates how to achieve all of this while maintaining 100% MAUI API compliance.\n\nThe journey from \u0060dotnet run\u0060 to a polished application available in distribution repositories involves several critical steps: unit and integration testing of your Skia-based views, packaging for different distribution formats (.deb, .rpm, Flatpak, Snap, AppImage), setting up continuous integration pipelines, and establishing update mechanisms. Each step presents unique challenges in the Linux ecosystem, where fragmentation across distributions, display servers (X11 vs Wayland), and desktop environments (GNOME, KDE, XFCE) requires careful consideration.\n\nThis guide walks through the complete production pipeline for OpenMaui applications, drawing from real-world implementation experience with SkiaSharp rendering, GTK integration, and cross-distribution compatibility. Whether you\u0027re targeting a single distribution or aiming for universal Linux support, these strategies will help you deliver a reliable, maintainable application." + }, + { + "header": "Unit Testing Skia Views and Handlers", + "content": "OpenMaui\u0027s architecture separates rendering logic from platform integration, making unit testing straightforward. With 217 passing unit tests using xUnit, Moq, and FluentAssertions, the project demonstrates comprehensive test coverage for Skia-based views and handlers.\n\n## Testing View Rendering Logic\n\nSkia views like \u0060SkiaButton\u0060, \u0060SkiaLabel\u0060, and \u0060SkiaEntry\u0060 contain rendering logic that should be tested independently of the windowing system. Focus your tests on:\n\n**Property mapping and updates:**\n\u0060\u0060\u0060csharp\n[Fact]\npublic void SkiaButton_TextProperty_UpdatesRendering()\n{\n var button = new SkiaButton();\n button.Text = \u0022Click Me\u0022;\n \n Assert.Equal(\u0022Click Me\u0022, button.Text);\n Assert.True(button.IsDirty); // Dirty flag triggers redraw\n}\n\u0060\u0060\u0060\n\n**Visual state management:**\n\u0060\u0060\u0060csharp\n[Fact]\npublic void SkiaButton_PressedState_ChangesAppearance()\n{\n var button = new SkiaButton\n {\n BackgroundColor = Colors.Blue\n };\n \n button.SetVisualState(VisualStates.Pressed);\n \n // Verify visual state affects rendering\n Assert.NotEqual(Colors.Blue, button.CurrentBackgroundColor);\n}\n\u0060\u0060\u0060\n\n**Layout calculations:**\n\u0060\u0060\u0060csharp\n[Fact]\npublic void SkiaGrid_MeasureAndArrange_CalculatesCorrectBounds()\n{\n var grid = new SkiaGrid\n {\n RowDefinitions = new RowDefinitionCollection\n {\n new RowDefinition { Height = GridLength.Auto },\n new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }\n }\n };\n \n var size = grid.Measure(new Size(400, 600));\n grid.Arrange(new Rect(0, 0, 400, 600));\n \n Assert.Equal(400, grid.Width);\n Assert.Equal(600, grid.Height);\n}\n\u0060\u0060\u0060\n\n## Testing Handler Implementations\n\nHandlers bridge MAUI controls to platform views. Test that property mappers correctly translate MAUI properties to Skia view properties:\n\n\u0060\u0060\u0060csharp\n[Fact]\npublic void ButtonHandler_TextProperty_MapsToSkiaButton()\n{\n var mauiButton = new Button { Text = \u0022Test\u0022 };\n var handler = new ButtonHandler();\n handler.SetVirtualView(mauiButton);\n \n var skiaButton = handler.PlatformView as SkiaButton;\n \n Assert.NotNull(skiaButton);\n Assert.Equal(\u0022Test\u0022, skiaButton.Text);\n}\n\n[Fact]\npublic void EntryHandler_TextChangedEvent_FiresCorrectly()\n{\n var mauiEntry = new Entry();\n var handler = new EntryHandler();\n handler.SetVirtualView(mauiEntry);\n \n string changedText = null;\n mauiEntry.TextChanged \u002B= (s, e) =\u003E changedText = e.NewTextValue;\n \n var skiaEntry = handler.PlatformView as SkiaEntry;\n skiaEntry.Text = \u0022New Text\u0022;\n skiaEntry.RaiseTextChanged();\n \n Assert.Equal(\u0022New Text\u0022, changedText);\n}\n\u0060\u0060\u0060\n\n## Mocking Platform Services\n\nUse Moq to isolate platform services during testing:\n\n\u0060\u0060\u0060csharp\n[Fact]\npublic async Task FilePicker_PickAsync_ReturnsSelectedFile()\n{\n var mockDialogService = new Mock\u003CILinuxDialogService\u003E();\n mockDialogService\n .Setup(x =\u003E x.ShowFilePickerAsync(It.IsAny\u003CFilePickerOptions\u003E()))\n .ReturnsAsync(\u0022/home/user/document.pdf\u0022);\n \n var filePicker = new LinuxFilePicker(mockDialogService.Object);\n var result = await filePicker.PickAsync();\n \n Assert.NotNull(result);\n Assert.Equal(\u0022document.pdf\u0022, result.FileName);\n}\n\u0060\u0060\u0060\n\n## Test Organization\n\nOrganize tests by component type:\n- \u0060Tests/Views/\u0060 - Skia view rendering and behavior\n- \u0060Tests/Handlers/\u0060 - Handler property mapping and events\n- \u0060Tests/Services/\u0060 - Platform service implementations\n- \u0060Tests/Layout/\u0060 - Layout container measure and arrange logic\n\nWith 217 passing unit tests covering views, services, and handlers, OpenMaui demonstrates that comprehensive testing is achievable even for complex platform implementations." + }, + { + "header": "Integration Testing with Display Servers", + "content": "Unit tests verify logic, but integration tests ensure your application works correctly with X11, Wayland, and different desktop environments. These tests require actual display servers and window managers.\n\n## Setting Up Test Environments\n\n**Xvfb for headless X11 testing:**\n\u0060\u0060\u0060bash\n# Install Xvfb (X Virtual Frame Buffer)\nsudo apt-get install xvfb\n\n# Run tests with virtual display\nxvfb-run -a dotnet test\n\n# With specific resolution and color depth\nxvfb-run -a -s \u0022-screen 0 1920x1080x24\u0022 dotnet test\n\u0060\u0060\u0060\n\n**Wayland testing with weston:**\n\u0060\u0060\u0060bash\n# Install weston compositor\nsudo apt-get install weston\n\n# Run tests in weston environment\nweston --backend=headless-backend.so \u0026\nexport WAYLAND_DISPLAY=wayland-0\ndotnet test\n\u0060\u0060\u0060\n\n## Window Lifecycle Tests\n\nTest window creation, showing, hiding, and destruction:\n\n\u0060\u0060\u0060csharp\n[Fact]\npublic void LinuxWindow_CreateAndShow_DisplaysCorrectly()\n{\n var app = new LinuxApplication();\n var window = new LinuxWindow(app)\n {\n Title = \u0022Test Window\u0022,\n Width = 800,\n Height = 600\n };\n \n window.Show();\n app.ProcessEvents(); // Process pending X11/Wayland events\n \n Assert.True(window.IsVisible);\n Assert.Equal(800, window.Width);\n Assert.Equal(600, window.Height);\n \n window.Close();\n}\n\u0060\u0060\u0060\n\n## Rendering Pipeline Tests\n\nVerify the SkiaSharp rendering pipeline works with actual GPU contexts:\n\n\u0060\u0060\u0060csharp\n[Fact]\npublic void GpuRenderingEngine_Initialize_CreatesOpenGLContext()\n{\n var window = new GtkHostWindow();\n window.Show();\n \n var renderEngine = new GpuRenderingEngine(window);\n var initialized = renderEngine.Initialize();\n \n Assert.True(initialized);\n Assert.NotNull(renderEngine.GRContext);\n Assert.True(renderEngine.SupportsHardwareAcceleration);\n \n window.Close();\n}\n\n[Fact]\npublic void SkiaRenderingEngine_DirtyRegion_OptimizesRedraws()\n{\n var surface = CreateTestSurface(800, 600);\n var engine = new SkiaRenderingEngine(surface);\n \n var button = new SkiaButton\n {\n Bounds = new Rect(10, 10, 100, 40)\n };\n \n engine.AddView(button);\n engine.Render(); // Initial full render\n \n button.Text = \u0022Updated\u0022;\n var dirtyRect = engine.GetDirtyRegion();\n \n // Only button region should be dirty\n Assert.Equal(new Rect(10, 10, 100, 40), dirtyRect);\n}\n\u0060\u0060\u0060\n\n## Multi-Monitor Tests\n\nTest HiDPI scaling and multi-monitor configurations:\n\n\u0060\u0060\u0060csharp\n[Fact]\npublic void LinuxWindow_HiDPIDisplay_ScalesCorrectly()\n{\n var window = new LinuxWindow(new LinuxApplication());\n var scaleFactor = window.GetScaleFactor();\n \n window.Width = 800;\n window.Height = 600;\n \n // Physical pixels should account for scale factor\n Assert.Equal(800 * scaleFactor, window.PhysicalWidth);\n Assert.Equal(600 * scaleFactor, window.PhysicalHeight);\n}\n\u0060\u0060\u0060\n\n## Input Method Editor (IME) Tests\n\nTest text input with IBus, Fcitx5, and XIM:\n\n\u0060\u0060\u0060csharp\n[Fact]\npublic void SkiaEntry_IMEInput_HandlesComposition()\n{\n var entry = new SkiaEntry();\n var imeContext = new LinuxIMEContext(entry);\n \n // Simulate IME composition\n imeContext.SetCompositionText(\u0022\u3053\u3093\u306B\u3061\u306F\u0022);\n Assert.Equal(\u0022\u3053\u3093\u306B\u3061\u306F\u0022, entry.CompositionText);\n \n // Commit composition\n imeContext.CommitComposition();\n Assert.Equal(\u0022\u3053\u3093\u306B\u3061\u306F\u0022, entry.Text);\n Assert.Null(entry.CompositionText);\n}\n\u0060\u0060\u0060\n\n## Theme Detection Tests\n\nVerify system theme detection across desktop environments:\n\n\u0060\u0060\u0060csharp\n[Theory]\n[InlineData(\u0022GNOME\u0022, \u0022Adwaita-dark\u0022)]\n[InlineData(\u0022KDE\u0022, \u0022Breeze-Dark\u0022)]\npublic void SystemThemeService_DetectDarkMode_ReturnsCorrectTheme(\n string desktopEnv, string themeName)\n{\n Environment.SetEnvironmentVariable(\u0022XDG_CURRENT_DESKTOP\u0022, desktopEnv);\n Environment.SetEnvironmentVariable(\u0022GTK_THEME\u0022, themeName);\n \n var themeService = new SystemThemeService();\n var isDark = themeService.IsDarkMode();\n \n Assert.True(isDark);\n}\n\u0060\u0060\u0060\n\n## Continuous Integration Setup\n\nConfigure GitHub Actions for automated integration testing:\n\n\u0060\u0060\u0060yaml\nname: Integration Tests\n\non: [push, pull_request]\n\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v3\n \n - name: Setup .NET\n uses: actions/setup-dotnet@v3\n with:\n dotnet-version: \u00278.0.x\u0027\n \n - name: Install dependencies\n run: |\n sudo apt-get update\n sudo apt-get install -y xvfb libgtk-3-0 libwebkit2gtk-4.0-37\n \n - name: Run integration tests\n run: xvfb-run -a dotnet test --filter Category=Integration\n\u0060\u0060\u0060\n\nIntegration tests catch issues that unit tests miss: display server compatibility, font rendering with HarfBuzz, gesture recognition accuracy, and theme application consistency." + }, + { + "header": "Packaging as .deb and .rpm", + "content": "Debian (.deb) and RPM packages are the foundation of Linux distribution, used by Ubuntu/Debian and Fedora/RHEL families respectively. Proper packaging ensures your application integrates seamlessly with system package managers.\n\n## Debian Package Structure\n\nCreate a \u0060debian/\u0060 directory in your project root:\n\n\u0060\u0060\u0060\ndebian/\n\u251C\u2500\u2500 changelog\n\u251C\u2500\u2500 control\n\u251C\u2500\u2500 copyright\n\u251C\u2500\u2500 rules\n\u251C\u2500\u2500 openmaui-app.install\n\u2514\u2500\u2500 openmaui-app.desktop\n\u0060\u0060\u0060\n\n**debian/control** - Package metadata:\n\u0060\u0060\u0060\nSource: openmaui-app\nSection: misc\nPriority: optional\nMaintainer: Your Name \u003Cyour.email@example.com\u003E\nBuild-Depends: debhelper (\u003E= 12), dotnet-sdk-8.0\nStandards-Version: 4.5.0\n\nPackage: openmaui-app\nArchitecture: amd64\nDepends: ${shlibs:Depends}, ${misc:Depends},\n libgtk-3-0 (\u003E= 3.24),\n libwebkit2gtk-4.0-37,\n libskia,\n libharfbuzz0b,\n aspnetcore-runtime-8.0\nDescription: OpenMaui Linux Application\n A cross-platform .NET MAUI application running natively on Linux\n with full Skia rendering and GTK integration.\n\u0060\u0060\u0060\n\n**debian/rules** - Build instructions:\n\u0060\u0060\u0060makefile\n#!/usr/bin/make -f\n\n%:\n\tdh $@\n\noverride_dh_auto_build:\n\tdotnet publish -c Release -r linux-x64 \\\n\t\t--self-contained false \\\n\t\t-p:PublishSingleFile=false \\\n\t\t-o debian/tmp/usr/lib/openmaui-app\n\noverride_dh_auto_install:\n\tdh_auto_install\n\tinstall -D -m 644 debian/openmaui-app.desktop \\\n\t\tdebian/openmaui-app/usr/share/applications/openmaui-app.desktop\n\tinstall -D -m 644 Resources/appicon.png \\\n\t\tdebian/openmaui-app/usr/share/icons/hicolor/256x256/apps/openmaui-app.png\n\u0060\u0060\u0060\n\n**debian/openmaui-app.install** - File installation paths:\n\u0060\u0060\u0060\nusr/lib/openmaui-app/*\nusr/share/applications/openmaui-app.desktop\nusr/share/icons/hicolor/256x256/apps/openmaui-app.png\n\u0060\u0060\u0060\n\n**debian/changelog** - Version history:\n\u0060\u0060\u0060\nopenmaui-app (1.0.0-1) unstable; urgency=medium\n\n * Initial release\n * Full MAUI API compliance\n * X11 and Wayland support\n\n -- Your Name \u003Cyour.email@example.com\u003E Mon, 20 Jan 2025 10:00:00 \u002B0000\n\u0060\u0060\u0060\n\n## Building the .deb Package\n\n\u0060\u0060\u0060bash\n# Install build tools\nsudo apt-get install debhelper devscripts\n\n# Build package\ndpkg-buildpackage -us -uc -b\n\n# Result: ../openmaui-app_1.0.0-1_amd64.deb\n\n# Test installation\nsudo dpkg -i ../openmaui-app_1.0.0-1_amd64.deb\nsudo apt-get install -f # Fix dependencies if needed\n\u0060\u0060\u0060\n\n## RPM Package Structure\n\nCreate an RPM spec file \u0060openmaui-app.spec\u0060:\n\n\u0060\u0060\u0060spec\nName: openmaui-app\nVersion: 1.0.0\nRelease: 1%{?dist}\nSummary: OpenMaui Linux Application\n\nLicense: MIT\nURL: https://github.com/yourusername/openmaui-app\nSource0: %{name}-%{version}.tar.gz\n\nBuildRequires: dotnet-sdk-8.0\nRequires: gtk3 \u003E= 3.24\nRequires: webkit2gtk3\nRequires: aspnetcore-runtime-8.0\nRequires: libSkiaSharp\nRequires: harfbuzz\n\n%description\nA cross-platform .NET MAUI application running natively on Linux\nwith full Skia rendering and GTK integration.\n\n%prep\n%setup -q\n\n%build\ndotnet publish -c Release -r linux-x64 \\\n --self-contained false \\\n -p:PublishSingleFile=false \\\n -o %{buildroot}/usr/lib/%{name}\n\n%install\nrm -rf %{buildroot}\nmkdir -p %{buildroot}/usr/lib/%{name}\nmkdir -p %{buildroot}/usr/share/applications\nmkdir -p %{buildroot}/usr/share/icons/hicolor/256x256/apps\n\ncp -r bin/Release/net8.0/linux-x64/publish/* %{buildroot}/usr/lib/%{name}/\ninstall -m 644 openmaui-app.desktop %{buildroot}/usr/share/applications/\ninstall -m 644 Resources/appicon.png %{buildroot}/usr/share/icons/hicolor/256x256/apps/%{name}.png\n\n%files\n/usr/lib/%{name}/\n/usr/share/applications/%{name}.desktop\n/usr/share/icons/hicolor/256x256/apps/%{name}.png\n\n%changelog\n* Mon Jan 20 2025 Your Name \u003Cyour.email@example.com\u003E - 1.0.0-1\n- Initial release\n\u0060\u0060\u0060\n\n## Building the RPM Package\n\n\u0060\u0060\u0060bash\n# Install build tools (Fedora/RHEL)\nsudo dnf install rpm-build rpmdevtools\n\n# Setup RPM build environment\nrpmdev-setuptree\n\n# Copy spec and source\ncp openmaui-app.spec ~/rpmbuild/SPECS/\ntar czf ~/rpmbuild/SOURCES/openmaui-app-1.0.0.tar.gz .\n\n# Build RPM\nrpmbuild -ba ~/rpmbuild/SPECS/openmaui-app.spec\n\n# Result: ~/rpmbuild/RPMS/x86_64/openmaui-app-1.0.0-1.x86_64.rpm\n\n# Test installation\nsudo dnf install ~/rpmbuild/RPMS/x86_64/openmaui-app-1.0.0-1.x86_64.rpm\n\u0060\u0060\u0060\n\n## Desktop Entry File\n\nCreate \u0060openmaui-app.desktop\u0060 for both package types:\n\n\u0060\u0060\u0060ini\n[Desktop Entry]\nVersion=1.0\nType=Application\nName=OpenMaui App\nComment=Cross-platform .NET MAUI application\nExec=/usr/lib/openmaui-app/OpenMauiApp\nIcon=openmaui-app\nTerminal=false\nCategories=Utility;Development;\nKeywords=maui;dotnet;cross-platform;\nStartupWMClass=OpenMauiApp\n\u0060\u0060\u0060\n\n## Dependency Management\n\nOpenMaui applications require specific runtime dependencies:\n\n**Core dependencies:**\n- \u0060aspnetcore-runtime-8.0\u0060 - .NET runtime\n- \u0060libgtk-3-0\u0060 / \u0060gtk3\u0060 - GTK windowing\n- \u0060libwebkit2gtk-4.0-37\u0060 / \u0060webkit2gtk3\u0060 - WebView support\n- \u0060libskia\u0060 - SkiaSharp native libraries\n- \u0060libharfbuzz0b\u0060 / \u0060harfbuzz\u0060 - Text shaping\n\n**Optional dependencies:**\n- \u0060libva2\u0060 - Hardware video acceleration\n- \u0060at-spi2-core\u0060 - Accessibility support\n- \u0060ibus\u0060 / \u0060fcitx5\u0060 - Input method support\n\nTest your packages on clean VMs to verify all dependencies are correctly specified." + }, + { + "header": "Flatpak and Snap Distribution", + "content": "Flatpak and Snap provide sandboxed, distribution-agnostic packaging with automatic updates. They\u0027re ideal for reaching users across all Linux distributions without maintaining separate .deb and .rpm packages.\n\n## Flatpak Packaging\n\nFlatpak uses manifest files to define your application and its dependencies.\n\n**com.example.OpenMauiApp.yml** - Flatpak manifest:\n\u0060\u0060\u0060yaml\napp-id: com.example.OpenMauiApp\nruntime: org.freedesktop.Platform\nruntime-version: \u002723.08\u0027\nsdk: org.freedesktop.Sdk\nsdk-extensions:\n - org.freedesktop.Sdk.Extension.dotnet8\ncommand: openmaui-app\n\nfinish-args:\n # Wayland and X11 access\n - --socket=wayland\n - --socket=fallback-x11\n - --share=ipc\n \n # GPU acceleration\n - --device=dri\n \n # File system access\n - --filesystem=home\n - --filesystem=xdg-documents\n \n # Network access (for WebView)\n - --share=network\n \n # Desktop integration\n - --talk-name=org.freedesktop.Notifications\n - --talk-name=org.freedesktop.secrets\n - --talk-name=org.gtk.vfs.*\n \n # Theme access\n - --filesystem=xdg-config/gtk-3.0:ro\n - --env=GTK_THEME=Adwaita:dark\n\nbuild-options:\n append-path: /usr/lib/sdk/dotnet8/bin\n env:\n PKG_CONFIG_PATH: /app/lib/pkgconfig:/app/share/pkgconfig:/usr/lib/pkgconfig:/usr/share/pkgconfig\n\nmodules:\n # SkiaSharp native dependencies\n - name: skia\n buildsystem: simple\n build-commands:\n - install -Dm755 libSkiaSharp.so /app/lib/libSkiaSharp.so\n sources:\n - type: file\n url: https://www.nuget.org/api/v2/package/SkiaSharp.NativeAssets.Linux/2.88.9\n sha256: \u003Chash\u003E\n \n # HarfBuzz for text shaping\n - name: harfbuzz\n buildsystem: meson\n config-opts:\n - -Dglib=enabled\n - -Dfreetype=enabled\n sources:\n - type: archive\n url: https://github.com/harfbuzz/harfbuzz/releases/download/7.3.0/harfbuzz-7.3.0.tar.xz\n sha256: \u003Chash\u003E\n \n # Main application\n - name: openmaui-app\n buildsystem: simple\n build-commands:\n - dotnet publish -c Release -r linux-x64 --self-contained false -o /app/bin\n - install -Dm644 com.example.OpenMauiApp.desktop /app/share/applications/com.example.OpenMauiApp.desktop\n - install -Dm644 Resources/appicon.png /app/share/icons/hicolor/256x256/apps/com.example.OpenMauiApp.png\n - install -Dm644 com.example.OpenMauiApp.metainfo.xml /app/share/metainfo/com.example.OpenMauiApp.metainfo.xml\n sources:\n - type: dir\n path: .\n\u0060\u0060\u0060\n\n**Building and testing Flatpak:**\n\u0060\u0060\u0060bash\n# Install flatpak-builder\nsudo apt-get install flatpak-builder\n\n# Add Flathub repository\nflatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo\n\n# Install required runtimes\nflatpak install flathub org.freedesktop.Platform//23.08\nflatpak install flathub org.freedesktop.Sdk//23.08\nflatpak install flathub org.freedesktop.Sdk.Extension.dotnet8//23.08\n\n# Build Flatpak\nflatpak-builder --force-clean build-dir com.example.OpenMauiApp.yml\n\n# Test locally\nflatpak-builder --run build-dir com.example.OpenMauiApp.yml openmaui-app\n\n# Create single-file bundle for distribution\nflatpak-builder --repo=repo --force-clean build-dir com.example.OpenMauiApp.yml\nflatpak build-bundle repo openmaui-app.flatpak com.example.OpenMauiApp\n\n# Install bundle\nflatpak install openmaui-app.flatpak\n\u0060\u0060\u0060\n\n**AppStream metadata** (com.example.OpenMauiApp.metainfo.xml):\n\u0060\u0060\u0060xml\n\u003C?xml version=\u00221.0\u0022 encoding=\u0022UTF-8\u0022?\u003E\n\u003Ccomponent type=\u0022desktop-application\u0022\u003E\n \u003Cid\u003Ecom.example.OpenMauiApp\u003C/id\u003E\n \u003Cmetadata_license\u003ECC0-1.0\u003C/metadata_license\u003E\n \u003Cproject_license\u003EMIT\u003C/project_license\u003E\n \u003Cname\u003EOpenMaui App\u003C/name\u003E\n \u003Csummary\u003ECross-platform .NET MAUI application\u003C/summary\u003E\n \u003Cdescription\u003E\n \u003Cp\u003E\n A production-ready .NET MAUI application running natively on Linux\n with full Skia rendering, GTK integration, and cross-platform compatibility.\n \u003C/p\u003E\n \u003C/description\u003E\n \u003Cscreenshots\u003E\n \u003Cscreenshot type=\u0022default\u0022\u003E\n \u003Cimage\u003Ehttps://example.com/screenshots/main.png\u003C/image\u003E\n \u003C/screenshot\u003E\n \u003C/screenshots\u003E\n \u003Curl type=\u0022homepage\u0022\u003Ehttps://example.com\u003C/url\u003E\n \u003Creleases\u003E\n \u003Crelease version=\u00221.0.0\u0022 date=\u00222025-01-20\u0022/\u003E\n \u003C/releases\u003E\n \u003Ccontent_rating type=\u0022oars-1.1\u0022/\u003E\n\u003C/component\u003E\n\u0060\u0060\u0060\n\n## Snap Packaging\n\nSnap uses a \u0060snapcraft.yaml\u0060 file for configuration.\n\n**snapcraft.yaml:**\n\u0060\u0060\u0060yaml\nname: openmaui-app\nbase: core22\nversion: \u00271.0.0\u0027\nsummary: OpenMaui Linux Application\ndescription: |\n A cross-platform .NET MAUI application running natively on Linux\n with full Skia rendering and GTK integration.\n\ngrade: stable\nconfinement: strict\n\napps:\n openmaui-app:\n command: bin/openmaui-app\n extensions: [gnome]\n plugs:\n - home\n - network\n - desktop\n - desktop-legacy\n - wayland\n - x11\n - opengl\n - audio-playback\n - removable-media\n\nparts:\n dotnet-runtime:\n plugin: nil\n build-packages:\n - wget\n override-build: |\n wget https://dot.net/v1/dotnet-install.sh\n chmod \u002Bx dotnet-install.sh\n ./dotnet-install.sh --channel 8.0 --runtime aspnetcore --install-dir $SNAPCRAFT_PART_INSTALL/dotnet\n stage:\n - dotnet/*\n\n openmaui-app:\n plugin: dotnet\n source: .\n dotnet-build-configuration: Release\n dotnet-self-contained-runtime-identifier: linux-x64\n build-packages:\n - dotnet-sdk-8.0\n stage-packages:\n - libgtk-3-0\n - libwebkit2gtk-4.0-37\n - libharfbuzz0b\n - libfontconfig1\n - libfreetype6\n override-build: |\n dotnet publish -c Release -r linux-x64 \\\n --self-contained false \\\n -o $SNAPCRAFT_PART_INSTALL/bin\n\u0060\u0060\u0060\n\n**Building and publishing Snap:**\n\u0060\u0060\u0060bash\n# Install snapcraft\nsudo snap install snapcraft --classic\n\n# Build snap\nsnapcraft\n\n# Test locally\nsudo snap install openmaui-app_1.0.0_amd64.snap --dangerous\n\n# Run application\nopenmaui-app\n\n# Publish to Snap Store (requires account)\nsnapcraft login\nsnapcraft upload openmaui-app_1.0.0_amd64.snap --release=stable\n\u0060\u0060\u0060\n\n## Portal Permissions\n\nBoth Flatpak and Snap use portals for secure file access:\n\n\u0060\u0060\u0060csharp\n// OpenMaui handles portals automatically\npublic class LinuxFilePicker : IFilePicker\n{\n public async Task\u003CFileResult\u003E PickAsync(PickOptions options)\n {\n // Detects if running in sandbox\n if (IsRunningInFlatpak() || IsRunningInSnap())\n {\n // Use xdg-desktop-portal for sandboxed file access\n return await PickViaPortalAsync(options);\n }\n else\n {\n // Direct file picker (zenity/kdialog)\n return await PickDirectAsync(options);\n }\n }\n \n private bool IsRunningInFlatpak()\n {\n return File.Exists(\u0022/.flatpak-info\u0022);\n }\n \n private bool IsRunningInSnap()\n {\n return !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(\u0022SNAP\u0022));\n }\n}\n\u0060\u0060\u0060\n\n## Comparison: Flatpak vs Snap\n\n**Flatpak advantages:**\n- Better desktop integration\n- More granular permissions\n- Preferred by traditional Linux distributions\n- Excellent for GTK applications\n\n**Snap advantages:**\n- Simpler manifest syntax\n- Better CLI tool support\n- Strong Ubuntu/Canonical ecosystem\n- Automatic delta updates\n\nFor OpenMaui applications, **Flatpak is recommended** due to superior GTK integration and broader community adoption." + }, + { + "header": "AppImage for Universal Compatibility", + "content": "AppImage provides the simplest distribution method: a single executable file that runs on any Linux distribution without installation. The beauty of AppImage is its simplicity: bundle everything your application needs into a single executable file that runs on virtually any Linux distribution without installation.\n\n## Creating an AppImage\n\nAppImages use the AppDir structure with all dependencies bundled:\n\n\u0060\u0060\u0060bash\n# Install appimagetool\nwget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage\nchmod \u002Bx appimagetool-x86_64.AppImage\n\n# Create AppDir structure\nmkdir -p OpenMauiApp.AppDir/usr/bin\nmkdir -p OpenMauiApp.AppDir/usr/lib\nmkdir -p OpenMauiApp.AppDir/usr/share/applications\nmkdir -p OpenMauiApp.AppDir/usr/share/icons/hicolor/256x256/apps\n\u0060\u0060\u0060\n\n## Publishing Self-Contained Build\n\nPublish your application with all dependencies:\n\n\u0060\u0060\u0060bash\ndotnet publish -c Release -r linux-x64 \\\n --self-contained true \\\n -p:PublishSingleFile=false \\\n -p:PublishTrimmed=false \\\n -o publish/\n\n# Copy to AppDir\ncp -r publish/* OpenMauiApp.AppDir/usr/bin/\n\u0060\u0060\u0060\n\n## Bundling Native Dependencies\n\nCopy required native libraries:\n\n\u0060\u0060\u0060bash\n#!/bin/bash\n# bundle-dependencies.sh\n\nAPPDIR=\u0022OpenMauiApp.AppDir\u0022\nLIBDIR=\u0022$APPDIR/usr/lib\u0022\n\n# Copy GTK and dependencies\ncp /usr/lib/x86_64-linux-gnu/libgtk-3.so.0 $LIBDIR/\ncp /usr/lib/x86_64-linux-gnu/libgdk-3.so.0 $LIBDIR/\ncp /usr/lib/x86_64-linux-gnu/libwebkit2gtk-4.0.so.37 $LIBDIR/\n\n# Copy SkiaSharp native\ncp ~/.nuget/packages/skiasharp.nativeassets.linux/2.88.9/runtimes/linux-x64/native/libSkiaSharp.so $LIBDIR/\n\n# Copy HarfBuzz\ncp /usr/lib/x86_64-linux-gnu/libharfbuzz.so.0 $LIBDIR/\n\n# Copy their dependencies\nfor lib in $LIBDIR/*.so*; do\n ldd $lib | grep \u0022=\u003E /\u0022 | awk \u0027{print $3}\u0027 | while read dep; do\n if [ ! -f \u0022$LIBDIR/$(basename $dep)\u0022 ]; then\n cp $dep $LIBDIR/\n fi\n done\ndone\n\u0060\u0060\u0060\n\n## AppRun Script\n\nCreate the launcher script \u0060OpenMauiApp.AppDir/AppRun\u0060:\n\n\u0060\u0060\u0060bash\n#!/bin/bash\n\n# Get the directory of this script\nHERE=\u0022$(dirname \u0022$(readlink -f \u0022${0}\u0022)\u0022)\u0022\n\n# Set library path to include bundled libraries\nexport LD_LIBRARY_PATH=\u0022$HERE/usr/lib:$LD_LIBRARY_PATH\u0022\n\n# Set GTK theme path\nexport GTK_PATH=\u0022$HERE/usr/lib/gtk-3.0\u0022\nexport GTK_DATA_PREFIX=\u0022$HERE/usr\u0022\n\n# Set GDK backend (prefer Wayland, fallback to X11)\nexport GDK_BACKEND=wayland,x11\n\n# Disable AppImage desktop integration (optional)\nexport APPIMAGE_DISABLE_INTEGRATION=1\n\n# Run the application\nexec \u0022$HERE/usr/bin/OpenMauiApp\u0022 \u0022$@\u0022\n\u0060\u0060\u0060\n\nMake it executable:\n\u0060\u0060\u0060bash\nchmod \u002Bx OpenMauiApp.AppDir/AppRun\n\u0060\u0060\u0060\n\n## Desktop Entry and Icon\n\n**OpenMauiApp.AppDir/openmaui-app.desktop:**\n\u0060\u0060\u0060ini\n[Desktop Entry]\nType=Application\nName=OpenMaui App\nExec=openmaui-app\nIcon=openmaui-app\nCategories=Utility;Development;\nTerminal=false\n\u0060\u0060\u0060\n\nCopy icon and create symlinks:\n\u0060\u0060\u0060bash\ncp Resources/appicon.png OpenMauiApp.AppDir/usr/share/icons/hicolor/256x256/apps/openmaui-app.png\ncp Resources/appicon.png OpenMauiApp.AppDir/openmaui-app.png\ncp Resources/appicon.png OpenMauiApp.AppDir/.DirIcon\n\nln -s usr/share/applications/openmaui-app.desktop OpenMauiApp.AppDir/openmaui-app.desktop\n\u0060\u0060\u0060\n\n## Building the AppImage\n\n\u0060\u0060\u0060bash\n# Build AppImage\n./appimagetool-x86_64.AppImage OpenMauiApp.AppDir OpenMauiApp-1.0.0-x86_64.AppImage\n\n# Make executable\nchmod \u002Bx OpenMauiApp-1.0.0-x86_64.AppImage\n\n# Test\n./OpenMauiApp-1.0.0-x86_64.AppImage\n\u0060\u0060\u0060\n\n## AppImage Updates\n\nImplement AppImageUpdate support by embedding update information:\n\n\u0060\u0060\u0060bash\n# Add update information to AppImage\n./appimagetool-x86_64.AppImage OpenMauiApp.AppDir \\\n OpenMauiApp-1.0.0-x86_64.AppImage \\\n -u \u0022gh-releases-zsync|yourusername|openmaui-app|latest|OpenMauiApp-*-x86_64.AppImage.zsync\u0022\n\u0060\u0060\u0060\n\nGenerate zsync file for delta updates:\n\u0060\u0060\u0060bash\nzsyncmake OpenMauiApp-1.0.0-x86_64.AppImage\n\u0060\u0060\u0060\n\n## Runtime Update Checks\n\nImplement update checking in your application:\n\n\u0060\u0060\u0060csharp\npublic class AppImageUpdateService\n{\n public async Task\u003Cbool\u003E CheckForUpdatesAsync()\n {\n var appImagePath = Environment.GetEnvironmentVariable(\u0022APPIMAGE\u0022);\n if (string.IsNullOrEmpty(appImagePath))\n return false; // Not running as AppImage\n \n var process = new Process\n {\n StartInfo = new ProcessStartInfo\n {\n FileName = \u0022appimageupdatetool\u0022,\n Arguments = $\u0022-j {appImagePath}\u0022,\n RedirectStandardOutput = true,\n UseShellExecute = false\n }\n };\n \n process.Start();\n var output = await process.StandardOutput.ReadToEndAsync();\n await process.WaitForExitAsync();\n \n var updateInfo = JsonSerializer.Deserialize\u003CUpdateInfo\u003E(output);\n return updateInfo?.UpdateAvailable ?? false;\n }\n \n public async Task ApplyUpdateAsync()\n {\n var appImagePath = Environment.GetEnvironmentVariable(\u0022APPIMAGE\u0022);\n var process = new Process\n {\n StartInfo = new ProcessStartInfo\n {\n FileName = \u0022appimageupdatetool\u0022,\n Arguments = appImagePath,\n UseShellExecute = true\n }\n };\n \n process.Start();\n await process.WaitForExitAsync();\n \n // Restart application\n Process.Start(appImagePath);\n Environment.Exit(0);\n }\n}\n\u0060\u0060\u0060\n\n## Automated AppImage Build Script\n\nCreate \u0060build-appimage.sh\u0060:\n\n\u0060\u0060\u0060bash\n#!/bin/bash\nset -e\n\nVERSION=\u00221.0.0\u0022\nAPPNAME=\u0022OpenMauiApp\u0022\nAPPDIR=\u0022$APPNAME.AppDir\u0022\n\necho \u0022Building $APPNAME v$VERSION AppImage...\u0022\n\n# Clean previous build\nrm -rf $APPDIR\nrm -f $APPNAME-*.AppImage\n\n# Publish application\ndotnet publish -c Release -r linux-x64 --self-contained true -o publish/\n\n# Create AppDir structure\nmkdir -p $APPDIR/usr/bin\nmkdir -p $APPDIR/usr/lib\nmkdir -p $APPDIR/usr/share/applications\nmkdir -p $APPDIR/usr/share/icons/hicolor/256x256/apps\n\n# Copy application\ncp -r publish/* $APPDIR/usr/bin/\n\n# Bundle dependencies\n./bundle-dependencies.sh\n\n# Copy desktop entry and icon\ncp openmaui-app.desktop $APPDIR/usr/share/applications/\ncp Resources/appicon.png $APPDIR/usr/share/icons/hicolor/256x256/apps/openmaui-app.png\ncp Resources/appicon.png $APPDIR/openmaui-app.png\ncp Resources/appicon.png $APPDIR/.DirIcon\nln -s usr/share/applications/openmaui-app.desktop $APPDIR/openmaui-app.desktop\n\n# Create AppRun\ncat \u003E $APPDIR/AppRun \u003C\u003C \u0027EOF\u0027\n#!/bin/bash\nHERE=\u0022$(dirname \u0022$(readlink -f \u0022${0}\u0022)\u0022)\u0022 \nexport LD_LIBRARY_PATH=\u0022$HERE/usr/lib:$LD_LIBRARY_PATH\u0022\nexport GTK_PATH=\u0022$HERE/usr/lib/gtk-3.0\u0022\nexec \u0022$HERE/usr/bin/OpenMauiApp\u0022 \u0022$@\u0022\nEOF\nchmod \u002Bx $APPDIR/AppRun\n\n# Build AppImage\n./appimagetool-x86_64.AppImage $APPDIR $APPNAME-$VERSION-x86_64.AppImage\n\necho \u0022AppImage created: $APPNAME-$VERSION-x86_64.AppImage\u0022\necho \u0022Size: $(du -h $APPNAME-$VERSION-x86_64.AppImage | cut -f1)\u0022\n\u0060\u0060\u0060\n\nAppImages are perfect for rapid distribution, beta testing, and users who prefer not to use package managers." + }, + { + "header": "CI/CD Pipeline Setup", + "content": "Automated build, test, and release pipelines ensure consistent quality and streamline distribution across multiple package formats. Here\u0027s how to set up comprehensive CI/CD for OpenMaui Linux applications.\n\n## GitHub Actions Workflow\n\nCreate \u0060.github/workflows/release.yml\u0060:\n\n\u0060\u0060\u0060yaml\nname: Build and Release\n\non:\n push:\n branches: [main]\n tags:\n - \u0027v*\u0027\n pull_request:\n branches: [main]\n\nenv:\n DOTNET_VERSION: \u00278.0.x\u0027\n PROJECT_NAME: OpenMauiApp\n\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v3\n \n - name: Setup .NET\n uses: actions/setup-dotnet@v3\n with:\n dotnet-version: ${{ env.DOTNET_VERSION }}\n \n - name: Install dependencies\n run: |\n sudo apt-get update\n sudo apt-get install -y xvfb libgtk-3-0 libwebkit2gtk-4.0-37 \\\n libharfbuzz0b libfontconfig1 libfreetype6\n \n - name: Restore dependencies\n run: dotnet restore\n \n - name: Build\n run: dotnet build -c Release --no-restore\n \n - name: Run unit tests\n run: dotnet test -c Release --no-build --verbosity normal \\\n --filter Category=Unit\n \n - name: Run integration tests\n run: xvfb-run -a dotnet test -c Release --no-build --verbosity normal \\\n --filter Category=Integration\n\n build-deb:\n needs: test\n runs-on: ubuntu-latest\n if: startsWith(github.ref, \u0027refs/tags/v\u0027)\n steps:\n - uses: actions/checkout@v3\n \n - name: Setup .NET\n uses: actions/setup-dotnet@v3\n with:\n dotnet-version: ${{ env.DOTNET_VERSION }}\n \n - name: Install build tools\n run: sudo apt-get install -y debhelper devscripts\n \n - name: Build .deb package\n run: |\n dpkg-buildpackage -us -uc -b\n mv ../*.deb .\n \n - name: Upload .deb artifact\n uses: actions/upload-artifact@v3\n with:\n name: deb-package\n path: \u0027*.deb\u0027\n\n build-rpm:\n needs: test\n runs-on: ubuntu-latest\n if: startsWith(github.ref, \u0027refs/tags/v\u0027)\n container:\n image: fedora:latest\n steps:\n - uses: actions/checkout@v3\n \n - name: Install dependencies\n run: |\n dnf install -y dotnet-sdk-8.0 rpm-build rpmdevtools\n \n - name: Setup RPM build tree\n run: rpmdev-setuptree\n \n - name: Build RPM\n run: |\n cp openmaui-app.spec ~/rpmbuild/SPECS/\n tar czf ~/rpmbuild/SOURCES/${{ env.PROJECT_NAME }}-${GITHUB_REF#refs/tags/v}.tar.gz .\n rpmbuild -ba ~/rpmbuild/SPECS/openmaui-app.spec\n cp ~/rpmbuild/RPMS/x86_64/*.rpm .\n \n - name: Upload RPM artifact\n uses: actions/upload-artifact@v3\n with:\n name: rpm-package\n path: \u0027*.rpm\u0027\n\n build-flatpak:\n needs: test\n runs-on: ubuntu-latest\n if: startsWith(github.ref, \u0027refs/tags/v\u0027)\n steps:\n - uses: actions/checkout@v3\n \n - name: Install Flatpak and flatpak-builder\n run: |\n sudo apt-get install -y flatpak flatpak-builder\n flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo\n flatpak install -y flathub org.freedesktop.Platform//23.08\n flatpak install -y flathub org.freedesktop.Sdk//23.08\n flatpak install -y flathub org.freedesktop.Sdk.Extension.dotnet8//23.08\n \n - name: Build Flatpak\n run: |\n flatpak-builder --repo=repo --force-clean build-dir \\\n com.example.OpenMauiApp.yml\n flatpak build-bundle repo ${{ env.PROJECT_NAME }}.flatpak \\\n com.example.OpenMauiApp\n \n - name: Upload Flatpak artifact\n uses: actions/upload-artifact@v3\n with:\n name: flatpak-package\n path: \u0027*.flatpak\u0027\n\n build-appimage:\n needs: test\n runs-on: ubuntu-latest\n if: startsWith(github.ref, \u0027refs/tags/v\u0027)\n steps:\n - uses: actions/checkout@v3\n \n - name: Setup .NET\n uses: actions/setup-dotnet@v3\n with:\n dotnet-version: ${{ env.DOTNET_VERSION }}\n \n - name: Install dependencies\n run: |\n sudo apt-get install -y libgtk-3-0 libwebkit2gtk-4.0-37 \\\n libharfbuzz0b zsync\n wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage\n chmod \u002Bx appimagetool-x86_64.AppImage\n \n - name: Build AppImage\n run: |\n chmod \u002Bx build-appimage.sh\n ./build-appimage.sh\n zsyncmake ${{ env.PROJECT_NAME }}-*.AppImage\n \n - name: Upload AppImage artifact\n uses: actions/upload-artifact@v3\n with:\n name: appimage-package\n path: |\n *.AppImage\n *.AppImage.zsync\n\n release:\n needs: [build-deb, build-rpm, build-flatpak, build-appimage]\n runs-on: ubuntu-latest\n if: startsWith(github.ref, \u0027refs/tags/v\u0027)\n steps:\n - name: Download all artifacts\n uses: actions/download-artifact@v3\n \n - name: Create Release\n uses: softprops/action-gh-release@v1\n with:\n files: |\n deb-package/*.deb\n rpm-package/*.rpm\n flatpak-package/*.flatpak\n appimage-package/*.AppImage\n appimage-package/*.AppImage.zsync\n draft: false\n prerelease: false\n env:\n GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\u0060\u0060\u0060\n\n## GitLab CI/CD Pipeline\n\nFor GitLab, create \u0060.gitlab-ci.yml\u0060:\n\n\u0060\u0060\u0060yaml\nstages:\n - test\n - build\n - release\n\nvariables:\n DOTNET_VERSION: \u00228.0\u0022\n PROJECT_NAME: \u0022OpenMauiApp\u0022\n\ntest:unit:\n stage: test\n image: mcr.microsoft.com/dotnet/sdk:8.0\n before_script:\n - apt-get update\n - apt-get install -y libgtk-3-0 libwebkit2gtk-4.0-37\n script:\n - dotnet restore\n - dotnet build -c Release\n - dotnet test -c Release --filter Category=Unit\n\ntest:integration:\n stage: test\n image: mcr.microsoft.com/dotnet/sdk:8.0\n before_script:\n - apt-get update\n - apt-get install -y xvfb libgtk-3-0 libwebkit2gtk-4.0-37\n script:\n - xvfb-run -a dotnet test -c Release --filter Category=Integration\n\nbuild:deb:\n stage: build\n image: ubuntu:22.04\n only:\n - tags\n script:\n - apt-get update\n - apt-get install -y debhelper devscripts dotnet-sdk-8.0\n - dpkg-buildpackage -us -uc -b\n - mv ../*.deb .\n artifacts:\n paths:\n - \u0022*.deb\u0022\n\nbuild:appimage:\n stage: build\n image: ubuntu:22.04\n only:\n - tags\n script:\n - apt-get update\n - apt-get install -y dotnet-sdk-8.0 libgtk-3-0 libwebkit2gtk-4.0-37 wget\n - wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage\n - chmod \u002Bx appimagetool-x86_64.AppImage build-appimage.sh\n - ./build-appimage.sh\n artifacts:\n paths:\n - \u0022*.AppImage\u0022\n\nrelease:\n stage: release\n image: registry.gitlab.com/gitlab-org/release-cli:latest\n only:\n - tags\n script:\n - echo \u0022Creating release $CI_COMMIT_TAG\u0022\n release:\n tag_name: $CI_COMMIT_TAG\n description: \u0022Release $CI_COMMIT_TAG\u0022\n assets:\n links:\n - name: \u0022Debian Package\u0022\n url: \u0022${CI_PROJECT_URL}/-/jobs/artifacts/${CI_COMMIT_TAG}/raw/*.deb?job=build:deb\u0022\n - name: \u0022AppImage\u0022\n url: \u0022${CI_PROJECT_URL}/-/jobs/artifacts/${CI_COMMIT_TAG}/raw/*.AppImage?job=build:appimage\u0022\n\u0060\u0060\u0060\n\n## Version Management\n\nAutomate version bumping in \u0060.csproj\u0060:\n\n\u0060\u0060\u0060xml\n\u003CProject Sdk=\u0022Microsoft.NET.Sdk\u0022\u003E\n \u003CPropertyGroup\u003E\n \u003CTargetFramework\u003Enet8.0\u003C/TargetFramework\u003E\n \u003CVersion\u003E1.0.0\u003C/Version\u003E\n \u003CAssemblyVersion\u003E1.0.0.0\u003C/AssemblyVersion\u003E\n \u003CFileVersion\u003E1.0.0.0\u003C/FileVersion\u003E\n \n \u003C!-- Auto-increment build number in CI --\u003E\n \u003CVersion Condition=\u0022\u0027$(BUILD_NUMBER)\u0027 != \u0027\u0027\u0022\u003E1.0.$(BUILD_NUMBER)\u003C/Version\u003E\n \u003C/PropertyGroup\u003E\n\u003C/Project\u003E\n\u0060\u0060\u0060\n\n## Automated Testing Matrix\n\nTest across multiple distributions:\n\n\u0060\u0060\u0060yaml\ntest-matrix:\n strategy:\n matrix:\n os:\n - ubuntu-22.04\n - ubuntu-20.04\n display-server:\n - x11\n - wayland\n runs-on: ${{ matrix.os }}\n steps:\n - name: Setup display server\n run: |\n if [ \u0022${{ matrix.display-server }}\u0022 = \u0022wayland\u0022 ]; then\n sudo apt-get install -y weston\n weston --backend=headless-backend.so \u0026\n export WAYLAND_DISPLAY=wayland-0\n else\n xvfb-run -a dotnet test\n fi\n\u0060\u0060\u0060\n\n## Code Signing\n\nSign packages for distribution:\n\n\u0060\u0060\u0060bash\n# GPG signing for .deb packages\ndpkg-sig --sign builder openmaui-app_1.0.0-1_amd64.deb\n\n# RPM signing\nrpmsign --addsign openmaui-app-1.0.0-1.x86_64.rpm\n\n# AppImage signing\ngpg --detach-sign --armor OpenMauiApp-1.0.0-x86_64.AppImage\n\u0060\u0060\u0060\n\nStore signing keys in CI secrets and automate signing in the pipeline." + }, + { + "header": "Debugging in WSL vs Native Linux", + "content": "Developing on Windows Subsystem for Linux (WSL) offers convenience, but debugging production issues often requires native Linux environments. Understanding the differences helps you debug effectively in both contexts.\n\n## WSL Development Setup\n\nWSL 2 with WSLg provides GUI support for Linux applications:\n\n\u0060\u0060\u0060bash\n# Check WSL version\nwsl --version\n\n# Ensure WSL 2 is enabled\nwsl --set-default-version 2\n\n# Install Ubuntu\nwsl --install -d Ubuntu-22.04\n\u0060\u0060\u0060\n\n**Install development dependencies:**\n\u0060\u0060\u0060bash\nsudo apt-get update\nsudo apt-get install -y dotnet-sdk-8.0 libgtk-3-0 libwebkit2gtk-4.0-37 \\\n libharfbuzz0b libfontconfig1 libfreetype6\n\u0060\u0060\u0060\n\n## Visual Studio Code Remote Development\n\nConfigure VS Code for WSL debugging:\n\n**.vscode/launch.json:**\n\u0060\u0060\u0060json\n{\n \u0022version\u0022: \u00220.2.0\u0022,\n \u0022configurations\u0022: [\n {\n \u0022name\u0022: \u0022Launch OpenMaui (WSL)\u0022,\n \u0022type\u0022: \u0022coreclr\u0022,\n \u0022request\u0022: \u0022launch\u0022,\n \u0022preLaunchTask\u0022: \u0022build\u0022,\n \u0022program\u0022: \u0022${workspaceFolder}/bin/Debug/net8.0/OpenMauiApp.dll\u0022,\n \u0022args\u0022: [],\n \u0022cwd\u0022: \u0022${workspaceFolder}\u0022,\n \u0022console\u0022: \u0022internalConsole\u0022,\n \u0022stopAtEntry\u0022: false,\n \u0022env\u0022: {\n \u0022DISPLAY\u0022: \u0022:0\u0022,\n \u0022WAYLAND_DISPLAY\u0022: \u0022wayland-0\u0022\n }\n },\n {\n \u0022name\u0022: \u0022Attach to Process (WSL)\u0022,\n \u0022type\u0022: \u0022coreclr\u0022,\n \u0022request\u0022: \u0022attach\u0022,\n \u0022processId\u0022: \u0022${command:pickProcess}\u0022\n }\n ]\n}\n\u0060\u0060\u0060\n\n## WSL-Specific Issues\n\n**Display server differences:**\nWSLg uses Weston as the Wayland compositor, which may behave differently from GNOME or KDE:\n\n\u0060\u0060\u0060csharp\npublic class DisplayServerDetector\n{\n public static bool IsRunningInWSL()\n {\n try\n {\n var version = File.ReadAllText(\u0022/proc/version\u0022);\n return version.Contains(\u0022microsoft\u0022, StringComparison.OrdinalIgnoreCase);\n }\n catch\n {\n return false;\n }\n }\n \n public static void LogEnvironment()\n {\n Console.WriteLine($\u0022WSL: {IsRunningInWSL()}\u0022);\n Console.WriteLine($\u0022DISPLAY: {Environment.GetEnvironmentVariable(\u0022DISPLAY\u0022)}\u0022);\n Console.WriteLine($\u0022WAYLAND_DISPLAY: {Environment.GetEnvironmentVariable(\u0022WAYLAND_DISPLAY\u0022)}\u0022);\n Console.WriteLine($\u0022XDG_SESSION_TYPE: {Environment.GetEnvironmentVariable(\u0022XDG_SESSION_TYPE\u0022)}\u0022);\n }\n}\n\u0060\u0060\u0060\n\n**Font rendering:**\nWSL may have limited font fallback support. Test with emoji and CJK characters:\n\n\u0060\u0060\u0060csharp\n[Fact]\npublic void SkiaLabel_ComplexText_RendersCorrectly()\n{\n var label = new SkiaLabel\n {\n Text = \u0022Hello \u4E16\u754C \uD83C\uDF0D \u0645\u0631\u062D\u0628\u0627\u0022\n };\n \n var surface = CreateTestSurface(400, 100);\n label.Measure(new Size(400, 100));\n label.Arrange(new Rect(0, 0, 400, 100));\n label.Draw(surface.Canvas);\n \n // Verify text was rendered (not just boxes)\n var bitmap = surface.PeekPixels();\n Assert.True(HasNonWhitePixels(bitmap));\n}\n\u0060\u0060\u0060\n\n**Hardware acceleration:**\nWSLg provides limited GPU access. Detect and fallback gracefully:\n\n\u0060\u0060\u0060csharp\npublic class RenderEngineFactory\n{\n public static ISkiaRenderingEngine Create(IWindow window)\n {\n if (DisplayServerDetector.IsRunningInWSL())\n {\n // WSL: Prefer software rendering\n Console.WriteLine(\u0022WSL detected, using software rendering\u0022);\n return new SkiaRenderingEngine(window);\n }\n \n // Native Linux: Try GPU first\n try\n {\n var gpuEngine = new GpuRenderingEngine(window);\n if (gpuEngine.Initialize())\n {\n Console.WriteLine(\u0022Using GPU-accelerated rendering\u0022);\n return gpuEngine;\n }\n }\n catch (Exception ex)\n {\n Console.WriteLine($\u0022GPU initialization failed: {ex.Message}\u0022);\n }\n \n Console.WriteLine(\u0022Falling back to software rendering\u0022);\n return new SkiaRenderingEngine(window);\n }\n}\n\u0060\u0060\u0060\n\n## Native Linux Debugging\n\nTesting in WSL provides convenience during development, but native Linux testing is essential for catching display server quirks, font rendering issues, and hardware acceleration problems.\n\n**SSH remote debugging:**\n\u0060\u0060\u0060bash\n# On Linux machine, install SSH server\nsudo apt-get install openssh-server\nsudo systemctl start ssh\n\n# From Windows, connect via VS Code Remote-SSH\n# Or use dotnet trace for profiling\nssh user@linux-machine\ndotnet trace collect --process-id $(pgrep OpenMauiApp)\n\u0060\u0060\u0060\n\n**Remote debugging with lldb:**\n\u0060\u0060\u0060bash\n# Install lldb\nsudo apt-get install lldb\n\n# Attach to running process\nsudo lldb -p $(pgrep OpenMauiApp)\n\n# Set breakpoint\n(lldb) breakpoint set --name SkiaButton.OnDraw\n(lldb) continue\n\u0060\u0060\u0060\n\n## Logging and Diagnostics\n\nImplement comprehensive logging for debugging:\n\n\u0060\u0060\u0060csharp\npublic static class DiagnosticLogger\n{\n private static readonly string LogPath = Path.Combine(\n Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),\n \u0022OpenMauiApp\u0022,\n \u0022logs\u0022,\n $\u0022app-{DateTime.Now:yyyyMMdd-HHmmss}.log\u0022\n );\n \n static DiagnosticLogger()\n {\n Directory.CreateDirectory(Path.GetDirectoryName(LogPath));\n }\n \n public static void LogEnvironment()\n {\n var sb = new StringBuilder();\n sb.AppendLine(\u0022=== Environment Information ===\u0022);\n sb.AppendLine($\u0022OS: {Environment.OSVersion}\u0022);\n sb.AppendLine($\u0022Runtime: {RuntimeInformation.FrameworkDescription}\u0022);\n sb.AppendLine($\u0022WSL: {DisplayServerDetector.IsRunningInWSL()}\u0022);\n sb.AppendLine($\u0022Display: {Environment.GetEnvironmentVariable(\u0022DISPLAY\u0022)}\u0022);\n sb.AppendLine($\u0022Session Type: {Environment.GetEnvironmentVariable(\u0022XDG_SESSION_TYPE\u0022)}\u0022);\n sb.AppendLine($\u0022Desktop: {Environment.GetEnvironmentVariable(\u0022XDG_CURRENT_DESKTOP\u0022)}\u0022);\n sb.AppendLine($\u0022Theme: {Environment.GetEnvironmentVariable(\u0022GTK_THEME\u0022)}\u0022);\n \n File.AppendAllText(LogPath, sb.ToString());\n }\n \n public static void LogException(Exception ex, string context)\n {\n var sb = new StringBuilder();\n sb.AppendLine($\u0022[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] ERROR in {context}\u0022);\n sb.AppendLine($\u0022Message: {ex.Message}\u0022);\n sb.AppendLine($\u0022Stack Trace:\\n{ex.StackTrace}\u0022);\n \n if (ex.InnerException != null)\n {\n sb.AppendLine($\u0022Inner Exception: {ex.InnerException.Message}\u0022);\n }\n \n File.AppendAllText(LogPath, sb.ToString());\n }\n}\n\u0060\u0060\u0060\n\n## Performance Profiling\n\nCompare performance between WSL and native Linux:\n\n\u0060\u0060\u0060csharp\npublic class PerformanceBenchmark\n{\n [Benchmark]\n public void RenderComplexLayout()\n {\n var grid = CreateComplexGrid();\n var surface = CreateTestSurface(1920, 1080);\n \n var stopwatch = Stopwatch.StartNew();\n grid.Measure(new Size(1920, 1080));\n grid.Arrange(new Rect(0, 0, 1920, 1080));\n grid.Draw(surface.Canvas);\n stopwatch.Stop();\n \n Console.WriteLine($\u0022Render time: {stopwatch.ElapsedMilliseconds}ms\u0022);\n Console.WriteLine($\u0022Environment: {(DisplayServerDetector.IsRunningInWSL() ? \u0022WSL\u0022 : \u0022Native\u0022)}\u0022);\n }\n}\n\u0060\u0060\u0060\n\n## Testing Checklist\n\n**WSL testing:**\n- \u2713 Basic functionality\n- \u2713 UI layout and rendering\n- \u2713 Unit and integration tests\n- \u2713 Rapid iteration during development\n\n**Native Linux testing:**\n- \u2713 Multiple distributions (Ubuntu, Fedora, Arch)\n- \u2713 Different desktop environments (GNOME, KDE, XFCE)\n- \u2713 X11 and Wayland display servers\n- \u2713 Hardware acceleration\n- \u2713 Font rendering with complex scripts\n- \u2713 System theme integration\n- \u2713 Multi-monitor configurations\n- \u2713 HiDPI scaling\n- \u2713 Performance profiling\n\nUse WSL for day-to-day development, but always validate on native Linux before release." + }, + { + "header": "Distribution and Update Strategies", + "content": "Getting your application into users\u0027 hands is just the beginning. A robust distribution and update strategy ensures users stay on the latest version with minimal friction.\n\n## Distribution Channels\n\n### Official Distribution Repositories\n\n**Ubuntu PPA (Personal Package Archive):**\n\u0060\u0060\u0060bash\n# Create Launchpad account and PPA\n# Upload source package\ndput ppa:yourusername/openmaui-app openmaui-app_1.0.0-1_source.changes\n\n# Users install via:\nsudo add-apt-repository ppa:yourusername/openmaui-app\nsudo apt-get update\nsudo apt-get install openmaui-app\n\u0060\u0060\u0060\n\n**Fedora COPR (Cool Other Package Repo):**\n\u0060\u0060\u0060bash\n# Create COPR project at copr.fedorainfracloud.org\ncopr-cli create openmaui-app --chroot fedora-39-x86_64\n\n# Build package\ncopr-cli build openmaui-app openmaui-app-1.0.0-1.src.rpm\n\n# Users install via:\nsudo dnf copr enable yourusername/openmaui-app\nsudo dnf install openmaui-app\n\u0060\u0060\u0060\n\n**Arch User Repository (AUR):**\n\nCreate \u0060PKGBUILD\u0060:\n\u0060\u0060\u0060bash\n# Maintainer: Your Name \u003Cyour.email@example.com\u003E\npkgname=openmaui-app\npkgver=1.0.0\npkgrel=1\npkgdesc=\u0022OpenMaui Linux Application\u0022\narch=(\u0027x86_64\u0027)\nurl=\u0022https://github.com/yourusername/openmaui-app\u0022\nlicense=(\u0027MIT\u0027)\ndepends=(\u0027dotnet-runtime\u0027 \u0027gtk3\u0027 \u0027webkit2gtk\u0027 \u0027harfbuzz\u0027)\nmakdepends=(\u0027dotnet-sdk\u0027)\nsource=(\u0022$pkgname-$pkgver.tar.gz::$url/archive/v$pkgver.tar.gz\u0022)\nsha256sums=(\u0027SKIP\u0027)\n\nbuild() {\n cd \u0022$pkgname-$pkgver\u0022\n dotnet publish -c Release -r linux-x64 --self-contained false -o out\n}\n\npackage() {\n cd \u0022$pkgname-$pkgver\u0022\n install -dm755 \u0022$pkgdir/usr/lib/$pkgname\u0022\n cp -r out/* \u0022$pkgdir/usr/lib/$pkgname/\u0022\n \n install -Dm644 \u0022openmaui-app.desktop\u0022 \u0022$pkgdir/usr/share/applications/$pkgname.desktop\u0022\n install -Dm644 \u0022Resources/appicon.png\u0022 \u0022$pkgdir/usr/share/icons/hicolor/256x256/apps/$pkgname.png\u0022\n}\n\u0060\u0060\u0060\n\n### Flathub Submission\n\nSubmit your Flatpak to Flathub for centralized distribution:\n\n\u0060\u0060\u0060bash\n# Fork flathub/flathub repository\ngit clone https://github.com/flathub/flathub.git\ncd flathub\n\n# Create application directory\nmkdir com.example.OpenMauiApp\ncd com.example.OpenMauiApp\n\n# Add manifest and metadata\ncp ~/openmaui-app/com.example.OpenMauiApp.yml .\ncp ~/openmaui-app/com.example.OpenMauiApp.metainfo.xml .\n\n# Submit pull request\ngit add .\ngit commit -m \u0022Add OpenMaui App\u0022\ngit push origin main\n\u0060\u0060\u0060\n\n## Automatic Update Mechanisms\n\n### In-App Update Checker\n\nImplement update checking for AppImage and standalone installations:\n\n\u0060\u0060\u0060csharp\npublic class UpdateService\n{\n private const string UpdateCheckUrl = \u0022https://api.github.com/repos/yourusername/openmaui-app/releases/latest\u0022;\n private readonly HttpClient _httpClient;\n \n public UpdateService()\n {\n _httpClient = new HttpClient();\n _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(\u0022OpenMauiApp/1.0\u0022);\n }\n \n public async Task\u003CUpdateInfo\u003E CheckForUpdatesAsync()\n {\n try\n {\n var response = await _httpClient.GetStringAsync(UpdateCheckUrl);\n var release = JsonSerializer.Deserialize\u003CGitHubRelease\u003E(response);\n \n var currentVersion = Assembly.GetExecutingAssembly().GetName().Version;\n var latestVersion = Version.Parse(release.TagName.TrimStart(\u0027v\u0027));\n \n return new UpdateInfo\n {\n UpdateAvailable = latestVersion \u003E currentVersion,\n CurrentVersion = currentVersion.ToString(),\n LatestVersion = latestVersion.ToString(),\n ReleaseNotes = release.Body,\n DownloadUrl = GetDownloadUrl(release)\n };\n }\n catch (Exception ex)\n {\n Console.WriteLine($\u0022Update check failed: {ex.Message}\u0022);\n return new UpdateInfo { UpdateAvailable = false };\n }\n }\n \n private string GetDownloadUrl(GitHubRelease release)\n {\n // Detect installation type and return appropriate download URL\n if (IsAppImage())\n return release.Assets.FirstOrDefault(a =\u003E a.Name.EndsWith(\u0022.AppImage\u0022))?.BrowserDownloadUrl;\n else if (IsDebian())\n return release.Assets.FirstOrDefault(a =\u003E a.Name.EndsWith(\u0022.deb\u0022))?.BrowserDownloadUrl;\n else if (IsFedora())\n return release.Assets.FirstOrDefault(a =\u003E a.Name.EndsWith(\u0022.rpm\u0022))?.BrowserDownloadUrl;\n \n return release.Assets.FirstOrDefault()?.BrowserDownloadUrl;\n }\n \n private bool IsAppImage() =\u003E !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(\u0022APPIMAGE\u0022));\n private bool IsDebian() =\u003E File.Exists(\u0022/etc/debian_version\u0022);\n private bool IsFedora() =\u003E File.Exists(\u0022/etc/fedora-release\u0022);\n}\n\npublic class UpdateInfo\n{\n public bool UpdateAvailable { get; set; }\n public string CurrentVersion { get; set; }\n public string LatestVersion { get; set; }\n public string ReleaseNotes { get; set; }\n public string DownloadUrl { get; set; }\n}\n\npublic class GitHubRelease\n{\n [JsonPropertyName(\u0022tag_name\u0022)]\n public string TagName { get; set; }\n \n [JsonPropertyName(\u0022body\u0022)]\n public string Body { get; set; }\n \n [JsonPropertyName(\u0022assets\u0022)]\n public List\u003CGitHubAsset\u003E Assets { get; set; }\n}\n\npublic class GitHubAsset\n{\n [JsonPropertyName(\u0022name\u0022)]\n public string Name { get; set; }\n \n [JsonPropertyName(\u0022browser_download_url\u0022)]\n public string BrowserDownloadUrl { get; set; }\n}\n\u0060\u0060\u0060\n\n### Update UI Integration\n\nShow update notifications in your application:\n\n\u0060\u0060\u0060csharp\npublic class MainPage : ContentPage\n{\n private readonly UpdateService _updateService;\n \n public MainPage()\n {\n InitializeComponent();\n _updateService = new UpdateService();\n \n // Check for updates on startup (after short delay)\n Dispatcher.DispatchDelayed(TimeSpan.FromSeconds(5), async () =\u003E\n {\n await CheckForUpdatesAsync();\n });\n }\n \n private async Task CheckForUpdatesAsync()\n {\n var updateInfo = await _updateService.CheckForUpdatesAsync();\n \n if (updateInfo.UpdateAvailable)\n {\n var result = await DisplayAlert(\n \u0022Update Available\u0022,\n $\u0022Version {updateInfo.LatestVersion} is available. \u0022 \u002B\n $\u0022You are currently running version {updateInfo.CurrentVersion}.\\n\\n\u0022 \u002B\n $\u0022Release Notes:\\n{updateInfo.ReleaseNotes}\\n\\n\u0022 \u002B\n \u0022Would you like to download the update?\u0022,\n \u0022Download\u0022,\n \u0022Later\u0022\n );\n \n if (result)\n {\n await Launcher.OpenAsync(updateInfo.DownloadUrl);\n }\n }\n }\n}\n\u0060\u0060\u0060\n\n### Flatpak/Snap Automatic Updates\n\nFlatpak and Snap handle updates automatically:\n\n**Flatpak:**\n\u0060\u0060\u0060bash\n# Users can manually update\nflatpak update com.example.OpenMauiApp\n\n# Or enable automatic updates\nflatpak update --assumeyes\n\n# Configure systemd timer for automatic updates\nsystemctl --user enable flatpak-update.timer\n\u0060\u0060\u0060\n\n**Snap:**\n\u0060\u0060\u0060bash\n# Snap updates automatically by default\n# Users can manually trigger\nsudo snap refresh openmaui-app\n\n# Check update status\nsnap refresh --list\n\u0060\u0060\u0060\n\n## Analytics and Telemetry\n\nTrack version adoption (with user consent):\n\n\u0060\u0060\u0060csharp\npublic class TelemetryService\n{\n private const string TelemetryEndpoint = \u0022https://telemetry.example.com/api/events\u0022;\n \n public async Task ReportAppStartAsync()\n {\n if (!HasUserConsent())\n return;\n \n var telemetry = new\n {\n EventType = \u0022app_start\u0022,\n Version = Assembly.GetExecutingAssembly().GetName().Version.ToString(),\n OS = Environment.OSVersion.ToString(),\n Distribution = GetLinuxDistribution(),\n DesktopEnvironment = Environment.GetEnvironmentVariable(\u0022XDG_CURRENT_DESKTOP\u0022),\n DisplayServer = Environment.GetEnvironmentVariable(\u0022XDG_SESSION_TYPE\u0022),\n InstallationType = GetInstallationType(),\n Timestamp = DateTime.UtcNow\n };\n \n try\n {\n var json = JsonSerializer.Serialize(telemetry);\n var content = new StringContent(json, Encoding.UTF8, \u0022application/json\u0022);\n await new HttpClient().PostAsync(TelemetryEndpoint, content);\n }\n catch\n {\n // Silently fail - telemetry should never break the app\n }\n }\n \n private string GetInstallationType()\n {\n if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(\u0022APPIMAGE\u0022)))\n return \u0022AppImage\u0022;\n if (File.Exists(\u0022/.flatpak-info\u0022))\n return \u0022Flatpak\u0022;\n if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(\u0022SNAP\u0022)))\n return \u0022Snap\u0022;\n if (File.Exists(\u0022/var/lib/dpkg/status\u0022))\n return \u0022Deb\u0022;\n if (File.Exists(\u0022/var/lib/rpm\u0022))\n return \u0022RPM\u0022;\n \n return \u0022Unknown\u0022;\n }\n \n private string GetLinuxDistribution()\n {\n try\n {\n var osRelease = File.ReadAllLines(\u0022/etc/os-release\u0022)\n .Select(line =\u003E line.Split(\u0027=\u0027))\n .Where(parts =\u003E parts.Length == 2)\n .ToDictionary(parts =\u003E parts[0], parts =\u003E parts[1].Trim(\u0027\u0022\u0027));\n \n return osRelease.GetValueOrDefault(\u0022PRETTY_NAME\u0022, \u0022Unknown\u0022);\n }\n catch\n {\n return \u0022Unknown\u0022;\n }\n }\n \n private bool HasUserConsent()\n {\n // Check if user has consented to telemetry\n return Preferences.Get(\u0022telemetry_enabled\u0022, false);\n }\n}\n\u0060\u0060\u0060\n\n## Release Checklist\n\nBefore each release:\n\n- \u2705 All tests passing (unit, integration, manual)\n- \u2705 Version numbers updated (.csproj, changelog, manifests)\n- \u2705 Release notes written\n- \u2705 Packages built for all formats (.deb, .rpm, Flatpak, AppImage)\n- \u2705 Packages tested on clean VMs\n- \u2705 Code signing completed\n- \u2705 Git tag created and pushed\n- \u2705 GitHub/GitLab release created with artifacts\n- \u2705 Distribution repositories updated (PPA, COPR, AUR)\n- \u2705 Flathub pull request submitted (if applicable)\n- \u2705 Documentation updated\n- \u2705 Social media/blog announcement prepared\n\nA well-executed distribution strategy ensures your OpenMaui application reaches users across the diverse Linux ecosystem while maintaining quality and reliability." + } + ], + "generatedAt": 1769751324711 +} \ No newline at end of file