diff --git a/docs/API.md b/docs/API.md index ce9eb38..f94ab12 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1,288 +1,590 @@ -# .NET MAUI Linux Platform API Documentation +# OpenMaui Linux Platform API Documentation ## Overview -The .NET MAUI Linux Platform provides native Linux desktop support for .NET MAUI applications using SkiaSharp for rendering. It supports both X11 and Wayland display servers. +The OpenMaui Linux Platform provides native Linux desktop support for .NET MAUI applications using SkiaSharp for rendering. All public APIs use standard .NET MAUI types (Color, Rect, Size, Thickness) for full compliance with the MAUI API specification. ## Getting Started ### Installation ```bash -dotnet add package Microsoft.Maui.Controls.Linux +dotnet add package OpenMaui.Controls.Linux --prerelease ``` Or using the project template: ```bash -dotnet new install Microsoft.Maui.Linux.Templates -dotnet new maui-linux -n MyApp +dotnet new install OpenMaui.Linux.Templates +dotnet new openmaui-linux-xaml -n MyApp ``` ### Basic Application Structure ```csharp -using Microsoft.Maui.Platform.Linux; +// MauiProgram.cs +using Microsoft.Maui.Hosting; -public class Program +public static class MauiProgram { - public static void Main(string[] args) + public static MauiApp CreateMauiApp() { - var app = LinuxApplication.CreateBuilder() - .UseApp() - .Build(); + var builder = MauiApp.CreateBuilder(); + builder + .UseMauiApp() + .UseOpenMauiLinux(); // Enable Linux platform - app.Run(); + return builder.Build(); } } ``` -## Core Components +## Core Types -### LinuxApplication +All public APIs use .NET MAUI types for full API compliance: -Entry point for Linux MAUI applications. - -```csharp -public class LinuxApplication -{ - // Creates a new application builder - public static LinuxApplicationBuilder CreateBuilder(); - - // Gets the current application instance - public static LinuxApplication Current { get; } - - // Gets the main window - public IWindow MainWindow { get; } - - // Runs the application - public void Run(); - - // Quits the application - public void Quit(); -} -``` - -### LinuxApplicationBuilder - -```csharp -public class LinuxApplicationBuilder -{ - // Sets the MAUI application type - public LinuxApplicationBuilder UseApp() where TApp : Application; - - // Configures the window - public LinuxApplicationBuilder ConfigureWindow(Action configure); - - // Forces a specific display server - public LinuxApplicationBuilder UseDisplayServer(DisplayServerType type); - - // Builds the application - public LinuxApplication Build(); -} -``` +| MAUI Type | Description | +|-----------|-------------| +| `Color` | Colors (e.g., `Colors.Red`, `Color.FromRgb(255, 0, 0)`) | +| `Rect` | Rectangle bounds (x, y, width, height) | +| `Size` | Size measurements (width, height) | +| `Point` | Point coordinates (x, y) | +| `Thickness` | Padding/margins (left, top, right, bottom) | +| `double` | All numeric properties (not float) | ## View Controls -### SkiaButton +### Button -A clickable button control. +A clickable button control implementing `IButton`. ```csharp -public class SkiaButton : SkiaView +public class Button : View, IButton { + // Text and appearance public string Text { get; set; } - public SKColor TextColor { get; set; } - public SKColor BackgroundColor { get; set; } - public float CornerRadius { get; set; } - public float FontSize { get; set; } - public event EventHandler? Clicked; + public Color TextColor { get; set; } + public Color BackgroundColor { get; set; } + public int CornerRadius { get; set; } + public Color BorderColor { get; set; } + public double BorderWidth { get; set; } + + // Font + public string FontFamily { get; set; } + public double FontSize { get; set; } + public FontAttributes FontAttributes { get; set; } + + // Image + public ImageSource ImageSource { get; set; } + public ButtonContentLayout ContentLayout { get; set; } + + // Commands + public ICommand Command { get; set; } + public object CommandParameter { get; set; } + + // Events + public event EventHandler Clicked; + public event EventHandler Pressed; + public event EventHandler Released; } ``` -### SkiaEntry +### Entry -A text input control. +A text input control implementing `IEntry`. ```csharp -public class SkiaEntry : SkiaView, IInputContext +public class Entry : View, IEntry, ITextInput { + // Text public string Text { get; set; } public string Placeholder { get; set; } - public SKColor TextColor { get; set; } - public SKColor PlaceholderColor { get; set; } - public float FontSize { get; set; } + public Color TextColor { get; set; } + public Color PlaceholderColor { get; set; } + + // Font + public string FontFamily { get; set; } + public double FontSize { get; set; } + public FontAttributes FontAttributes { get; set; } + public double CharacterSpacing { get; set; } + + // Behavior public bool IsPassword { get; set; } public int MaxLength { get; set; } - public event EventHandler? TextChanged; - public event EventHandler? Completed; + public Keyboard Keyboard { get; set; } + public ReturnType ReturnType { get; set; } + public ClearButtonVisibility ClearButtonVisibility { get; set; } + + // Selection + public int CursorPosition { get; set; } + public int SelectionLength { get; set; } + + // Commands + public ICommand ReturnCommand { get; set; } + + // Events + public event EventHandler TextChanged; + public event EventHandler Completed; } ``` -### SkiaSlider +### Label -A value slider control. +A text display control implementing `ILabel`. ```csharp -public class SkiaSlider : SkiaView +public class Label : View, ILabel { + // Text + public string Text { get; set; } + public FormattedString FormattedText { get; set; } + public Color TextColor { get; set; } + + // Font + public string FontFamily { get; set; } + public double FontSize { get; set; } + public FontAttributes FontAttributes { get; set; } + public double CharacterSpacing { get; set; } + + // Layout + public TextAlignment HorizontalTextAlignment { get; set; } + public TextAlignment VerticalTextAlignment { get; set; } + public LineBreakMode LineBreakMode { get; set; } + public int MaxLines { get; set; } + public double LineHeight { get; set; } + + // Decoration + public TextDecorations TextDecorations { get; set; } + public TextTransform TextTransform { get; set; } +} +``` + +### Slider + +A value slider control implementing `ISlider`. + +```csharp +public class Slider : View, ISlider +{ + // Value public double Value { get; set; } - public double Minimum { get; set; } - public double Maximum { get; set; } - public SKColor TrackColor { get; set; } - public SKColor ThumbColor { get; set; } - public event EventHandler? ValueChanged; + public double Minimum { get; set; } // Default: 0.0 + public double Maximum { get; set; } // Default: 1.0 + + // Colors + public Color MinimumTrackColor { get; set; } + public Color MaximumTrackColor { get; set; } + public Color ThumbColor { get; set; } + + // Events + public event EventHandler ValueChanged; + public event EventHandler DragStarted; + public event EventHandler DragCompleted; } ``` -### SkiaScrollView +### Image -A scrollable container. +An image display control implementing `IImage`. ```csharp -public class SkiaScrollView : SkiaView +public class Image : View, IImage { - public SkiaView? Content { get; set; } - public float HorizontalScrollOffset { get; set; } - public float VerticalScrollOffset { get; set; } - public ScrollOrientation Orientation { get; set; } - public event EventHandler? Scrolled; + public ImageSource Source { get; set; } + public Aspect Aspect { get; set; } + public bool IsOpaque { get; set; } + public bool IsAnimationPlaying { get; set; } + public bool IsLoading { get; } } ``` -### SkiaImage +### CheckBox -An image display control. +A checkbox control implementing `ICheckBox`. ```csharp -public class SkiaImage : SkiaView +public class CheckBox : View, ICheckBox { - public SKBitmap? Source { get; set; } - public ImageAspect Aspect { get; set; } - public void LoadFromFile(string path); - public void LoadFromStream(Stream stream); + public bool IsChecked { get; set; } + public Color Color { get; set; } + + public event EventHandler CheckedChanged; +} +``` + +### Switch + +A toggle switch control implementing `ISwitch`. + +```csharp +public class Switch : View, ISwitch +{ + public bool IsOn { get; set; } + public Color OnColor { get; set; } + public Color ThumbColor { get; set; } + + public event EventHandler Toggled; } ``` ## Layout Controls -### SkiaStackLayout +### StackLayout Arranges children in a stack. ```csharp -public class SkiaStackLayout : SkiaLayoutView +public class StackLayout : Layout { public StackOrientation Orientation { get; set; } - public float Spacing { get; set; } + public double Spacing { get; set; } } + +public class VerticalStackLayout : StackLayout { } +public class HorizontalStackLayout : StackLayout { } ``` -### SkiaGrid +### Grid Arranges children in a grid. ```csharp -public class SkiaGrid : SkiaLayoutView +public class Grid : Layout { - public List RowDefinitions { get; } - public List ColumnDefinitions { get; } - public float RowSpacing { get; set; } - public float ColumnSpacing { get; set; } + public RowDefinitionCollection RowDefinitions { get; } + public ColumnDefinitionCollection ColumnDefinitions { get; } + public double RowSpacing { get; set; } + public double ColumnSpacing { get; set; } - public static void SetRow(SkiaView view, int row); - public static void SetColumn(SkiaView view, int column); - public static void SetRowSpan(SkiaView view, int span); - public static void SetColumnSpan(SkiaView view, int span); + // Attached properties + public static int GetRow(BindableObject view); + public static void SetRow(BindableObject view, int row); + public static int GetColumn(BindableObject view); + public static void SetColumn(BindableObject view, int column); + public static int GetRowSpan(BindableObject view); + public static void SetRowSpan(BindableObject view, int span); + public static int GetColumnSpan(BindableObject view); + public static void SetColumnSpan(BindableObject view, int span); } ``` -## Page Controls +### FlexLayout -### SkiaTabbedPage - -A page with tab navigation. +CSS Flexbox-compatible layout. ```csharp -public class SkiaTabbedPage : SkiaLayoutView +public class FlexLayout : Layout { - public int SelectedIndex { get; set; } - public void AddTab(string title, SkiaView content, string? iconPath = null); - public void RemoveTab(int index); - public void ClearTabs(); - public event EventHandler? SelectedIndexChanged; + public FlexDirection Direction { get; set; } + public FlexWrap Wrap { get; set; } + public FlexJustify JustifyContent { get; set; } + public FlexAlignItems AlignItems { get; set; } + public FlexAlignContent AlignContent { get; set; } + + // Attached properties + public static int GetOrder(BindableObject view); + public static float GetGrow(BindableObject view); + public static float GetShrink(BindableObject view); + public static FlexBasis GetBasis(BindableObject view); + public static FlexAlignSelf GetAlignSelf(BindableObject view); } ``` -### SkiaFlyoutPage +### ScrollView -A page with flyout/drawer navigation. +A scrollable container. ```csharp -public class SkiaFlyoutPage : SkiaLayoutView +public class ScrollView : Layout, IScrollView { - public SkiaView? Flyout { get; set; } - public SkiaView? Detail { get; set; } + public View Content { get; set; } + public ScrollOrientation Orientation { get; set; } + public ScrollBarVisibility HorizontalScrollBarVisibility { get; set; } + public ScrollBarVisibility VerticalScrollBarVisibility { get; set; } + public double ScrollX { get; } + public double ScrollY { get; } + public Size ContentSize { get; } + + public Task ScrollToAsync(double x, double y, bool animated); + public Task ScrollToAsync(Element element, ScrollToPosition position, bool animated); + + public event EventHandler Scrolled; +} +``` + +## Collection Views + +### CollectionView + +A virtualized list/grid control. + +```csharp +public class CollectionView : ItemsView +{ + public IEnumerable ItemsSource { get; set; } + public DataTemplate ItemTemplate { get; set; } + public IItemsLayout ItemsLayout { get; set; } + public SelectionMode SelectionMode { get; set; } + public object SelectedItem { get; set; } + public IList SelectedItems { get; } + public View EmptyView { get; set; } + public DataTemplate EmptyViewTemplate { get; set; } + public object Header { get; set; } + public object Footer { get; set; } + + public event EventHandler SelectionChanged; +} +``` + +### CarouselView + +A carousel/pager control. + +```csharp +public class CarouselView : ItemsView +{ + public bool Loop { get; set; } + public bool IsSwipeEnabled { get; set; } + public int Position { get; set; } + public Thickness PeekAreaInsets { get; set; } + + public event EventHandler PositionChanged; +} +``` + +### RefreshView + +Pull-to-refresh container. + +```csharp +public class RefreshView : ContentView +{ + public bool IsRefreshing { get; set; } + public ICommand Command { get; set; } + public object CommandParameter { get; set; } + public Color RefreshColor { get; set; } + + public event EventHandler Refreshing; +} +``` + +### SwipeView + +Swipe-to-reveal actions. + +```csharp +public class SwipeView : ContentView +{ + public SwipeItems LeftItems { get; set; } + public SwipeItems RightItems { get; set; } + public SwipeItems TopItems { get; set; } + public SwipeItems BottomItems { get; set; } + + public void Open(OpenSwipeItem openSwipeItem); + public void Close(); + + public event EventHandler SwipeStarted; + public event EventHandler SwipeEnded; +} +``` + +## Navigation + +### NavigationPage + +Stack-based navigation. + +```csharp +public class NavigationPage : Page +{ + public Page CurrentPage { get; } + public Page RootPage { get; } + public Color BarBackgroundColor { get; set; } + public Color BarTextColor { get; set; } + public bool HasNavigationBar { get; set; } + + public Task PushAsync(Page page, bool animated = true); + public Task PopAsync(bool animated = true); + public Task PopToRootAsync(bool animated = true); + + public event EventHandler Pushed; + public event EventHandler Popped; + public event EventHandler PoppedToRoot; +} +``` + +### TabbedPage + +Tab-based navigation. + +```csharp +public class TabbedPage : Page +{ + public IList Children { get; } + public Page CurrentPage { get; set; } + public Color BarBackgroundColor { get; set; } + public Color SelectedTabColor { get; set; } + public Color UnselectedTabColor { get; set; } + + public event EventHandler CurrentPageChanged; +} +``` + +### FlyoutPage + +Flyout/drawer navigation. + +```csharp +public class FlyoutPage : Page +{ + public Page Flyout { get; set; } + public Page Detail { get; set; } public bool IsPresented { get; set; } - public float FlyoutWidth { get; set; } - public bool GestureEnabled { get; set; } public FlyoutLayoutBehavior FlyoutLayoutBehavior { get; set; } - public event EventHandler? IsPresentedChanged; + public bool IsGestureEnabled { get; set; } + + public event EventHandler IsPresentedChanged; } ``` -### SkiaShell +### Shell -Full navigation container with flyout, tabs, and URI routing. +Comprehensive navigation with URI routing. ```csharp -public class SkiaShell : SkiaLayoutView +public class Shell : Page { - public bool FlyoutIsPresented { get; set; } + public IList Items { get; } + public ShellItem CurrentItem { get; set; } public ShellFlyoutBehavior FlyoutBehavior { get; set; } - public float FlyoutWidth { get; set; } - public string Title { get; set; } - public bool NavBarIsVisible { get; set; } - public bool TabBarIsVisible { get; set; } + public bool FlyoutIsPresented { get; set; } + public Color FlyoutBackgroundColor { get; set; } + public object FlyoutHeader { get; set; } + public object FlyoutFooter { get; set; } - public void AddSection(ShellSection section); - public void NavigateToSection(int sectionIndex, int itemIndex = 0); - public void GoToAsync(string route); + public static void RegisterRoute(string route, Type pageType); + public Task GoToAsync(string route); + public Task GoToAsync(ShellNavigationState state); - public event EventHandler? FlyoutIsPresentedChanged; - public event EventHandler? Navigated; + public event EventHandler Navigated; + public event EventHandler Navigating; } ``` -## Services +## Platform Services -### Input Method Service (IME) - -Provides international text input support. +### IClipboard ```csharp -public interface IInputMethodService +public interface IClipboard { - bool IsActive { get; } - string PreEditText { get; } - - void Initialize(nint windowHandle); - void SetFocus(IInputContext? context); - void SetCursorLocation(int x, int y, int width, int height); - bool ProcessKeyEvent(uint keyCode, KeyModifiers modifiers, bool isKeyDown); - void Reset(); - - event EventHandler? TextCommitted; - event EventHandler? PreEditChanged; + bool HasText { get; } + Task GetTextAsync(); + Task SetTextAsync(string text); + event EventHandler ClipboardContentChanged; } - -// Factory -var imeService = InputMethodServiceFactory.Instance; ``` -### Accessibility Service (AT-SPI2) +### IFilePicker -Provides screen reader support. +```csharp +public interface IFilePicker +{ + Task PickAsync(PickOptions options = null); + Task> PickMultipleAsync(PickOptions options = null); +} +``` + +### IShare + +```csharp +public interface IShare +{ + Task RequestAsync(ShareTextRequest request); + Task RequestAsync(ShareFileRequest request); + Task RequestAsync(ShareMultipleFilesRequest request); +} +``` + +### ILauncher + +```csharp +public interface ILauncher +{ + Task CanOpenAsync(Uri uri); + Task OpenAsync(Uri uri); + Task TryOpenAsync(Uri uri); +} +``` + +### IBrowser + +```csharp +public interface IBrowser +{ + Task OpenAsync(Uri uri, BrowserLaunchOptions options); +} +``` + +### IEmail + +```csharp +public interface IEmail +{ + bool IsComposeSupported { get; } + Task ComposeAsync(EmailMessage message); +} +``` + +### IPreferences + +```csharp +public interface IPreferences +{ + bool ContainsKey(string key, string sharedName = null); + void Remove(string key, string sharedName = null); + void Clear(string sharedName = null); + T Get(string key, T defaultValue, string sharedName = null); + void Set(string key, T value, string sharedName = null); +} +``` + +### ISecureStorage + +```csharp +public interface ISecureStorage +{ + Task GetAsync(string key); + Task SetAsync(string key, string value); + bool Remove(string key); + void RemoveAll(); +} +``` + +## Accessibility + +### IAccessible + +Interface for accessible UI elements. + +```csharp +public interface IAccessible +{ + string AccessibleId { get; } + string AccessibleName { get; } + string AccessibleDescription { get; } + AccessibleRole Role { get; } + AccessibleStates States { get; } + IAccessible Parent { get; } + IReadOnlyList Children { get; } + AccessibleRect Bounds { get; } + IReadOnlyList Actions { get; } + + bool DoAction(string actionName); +} +``` + +### IAccessibilityService ```csharp public interface IAccessibilityService @@ -292,73 +594,37 @@ public interface IAccessibilityService void Initialize(); void Register(IAccessible accessible); void Unregister(IAccessible accessible); - void NotifyFocusChanged(IAccessible? accessible); + void NotifyFocusChanged(IAccessible accessible); void NotifyPropertyChanged(IAccessible accessible, AccessibleProperty property); - void NotifyStateChanged(IAccessible accessible, AccessibleState state, bool value); + void NotifyStateChanged(IAccessible accessible, AccessibleStates state, bool value); void Announce(string text, AnnouncementPriority priority = AnnouncementPriority.Polite); + void Shutdown(); } - -// Factory -var accessibilityService = AccessibilityServiceFactory.Instance; ``` -## Rendering Optimization +## Input Method (IME) -### DirtyRectManager - -Tracks invalidated regions for efficient redraw. +### IInputMethodService ```csharp -public class DirtyRectManager +public interface IInputMethodService { - public int MaxDirtyRects { get; set; } - public bool NeedsFullRedraw { get; } - public bool HasDirtyRegions { get; } + bool IsActive { get; } + string PreEditText { get; } - public void SetBounds(SKRect bounds); - public void Invalidate(SKRect rect); - public void InvalidateAll(); - public void Clear(); - public SKRect GetCombinedDirtyRect(); - public void ApplyClipping(SKCanvas canvas); + void Initialize(IntPtr windowHandle); + void SetFocus(IInputContext context); + void SetCursorLocation(int x, int y, int width, int height); + bool ProcessKeyEvent(uint keyCode, KeyModifiers modifiers, bool isKeyDown); + void Reset(); + void Shutdown(); + + event EventHandler TextCommitted; + event EventHandler PreEditChanged; } ``` -### RenderCache - -Caches rendered content for static views. - -```csharp -public class RenderCache : IDisposable -{ - public long MaxCacheSize { get; set; } - public long CurrentCacheSize { get; } - - public bool TryGet(string key, out SKBitmap? bitmap); - public void Set(string key, SKBitmap bitmap); - public void Invalidate(string key); - public void InvalidatePrefix(string prefix); - public void Clear(); - public SKBitmap GetOrCreate(string key, int width, int height, Action render); -} -``` - -### TextRenderCache - -Caches rendered text for performance. - -```csharp -public class TextRenderCache : IDisposable -{ - public int MaxEntries { get; set; } - public SKBitmap GetOrCreate(string text, SKPaint paint); - public void Clear(); -} -``` - -## Event Args - -### TextChangedEventArgs +## Event Arguments ```csharp public class TextChangedEventArgs : EventArgs @@ -366,68 +632,65 @@ public class TextChangedEventArgs : EventArgs public string OldTextValue { get; } public string NewTextValue { get; } } -``` -### ValueChangedEventArgs - -```csharp public class ValueChangedEventArgs : EventArgs { public double OldValue { get; } public double NewValue { get; } } -``` -### PointerEventArgs - -```csharp -public class PointerEventArgs : EventArgs +public class CheckedChangedEventArgs : EventArgs { - public float X { get; } - public float Y { get; } - public PointerButton Button { get; } - public bool Handled { get; set; } + public bool Value { get; } +} + +public class ToggledEventArgs : EventArgs +{ + public bool Value { get; } +} + +public class SelectionChangedEventArgs : EventArgs +{ + public IReadOnlyList PreviousSelection { get; } + public IReadOnlyList CurrentSelection { get; } +} + +public class PositionChangedEventArgs : EventArgs +{ + public int PreviousPosition { get; } + public int CurrentPosition { get; } +} + +public class ScrolledEventArgs : EventArgs +{ + public double ScrollX { get; } + public double ScrollY { get; } } ``` ## Enumerations -### DisplayServerType +### Common Enums ```csharp -public enum DisplayServerType -{ - Auto, - X11, - Wayland -} +public enum Aspect { AspectFit, AspectFill, Fill, Center } +public enum TextAlignment { Start, Center, End } +public enum LineBreakMode { NoWrap, WordWrap, CharacterWrap, HeadTruncation, TailTruncation, MiddleTruncation } +public enum FontAttributes { None, Bold, Italic } +public enum TextTransform { None, Default, Lowercase, Uppercase } +public enum TextDecorations { None, Underline, Strikethrough } +public enum ReturnType { Default, Done, Go, Next, Search, Send } +public enum Keyboard { Default, Chat, Email, Numeric, Telephone, Text, Url } +public enum ClearButtonVisibility { Never, WhileEditing } +public enum SelectionMode { None, Single, Multiple } +public enum ScrollOrientation { Vertical, Horizontal, Both, Neither } +public enum ScrollBarVisibility { Default, Always, Never } +public enum StackOrientation { Vertical, Horizontal } +public enum FlyoutLayoutBehavior { Default, Popover, Split } +public enum ShellFlyoutBehavior { Disabled, Flyout, Locked } ``` -### FlyoutLayoutBehavior - -```csharp -public enum FlyoutLayoutBehavior -{ - Default, - Popover, - Split, - SplitOnLandscape, - SplitOnPortrait -} -``` - -### ShellFlyoutBehavior - -```csharp -public enum ShellFlyoutBehavior -{ - Disabled, - Flyout, - Locked -} -``` - -### AccessibleRole +### Accessibility Enums ```csharp public enum AccessibleRole @@ -435,9 +698,28 @@ public enum AccessibleRole Unknown, Window, Application, Panel, Frame, Button, CheckBox, RadioButton, ComboBox, Entry, Label, List, ListItem, Menu, MenuItem, ScrollBar, - Slider, StatusBar, Tab, Text, ProgressBar, + Slider, StatusBar, Tab, TabPanel, Text, ProgressBar, + SpinButton, Table, TableCell, TableRow, ToolBar, + TreeItem, TreeView, // ... and more +} + +[Flags] +public enum AccessibleStates +{ + None = 0, + Active = 1 << 0, + Checked = 1 << 1, + Collapsed = 1 << 2, + Enabled = 1 << 3, + Expanded = 1 << 4, + Focusable = 1 << 5, + Focused = 1 << 6, + Selected = 1 << 7, + Visible = 1 << 8, // ... and more } + +public enum AnnouncementPriority { Polite, Assertive } ``` ## Environment Variables @@ -445,14 +727,24 @@ public enum AccessibleRole | Variable | Description | |----------|-------------| | `MAUI_DISPLAY_SERVER` | Force display server: `x11`, `wayland`, or `auto` | -| `MAUI_INPUT_METHOD` | Force IME: `ibus`, `xim`, or `none` | +| `MAUI_INPUT_METHOD` | Force IME: `ibus`, `fcitx5`, `xim`, or `none` | | `GTK_A11Y` | Set to `none` to disable accessibility | +| `DISPLAY` | X11 display to connect to | +| `WAYLAND_DISPLAY` | Wayland display to connect to | ## System Requirements -- .NET 8.0 or .NET 9.0 -- Linux with X11 or Wayland -- libX11 (for X11 support) -- libwayland-client (for Wayland support) -- libibus-1.0 (optional, for IBus IME) -- libatspi (optional, for accessibility) +- .NET 9.0 SDK or later +- Linux (kernel 5.4+) +- X11 or Wayland display server +- SkiaSharp native libraries (included via NuGet) + +### Optional Dependencies + +| Package | Purpose | +|---------|---------| +| libibus-1.0 | IBus input method support | +| libatspi | AT-SPI2 accessibility support | +| libnotify | Desktop notification support | +| xclip/xsel | Clipboard support | +| zenity/kdialog | Native file dialogs | diff --git a/docs/CUSTOM_CONTROLS.md b/docs/CUSTOM_CONTROLS.md new file mode 100644 index 0000000..e28e987 --- /dev/null +++ b/docs/CUSTOM_CONTROLS.md @@ -0,0 +1,689 @@ +# Creating Custom Controls for OpenMaui Linux + +This guide explains how to create custom controls that integrate with the OpenMaui Linux platform. + +## Overview + +OpenMaui Linux uses a layered architecture: + +1. **MAUI Virtual Views** - Standard .NET MAUI controls (Button, Label, etc.) +2. **Handlers** - Bridge between MAUI and platform views +3. **Platform Views** - SkiaSharp-rendered controls + +When creating custom controls, you can either: +- Create a MAUI control with a custom handler (recommended for reusable controls) +- Create a platform-specific SkiaView directly (for Linux-only functionality) + +## Creating a MAUI Control with Handler + +### Step 1: Define the MAUI Control + +Create a standard MAUI control that inherits from `View`: + +```csharp +// Controls/RatingControl.cs +using Microsoft.Maui.Controls; +using Microsoft.Maui.Graphics; + +namespace MyApp.Controls; + +public class RatingControl : View +{ + public static readonly BindableProperty ValueProperty = + BindableProperty.Create( + nameof(Value), + typeof(int), + typeof(RatingControl), + 0, + BindingMode.TwoWay); + + public static readonly BindableProperty MaxValueProperty = + BindableProperty.Create( + nameof(MaxValue), + typeof(int), + typeof(RatingControl), + 5); + + public static readonly BindableProperty StarColorProperty = + BindableProperty.Create( + nameof(StarColor), + typeof(Color), + typeof(RatingControl), + Colors.Gold); + + public static readonly BindableProperty EmptyStarColorProperty = + BindableProperty.Create( + nameof(EmptyStarColor), + typeof(Color), + typeof(RatingControl), + Colors.Gray); + + public int Value + { + get => (int)GetValue(ValueProperty); + set => SetValue(ValueProperty, value); + } + + public int MaxValue + { + get => (int)GetValue(MaxValueProperty); + set => SetValue(MaxValueProperty, value); + } + + public Color StarColor + { + get => (Color)GetValue(StarColorProperty); + set => SetValue(StarColorProperty, value); + } + + public Color EmptyStarColor + { + get => (Color)GetValue(EmptyStarColorProperty); + set => SetValue(EmptyStarColorProperty, value); + } + + public event EventHandler? ValueChanged; + + internal void SendValueChanged(int value) + { + ValueChanged?.Invoke(this, value); + } +} +``` + +### Step 2: Create the Platform View + +Create a SkiaView that renders your control: + +```csharp +// Platforms/Linux/SkiaRatingControl.cs +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Platform; +using SkiaSharp; + +namespace MyApp.Platforms.Linux; + +public class SkiaRatingControl : SkiaView +{ + private int _value; + private int _maxValue = 5; + private Color _starColor = Colors.Gold; + private Color _emptyStarColor = Colors.Gray; + private const float StarSize = 24f; + private const float StarSpacing = 4f; + + public int Value + { + get => _value; + set + { + if (_value != value) + { + _value = Math.Clamp(value, 0, MaxValue); + Invalidate(); + } + } + } + + public int MaxValue + { + get => _maxValue; + set + { + if (_maxValue != value) + { + _maxValue = Math.Max(1, value); + InvalidateMeasure(); + } + } + } + + public Color StarColor + { + get => _starColor; + set + { + if (_starColor != value) + { + _starColor = value; + Invalidate(); + } + } + } + + public Color EmptyStarColor + { + get => _emptyStarColor; + set + { + if (_emptyStarColor != value) + { + _emptyStarColor = value; + Invalidate(); + } + } + } + + public event EventHandler? ValueChanged; + + protected override Size MeasureOverride(Size availableSize) + { + var width = MaxValue * StarSize + (MaxValue - 1) * StarSpacing; + return new Size(width, StarSize); + } + + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + for (int i = 0; i < MaxValue; i++) + { + var x = bounds.Left + i * (StarSize + StarSpacing); + var y = bounds.Top; + var color = i < Value ? StarColor : EmptyStarColor; + + DrawStar(canvas, x, y, StarSize, color); + } + } + + private void DrawStar(SKCanvas canvas, float x, float y, float size, Color color) + { + using var paint = new SKPaint + { + Color = color.ToSKColor(), + IsAntialias = true, + Style = SKPaintStyle.Fill + }; + + var path = CreateStarPath(x + size / 2, y + size / 2, size / 2, size / 4); + canvas.DrawPath(path, paint); + } + + private SKPath CreateStarPath(float cx, float cy, float outerRadius, float innerRadius) + { + var path = new SKPath(); + var angle = -Math.PI / 2; + var step = Math.PI / 5; + + path.MoveTo( + cx + (float)(outerRadius * Math.Cos(angle)), + cy + (float)(outerRadius * Math.Sin(angle))); + + for (int i = 0; i < 10; i++) + { + angle += step; + var radius = i % 2 == 0 ? innerRadius : outerRadius; + path.LineTo( + cx + (float)(radius * Math.Cos(angle)), + cy + (float)(radius * Math.Sin(angle))); + } + + path.Close(); + return path; + } + + public override void OnPointerPressed(PointerEventArgs e) + { + if (!IsEnabled) return; + + var starIndex = (int)((e.X - Bounds.Left) / (StarSize + StarSpacing)); + var newValue = Math.Clamp(starIndex + 1, 0, MaxValue); + + if (newValue != Value) + { + Value = newValue; + ValueChanged?.Invoke(this, Value); + } + + e.Handled = true; + } +} +``` + +### Step 3: Create the Handler + +Connect the MAUI control to the platform view: + +```csharp +// Handlers/RatingControlHandler.cs +using Microsoft.Maui.Handlers; +using MyApp.Controls; +using MyApp.Platforms.Linux; + +namespace MyApp.Handlers; + +public partial class RatingControlHandler : ViewHandler +{ + public static IPropertyMapper PropertyMapper = + new PropertyMapper(ViewMapper) + { + [nameof(RatingControl.Value)] = MapValue, + [nameof(RatingControl.MaxValue)] = MapMaxValue, + [nameof(RatingControl.StarColor)] = MapStarColor, + [nameof(RatingControl.EmptyStarColor)] = MapEmptyStarColor, + }; + + public static CommandMapper CommandMapper = + new(ViewCommandMapper); + + public RatingControlHandler() : base(PropertyMapper, CommandMapper) + { + } + + protected override SkiaRatingControl CreatePlatformView() + { + return new SkiaRatingControl(); + } + + protected override void ConnectHandler(SkiaRatingControl platformView) + { + base.ConnectHandler(platformView); + platformView.ValueChanged += OnPlatformValueChanged; + } + + protected override void DisconnectHandler(SkiaRatingControl platformView) + { + platformView.ValueChanged -= OnPlatformValueChanged; + base.DisconnectHandler(platformView); + } + + private void OnPlatformValueChanged(object? sender, int value) + { + VirtualView.Value = value; + VirtualView.SendValueChanged(value); + } + + private static void MapValue(RatingControlHandler handler, RatingControl control) + { + handler.PlatformView.Value = control.Value; + } + + private static void MapMaxValue(RatingControlHandler handler, RatingControl control) + { + handler.PlatformView.MaxValue = control.MaxValue; + } + + private static void MapStarColor(RatingControlHandler handler, RatingControl control) + { + handler.PlatformView.StarColor = control.StarColor; + } + + private static void MapEmptyStarColor(RatingControlHandler handler, RatingControl control) + { + handler.PlatformView.EmptyStarColor = control.EmptyStarColor; + } +} +``` + +### Step 4: Register the Handler + +Register your handler in `MauiProgram.cs`: + +```csharp +using Microsoft.Maui.Hosting; +using MyApp.Controls; +using MyApp.Handlers; + +public static class MauiProgram +{ + public static MauiApp CreateMauiApp() + { + var builder = MauiApp.CreateBuilder(); + builder + .UseMauiApp() + .UseOpenMauiLinux() + .ConfigureMauiHandlers(handlers => + { + handlers.AddHandler(); + }); + + return builder.Build(); + } +} +``` + +### Step 5: Use in XAML + +```xml + + + + + +``` + +## Creating a Direct SkiaView + +For Linux-only controls or simpler use cases, inherit directly from `SkiaView`: + +```csharp +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Platform; +using SkiaSharp; + +public class CustomGauge : SkiaView +{ + private double _value; + private double _minimum; + private double _maximum = 100; + + public double Value + { + get => _value; + set + { + _value = Math.Clamp(value, Minimum, Maximum); + Invalidate(); + } + } + + public double Minimum + { + get => _minimum; + set { _minimum = value; Invalidate(); } + } + + public double Maximum + { + get => _maximum; + set { _maximum = value; Invalidate(); } + } + + protected override Size MeasureOverride(Size availableSize) + { + return new Size(100, 100); // Fixed size gauge + } + + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + var center = new SKPoint(bounds.MidX, bounds.MidY); + var radius = Math.Min(bounds.Width, bounds.Height) / 2 - 10; + + // Draw background arc + using var bgPaint = new SKPaint + { + Color = SKColors.LightGray, + Style = SKPaintStyle.Stroke, + StrokeWidth = 10, + IsAntialias = true, + StrokeCap = SKStrokeCap.Round + }; + + canvas.DrawArc( + new SKRect(center.X - radius, center.Y - radius, center.X + radius, center.Y + radius), + 135, 270, false, bgPaint); + + // Draw value arc + var percentage = (Value - Minimum) / (Maximum - Minimum); + var sweepAngle = (float)(270 * percentage); + + using var valuePaint = new SKPaint + { + Color = SKColors.Blue, + Style = SKPaintStyle.Stroke, + StrokeWidth = 10, + IsAntialias = true, + StrokeCap = SKStrokeCap.Round + }; + + canvas.DrawArc( + new SKRect(center.X - radius, center.Y - radius, center.X + radius, center.Y + radius), + 135, sweepAngle, false, valuePaint); + + // Draw value text + using var textPaint = new SKPaint + { + Color = SKColors.Black, + TextSize = 24, + TextAlign = SKTextAlign.Center, + IsAntialias = true + }; + + canvas.DrawText( + $"{Value:F0}", + center.X, center.Y + 8, + textPaint); + } +} +``` + +## Best Practices + +### 1. Use MAUI Types for Public APIs + +Always use MAUI types (Color, Rect, Size, Thickness, double) in public APIs: + +```csharp +// Good - MAUI types +public Color ForegroundColor { get; set; } +public double BorderWidth { get; set; } +public Thickness Padding { get; set; } + +// Bad - SkiaSharp types (internal only) +// public SKColor ForegroundColor { get; set; } // Don't expose +// public float BorderWidth { get; set; } // Don't expose +``` + +### 2. Implement Visual States + +Support visual state changes for interactive controls: + +```csharp +public override void OnPointerEntered(PointerEventArgs e) +{ + SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.PointerOver); + base.OnPointerEntered(e); +} + +public override void OnPointerExited(PointerEventArgs e) +{ + SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.Normal); + base.OnPointerExited(e); +} + +public override void OnPointerPressed(PointerEventArgs e) +{ + SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.Pressed); + base.OnPointerPressed(e); +} +``` + +### 3. Support Accessibility + +Override accessibility methods for screen reader support: + +```csharp +protected override string GetDefaultAccessibleName() +{ + return $"Rating: {Value} of {MaxValue} stars"; +} + +protected override AccessibleRole GetAccessibleRole() +{ + return AccessibleRole.Slider; +} + +protected override IReadOnlyList GetAccessibleActions() +{ + return new[] + { + new AccessibleAction("Increment", "Increase rating"), + new AccessibleAction("Decrement", "Decrease rating") + }; +} + +protected override bool DoAccessibleAction(string actionName) +{ + switch (actionName) + { + case "Increment": + Value = Math.Min(Value + 1, MaxValue); + return true; + case "Decrement": + Value = Math.Max(Value - 1, 0); + return true; + } + return false; +} +``` + +### 4. Implement Keyboard Navigation + +Support keyboard input for accessibility: + +```csharp +public CustomControl() +{ + IsFocusable = true; +} + +public override void OnKeyDown(KeyEventArgs e) +{ + if (!IsEnabled) return; + + switch (e.Key) + { + case Key.Left: + case Key.Down: + Value--; + e.Handled = true; + break; + case Key.Right: + case Key.Up: + Value++; + e.Handled = true; + break; + case Key.Home: + Value = Minimum; + e.Handled = true; + break; + case Key.End: + Value = Maximum; + e.Handled = true; + break; + } +} +``` + +### 5. Handle Focus Visuals + +Draw focus indicators when the control is focused: + +```csharp +protected override void OnDraw(SKCanvas canvas, SKRect bounds) +{ + // Draw control content... + + // Draw focus ring + if (IsFocused) + { + using var focusPaint = new SKPaint + { + Color = SkiaTheme.PrimarySK.WithAlpha(100), + Style = SKPaintStyle.Stroke, + StrokeWidth = 2, + IsAntialias = true + }; + + canvas.DrawRoundRect(bounds.Inflate(2, 2), 4, 4, focusPaint); + } +} +``` + +### 6. Optimize Rendering + +Use `Invalidate()` sparingly and implement dirty region tracking: + +```csharp +// Only invalidate what changed +public int Value +{ + get => _value; + set + { + if (_value != value) + { + _value = value; + Invalidate(); // Request redraw + } + } +} + +// Use InvalidateMeasure() when size changes +public int MaxValue +{ + get => _maxValue; + set + { + if (_maxValue != value) + { + _maxValue = value; + InvalidateMeasure(); // Size changed, remeasure needed + } + } +} +``` + +## Testing Custom Controls + +Create unit tests for your controls: + +```csharp +using FluentAssertions; +using Microsoft.Maui.Graphics; +using Xunit; + +public class SkiaRatingControlTests +{ + [Fact] + public void Value_ClampedToMaxValue() + { + var control = new SkiaRatingControl { MaxValue = 5 }; + + control.Value = 10; + + control.Value.Should().Be(5); + } + + [Fact] + public void Value_ClampedToZero() + { + var control = new SkiaRatingControl(); + + control.Value = -5; + + control.Value.Should().Be(0); + } + + [Fact] + public void Measure_ReturnsCorrectSize() + { + var control = new SkiaRatingControl { MaxValue = 5 }; + + var size = control.Measure(new Size(500, 500)); + + size.Width.Should().BeGreaterThan(0); + size.Height.Should().Be(24); // StarSize + } +} +``` + +## Summary + +Creating custom controls for OpenMaui Linux follows these patterns: + +1. **Define MAUI control** with BindableProperties and events +2. **Create platform view** inheriting from SkiaView +3. **Create handler** to connect MAUI and platform views +4. **Register handler** in MauiProgram.cs +5. **Follow best practices** for accessibility, visual states, and performance + +For more examples, see the existing controls in the `Views/` directory. diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md index 348dded..afb9e29 100644 --- a/docs/GETTING_STARTED.md +++ b/docs/GETTING_STARTED.md @@ -97,51 +97,56 @@ MyApp/ ## Basic Controls +All controls use standard .NET MAUI types (Color, Rect, Size, Thickness) for full API compliance. + ### Labels ```csharp -var label = new SkiaLabel +using Microsoft.Maui.Graphics; + +var label = new Label { Text = "Hello World", - TextColor = new SKColor(33, 33, 33), - FontSize = 16f + TextColor = Color.FromRgb(33, 33, 33), // MAUI Color + FontSize = 16 }; ``` ### Buttons ```csharp -var button = new SkiaButton +var button = new Button { Text = "Click Me", - BackgroundColor = new SKColor(33, 150, 243) + BackgroundColor = Color.FromRgb(33, 150, 243) // MAUI Color }; button.Clicked += (s, e) => Console.WriteLine("Clicked!"); ``` ### Text Input ```csharp -var entry = new SkiaEntry +var entry = new Entry { Placeholder = "Enter text...", MaxLength = 100 }; -entry.TextChanged += (s, e) => Console.WriteLine($"Text: {e.NewValue}"); +entry.TextChanged += (s, e) => Console.WriteLine($"Text: {e.NewTextValue}"); ``` ### Layouts ```csharp // Vertical stack -var vstack = new SkiaStackLayout +var vstack = new VerticalStackLayout { - Orientation = StackOrientation.Vertical, - Spacing = 10 + Spacing = 10, + Children = + { + new Label { Text = "Item 1" }, + new Label { Text = "Item 2" } + } }; -vstack.AddChild(new SkiaLabel { Text = "Item 1" }); -vstack.AddChild(new SkiaLabel { Text = "Item 2" }); // Horizontal stack -var hstack = new SkiaStackLayout +var hstack = new HorizontalStackLayout { - Orientation = StackOrientation.Horizontal, Spacing = 8 }; ``` @@ -150,24 +155,28 @@ var hstack = new SkiaStackLayout ### CarouselView ```csharp -var carousel = new SkiaCarouselView +var carousel = new CarouselView { Loop = true, - PeekAreaInsets = 20f, - ShowIndicators = true + PeekAreaInsets = new Thickness(20), + ItemsSource = new[] { "Page 1", "Page 2", "Page 3" }, + ItemTemplate = new DataTemplate(() => + { + var label = new Label(); + label.SetBinding(Label.TextProperty, "."); + return label; + }) }; -carousel.AddItem(new SkiaLabel { Text = "Page 1" }); -carousel.AddItem(new SkiaLabel { Text = "Page 2" }); carousel.PositionChanged += (s, e) => Console.WriteLine($"Position: {e.CurrentPosition}"); ``` ### RefreshView ```csharp -var refreshView = new SkiaRefreshView +var refreshView = new RefreshView { Content = myScrollableContent, - RefreshColor = SKColors.Blue + RefreshColor = Colors.Blue // MAUI Color }; refreshView.Refreshing += async (s, e) => { @@ -178,26 +187,29 @@ refreshView.Refreshing += async (s, e) => ### SwipeView ```csharp -var swipeView = new SkiaSwipeView +var swipeView = new SwipeView { - Content = new SkiaLabel { Text = "Swipe me" } + Content = new Label { Text = "Swipe me" } }; -swipeView.RightItems.Add(new SwipeItem +swipeView.RightItems = new SwipeItems { - Text = "Delete", - BackgroundColor = SKColors.Red -}); + new SwipeItem + { + Text = "Delete", + BackgroundColor = Colors.Red // MAUI Color + } +}; ``` ### MenuBar ```csharp -var menuBar = new SkiaMenuBar(); +var menuBar = new MenuBar(); var fileMenu = new MenuBarItem { Text = "File" }; -fileMenu.Items.Add(new MenuItem { Text = "New", Shortcut = "Ctrl+N" }); -fileMenu.Items.Add(new MenuItem { Text = "Open", Shortcut = "Ctrl+O" }); -fileMenu.Items.Add(new MenuItem { IsSeparator = true }); -fileMenu.Items.Add(new MenuItem { Text = "Exit" }); -menuBar.Items.Add(fileMenu); +fileMenu.Add(new MenuFlyoutItem { Text = "New", KeyboardAccelerators = { new KeyboardAccelerator { Modifiers = KeyboardAcceleratorModifiers.Ctrl, Key = "N" } } }); +fileMenu.Add(new MenuFlyoutItem { Text = "Open" }); +fileMenu.Add(new MenuFlyoutSeparator()); +fileMenu.Add(new MenuFlyoutItem { Text = "Exit" }); +menuBar.Add(fileMenu); ``` ## Platform Services