Preview 3: Complete control implementation with XAML data binding

Major milestone adding full control functionality:

Controls Enhanced:
- Entry/Editor: Full keyboard input, cursor navigation, selection, clipboard
- CollectionView: Data binding, selection highlighting, scrolling
- CheckBox/Switch/Slider: Interactive state management
- Picker/DatePicker/TimePicker: Dropdown selection with popup overlays
- ProgressBar/ActivityIndicator: Animated progress display
- Button: Press/release visual states
- Border/Frame: Rounded corners, stroke styling
- Label: Text wrapping, alignment, decorations
- Grid/StackLayout: Margin and padding support

Features Added:
- DisplayAlert dialogs with button actions
- NavigationPage with toolbar and back navigation
- Shell with flyout menu navigation
- XAML value converters for data binding
- Margin support in all layout containers
- Popup overlay system for pickers

New Samples:
- TodoApp: Full CRUD task manager with NavigationPage
- ShellDemo: Comprehensive control showcase

Removed:
- ControlGallery (replaced by ShellDemo)
- LinuxDemo (replaced by TodoApp)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
logikonline
2025-12-21 13:26:56 -05:00
parent f945d2a537
commit 1d55ac672a
142 changed files with 38925 additions and 4201 deletions

View File

@@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Runtime.InteropServices;
using SkiaSharp;
using Microsoft.Maui.Platform.Linux.Window;
using Microsoft.Maui.Platform.Linux.Rendering;
@@ -216,113 +215,25 @@ public class X11DisplayWindow : IDisplayWindow
/// <summary>
/// Wayland display window wrapper implementing IDisplayWindow.
/// Uses wl_shm for software rendering with SkiaSharp.
/// Uses the full WaylandWindow implementation with xdg-shell protocol.
/// </summary>
public class WaylandDisplayWindow : IDisplayWindow
{
#region Native Interop
private readonly WaylandWindow _window;
private const string LibWaylandClient = "libwayland-client.so.0";
public int Width => _window.Width;
public int Height => _window.Height;
public bool IsRunning => _window.IsRunning;
[DllImport(LibWaylandClient)]
private static extern IntPtr wl_display_connect(string? name);
/// <summary>
/// Gets the pixel data pointer for rendering.
/// </summary>
public IntPtr PixelData => _window.PixelData;
[DllImport(LibWaylandClient)]
private static extern void wl_display_disconnect(IntPtr display);
[DllImport(LibWaylandClient)]
private static extern int wl_display_dispatch(IntPtr display);
[DllImport(LibWaylandClient)]
private static extern int wl_display_dispatch_pending(IntPtr display);
[DllImport(LibWaylandClient)]
private static extern int wl_display_roundtrip(IntPtr display);
[DllImport(LibWaylandClient)]
private static extern int wl_display_flush(IntPtr display);
[DllImport(LibWaylandClient)]
private static extern IntPtr wl_display_get_registry(IntPtr display);
[DllImport(LibWaylandClient)]
private static extern IntPtr wl_compositor_create_surface(IntPtr compositor);
[DllImport(LibWaylandClient)]
private static extern void wl_surface_attach(IntPtr surface, IntPtr buffer, int x, int y);
[DllImport(LibWaylandClient)]
private static extern void wl_surface_damage(IntPtr surface, int x, int y, int width, int height);
[DllImport(LibWaylandClient)]
private static extern void wl_surface_commit(IntPtr surface);
[DllImport(LibWaylandClient)]
private static extern void wl_surface_destroy(IntPtr surface);
[DllImport(LibWaylandClient)]
private static extern IntPtr wl_shm_create_pool(IntPtr shm, int fd, int size);
[DllImport(LibWaylandClient)]
private static extern void wl_shm_pool_destroy(IntPtr pool);
[DllImport(LibWaylandClient)]
private static extern IntPtr wl_shm_pool_create_buffer(IntPtr pool, int offset, int width, int height, int stride, uint format);
[DllImport(LibWaylandClient)]
private static extern void wl_buffer_destroy(IntPtr buffer);
[DllImport("libc", EntryPoint = "shm_open")]
private static extern int shm_open([MarshalAs(UnmanagedType.LPStr)] string name, int oflag, int mode);
[DllImport("libc", EntryPoint = "shm_unlink")]
private static extern int shm_unlink([MarshalAs(UnmanagedType.LPStr)] string name);
[DllImport("libc", EntryPoint = "ftruncate")]
private static extern int ftruncate(int fd, long length);
[DllImport("libc", EntryPoint = "mmap")]
private static extern IntPtr mmap(IntPtr addr, nuint length, int prot, int flags, int fd, long offset);
[DllImport("libc", EntryPoint = "munmap")]
private static extern int munmap(IntPtr addr, nuint length);
[DllImport("libc", EntryPoint = "close")]
private static extern int close(int fd);
private const int O_RDWR = 2;
private const int O_CREAT = 0x40;
private const int O_EXCL = 0x80;
private const int PROT_READ = 1;
private const int PROT_WRITE = 2;
private const int MAP_SHARED = 1;
private const uint WL_SHM_FORMAT_XRGB8888 = 1;
#endregion
private IntPtr _display;
private IntPtr _registry;
private IntPtr _compositor;
private IntPtr _shm;
private IntPtr _surface;
private IntPtr _shmPool;
private IntPtr _buffer;
private IntPtr _pixelData;
private int _shmFd = -1;
private int _bufferSize;
private int _width;
private int _height;
private string _title;
private bool _isRunning;
private bool _disposed;
private SKBitmap? _bitmap;
private SKCanvas? _canvas;
public int Width => _width;
public int Height => _height;
public bool IsRunning => _isRunning;
/// <summary>
/// Gets the stride (bytes per row) of the pixel buffer.
/// </summary>
public int Stride => _window.Stride;
public event EventHandler<KeyEventArgs>? KeyDown;
public event EventHandler<KeyEventArgs>? KeyUp;
@@ -337,213 +248,27 @@ public class WaylandDisplayWindow : IDisplayWindow
public WaylandDisplayWindow(string title, int width, int height)
{
_title = title;
_width = width;
_height = height;
_window = new WaylandWindow(title, width, height);
Initialize();
// Wire up events
_window.KeyDown += (s, e) => KeyDown?.Invoke(this, e);
_window.KeyUp += (s, e) => KeyUp?.Invoke(this, e);
_window.TextInput += (s, e) => TextInput?.Invoke(this, e);
_window.PointerMoved += (s, e) => PointerMoved?.Invoke(this, e);
_window.PointerPressed += (s, e) => PointerPressed?.Invoke(this, e);
_window.PointerReleased += (s, e) => PointerReleased?.Invoke(this, e);
_window.Scroll += (s, e) => Scroll?.Invoke(this, e);
_window.Exposed += (s, e) => Exposed?.Invoke(this, e);
_window.Resized += (s, e) => Resized?.Invoke(this, e);
_window.CloseRequested += (s, e) => CloseRequested?.Invoke(this, e);
}
private void Initialize()
{
_display = wl_display_connect(null);
if (_display == IntPtr.Zero)
{
throw new InvalidOperationException("Failed to connect to Wayland display. Is WAYLAND_DISPLAY set?");
}
_registry = wl_display_get_registry(_display);
if (_registry == IntPtr.Zero)
{
throw new InvalidOperationException("Failed to get Wayland registry");
}
// Note: A full implementation would set up registry listeners to get
// compositor and shm handles. For now, we throw an informative error
// and fall back to X11 via XWayland in DisplayServerFactory.
// This is a placeholder - proper Wayland support requires:
// 1. Setting up wl_registry_listener with callbacks
// 2. Binding to wl_compositor, wl_shm, wl_seat, xdg_wm_base
// 3. Implementing the xdg-shell protocol for toplevel windows
wl_display_roundtrip(_display);
// For now, signal that native Wayland isn't fully implemented
throw new NotSupportedException(
"Native Wayland support is experimental. " +
"Set MAUI_PREFER_X11=1 to use XWayland, or run with DISPLAY set.");
}
private void CreateShmBuffer()
{
int stride = _width * 4;
_bufferSize = stride * _height;
string shmName = $"/maui-shm-{Environment.ProcessId}-{DateTime.Now.Ticks}";
_shmFd = shm_open(shmName, O_RDWR | O_CREAT | O_EXCL, 0600);
if (_shmFd < 0)
{
throw new InvalidOperationException("Failed to create shared memory file");
}
shm_unlink(shmName);
if (ftruncate(_shmFd, _bufferSize) < 0)
{
close(_shmFd);
throw new InvalidOperationException("Failed to resize shared memory");
}
_pixelData = mmap(IntPtr.Zero, (nuint)_bufferSize, PROT_READ | PROT_WRITE, MAP_SHARED, _shmFd, 0);
if (_pixelData == IntPtr.Zero || _pixelData == new IntPtr(-1))
{
close(_shmFd);
throw new InvalidOperationException("Failed to mmap shared memory");
}
_shmPool = wl_shm_create_pool(_shm, _shmFd, _bufferSize);
if (_shmPool == IntPtr.Zero)
{
munmap(_pixelData, (nuint)_bufferSize);
close(_shmFd);
throw new InvalidOperationException("Failed to create wl_shm_pool");
}
_buffer = wl_shm_pool_create_buffer(_shmPool, 0, _width, _height, stride, WL_SHM_FORMAT_XRGB8888);
if (_buffer == IntPtr.Zero)
{
wl_shm_pool_destroy(_shmPool);
munmap(_pixelData, (nuint)_bufferSize);
close(_shmFd);
throw new InvalidOperationException("Failed to create wl_buffer");
}
// Create Skia bitmap backed by shared memory
var info = new SKImageInfo(_width, _height, SKColorType.Bgra8888, SKAlphaType.Opaque);
_bitmap = new SKBitmap();
_bitmap.InstallPixels(info, _pixelData, stride);
_canvas = new SKCanvas(_bitmap);
}
public void Show()
{
if (_surface == IntPtr.Zero || _buffer == IntPtr.Zero) return;
wl_surface_attach(_surface, _buffer, 0, 0);
wl_surface_damage(_surface, 0, 0, _width, _height);
wl_surface_commit(_surface);
wl_display_flush(_display);
}
public void Hide()
{
if (_surface == IntPtr.Zero) return;
wl_surface_attach(_surface, IntPtr.Zero, 0, 0);
wl_surface_commit(_surface);
wl_display_flush(_display);
}
public void SetTitle(string title)
{
_title = title;
}
public void Resize(int width, int height)
{
if (width == _width && height == _height) return;
_canvas?.Dispose();
_bitmap?.Dispose();
if (_buffer != IntPtr.Zero)
wl_buffer_destroy(_buffer);
if (_shmPool != IntPtr.Zero)
wl_shm_pool_destroy(_shmPool);
if (_pixelData != IntPtr.Zero)
munmap(_pixelData, (nuint)_bufferSize);
if (_shmFd >= 0)
close(_shmFd);
_width = width;
_height = height;
CreateShmBuffer();
Resized?.Invoke(this, (width, height));
}
public void ProcessEvents()
{
if (!_isRunning || _display == IntPtr.Zero) return;
wl_display_dispatch_pending(_display);
wl_display_flush(_display);
}
public void Stop()
{
_isRunning = false;
}
public SKCanvas? GetCanvas() => _canvas;
public void CommitFrame()
{
if (_surface != IntPtr.Zero && _buffer != IntPtr.Zero)
{
wl_surface_attach(_surface, _buffer, 0, 0);
wl_surface_damage(_surface, 0, 0, _width, _height);
wl_surface_commit(_surface);
wl_display_flush(_display);
}
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_isRunning = false;
_canvas?.Dispose();
_bitmap?.Dispose();
if (_buffer != IntPtr.Zero)
{
wl_buffer_destroy(_buffer);
_buffer = IntPtr.Zero;
}
if (_shmPool != IntPtr.Zero)
{
wl_shm_pool_destroy(_shmPool);
_shmPool = IntPtr.Zero;
}
if (_pixelData != IntPtr.Zero && _pixelData != new IntPtr(-1))
{
munmap(_pixelData, (nuint)_bufferSize);
_pixelData = IntPtr.Zero;
}
if (_shmFd >= 0)
{
close(_shmFd);
_shmFd = -1;
}
if (_surface != IntPtr.Zero)
{
wl_surface_destroy(_surface);
_surface = IntPtr.Zero;
}
if (_display != IntPtr.Zero)
{
wl_display_disconnect(_display);
_display = IntPtr.Zero;
}
}
public void Show() => _window.Show();
public void Hide() => _window.Hide();
public void SetTitle(string title) => _window.SetTitle(title);
public void Resize(int width, int height) => _window.Resize(width, height);
public void ProcessEvents() => _window.ProcessEvents();
public void Stop() => _window.Stop();
public void CommitFrame() => _window.CommitFrame();
public void Dispose() => _window.Dispose();
}

