// 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 Microsoft.Maui.Platform;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Hosting;
///
/// Entry point for running MAUI applications on Linux.
///
public static class LinuxProgramHost
{
///
/// Runs the MAUI application on Linux.
///
/// The application type.
/// Command line arguments.
public static void Run(string[] args) where TApp : class, IApplication, new()
{
Run(args, null);
}
///
/// Runs the MAUI application on Linux with additional configuration.
///
/// The application type.
/// Command line arguments.
/// Optional builder configuration action.
public static void Run(string[] args, Action? configure) where TApp : class, IApplication, new()
{
// Build the MAUI application
var builder = MauiApp.CreateBuilder();
builder.UseLinux();
configure?.Invoke(builder);
builder.UseMauiApp();
var mauiApp = builder.Build();
// Get application options
var options = mauiApp.Services.GetService()
?? new LinuxApplicationOptions();
ParseCommandLineOptions(args, options);
// Create Linux application
using var linuxApp = new LinuxApplication();
linuxApp.Initialize(options);
// Create MAUI context
var mauiContext = new LinuxMauiContext(mauiApp.Services, linuxApp);
// Get the MAUI application instance
var application = mauiApp.Services.GetService();
// Ensure Application.Current is set - required for Shell.Current to work
if (application is Application app && Application.Current == null)
{
// Use reflection to set Current since it has a protected setter
var currentProperty = typeof(Application).GetProperty("Current");
currentProperty?.SetValue(null, app);
}
// Try to render the application's main page
SkiaView? rootView = null;
if (application != null)
{
rootView = RenderApplication(application, mauiContext, options);
}
// Fallback to demo if no application view is available
if (rootView == null)
{
Console.WriteLine("No application page found. Showing demo UI.");
rootView = CreateDemoView();
}
linuxApp.RootView = rootView;
linuxApp.Run();
}
///
/// Renders the MAUI application and returns the root SkiaView.
///
private static SkiaView? RenderApplication(IApplication application, LinuxMauiContext mauiContext, LinuxApplicationOptions options)
{
try
{
// For Applications, we need to create a window
if (application is Application app)
{
Page? mainPage = app.MainPage;
// If no MainPage set, check for windows
if (mainPage == null && application.Windows.Count > 0)
{
var existingWindow = application.Windows[0];
if (existingWindow.Content is Page page)
{
mainPage = page;
}
}
if (mainPage != null)
{
// Create a MAUI Window and add it to the application
// This ensures Shell.Current works properly (it reads from Application.Current.Windows[0].Page)
if (app.Windows.Count == 0)
{
var mauiWindow = new Microsoft.Maui.Controls.Window(mainPage);
// Try OpenWindow first
app.OpenWindow(mauiWindow);
// If that didn't work, use reflection to add directly to _windows
if (app.Windows.Count == 0)
{
var windowsField = typeof(Application).GetField("_windows",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (windowsField?.GetValue(app) is System.Collections.IList windowsList)
{
windowsList.Add(mauiWindow);
}
}
}
return RenderPage(mainPage, mauiContext);
}
}
return null;
}
catch (Exception ex)
{
Console.WriteLine($"Error rendering application: {ex.Message}");
Console.WriteLine(ex.StackTrace);
return null;
}
}
///
/// Renders a MAUI Page to a SkiaView.
///
private static SkiaView? RenderPage(Page page, LinuxMauiContext mauiContext)
{
var renderer = new LinuxViewRenderer(mauiContext);
return renderer.RenderPage(page);
}
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;
case "--demo":
// Force demo mode
options.ForceDemo = true;
break;
}
}
}
///
/// Creates a demo view showcasing all controls.
///
public static SkiaView CreateDemoView()
{
// 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 = "OpenMaui 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;
}
}