Initial commit: .NET MAUI Linux Platform

Complete Linux platform implementation for .NET MAUI with:

- 35+ Skia-rendered controls (Button, Label, Entry, CarouselView, etc.)
- Platform services (Clipboard, FilePicker, Notifications, DragDrop, etc.)
- Accessibility support (AT-SPI2, High Contrast)
- HiDPI and Input Method support
- 216 unit tests
- CI/CD workflows
- Project templates
- Documentation

🤖 Generated with Claude Code
This commit is contained in:
logikonline
2025-12-19 09:30:16 +00:00
commit d87124fef2
138 changed files with 32939 additions and 0 deletions

View File

@@ -0,0 +1,120 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Maui.ApplicationModel;
using Microsoft.Maui.ApplicationModel.Communication;
using Microsoft.Maui.ApplicationModel.DataTransfer;
using Microsoft.Maui.Hosting;
using Microsoft.Maui.Platform.Linux.Services;
using Microsoft.Maui.Storage;
using Microsoft.Maui.Platform.Linux.Handlers;
using Microsoft.Maui.Controls;
namespace Microsoft.Maui.Platform.Linux.Hosting;
/// <summary>
/// Extension methods for configuring MAUI applications for Linux.
/// </summary>
public static class LinuxMauiAppBuilderExtensions
{
/// <summary>
/// Configures the MAUI application to run on Linux.
/// </summary>
public static MauiAppBuilder UseLinux(this MauiAppBuilder builder)
{
return builder.UseLinux(configure: null);
}
/// <summary>
/// Configures the MAUI application to run on Linux with options.
/// </summary>
public static MauiAppBuilder UseLinux(this MauiAppBuilder builder, Action<LinuxApplicationOptions>? configure)
{
var options = new LinuxApplicationOptions();
configure?.Invoke(options);
// Register platform services
builder.Services.TryAddSingleton<ILauncher, LauncherService>();
builder.Services.TryAddSingleton<IPreferences, PreferencesService>();
builder.Services.TryAddSingleton<IFilePicker, FilePickerService>();
builder.Services.TryAddSingleton<IClipboard, ClipboardService>();
builder.Services.TryAddSingleton<IShare, ShareService>();
builder.Services.TryAddSingleton<ISecureStorage, SecureStorageService>();
builder.Services.TryAddSingleton<IVersionTracking, VersionTrackingService>();
builder.Services.TryAddSingleton<IAppActions, AppActionsService>();
builder.Services.TryAddSingleton<IBrowser, BrowserService>();
builder.Services.TryAddSingleton<IEmail, EmailService>();
// Register Linux-specific handlers
builder.ConfigureMauiHandlers(handlers =>
{
// Phase 1 - MVP controls
handlers.AddHandler<IButton, ButtonHandler>();
handlers.AddHandler<ILabel, LabelHandler>();
handlers.AddHandler<IEntry, EntryHandler>();
handlers.AddHandler<ICheckBox, CheckBoxHandler>();
handlers.AddHandler<ILayout, LayoutHandler>();
handlers.AddHandler<IStackLayout, StackLayoutHandler>();
handlers.AddHandler<IGridLayout, GridHandler>();
// Phase 2 - Input controls
handlers.AddHandler<ISlider, SliderHandler>();
handlers.AddHandler<ISwitch, SwitchHandler>();
handlers.AddHandler<IProgress, ProgressBarHandler>();
handlers.AddHandler<IActivityIndicator, ActivityIndicatorHandler>();
handlers.AddHandler<ISearchBar, SearchBarHandler>();
// Phase 2 - Image & Graphics
handlers.AddHandler<IImage, ImageHandler>();
handlers.AddHandler<IImageButton, ImageButtonHandler>();
handlers.AddHandler<IGraphicsView, GraphicsViewHandler>();
// Phase 3 - Collection Views
handlers.AddHandler<CollectionView, CollectionViewHandler>();
// Phase 4 - Pages & Navigation
handlers.AddHandler<Page, PageHandler>();
handlers.AddHandler<ContentPage, ContentPageHandler>();
handlers.AddHandler<NavigationPage, NavigationPageHandler>();
// Phase 5 - Advanced Controls
handlers.AddHandler<IPicker, PickerHandler>();
handlers.AddHandler<IDatePicker, DatePickerHandler>();
handlers.AddHandler<ITimePicker, TimePickerHandler>();
handlers.AddHandler<IEditor, EditorHandler>();
// Phase 7 - Additional Controls
handlers.AddHandler<IStepper, StepperHandler>();
handlers.AddHandler<IRadioButton, RadioButtonHandler>();
handlers.AddHandler<IBorderView, BorderHandler>();
// Window handler
handlers.AddHandler<IWindow, WindowHandler>();
});
// Store options for later use
builder.Services.AddSingleton(options);
return builder;
}
}
/// <summary>
/// Handler registration extensions.
/// </summary>
public static class HandlerMappingExtensions
{
/// <summary>
/// Adds a handler for the specified view type.
/// </summary>
public static IMauiHandlersCollection AddHandler<TView, THandler>(
this IMauiHandlersCollection handlers)
where TView : class
where THandler : class
{
handlers.AddHandler(typeof(TView), typeof(THandler));
return handlers;
}
}