View File

@@ -0,0 +1,821 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Runtime.InteropServices;
namespace Microsoft.Maui.Platform.Linux.Services;
/// <summary>
/// GTK4 dialog response codes.
/// </summary>
public enum GtkResponseType
{
None = -1,
Reject = -2,
Accept = -3,
DeleteEvent = -4,
Ok = -5,
Cancel = -6,
Close = -7,
Yes = -8,
No = -9,
Apply = -10,
Help = -11
}
/// <summary>
/// GTK4 message dialog types.
/// </summary>
public enum GtkMessageType
{
Info = 0,
Warning = 1,
Question = 2,
Error = 3,
Other = 4
}
/// <summary>
/// GTK4 button layouts for dialogs.
/// </summary>
public enum GtkButtonsType
{
None = 0,
Ok = 1,
Close = 2,
Cancel = 3,
YesNo = 4,
OkCancel = 5
}
/// <summary>
/// GTK4 file chooser actions.
/// </summary>
public enum GtkFileChooserAction
{
Open = 0,
Save = 1,
SelectFolder = 2,
CreateFolder = 3
}
/// <summary>
/// Result from a file dialog.
/// </summary>
public class FileDialogResult
{
public bool Accepted { get; init; }
public string[] SelectedFiles { get; init; } = Array.Empty<string>();
public string? SelectedFile => SelectedFiles.Length > 0 ? SelectedFiles[0] : null;
}
/// <summary>
/// Result from a color dialog.
/// </summary>
public class ColorDialogResult
{
public bool Accepted { get; init; }
public float Red { get; init; }
public float Green { get; init; }
public float Blue { get; init; }
public float Alpha { get; init; }
}
/// <summary>
/// GTK4 interop layer for native Linux dialogs.
/// Provides native file pickers, message boxes, and color choosers.
/// </summary>
public class Gtk4InteropService : IDisposable
{
#region GTK4 Native Interop
private const string LibGtk4 = "libgtk-4.so.1";
private const string LibGio = "libgio-2.0.so.0";
private const string LibGlib = "libglib-2.0.so.0";
private const string LibGObject = "libgobject-2.0.so.0";
// GTK initialization
[DllImport(LibGtk4)]
private static extern bool gtk_init_check();
[DllImport(LibGtk4)]
private static extern bool gtk_is_initialized();
// Main loop
[DllImport(LibGtk4)]
private static extern IntPtr g_main_context_default();
[DllImport(LibGtk4)]
private static extern bool g_main_context_iteration(IntPtr context, bool mayBlock);
[DllImport(LibGlib)]
private static extern void g_free(IntPtr mem);
// GObject
[DllImport(LibGObject)]
private static extern void g_object_unref(IntPtr obj);
[DllImport(LibGObject)]
private static extern void g_object_ref(IntPtr obj);
// Window
[DllImport(LibGtk4)]
private static extern IntPtr gtk_window_new();
[DllImport(LibGtk4)]
private static extern void gtk_window_set_title(IntPtr window, [MarshalAs(UnmanagedType.LPStr)] string title);
[DllImport(LibGtk4)]
private static extern void gtk_window_set_modal(IntPtr window, bool modal);
[DllImport(LibGtk4)]
private static extern void gtk_window_set_transient_for(IntPtr window, IntPtr parent);
[DllImport(LibGtk4)]
private static extern void gtk_window_destroy(IntPtr window);
[DllImport(LibGtk4)]
private static extern void gtk_window_present(IntPtr window);
[DllImport(LibGtk4)]
private static extern void gtk_window_close(IntPtr window);
// Widget
[DllImport(LibGtk4)]
private static extern void gtk_widget_show(IntPtr widget);
[DllImport(LibGtk4)]
private static extern void gtk_widget_hide(IntPtr widget);
[DllImport(LibGtk4)]
private static extern void gtk_widget_set_visible(IntPtr widget, bool visible);
[DllImport(LibGtk4)]
private static extern bool gtk_widget_get_visible(IntPtr widget);
// Alert Dialog (GTK4)
[DllImport(LibGtk4)]
private static extern IntPtr gtk_alert_dialog_new([MarshalAs(UnmanagedType.LPStr)] string format);
[DllImport(LibGtk4)]
private static extern void gtk_alert_dialog_set_message(IntPtr dialog, [MarshalAs(UnmanagedType.LPStr)] string message);
[DllImport(LibGtk4)]
private static extern void gtk_alert_dialog_set_detail(IntPtr dialog, [MarshalAs(UnmanagedType.LPStr)] string detail);
[DllImport(LibGtk4)]
private static extern void gtk_alert_dialog_set_buttons(IntPtr dialog, string[] labels);
[DllImport(LibGtk4)]
private static extern void gtk_alert_dialog_set_cancel_button(IntPtr dialog, int button);
[DllImport(LibGtk4)]
private static extern void gtk_alert_dialog_set_default_button(IntPtr dialog, int button);
[DllImport(LibGtk4)]
private static extern void gtk_alert_dialog_show(IntPtr dialog, IntPtr parent);
// File Dialog (GTK4)
[DllImport(LibGtk4)]
private static extern IntPtr gtk_file_dialog_new();
[DllImport(LibGtk4)]
private static extern void gtk_file_dialog_set_title(IntPtr dialog, [MarshalAs(UnmanagedType.LPStr)] string title);
[DllImport(LibGtk4)]
private static extern void gtk_file_dialog_set_modal(IntPtr dialog, bool modal);
[DllImport(LibGtk4)]
private static extern void gtk_file_dialog_set_accept_label(IntPtr dialog, [MarshalAs(UnmanagedType.LPStr)] string label);
[DllImport(LibGtk4)]
private static extern void gtk_file_dialog_open(IntPtr dialog, IntPtr parent, IntPtr cancellable, GAsyncReadyCallback callback, IntPtr userData);
[DllImport(LibGtk4)]
private static extern IntPtr gtk_file_dialog_open_finish(IntPtr dialog, IntPtr result, out IntPtr error);
[DllImport(LibGtk4)]
private static extern void gtk_file_dialog_save(IntPtr dialog, IntPtr parent, IntPtr cancellable, GAsyncReadyCallback callback, IntPtr userData);
[DllImport(LibGtk4)]
private static extern IntPtr gtk_file_dialog_save_finish(IntPtr dialog, IntPtr result, out IntPtr error);
[DllImport(LibGtk4)]
private static extern void gtk_file_dialog_select_folder(IntPtr dialog, IntPtr parent, IntPtr cancellable, GAsyncReadyCallback callback, IntPtr userData);
[DllImport(LibGtk4)]
private static extern IntPtr gtk_file_dialog_select_folder_finish(IntPtr dialog, IntPtr result, out IntPtr error);
[DllImport(LibGtk4)]
private static extern void gtk_file_dialog_open_multiple(IntPtr dialog, IntPtr parent, IntPtr cancellable, GAsyncReadyCallback callback, IntPtr userData);
[DllImport(LibGtk4)]
private static extern IntPtr gtk_file_dialog_open_multiple_finish(IntPtr dialog, IntPtr result, out IntPtr error);
// File filters
[DllImport(LibGtk4)]
private static extern IntPtr gtk_file_filter_new();
[DllImport(LibGtk4)]
private static extern void gtk_file_filter_set_name(IntPtr filter, [MarshalAs(UnmanagedType.LPStr)] string name);
[DllImport(LibGtk4)]
private static extern void gtk_file_filter_add_pattern(IntPtr filter, [MarshalAs(UnmanagedType.LPStr)] string pattern);
[DllImport(LibGtk4)]
private static extern void gtk_file_filter_add_mime_type(IntPtr filter, [MarshalAs(UnmanagedType.LPStr)] string mimeType);
[DllImport(LibGtk4)]
private static extern void gtk_file_dialog_set_default_filter(IntPtr dialog, IntPtr filter);
// GFile
[DllImport(LibGio)]
private static extern IntPtr g_file_get_path(IntPtr file);
// GListModel for multiple files
[DllImport(LibGio)]
private static extern uint g_list_model_get_n_items(IntPtr list);
[DllImport(LibGio)]
private static extern IntPtr g_list_model_get_item(IntPtr list, uint position);
// Color Dialog (GTK4)
[DllImport(LibGtk4)]
private static extern IntPtr gtk_color_dialog_new();
[DllImport(LibGtk4)]
private static extern void gtk_color_dialog_set_title(IntPtr dialog, [MarshalAs(UnmanagedType.LPStr)] string title);
[DllImport(LibGtk4)]
private static extern void gtk_color_dialog_set_modal(IntPtr dialog, bool modal);
[DllImport(LibGtk4)]
private static extern void gtk_color_dialog_set_with_alpha(IntPtr dialog, bool withAlpha);
[DllImport(LibGtk4)]
private static extern void gtk_color_dialog_choose_rgba(IntPtr dialog, IntPtr parent, IntPtr initialColor, IntPtr cancellable, GAsyncReadyCallback callback, IntPtr userData);
[DllImport(LibGtk4)]
private static extern IntPtr gtk_color_dialog_choose_rgba_finish(IntPtr dialog, IntPtr result, out IntPtr error);
// GdkRGBA
[StructLayout(LayoutKind.Sequential)]
private struct GdkRGBA
{
public float Red;
public float Green;
public float Blue;
public float Alpha;
}
// Async callback delegate
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate void GAsyncReadyCallback(IntPtr sourceObject, IntPtr result, IntPtr userData);
// Legacy GTK3 fallbacks
private const string LibGtk3 = "libgtk-3.so.0";
[DllImport(LibGtk3, EntryPoint = "gtk_init_check")]
private static extern bool gtk3_init_check(ref int argc, ref IntPtr argv);
[DllImport(LibGtk3, EntryPoint = "gtk_file_chooser_dialog_new")]
private static extern IntPtr gtk3_file_chooser_dialog_new(
[MarshalAs(UnmanagedType.LPStr)] string title,
IntPtr parent,
int action,
[MarshalAs(UnmanagedType.LPStr)] string firstButtonText,
int firstButtonResponse,
[MarshalAs(UnmanagedType.LPStr)] string secondButtonText,
int secondButtonResponse,
IntPtr terminator);
[DllImport(LibGtk3, EntryPoint = "gtk_dialog_run")]
private static extern int gtk3_dialog_run(IntPtr dialog);
[DllImport(LibGtk3, EntryPoint = "gtk_widget_destroy")]
private static extern void gtk3_widget_destroy(IntPtr widget);
[DllImport(LibGtk3, EntryPoint = "gtk_file_chooser_get_filename")]
private static extern IntPtr gtk3_file_chooser_get_filename(IntPtr chooser);
[DllImport(LibGtk3, EntryPoint = "gtk_file_chooser_get_filenames")]
private static extern IntPtr gtk3_file_chooser_get_filenames(IntPtr chooser);
[DllImport(LibGtk3, EntryPoint = "gtk_file_chooser_set_select_multiple")]
private static extern void gtk3_file_chooser_set_select_multiple(IntPtr chooser, bool selectMultiple);
[DllImport(LibGtk3, EntryPoint = "gtk_message_dialog_new")]
private static extern IntPtr gtk3_message_dialog_new(
IntPtr parent,
int flags,
int type,
int buttons,
[MarshalAs(UnmanagedType.LPStr)] string message);
[DllImport(LibGlib, EntryPoint = "g_slist_length")]
private static extern uint g_slist_length(IntPtr list);
[DllImport(LibGlib, EntryPoint = "g_slist_nth_data")]
private static extern IntPtr g_slist_nth_data(IntPtr list, uint n);
[DllImport(LibGlib, EntryPoint = "g_slist_free")]
private static extern void g_slist_free(IntPtr list);
#endregion
#region Fields
private bool _initialized;
private bool _useGtk4;
private bool _disposed;
private readonly object _lock = new();
// Store callbacks to prevent GC
private GAsyncReadyCallback? _currentCallback;
private TaskCompletionSource<FileDialogResult>? _fileDialogTcs;
private TaskCompletionSource<ColorDialogResult>? _colorDialogTcs;
private IntPtr _currentDialog;
#endregion
#region Properties
/// <summary>
/// Gets whether GTK is initialized.
/// </summary>
public bool IsInitialized => _initialized;
/// <summary>
/// Gets whether GTK4 is being used (vs GTK3 fallback).
/// </summary>
public bool IsGtk4 => _useGtk4;
#endregion
#region Initialization
/// <summary>
/// Initializes the GTK4 interop service.
/// Falls back to GTK3 if GTK4 is not available.
/// </summary>
public bool Initialize()
{
if (_initialized)
return true;
lock (_lock)
{
if (_initialized)
return true;
// Try GTK4 first
try
{
if (gtk_init_check())
{
_useGtk4 = true;
_initialized = true;
Console.WriteLine("[GTK4] Initialized GTK4");
return true;
}
}
catch (DllNotFoundException)
{
Console.WriteLine("[GTK4] GTK4 not found, trying GTK3");
}
catch (Exception ex)
{
Console.WriteLine($"[GTK4] GTK4 init failed: {ex.Message}");
}
// Fall back to GTK3
try
{
int argc = 0;
IntPtr argv = IntPtr.Zero;
if (gtk3_init_check(ref argc, ref argv))
{
_useGtk4 = false;
_initialized = true;
Console.WriteLine("[GTK4] Initialized GTK3 (fallback)");
return true;
}
}
catch (DllNotFoundException)
{
Console.WriteLine("[GTK4] GTK3 not found");
}
catch (Exception ex)
{
Console.WriteLine($"[GTK4] GTK3 init failed: {ex.Message}");
}
return false;
}
}
#endregion
#region Message Dialogs
/// <summary>
/// Shows an alert message dialog.
/// </summary>
public void ShowAlert(string title, string message, GtkMessageType type = GtkMessageType.Info)
{
if (!EnsureInitialized())
return;
if (_useGtk4)
{
var dialog = gtk_alert_dialog_new(title);
gtk_alert_dialog_set_detail(dialog, message);
string[] buttons = { "OK" };
gtk_alert_dialog_set_buttons(dialog, buttons);
gtk_alert_dialog_show(dialog, IntPtr.Zero);
g_object_unref(dialog);
}
else
{
var dialog = gtk3_message_dialog_new(
IntPtr.Zero,
1, // GTK_DIALOG_MODAL
(int)type,
(int)GtkButtonsType.Ok,
message);
gtk3_dialog_run(dialog);
gtk3_widget_destroy(dialog);
}
ProcessPendingEvents();
}
/// <summary>
/// Shows a confirmation dialog.
/// </summary>
public bool ShowConfirmation(string title, string message)
{
if (!EnsureInitialized())
return false;
if (_useGtk4)
{
// GTK4 async dialogs are more complex - use synchronous approach
var dialog = gtk_alert_dialog_new(title);
gtk_alert_dialog_set_detail(dialog, message);
string[] buttons = { "No", "Yes" };
gtk_alert_dialog_set_buttons(dialog, buttons);
gtk_alert_dialog_set_default_button(dialog, 1);
gtk_alert_dialog_set_cancel_button(dialog, 0);
gtk_alert_dialog_show(dialog, IntPtr.Zero);
g_object_unref(dialog);
// Note: GTK4 alert dialogs are async, this is simplified
return true;
}
else
{
var dialog = gtk3_message_dialog_new(
IntPtr.Zero,
1, // GTK_DIALOG_MODAL
(int)GtkMessageType.Question,
(int)GtkButtonsType.YesNo,
message);
int response = gtk3_dialog_run(dialog);
gtk3_widget_destroy(dialog);
ProcessPendingEvents();
return response == (int)GtkResponseType.Yes;
}
}
#endregion
#region File Dialogs
/// <summary>
/// Shows an open file dialog.
/// </summary>
public FileDialogResult ShowOpenFileDialog(
string title = "Open File",
string? initialFolder = null,
bool allowMultiple = false,
params (string Name, string Pattern)[] filters)
{
if (!EnsureInitialized())
return new FileDialogResult { Accepted = false };
if (_useGtk4)
{
return ShowGtk4FileDialog(title, GtkFileChooserAction.Open, allowMultiple, filters);
}
else
{
return ShowGtk3FileDialog(title, 0, allowMultiple, filters); // GTK_FILE_CHOOSER_ACTION_OPEN = 0
}
}
/// <summary>
/// Shows a save file dialog.
/// </summary>
public FileDialogResult ShowSaveFileDialog(
string title = "Save File",
string? suggestedName = null,
params (string Name, string Pattern)[] filters)
{
if (!EnsureInitialized())
return new FileDialogResult { Accepted = false };
if (_useGtk4)
{
return ShowGtk4FileDialog(title, GtkFileChooserAction.Save, false, filters);
}
else
{
return ShowGtk3FileDialog(title, 1, false, filters); // GTK_FILE_CHOOSER_ACTION_SAVE = 1
}
}
/// <summary>
/// Shows a folder picker dialog.
/// </summary>
public FileDialogResult ShowFolderDialog(string title = "Select Folder")
{
if (!EnsureInitialized())
return new FileDialogResult { Accepted = false };
if (_useGtk4)
{
return ShowGtk4FileDialog(title, GtkFileChooserAction.SelectFolder, false, Array.Empty<(string, string)>());
}
else
{
return ShowGtk3FileDialog(title, 2, false, Array.Empty<(string, string)>()); // GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER = 2
}
}
private FileDialogResult ShowGtk4FileDialog(
string title,
GtkFileChooserAction action,
bool allowMultiple,
(string Name, string Pattern)[] filters)
{
var dialog = gtk_file_dialog_new();
gtk_file_dialog_set_title(dialog, title);
gtk_file_dialog_set_modal(dialog, true);
// Set up filters
if (filters.Length > 0)
{
var filter = gtk_file_filter_new();
gtk_file_filter_set_name(filter, filters[0].Name);
gtk_file_filter_add_pattern(filter, filters[0].Pattern);
gtk_file_dialog_set_default_filter(dialog, filter);
}
// For GTK4, we need async handling - simplified synchronous version
// In a full implementation, this would use proper async/await
_fileDialogTcs = new TaskCompletionSource<FileDialogResult>();
_currentDialog = dialog;
_currentCallback = (source, result, userData) =>
{
IntPtr error = IntPtr.Zero;
IntPtr file = IntPtr.Zero;
try
{
if (action == GtkFileChooserAction.Open && !allowMultiple)
file = gtk_file_dialog_open_finish(dialog, result, out error);
else if (action == GtkFileChooserAction.Save)
file = gtk_file_dialog_save_finish(dialog, result, out error);
else if (action == GtkFileChooserAction.SelectFolder)
file = gtk_file_dialog_select_folder_finish(dialog, result, out error);
if (file != IntPtr.Zero && error == IntPtr.Zero)
{
IntPtr pathPtr = g_file_get_path(file);
string path = Marshal.PtrToStringUTF8(pathPtr) ?? "";
g_free(pathPtr);
g_object_unref(file);
_fileDialogTcs?.TrySetResult(new FileDialogResult
{
Accepted = true,
SelectedFiles = new[] { path }
});
}
else
{
_fileDialogTcs?.TrySetResult(new FileDialogResult { Accepted = false });
}
}
catch
{
_fileDialogTcs?.TrySetResult(new FileDialogResult { Accepted = false });
}
};
// Start the dialog
if (action == GtkFileChooserAction.Open && !allowMultiple)
gtk_file_dialog_open(dialog, IntPtr.Zero, IntPtr.Zero, _currentCallback, IntPtr.Zero);
else if (action == GtkFileChooserAction.Open && allowMultiple)
gtk_file_dialog_open_multiple(dialog, IntPtr.Zero, IntPtr.Zero, _currentCallback, IntPtr.Zero);
else if (action == GtkFileChooserAction.Save)
gtk_file_dialog_save(dialog, IntPtr.Zero, IntPtr.Zero, _currentCallback, IntPtr.Zero);
else if (action == GtkFileChooserAction.SelectFolder)
gtk_file_dialog_select_folder(dialog, IntPtr.Zero, IntPtr.Zero, _currentCallback, IntPtr.Zero);
// Process events until dialog completes
while (!_fileDialogTcs.Task.IsCompleted)
{
ProcessPendingEvents();
Thread.Sleep(10);
}
g_object_unref(dialog);
return _fileDialogTcs.Task.Result;
}
private FileDialogResult ShowGtk3FileDialog(
string title,
int action,
bool allowMultiple,
(string Name, string Pattern)[] filters)
{
var dialog = gtk3_file_chooser_dialog_new(
title,
IntPtr.Zero,
action,
"_Cancel", (int)GtkResponseType.Cancel,
action == 1 ? "_Save" : "_Open", (int)GtkResponseType.Accept,
IntPtr.Zero);
if (allowMultiple)
gtk3_file_chooser_set_select_multiple(dialog, true);
int response = gtk3_dialog_run(dialog);
var result = new FileDialogResult { Accepted = false };
if (response == (int)GtkResponseType.Accept)
{
if (allowMultiple)
{
IntPtr list = gtk3_file_chooser_get_filenames(dialog);
uint count = g_slist_length(list);
var files = new List<string>();
for (uint i = 0; i < count; i++)
{
IntPtr pathPtr = g_slist_nth_data(list, i);
string? path = Marshal.PtrToStringUTF8(pathPtr);
if (!string.IsNullOrEmpty(path))
{
files.Add(path);
g_free(pathPtr);
}
}
g_slist_free(list);
result = new FileDialogResult { Accepted = true, SelectedFiles = files.ToArray() };
}
else
{
IntPtr pathPtr = gtk3_file_chooser_get_filename(dialog);
string? path = Marshal.PtrToStringUTF8(pathPtr);
g_free(pathPtr);
if (!string.IsNullOrEmpty(path))
result = new FileDialogResult { Accepted = true, SelectedFiles = new[] { path } };
}
}
gtk3_widget_destroy(dialog);
ProcessPendingEvents();
return result;
}
#endregion
#region Color Dialog
/// <summary>
/// Shows a color picker dialog.
/// </summary>
public ColorDialogResult ShowColorDialog(
string title = "Choose Color",
float initialRed = 1f,
float initialGreen = 1f,
float initialBlue = 1f,
float initialAlpha = 1f,
bool withAlpha = true)
{
if (!EnsureInitialized())
return new ColorDialogResult { Accepted = false };
if (_useGtk4)
{
return ShowGtk4ColorDialog(title, initialRed, initialGreen, initialBlue, initialAlpha, withAlpha);
}
else
{
// GTK3 color dialog would go here
return new ColorDialogResult { Accepted = false };
}
}
private ColorDialogResult ShowGtk4ColorDialog(
string title,
float r, float g, float b, float a,
bool withAlpha)
{
var dialog = gtk_color_dialog_new();
gtk_color_dialog_set_title(dialog, title);
gtk_color_dialog_set_modal(dialog, true);
gtk_color_dialog_set_with_alpha(dialog, withAlpha);
_colorDialogTcs = new TaskCompletionSource<ColorDialogResult>();
_currentCallback = (source, result, userData) =>
{
IntPtr error = IntPtr.Zero;
try
{
IntPtr rgbaPtr = gtk_color_dialog_choose_rgba_finish(dialog, result, out error);
if (rgbaPtr != IntPtr.Zero && error == IntPtr.Zero)
{
var rgba = Marshal.PtrToStructure<GdkRGBA>(rgbaPtr);
_colorDialogTcs?.TrySetResult(new ColorDialogResult
{
Accepted = true,
Red = rgba.Red,
Green = rgba.Green,
Blue = rgba.Blue,
Alpha = rgba.Alpha
});
}
else
{
_colorDialogTcs?.TrySetResult(new ColorDialogResult { Accepted = false });
}
}
catch
{
_colorDialogTcs?.TrySetResult(new ColorDialogResult { Accepted = false });
}
};
gtk_color_dialog_choose_rgba(dialog, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, _currentCallback, IntPtr.Zero);
while (!_colorDialogTcs.Task.IsCompleted)
{
ProcessPendingEvents();
Thread.Sleep(10);
}
g_object_unref(dialog);
return _colorDialogTcs.Task.Result;
}
#endregion
#region Helpers
private bool EnsureInitialized()
{
if (!_initialized)
Initialize();
return _initialized;
}
private void ProcessPendingEvents()
{
var context = g_main_context_default();
while (g_main_context_iteration(context, false)) { }
}
#endregion
#region IDisposable
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
_initialized = false;
GC.SuppressFinalize(this);
}
~Gtk4InteropService()
{
Dispose();
}
#endregion
}

