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:
@@ -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();
|
||||
}
|
||||
|
||||
821
Services/Gtk4InteropService.cs
Normal file
821
Services/Gtk4InteropService.cs
Normal 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
|
||||
}
|
||||
722
Services/HardwareVideoService.cs
Normal file
722
Services/HardwareVideoService.cs
Normal 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
|
||||
}
|
||||
53
Services/LinuxResourcesProvider.cs
Normal file
53
Services/LinuxResourcesProvider.cs
Normal 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
248
Services/SystemClipboard.cs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user