444
Hosting/LinuxProgramHost.cs Normal file
View File

@@ -0,0 +1,444 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Maui.Hosting;
using Microsoft.Maui.Controls;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Hosting;
public static class LinuxProgramHost
{
public static void Run<TApp>(string[] args) where TApp : class, IApplication, new()
{
Run<TApp>(args, null);
}
public static void Run<TApp>(string[] args, Action<MauiAppBuilder>? configure) where TApp : class, IApplication, new()
{
var builder = MauiApp.CreateBuilder();
builder.UseLinux();
configure?.Invoke(builder);
builder.UseMauiApp<TApp>();
var mauiApp = builder.Build();
var options = mauiApp.Services.GetService<LinuxApplicationOptions>()
?? new LinuxApplicationOptions();
ParseCommandLineOptions(args, options);
using var linuxApp = new LinuxApplication();
linuxApp.Initialize(options);
// Create comprehensive demo UI with ALL controls
var rootView = CreateComprehensiveDemo();
linuxApp.RootView = rootView;
linuxApp.Run();
}
private static void ParseCommandLineOptions(string[] args, LinuxApplicationOptions options)
{
for (int i = 0; i < args.Length; i++)
{
switch (args[i].ToLowerInvariant())
{
case "--title" when i + 1 < args.Length:
options.Title = args[++i];
break;
case "--width" when i + 1 < args.Length && int.TryParse(args[i + 1], out var w):
options.Width = w;
i++;
break;
case "--height" when i + 1 < args.Length && int.TryParse(args[i + 1], out var h):
options.Height = h;
i++;
break;
}
}
}
private static SkiaView CreateComprehensiveDemo()
{
// Create scrollable container
var scroll = new SkiaScrollView();
var root = new SkiaStackLayout
{
Orientation = StackOrientation.Vertical,
Spacing = 15,
BackgroundColor = new SKColor(0xF5, 0xF5, 0xF5)
};
root.Padding = new SKRect(20, 20, 20, 20);
// ========== TITLE ==========
root.AddChild(new SkiaLabel
{
Text = "MAUI Linux Control Demo",
FontSize = 28,
TextColor = new SKColor(0x1A, 0x23, 0x7E),
IsBold = true
});
root.AddChild(new SkiaLabel
{
Text = "All controls rendered using SkiaSharp on X11",
FontSize = 14,
TextColor = SKColors.Gray
});
// ========== LABELS SECTION ==========
root.AddChild(CreateSeparator());
root.AddChild(CreateSectionHeader("Labels"));
var labelSection = new SkiaStackLayout { Orientation = StackOrientation.Vertical, Spacing = 5 };
labelSection.AddChild(new SkiaLabel { Text = "Normal Label", FontSize = 16, TextColor = SKColors.Black });
labelSection.AddChild(new SkiaLabel { Text = "Bold Label", FontSize = 16, TextColor = SKColors.Black, IsBold = true });
labelSection.AddChild(new SkiaLabel { Text = "Italic Label", FontSize = 16, TextColor = SKColors.Gray, IsItalic = true });
labelSection.AddChild(new SkiaLabel { Text = "Colored Label (Pink)", FontSize = 16, TextColor = new SKColor(0xE9, 0x1E, 0x63) });
root.AddChild(labelSection);
// ========== BUTTONS SECTION ==========
root.AddChild(CreateSeparator());
root.AddChild(CreateSectionHeader("Buttons"));
var buttonSection = new SkiaStackLayout { Orientation = StackOrientation.Horizontal, Spacing = 10 };
var btnPrimary = new SkiaButton { Text = "Primary", FontSize = 14 };
btnPrimary.BackgroundColor = new SKColor(0x21, 0x96, 0xF3);
btnPrimary.TextColor = SKColors.White;
var clickCount = 0;
btnPrimary.Clicked += (s, e) => { clickCount++; btnPrimary.Text = $"Clicked {clickCount}x"; };
buttonSection.AddChild(btnPrimary);
var btnSuccess = new SkiaButton { Text = "Success", FontSize = 14 };
btnSuccess.BackgroundColor = new SKColor(0x4C, 0xAF, 0x50);
btnSuccess.TextColor = SKColors.White;
buttonSection.AddChild(btnSuccess);
var btnDanger = new SkiaButton { Text = "Danger", FontSize = 14 };
btnDanger.BackgroundColor = new SKColor(0xF4, 0x43, 0x36);
btnDanger.TextColor = SKColors.White;
buttonSection.AddChild(btnDanger);
root.AddChild(buttonSection);
// ========== ENTRY SECTION ==========
root.AddChild(CreateSeparator());
root.AddChild(CreateSectionHeader("Text Entry"));
var entry = new SkiaEntry { Placeholder = "Type here...", FontSize = 14 };
root.AddChild(entry);
// ========== SEARCHBAR SECTION ==========
root.AddChild(CreateSeparator());
root.AddChild(CreateSectionHeader("SearchBar"));
var searchBar = new SkiaSearchBar { Placeholder = "Search for items..." };
var searchResultLabel = new SkiaLabel { Text = "", FontSize = 12, TextColor = SKColors.Gray };
searchBar.TextChanged += (s, e) => searchResultLabel.Text = $"Searching: {e.NewTextValue}";
searchBar.SearchButtonPressed += (s, e) => searchResultLabel.Text = $"Search submitted: {searchBar.Text}";
root.AddChild(searchBar);
root.AddChild(searchResultLabel);
// ========== EDITOR SECTION ==========
root.AddChild(CreateSeparator());
root.AddChild(CreateSectionHeader("Editor (Multi-line)"));
var editor = new SkiaEditor
{
Placeholder = "Enter multiple lines of text...",
FontSize = 14,
BackgroundColor = SKColors.White
};
root.AddChild(editor);
// ========== CHECKBOX SECTION ==========
root.AddChild(CreateSeparator());
root.AddChild(CreateSectionHeader("CheckBox"));
var checkSection = new SkiaStackLayout { Orientation = StackOrientation.Horizontal, Spacing = 20 };
var cb1 = new SkiaCheckBox { IsChecked = true };
checkSection.AddChild(cb1);
checkSection.AddChild(new SkiaLabel { Text = "Checked", FontSize = 14 });
var cb2 = new SkiaCheckBox { IsChecked = false };
checkSection.AddChild(cb2);
checkSection.AddChild(new SkiaLabel { Text = "Unchecked", FontSize = 14 });
root.AddChild(checkSection);
// ========== SWITCH SECTION ==========
root.AddChild(CreateSeparator());
root.AddChild(CreateSectionHeader("Switch"));
var switchSection = new SkiaStackLayout { Orientation = StackOrientation.Horizontal, Spacing = 20 };
var sw1 = new SkiaSwitch { IsOn = true };
switchSection.AddChild(sw1);
switchSection.AddChild(new SkiaLabel { Text = "On", FontSize = 14 });
var sw2 = new SkiaSwitch { IsOn = false };
switchSection.AddChild(sw2);
switchSection.AddChild(new SkiaLabel { Text = "Off", FontSize = 14 });
root.AddChild(switchSection);
// ========== RADIOBUTTON SECTION ==========
root.AddChild(CreateSeparator());
root.AddChild(CreateSectionHeader("RadioButton"));
var radioSection = new SkiaStackLayout { Orientation = StackOrientation.Horizontal, Spacing = 15 };
radioSection.AddChild(new SkiaRadioButton { Content = "Option A", IsChecked = true, GroupName = "demo" });
radioSection.AddChild(new SkiaRadioButton { Content = "Option B", IsChecked = false, GroupName = "demo" });
radioSection.AddChild(new SkiaRadioButton { Content = "Option C", IsChecked = false, GroupName = "demo" });
root.AddChild(radioSection);
// ========== SLIDER SECTION ==========
root.AddChild(CreateSeparator());
root.AddChild(CreateSectionHeader("Slider"));
var sliderLabel = new SkiaLabel { Text = "Value: 50", FontSize = 14 };
var slider = new SkiaSlider { Minimum = 0, Maximum = 100, Value = 50 };
slider.ValueChanged += (s, e) => sliderLabel.Text = $"Value: {(int)slider.Value}";
root.AddChild(slider);
root.AddChild(sliderLabel);
// ========== STEPPER SECTION ==========
root.AddChild(CreateSeparator());
root.AddChild(CreateSectionHeader("Stepper"));
var stepperSection = new SkiaStackLayout { Orientation = StackOrientation.Horizontal, Spacing = 10 };
var stepperLabel = new SkiaLabel { Text = "Value: 5", FontSize = 14 };
var stepper = new SkiaStepper { Value = 5, Minimum = 0, Maximum = 10, Increment = 1 };
stepper.ValueChanged += (s, e) => stepperLabel.Text = $"Value: {(int)stepper.Value}";
stepperSection.AddChild(stepper);
stepperSection.AddChild(stepperLabel);
root.AddChild(stepperSection);
// ========== PROGRESSBAR SECTION ==========
root.AddChild(CreateSeparator());
root.AddChild(CreateSectionHeader("ProgressBar"));
var progress = new SkiaProgressBar { Progress = 0.7f };
root.AddChild(progress);
root.AddChild(new SkiaLabel { Text = "70% Complete", FontSize = 12, TextColor = SKColors.Gray });
// ========== ACTIVITYINDICATOR SECTION ==========
root.AddChild(CreateSeparator());
root.AddChild(CreateSectionHeader("ActivityIndicator"));
var activitySection = new SkiaStackLayout { Orientation = StackOrientation.Horizontal, Spacing = 10 };
var activity = new SkiaActivityIndicator { IsRunning = true };
activitySection.AddChild(activity);
activitySection.AddChild(new SkiaLabel { Text = "Loading...", FontSize = 14, TextColor = SKColors.Gray });
root.AddChild(activitySection);
// ========== PICKER SECTION ==========
root.AddChild(CreateSeparator());
root.AddChild(CreateSectionHeader("Picker (Dropdown)"));
var picker = new SkiaPicker { Title = "Select an item" };
picker.SetItems(new[] { "Apple", "Banana", "Cherry", "Date", "Elderberry", "Fig", "Grape" });
var pickerLabel = new SkiaLabel { Text = "Selected: (none)", FontSize = 12, TextColor = SKColors.Gray };
picker.SelectedIndexChanged += (s, e) => pickerLabel.Text = $"Selected: {picker.SelectedItem}";
root.AddChild(picker);
root.AddChild(pickerLabel);
// ========== DATEPICKER SECTION ==========
root.AddChild(CreateSeparator());
root.AddChild(CreateSectionHeader("DatePicker"));
var datePicker = new SkiaDatePicker { Date = DateTime.Today };
var dateLabel = new SkiaLabel { Text = $"Date: {DateTime.Today:d}", FontSize = 12, TextColor = SKColors.Gray };
datePicker.DateSelected += (s, e) => dateLabel.Text = $"Date: {datePicker.Date:d}";
root.AddChild(datePicker);
root.AddChild(dateLabel);
// ========== TIMEPICKER SECTION ==========
root.AddChild(CreateSeparator());
root.AddChild(CreateSectionHeader("TimePicker"));
var timePicker = new SkiaTimePicker();
var timeLabel = new SkiaLabel { Text = $"Time: {DateTime.Now:t}", FontSize = 12, TextColor = SKColors.Gray };
timePicker.TimeSelected += (s, e) => timeLabel.Text = $"Time: {DateTime.Today.Add(timePicker.Time):t}";
root.AddChild(timePicker);
root.AddChild(timeLabel);
// ========== BORDER SECTION ==========
root.AddChild(CreateSeparator());
root.AddChild(CreateSectionHeader("Border"));
var border = new SkiaBorder
{
CornerRadius = 8,
StrokeThickness = 2,
Stroke = new SKColor(0x21, 0x96, 0xF3),
BackgroundColor = new SKColor(0xE3, 0xF2, 0xFD)
};
border.SetPadding(15);
border.AddChild(new SkiaLabel { Text = "Content inside a styled Border", FontSize = 14, TextColor = new SKColor(0x1A, 0x23, 0x7E) });
root.AddChild(border);
// ========== FRAME SECTION ==========
root.AddChild(CreateSeparator());
root.AddChild(CreateSectionHeader("Frame (with shadow)"));
var frame = new SkiaFrame();
frame.BackgroundColor = SKColors.White;
frame.AddChild(new SkiaLabel { Text = "Content inside a Frame with shadow effect", FontSize = 14 });
root.AddChild(frame);
// ========== COLLECTIONVIEW SECTION ==========
root.AddChild(CreateSeparator());
root.AddChild(CreateSectionHeader("CollectionView (List)"));
var collectionView = new SkiaCollectionView
{
SelectionMode = SkiaSelectionMode.Single,
Header = "Fruits",
Footer = "End of list"
};
collectionView.ItemsSource =(new object[] { "Apple", "Banana", "Cherry", "Date", "Elderberry", "Fig", "Grape", "Honeydew" });
var collectionLabel = new SkiaLabel { Text = "Selected: (none)", FontSize = 12, TextColor = SKColors.Gray };
collectionView.SelectionChanged += (s, e) =>
{
var selected = e.CurrentSelection.FirstOrDefault();
collectionLabel.Text = $"Selected: {selected}";
};
root.AddChild(collectionView);
root.AddChild(collectionLabel);
// ========== IMAGEBUTTON SECTION ==========
root.AddChild(CreateSeparator());
root.AddChild(CreateSectionHeader("ImageButton"));
var imageButtonSection = new SkiaStackLayout { Orientation = StackOrientation.Horizontal, Spacing = 10 };
// Create ImageButton with a generated icon (since we don't have image files)
var imgBtn = new SkiaImageButton
{
CornerRadius = 8,
StrokeColor = new SKColor(0x21, 0x96, 0xF3),
StrokeThickness = 1,
BackgroundColor = new SKColor(0xE3, 0xF2, 0xFD),
PaddingLeft = 10,
PaddingRight = 10,
PaddingTop = 10,
PaddingBottom = 10
};
// Generate a simple star icon bitmap
var iconBitmap = CreateStarIcon(32, new SKColor(0x21, 0x96, 0xF3));
imgBtn.Bitmap = iconBitmap;
var imgBtnLabel = new SkiaLabel { Text = "Click the star!", FontSize = 12, TextColor = SKColors.Gray };
imgBtn.Clicked += (s, e) => imgBtnLabel.Text = "Star clicked!";
imageButtonSection.AddChild(imgBtn);
imageButtonSection.AddChild(imgBtnLabel);
root.AddChild(imageButtonSection);
// ========== IMAGE SECTION ==========
root.AddChild(CreateSeparator());
root.AddChild(CreateSectionHeader("Image"));
var imageSection = new SkiaStackLayout { Orientation = StackOrientation.Horizontal, Spacing = 10 };
// Create Image with a generated sample image
var img = new SkiaImage();
var sampleBitmap = CreateSampleImage(80, 60);
img.Bitmap = sampleBitmap;
imageSection.AddChild(img);
imageSection.AddChild(new SkiaLabel { Text = "Sample generated image", FontSize = 12, TextColor = SKColors.Gray });
root.AddChild(imageSection);
// ========== FOOTER ==========
root.AddChild(CreateSeparator());
root.AddChild(new SkiaLabel
{
Text = "All 25+ controls are interactive - try them all!",
FontSize = 16,
TextColor = new SKColor(0x4C, 0xAF, 0x50),
IsBold = true
});
root.AddChild(new SkiaLabel
{
Text = "Scroll down to see more controls",
FontSize = 12,
TextColor = SKColors.Gray
});
scroll.Content = root;
return scroll;
}
private static SkiaLabel CreateSectionHeader(string text)
{
return new SkiaLabel
{
Text = text,
FontSize = 18,
TextColor = new SKColor(0x37, 0x47, 0x4F),
IsBold = true
};
}
private static SkiaView CreateSeparator()
{
var sep = new SkiaLabel { Text = "", BackgroundColor = new SKColor(0xE0, 0xE0, 0xE0), RequestedHeight = 1 };
return sep;
}
private static SKBitmap CreateStarIcon(int size, SKColor color)
{
var bitmap = new SKBitmap(size, size);
using var canvas = new SKCanvas(bitmap);
canvas.Clear(SKColors.Transparent);
using var paint = new SKPaint
{
Color = color,
Style = SKPaintStyle.Fill,
IsAntialias = true
};
// Draw a 5-point star
using var path = new SKPath();
var cx = size / 2f;
var cy = size / 2f;
var outerRadius = size / 2f - 2;
var innerRadius = outerRadius * 0.4f;
for (int i = 0; i < 5; i++)
{
var outerAngle = (i * 72 - 90) * Math.PI / 180;
var innerAngle = ((i * 72) + 36 - 90) * Math.PI / 180;
var ox = cx + outerRadius * (float)Math.Cos(outerAngle);
var oy = cy + outerRadius * (float)Math.Sin(outerAngle);
var ix = cx + innerRadius * (float)Math.Cos(innerAngle);
var iy = cy + innerRadius * (float)Math.Sin(innerAngle);
if (i == 0)
path.MoveTo(ox, oy);
else
path.LineTo(ox, oy);
path.LineTo(ix, iy);
}
path.Close();
canvas.DrawPath(path, paint);
return bitmap;
}
private static SKBitmap CreateSampleImage(int width, int height)
{
var bitmap = new SKBitmap(width, height);
using var canvas = new SKCanvas(bitmap);
// Draw gradient background
using var bgPaint = new SKPaint();
using var shader = SKShader.CreateLinearGradient(
new SKPoint(0, 0),
new SKPoint(width, height),
new SKColor[] { new SKColor(0x42, 0xA5, 0xF5), new SKColor(0x7E, 0x57, 0xC2) },
new float[] { 0, 1 },
SKShaderTileMode.Clamp);
bgPaint.Shader = shader;
canvas.DrawRect(0, 0, width, height, bgPaint);
// Draw some shapes
using var shapePaint = new SKPaint
{
Color = SKColors.White.WithAlpha(180),
Style = SKPaintStyle.Fill,
IsAntialias = true
};
canvas.DrawCircle(width * 0.3f, height * 0.4f, 15, shapePaint);
canvas.DrawRect(width * 0.5f, height * 0.3f, 20, 20, shapePaint);
// Draw "IMG" text
using var font = new SKFont(SKTypeface.Default, 12);
using var textPaint = new SKPaint(font)
{
Color = SKColors.White,
IsAntialias = true
};
canvas.DrawText("IMG", 10, height - 8, textPaint);
return bitmap;
}
}