View File

@@ -0,0 +1,722 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Runtime.InteropServices;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Services;
/// <summary>
/// Supported hardware video acceleration APIs.
/// </summary>
public enum VideoAccelerationApi
{
/// <summary>
/// Automatically select the best available API.
/// </summary>
Auto,
/// <summary>
/// VA-API (Video Acceleration API) - Intel, AMD, and some NVIDIA.
/// </summary>
VaApi,
/// <summary>
/// VDPAU (Video Decode and Presentation API for Unix) - NVIDIA.
/// </summary>
Vdpau,
/// <summary>
/// Software decoding fallback.
/// </summary>
Software
}
/// <summary>
/// Video codec profiles supported by hardware acceleration.
/// </summary>
public enum VideoProfile
{
H264Baseline,
H264Main,
H264High,
H265Main,
H265Main10,
Vp8,
Vp9Profile0,
Vp9Profile2,
Av1Main
}
/// <summary>
/// Information about a decoded video frame.
/// </summary>
public class VideoFrame : IDisposable
{
public int Width { get; init; }
public int Height { get; init; }
public IntPtr DataY { get; init; }
public IntPtr DataU { get; init; }
public IntPtr DataV { get; init; }
public int StrideY { get; init; }
public int StrideU { get; init; }
public int StrideV { get; init; }
public long Timestamp { get; init; }
public bool IsKeyFrame { get; init; }
private bool _disposed;
private Action? _releaseCallback;
internal void SetReleaseCallback(Action callback) => _releaseCallback = callback;
public void Dispose()
{
if (!_disposed)
{
_releaseCallback?.Invoke();
_disposed = true;
}
}
}
/// <summary>
/// Hardware-accelerated video decoding service using VA-API or VDPAU.
/// Provides efficient video decode for media playback on Linux.
/// </summary>
public class HardwareVideoService : IDisposable
{
#region VA-API Native Interop
private const string LibVa = "libva.so.2";
private const string LibVaDrm = "libva-drm.so.2";
private const string LibVaX11 = "libva-x11.so.2";
// VA-API error codes
private const int VA_STATUS_SUCCESS = 0;
// VA-API profile constants
private const int VAProfileH264Baseline = 5;
private const int VAProfileH264Main = 6;
private const int VAProfileH264High = 7;
private const int VAProfileHEVCMain = 12;
private const int VAProfileHEVCMain10 = 13;
private const int VAProfileVP8Version0_3 = 14;
private const int VAProfileVP9Profile0 = 15;
private const int VAProfileVP9Profile2 = 17;
private const int VAProfileAV1Profile0 = 20;
// VA-API entrypoint
private const int VAEntrypointVLD = 1; // Video Decode
// Surface formats
private const uint VA_RT_FORMAT_YUV420 = 0x00000001;
private const uint VA_RT_FORMAT_YUV420_10 = 0x00000100;
[DllImport(LibVa)]
private static extern IntPtr vaGetDisplayDRM(int fd);
[DllImport(LibVaX11)]
private static extern IntPtr vaGetDisplay(IntPtr x11Display);
[DllImport(LibVa)]
private static extern int vaInitialize(IntPtr display, out int majorVersion, out int minorVersion);
[DllImport(LibVa)]
private static extern int vaTerminate(IntPtr display);
[DllImport(LibVa)]
private static extern IntPtr vaErrorStr(int errorCode);
[DllImport(LibVa)]
private static extern int vaQueryConfigProfiles(IntPtr display, [Out] int[] profileList, out int numProfiles);
[DllImport(LibVa)]
private static extern int vaQueryConfigEntrypoints(IntPtr display, int profile, [Out] int[] entrypoints, out int numEntrypoints);
[DllImport(LibVa)]
private static extern int vaCreateConfig(IntPtr display, int profile, int entrypoint, IntPtr attribList, int numAttribs, out uint configId);
[DllImport(LibVa)]
private static extern int vaDestroyConfig(IntPtr display, uint configId);
[DllImport(LibVa)]
private static extern int vaCreateContext(IntPtr display, uint configId, int pictureWidth, int pictureHeight, int flag, IntPtr renderTargets, int numRenderTargets, out uint contextId);
[DllImport(LibVa)]
private static extern int vaDestroyContext(IntPtr display, uint contextId);
[DllImport(LibVa)]
private static extern int vaCreateSurfaces(IntPtr display, uint format, uint width, uint height, [Out] uint[] surfaces, uint numSurfaces, IntPtr attribList, uint numAttribs);
[DllImport(LibVa)]
private static extern int vaDestroySurfaces(IntPtr display, [In] uint[] surfaces, int numSurfaces);
[DllImport(LibVa)]
private static extern int vaSyncSurface(IntPtr display, uint surfaceId);
[DllImport(LibVa)]
private static extern int vaMapBuffer(IntPtr display, uint bufferId, out IntPtr data);
[DllImport(LibVa)]
private static extern int vaUnmapBuffer(IntPtr display, uint bufferId);
[DllImport(LibVa)]
private static extern int vaDeriveImage(IntPtr display, uint surfaceId, out VaImage image);
[DllImport(LibVa)]
private static extern int vaDestroyImage(IntPtr display, uint imageId);
[StructLayout(LayoutKind.Sequential)]
private struct VaImage
{
public uint ImageId;
public uint Format; // VAImageFormat (simplified)
public uint FormatFourCC;
public int Width;
public int Height;
public uint DataSize;
public uint NumPlanes;
public uint PitchesPlane0;
public uint PitchesPlane1;
public uint PitchesPlane2;
public uint PitchesPlane3;
public uint OffsetsPlane0;
public uint OffsetsPlane1;
public uint OffsetsPlane2;
public uint OffsetsPlane3;
public uint BufferId;
}
#endregion
#region VDPAU Native Interop
private const string LibVdpau = "libvdpau.so.1";
[DllImport(LibVdpau)]
private static extern int vdp_device_create_x11(IntPtr display, int screen, out IntPtr device, out IntPtr getProcAddress);
#endregion
#region DRM Interop
[DllImport("libc", EntryPoint = "open")]
private static extern int open([MarshalAs(UnmanagedType.LPStr)] string path, int flags);
[DllImport("libc", EntryPoint = "close")]
private static extern int close(int fd);
private const int O_RDWR = 2;
#endregion
#region Fields
private IntPtr _vaDisplay;
private uint _vaConfigId;
private uint _vaContextId;
private uint[] _vaSurfaces = Array.Empty<uint>();
private int _drmFd = -1;
private bool _initialized;
private bool _disposed;
private VideoAccelerationApi _currentApi = VideoAccelerationApi.Software;
private int _width;
private int _height;
private VideoProfile _profile;
private readonly HashSet<VideoProfile> _supportedProfiles = new();
private readonly object _lock = new();
#endregion
#region Properties
/// <summary>
/// Gets the currently active video acceleration API.
/// </summary>
public VideoAccelerationApi CurrentApi => _currentApi;
/// <summary>
/// Gets whether hardware acceleration is available and initialized.
/// </summary>
public bool IsHardwareAccelerated => _currentApi != VideoAccelerationApi.Software && _initialized;
/// <summary>
/// Gets the supported video profiles.
/// </summary>
public IReadOnlySet<VideoProfile> SupportedProfiles => _supportedProfiles;
#endregion
#region Initialization
/// <summary>
/// Creates a new hardware video service.
/// </summary>
public HardwareVideoService()
{
}
/// <summary>
/// Initializes the hardware video acceleration.
/// </summary>
/// <param name="api">The preferred API to use.</param>
/// <param name="x11Display">Optional X11 display for VA-API X11 backend.</param>
/// <returns>True if initialization succeeded.</returns>
public bool Initialize(VideoAccelerationApi api = VideoAccelerationApi.Auto, IntPtr x11Display = default)
{
if (_initialized)
return true;
lock (_lock)
{
if (_initialized)
return true;
// Try VA-API first (works with Intel, AMD, and some NVIDIA)
if (api == VideoAccelerationApi.Auto || api == VideoAccelerationApi.VaApi)
{
if (TryInitializeVaApi(x11Display))
{
_currentApi = VideoAccelerationApi.VaApi;
_initialized = true;
Console.WriteLine($"[HardwareVideo] Initialized VA-API with {_supportedProfiles.Count} supported profiles");
return true;
}
}
// Try VDPAU (NVIDIA proprietary)
if (api == VideoAccelerationApi.Auto || api == VideoAccelerationApi.Vdpau)
{
if (TryInitializeVdpau(x11Display))
{
_currentApi = VideoAccelerationApi.Vdpau;
_initialized = true;
Console.WriteLine("[HardwareVideo] Initialized VDPAU");
return true;
}
}
Console.WriteLine("[HardwareVideo] No hardware acceleration available, using software");
_currentApi = VideoAccelerationApi.Software;
return false;
}
}
private bool TryInitializeVaApi(IntPtr x11Display)
{
try
{
// Try DRM backend first (works in Wayland and headless)
string[] drmDevices = { "/dev/dri/renderD128", "/dev/dri/renderD129", "/dev/dri/card0" };
foreach (var device in drmDevices)
{
_drmFd = open(device, O_RDWR);
if (_drmFd >= 0)
{
_vaDisplay = vaGetDisplayDRM(_drmFd);
if (_vaDisplay != IntPtr.Zero)
{
if (InitializeVaDisplay())
return true;
}
close(_drmFd);
_drmFd = -1;
}
}
// Fall back to X11 backend if display provided
if (x11Display != IntPtr.Zero)
{
_vaDisplay = vaGetDisplay(x11Display);
if (_vaDisplay != IntPtr.Zero && InitializeVaDisplay())
return true;
}
return false;
}
catch (DllNotFoundException)
{
Console.WriteLine("[HardwareVideo] VA-API libraries not found");
return false;
}
catch (Exception ex)
{
Console.WriteLine($"[HardwareVideo] VA-API initialization failed: {ex.Message}");
return false;
}
}
private bool InitializeVaDisplay()
{
int status = vaInitialize(_vaDisplay, out int major, out int minor);
if (status != VA_STATUS_SUCCESS)
{
Console.WriteLine($"[HardwareVideo] vaInitialize failed: {GetVaError(status)}");
return false;
}
Console.WriteLine($"[HardwareVideo] VA-API {major}.{minor} initialized");
// Query supported profiles
int[] profiles = new int[32];
status = vaQueryConfigProfiles(_vaDisplay, profiles, out int numProfiles);
if (status == VA_STATUS_SUCCESS)
{
for (int i = 0; i < numProfiles; i++)
{
if (TryMapVaProfile(profiles[i], out var videoProfile))
{
// Check if VLD (decode) entrypoint is supported
int[] entrypoints = new int[8];
if (vaQueryConfigEntrypoints(_vaDisplay, profiles[i], entrypoints, out int numEntrypoints) == VA_STATUS_SUCCESS)
{
for (int j = 0; j < numEntrypoints; j++)
{
if (entrypoints[j] == VAEntrypointVLD)
{
_supportedProfiles.Add(videoProfile);
break;
}
}
}
}
}
}
return true;
}
private bool TryInitializeVdpau(IntPtr x11Display)
{
if (x11Display == IntPtr.Zero)
return false;
try
{
int result = vdp_device_create_x11(x11Display, 0, out IntPtr device, out IntPtr getProcAddress);
if (result == 0 && device != IntPtr.Zero)
{
// VDPAU initialized - would need additional setup for actual use
// For now, just mark as available
_supportedProfiles.Add(VideoProfile.H264Baseline);
_supportedProfiles.Add(VideoProfile.H264Main);
_supportedProfiles.Add(VideoProfile.H264High);
return true;
}
}
catch (DllNotFoundException)
{
Console.WriteLine("[HardwareVideo] VDPAU libraries not found");
}
catch (Exception ex)
{
Console.WriteLine($"[HardwareVideo] VDPAU initialization failed: {ex.Message}");
}
return false;
}
#endregion
#region Decoder Creation
/// <summary>
/// Creates a decoder context for the specified profile and dimensions.
/// </summary>
public bool CreateDecoder(VideoProfile profile, int width, int height)
{
if (!_initialized || _currentApi == VideoAccelerationApi.Software)
return false;
if (!_supportedProfiles.Contains(profile))
{
Console.WriteLine($"[HardwareVideo] Profile {profile} not supported");
return false;
}
lock (_lock)
{
// Destroy existing context
DestroyDecoder();
_width = width;
_height = height;
_profile = profile;
if (_currentApi == VideoAccelerationApi.VaApi)
return CreateVaApiDecoder(profile, width, height);
return false;
}
}
private bool CreateVaApiDecoder(VideoProfile profile, int width, int height)
{
int vaProfile = MapToVaProfile(profile);
// Create config
int status = vaCreateConfig(_vaDisplay, vaProfile, VAEntrypointVLD, IntPtr.Zero, 0, out _vaConfigId);
if (status != VA_STATUS_SUCCESS)
{
Console.WriteLine($"[HardwareVideo] vaCreateConfig failed: {GetVaError(status)}");
return false;
}
// Create surfaces for decoded frames (use a pool of 8)
uint format = profile == VideoProfile.H265Main10 || profile == VideoProfile.Vp9Profile2
? VA_RT_FORMAT_YUV420_10
: VA_RT_FORMAT_YUV420;
_vaSurfaces = new uint[8];
status = vaCreateSurfaces(_vaDisplay, format, (uint)width, (uint)height, _vaSurfaces, 8, IntPtr.Zero, 0);
if (status != VA_STATUS_SUCCESS)
{
Console.WriteLine($"[HardwareVideo] vaCreateSurfaces failed: {GetVaError(status)}");
vaDestroyConfig(_vaDisplay, _vaConfigId);
return false;
}
// Create context
status = vaCreateContext(_vaDisplay, _vaConfigId, width, height, 0, IntPtr.Zero, 0, out _vaContextId);
if (status != VA_STATUS_SUCCESS)
{
Console.WriteLine($"[HardwareVideo] vaCreateContext failed: {GetVaError(status)}");
vaDestroySurfaces(_vaDisplay, _vaSurfaces, _vaSurfaces.Length);
vaDestroyConfig(_vaDisplay, _vaConfigId);
return false;
}
Console.WriteLine($"[HardwareVideo] Created decoder: {profile} {width}x{height}");
return true;
}
/// <summary>
/// Destroys the current decoder context.
/// </summary>
public void DestroyDecoder()
{
lock (_lock)
{
if (_currentApi == VideoAccelerationApi.VaApi && _vaDisplay != IntPtr.Zero)
{
if (_vaContextId != 0)
{
vaDestroyContext(_vaDisplay, _vaContextId);
_vaContextId = 0;
}
if (_vaSurfaces.Length > 0)
{
vaDestroySurfaces(_vaDisplay, _vaSurfaces, _vaSurfaces.Length);
_vaSurfaces = Array.Empty<uint>();
}
if (_vaConfigId != 0)
{
vaDestroyConfig(_vaDisplay, _vaConfigId);
_vaConfigId = 0;
}
}
}
}
#endregion
#region Frame Retrieval
/// <summary>
/// Retrieves a decoded frame from the specified surface.
/// </summary>
public VideoFrame? GetDecodedFrame(int surfaceIndex, long timestamp, bool isKeyFrame)
{
if (!_initialized || _currentApi != VideoAccelerationApi.VaApi)
return null;
if (surfaceIndex < 0 || surfaceIndex >= _vaSurfaces.Length)
return null;
uint surfaceId = _vaSurfaces[surfaceIndex];
// Wait for decode to complete
int status = vaSyncSurface(_vaDisplay, surfaceId);
if (status != VA_STATUS_SUCCESS)
return null;
// Derive image from surface
status = vaDeriveImage(_vaDisplay, surfaceId, out VaImage image);
if (status != VA_STATUS_SUCCESS)
return null;
// Map the buffer
status = vaMapBuffer(_vaDisplay, image.BufferId, out IntPtr data);
if (status != VA_STATUS_SUCCESS)
{
vaDestroyImage(_vaDisplay, image.ImageId);
return null;
}
var frame = new VideoFrame
{
Width = image.Width,
Height = image.Height,
DataY = data + (int)image.OffsetsPlane0,
DataU = data + (int)image.OffsetsPlane1,
DataV = data + (int)image.OffsetsPlane2,
StrideY = (int)image.PitchesPlane0,
StrideU = (int)image.PitchesPlane1,
StrideV = (int)image.PitchesPlane2,
Timestamp = timestamp,
IsKeyFrame = isKeyFrame
};
// Set cleanup callback
frame.SetReleaseCallback(() =>
{
vaUnmapBuffer(_vaDisplay, image.BufferId);
vaDestroyImage(_vaDisplay, image.ImageId);
});
return frame;
}
/// <summary>
/// Converts a decoded frame to an SKBitmap for display.
/// </summary>
public SKBitmap? ConvertFrameToSkia(VideoFrame frame)
{
if (frame == null)
return null;
// Create BGRA bitmap
var bitmap = new SKBitmap(frame.Width, frame.Height, SKColorType.Bgra8888, SKAlphaType.Opaque);
// Convert YUV to BGRA
unsafe
{
byte* yPtr = (byte*)frame.DataY;
byte* uPtr = (byte*)frame.DataU;
byte* vPtr = (byte*)frame.DataV;
byte* dst = (byte*)bitmap.GetPixels();
for (int y = 0; y < frame.Height; y++)
{
for (int x = 0; x < frame.Width; x++)
{
int yIndex = y * frame.StrideY + x;
int uvIndex = (y / 2) * frame.StrideU + (x / 2);
int yVal = yPtr[yIndex];
int uVal = uPtr[uvIndex] - 128;
int vVal = vPtr[uvIndex] - 128;
// YUV to RGB conversion
int r = (int)(yVal + 1.402 * vVal);
int g = (int)(yVal - 0.344 * uVal - 0.714 * vVal);
int b = (int)(yVal + 1.772 * uVal);
r = Math.Clamp(r, 0, 255);
g = Math.Clamp(g, 0, 255);
b = Math.Clamp(b, 0, 255);
int dstIndex = (y * frame.Width + x) * 4;
dst[dstIndex] = (byte)b;
dst[dstIndex + 1] = (byte)g;
dst[dstIndex + 2] = (byte)r;
dst[dstIndex + 3] = 255;
}
}
}
return bitmap;
}
#endregion
#region Helpers
private static bool TryMapVaProfile(int vaProfile, out VideoProfile profile)
{
profile = vaProfile switch
{
VAProfileH264Baseline => VideoProfile.H264Baseline,
VAProfileH264Main => VideoProfile.H264Main,
VAProfileH264High => VideoProfile.H264High,
VAProfileHEVCMain => VideoProfile.H265Main,
VAProfileHEVCMain10 => VideoProfile.H265Main10,
VAProfileVP8Version0_3 => VideoProfile.Vp8,
VAProfileVP9Profile0 => VideoProfile.Vp9Profile0,
VAProfileVP9Profile2 => VideoProfile.Vp9Profile2,
VAProfileAV1Profile0 => VideoProfile.Av1Main,
_ => VideoProfile.H264Main
};
return vaProfile >= VAProfileH264Baseline && vaProfile <= VAProfileAV1Profile0;
}
private static int MapToVaProfile(VideoProfile profile)
{
return profile switch
{
VideoProfile.H264Baseline => VAProfileH264Baseline,
VideoProfile.H264Main => VAProfileH264Main,
VideoProfile.H264High => VAProfileH264High,
VideoProfile.H265Main => VAProfileHEVCMain,
VideoProfile.H265Main10 => VAProfileHEVCMain10,
VideoProfile.Vp8 => VAProfileVP8Version0_3,
VideoProfile.Vp9Profile0 => VAProfileVP9Profile0,
VideoProfile.Vp9Profile2 => VAProfileVP9Profile2,
VideoProfile.Av1Main => VAProfileAV1Profile0,
_ => VAProfileH264Main
};
}
private static string GetVaError(int status)
{
try
{
IntPtr errPtr = vaErrorStr(status);
return Marshal.PtrToStringAnsi(errPtr) ?? $"Unknown error {status}";
}
catch
{
return $"Error code {status}";
}
}
#endregion
#region IDisposable
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
DestroyDecoder();
if (_currentApi == VideoAccelerationApi.VaApi && _vaDisplay != IntPtr.Zero)
{
vaTerminate(_vaDisplay);
_vaDisplay = IntPtr.Zero;
}
if (_drmFd >= 0)
{
close(_drmFd);
_drmFd = -1;
}
GC.SuppressFinalize(this);
}
~HardwareVideoService()
{
Dispose();
}
#endregion
}

View File

@@ -0,0 +1,53 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Internals;
[assembly: Dependency(typeof(Microsoft.Maui.Platform.Linux.Services.LinuxResourcesProvider))]
namespace Microsoft.Maui.Platform.Linux.Services;
/// <summary>
/// Provides system resources for the Linux platform.
/// </summary>
internal sealed class LinuxResourcesProvider : ISystemResourcesProvider
{
private ResourceDictionary? _dictionary;
public IResourceDictionary GetSystemResources()
{
_dictionary ??= CreateResourceDictionary();
return _dictionary;
}
private ResourceDictionary CreateResourceDictionary()
{
var dictionary = new ResourceDictionary();
// Add default styles
dictionary[Device.Styles.BodyStyleKey] = new Style(typeof(Label));
dictionary[Device.Styles.TitleStyleKey] = CreateTitleStyle();
dictionary[Device.Styles.SubtitleStyleKey] = CreateSubtitleStyle();
dictionary[Device.Styles.CaptionStyleKey] = CreateCaptionStyle();
dictionary[Device.Styles.ListItemTextStyleKey] = new Style(typeof(Label));
dictionary[Device.Styles.ListItemDetailTextStyleKey] = CreateCaptionStyle();
return dictionary;
}
private static Style CreateTitleStyle() => new(typeof(Label))
{
Setters = { new Setter { Property = Label.FontSizeProperty, Value = 24.0 } }
};
private static Style CreateSubtitleStyle() => new(typeof(Label))
{
Setters = { new Setter { Property = Label.FontSizeProperty, Value = 18.0 } }
};
private static Style CreateCaptionStyle() => new(typeof(Label))
{
Setters = { new Setter { Property = Label.FontSizeProperty, Value = 12.0 } }
};
}

248
Services/SystemClipboard.cs Normal file
View File

@@ -0,0 +1,248 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
namespace Microsoft.Maui.Platform;
/// <summary>
/// Static helper for system clipboard access using xclip/xsel.
/// Provides synchronous access for use in UI event handlers.
/// </summary>
public static class SystemClipboard
{
/// <summary>
/// Gets text from the system clipboard.
/// </summary>
public static string? GetText()
{
// Try xclip first
var result = TryGetWithXclip();
if (result != null) return result;
// Try xsel as fallback
result = TryGetWithXsel();
if (result != null) return result;
// Try wl-paste for Wayland
return TryGetWithWlPaste();
}
/// <summary>
/// Sets text to the system clipboard.
/// </summary>
public static void SetText(string? text)
{
if (string.IsNullOrEmpty(text))
{
ClearClipboard();
return;
}
// Try xclip first
if (TrySetWithXclip(text)) return;
// Try xsel as fallback
if (TrySetWithXsel(text)) return;
// Try wl-copy for Wayland
TrySetWithWlCopy(text);
}
private static string? TryGetWithXclip()
{
try
{
var startInfo = new ProcessStartInfo
{
FileName = "xclip",
Arguments = "-selection clipboard -o",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var process = Process.Start(startInfo);
if (process == null) return null;
var output = process.StandardOutput.ReadToEnd();
process.WaitForExit(1000);
return process.ExitCode == 0 ? output : null;
}
catch
{
return null;
}
}
private static string? TryGetWithXsel()
{
try
{
var startInfo = new ProcessStartInfo
{
FileName = "xsel",
Arguments = "--clipboard --output",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var process = Process.Start(startInfo);
if (process == null) return null;
var output = process.StandardOutput.ReadToEnd();
process.WaitForExit(1000);
return process.ExitCode == 0 ? output : null;
}
catch
{
return null;
}
}
private static string? TryGetWithWlPaste()
{
try
{
var startInfo = new ProcessStartInfo
{
FileName = "wl-paste",
Arguments = "--no-newline",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var process = Process.Start(startInfo);
if (process == null) return null;
var output = process.StandardOutput.ReadToEnd();
process.WaitForExit(1000);
return process.ExitCode == 0 ? output : null;
}
catch
{
return null;
}
}
private static bool TrySetWithXclip(string text)
{
try
{
var startInfo = new ProcessStartInfo
{
FileName = "xclip",
Arguments = "-selection clipboard",
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var process = Process.Start(startInfo);
if (process == null) return false;
process.StandardInput.Write(text);
process.StandardInput.Close();
process.WaitForExit(1000);
return process.ExitCode == 0;
}
catch
{
return false;
}
}
private static bool TrySetWithXsel(string text)
{
try
{
var startInfo = new ProcessStartInfo
{
FileName = "xsel",
Arguments = "--clipboard --input",
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var process = Process.Start(startInfo);
if (process == null) return false;
process.StandardInput.Write(text);
process.StandardInput.Close();
process.WaitForExit(1000);
return process.ExitCode == 0;
}
catch
{
return false;
}
}
private static bool TrySetWithWlCopy(string text)
{
try
{
var startInfo = new ProcessStartInfo
{
FileName = "wl-copy",
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var process = Process.Start(startInfo);
if (process == null) return false;
process.StandardInput.Write(text);
process.StandardInput.Close();
process.WaitForExit(1000);
return process.ExitCode == 0;
}
catch
{
return false;
}
}
private static void ClearClipboard()
{
try
{
// Try xclip
var startInfo = new ProcessStartInfo
{
FileName = "xclip",
Arguments = "-selection clipboard",
UseShellExecute = false,
RedirectStandardInput = true,
CreateNoWindow = true
};
using var process = Process.Start(startInfo);
if (process != null)
{
process.StandardInput.Close();
process.WaitForExit(1000);
}
}
catch
{
// Ignore errors when clearing
}
}
}