diff --git a/host/Com/AvnGuids.cs b/host/Com/AvnGuids.cs
index 52aa969..492e782 100644
--- a/host/Com/AvnGuids.cs
+++ b/host/Com/AvnGuids.cs
@@ -37,4 +37,13 @@ internal static class AvnGuids
// window-level settings a capture tool needs. Another separately versioned
// capability; nothing below is added to a published vtable.
public const string IAvnApplication6 = "6B2E8F10-4C91-4E3A-9A77-1F0C2B3A4D80";
+
+ // Stage 34 Rust-drawn surfaces: bitmap layers Rust fills and the raw
+ // pointer stream. Another separately versioned capability; nothing below is
+ // added to a published vtable.
+ public const string IAvnApplication7 = "6B2E8F10-4C91-4E3A-9A77-1F0C2B3A4D90";
+ public const string IAvnPixelSurface = "6B2E8F10-4C91-4E3A-9A77-1F0C2B3A4D91";
+ public const string IAvnPixelSurfaceHandler = "6B2E8F10-4C91-4E3A-9A77-1F0C2B3A4D92";
+ public const string IAvnShellTray = "6B2E8F10-4C91-4E3A-9A77-1F0C2B3A4D93";
+ public const string IAvnShellTrayHandler = "6B2E8F10-4C91-4E3A-9A77-1F0C2B3A4D94";
}
diff --git a/host/Com/AvnPixelSurface.cs b/host/Com/AvnPixelSurface.cs
new file mode 100644
index 0000000..6a626e2
--- /dev/null
+++ b/host/Com/AvnPixelSurface.cs
@@ -0,0 +1,367 @@
+using System;
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.Marshalling;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Media;
+using Avalonia.Media.Imaging;
+using Avalonia.Platform;
+using Avalonia.Threading;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Host.Com;
+
+/// Stage 34: creates Rust-drawn pixel surfaces.
+public partial class AvnApplication : IAvnApplication7
+{
+ public int CreatePixelSurface(IAvnPixelSurfaceHandler? handler, out IAvnPixelSurface? surface)
+ {
+ surface = null;
+ try
+ {
+ Dispatcher.UIThread.VerifyAccess();
+ surface = new AvnPixelSurface(new RustPixelSurface { Handler = handler });
+ return HResults.S_OK;
+ }
+ catch (Exception e)
+ {
+ return AbiError.Capture(e);
+ }
+ }
+}
+
+/// The COM face of a .
+[GeneratedComClass]
+internal sealed partial class AvnPixelSurface(RustPixelSurface control) : IAvnPixelSurface
+{
+ public const int MaxLayers = 8;
+
+ public int GetControl(out IAvnControl? value)
+ {
+ IAvnControl? found = null;
+ var hr = Run(() => found = (IAvnControl?)ProjectionRuntime.Wrap(control));
+ value = found;
+ return hr;
+ }
+
+ public int SetLayer(int layer, int width, int height, int format)
+ {
+ if (layer is < 0 or >= MaxLayers || width < 0 || height < 0 || format is < 0 or > 1)
+ return HResults.E_INVALIDARG;
+ return Run(() => control.SetLayer(layer, width, height, format == 0 ? PixelFormat.Rgba8888 : PixelFormat.Bgra8888));
+ }
+
+ public int WriteLayer(int layer, int x, int y, int width, int height, nint pixels, int stride)
+ {
+ if (layer is < 0 or >= MaxLayers || width < 0 || height < 0 || stride < width * 4)
+ return HResults.E_INVALIDARG;
+ if (pixels == 0 && width > 0 && height > 0)
+ return HResults.E_POINTER;
+ return Run(() => control.WriteLayer(layer, x, y, width, height, pixels, stride));
+ }
+
+ public int PlaceLayer(int layer, double x, double y, double width, double height, int fill, int interpolation, double opacity)
+ {
+ if (layer is < 0 or >= MaxLayers || interpolation is < 0 or > 4)
+ return HResults.E_INVALIDARG;
+ return Run(() => control.PlaceLayer(layer, new Rect(x, y, Math.Max(0, width), Math.Max(0, height)), fill != 0,
+ (BitmapInterpolationMode)interpolation, Math.Clamp(opacity, 0, 1)));
+ }
+
+ public int SetBackground(uint argb) => Run(() =>
+ {
+ control.Backdrop = argb == 0 ? null : new SolidColorBrush(Color.FromUInt32(argb));
+ control.InvalidateVisual();
+ });
+
+ public int SetCursor(int cursor)
+ {
+ if (cursor != -1 && !Enum.IsDefined((StandardCursorType)cursor))
+ return HResults.E_INVALIDARG;
+ return Run(() => control.Cursor = cursor < 0 ? null : new Cursor((StandardCursorType)cursor));
+ }
+
+ public int SetDesiredSize(double width, double height) => Run(() =>
+ {
+ control.Desired = new Size(Math.Max(0, width), Math.Max(0, height));
+ control.InvalidateMeasure();
+ });
+
+ public int Focus() => Run(() => control.Focus());
+
+ public int GetMetrics(out double width, out double height, out double scaling)
+ {
+ Size size = default;
+ double found = 1;
+ var hr = Run(() =>
+ {
+ size = control.Bounds.Size;
+ found = control.Scaling;
+ });
+ width = size.Width;
+ height = size.Height;
+ scaling = found;
+ return hr;
+ }
+
+ public int Detach() => Run(() => control.Handler = null);
+
+ private static int Run(Action action)
+ {
+ try
+ {
+ Dispatcher.UIThread.VerifyAccess();
+ action();
+ return HResults.S_OK;
+ }
+ catch (Exception e)
+ {
+ return AbiError.Capture(e);
+ }
+ }
+}
+
+///
+/// A control drawing Rust-owned bitmap layers and forwarding its pointer and
+/// keyboard input to an .
+///
+internal sealed class RustPixelSurface : Control
+{
+ private sealed class Layer
+ {
+ public WriteableBitmap? Bitmap;
+ public Rect Destination;
+ public bool Fill = true;
+ public BitmapInterpolationMode Interpolation;
+ public double Opacity = 1;
+ }
+
+ private readonly Layer[] _layers = new Layer[AvnPixelSurface.MaxLayers];
+ private TopLevel? _topLevel;
+
+ public RustPixelSurface()
+ {
+ ClipToBounds = true;
+ Focusable = true;
+ for (var i = 0; i < _layers.Length; i++)
+ _layers[i] = new Layer();
+ }
+
+ public IAvnPixelSurfaceHandler? Handler { get; set; }
+
+ public IBrush? Backdrop { get; set; }
+
+ public Size Desired { get; set; }
+
+ public double Scaling => _topLevel?.RenderScaling ?? 1;
+
+ public void SetLayer(int index, int width, int height, PixelFormat format)
+ {
+ var layer = _layers[index];
+ if (width == 0 || height == 0)
+ {
+ layer.Bitmap?.Dispose();
+ layer.Bitmap = null;
+ }
+ else if (layer.Bitmap is not { } bitmap || bitmap.PixelSize.Width != width || bitmap.PixelSize.Height != height || bitmap.Format != format)
+ {
+ layer.Bitmap?.Dispose();
+ // New WriteableBitmap memory is zeroed: transparent in both formats.
+ layer.Bitmap = new WriteableBitmap(new PixelSize(width, height), new Vector(96, 96), format, AlphaFormat.Premul);
+ }
+ InvalidateVisual();
+ }
+
+ public unsafe void WriteLayer(int index, int x, int y, int width, int height, nint pixels, int stride)
+ {
+ var bitmap = _layers[index].Bitmap ?? throw new InvalidOperationException($"Layer {index} has no bitmap; call SetLayer first.");
+ var size = bitmap.PixelSize;
+ var left = Math.Max(0, x);
+ var top = Math.Max(0, y);
+ var right = Math.Min(size.Width, x + width);
+ var bottom = Math.Min(size.Height, y + height);
+ if (right > left && bottom > top)
+ {
+ using var target = bitmap.Lock();
+ var rowBytes = (right - left) * 4;
+ for (var row = top; row < bottom; row++)
+ {
+ var source = (byte*)pixels + (long)(row - y) * stride + (long)(left - x) * 4;
+ var destination = (byte*)target.Address + (long)row * target.RowBytes + (long)left * 4;
+ Buffer.MemoryCopy(source, destination, rowBytes, rowBytes);
+ }
+ }
+ InvalidateVisual();
+ }
+
+ public void PlaceLayer(int index, Rect destination, bool fill, BitmapInterpolationMode interpolation, double opacity)
+ {
+ var layer = _layers[index];
+ layer.Destination = destination;
+ layer.Fill = fill;
+ layer.Interpolation = interpolation;
+ layer.Opacity = opacity;
+ InvalidateVisual();
+ }
+
+ protected override Size MeasureOverride(Size availableSize) => Desired;
+
+ public override void Render(DrawingContext context)
+ {
+ var bounds = new Rect(Bounds.Size);
+ if (Backdrop is { } backdrop)
+ context.FillRectangle(backdrop, bounds);
+ foreach (var layer in _layers)
+ {
+ if (layer.Bitmap is not { } bitmap || layer.Opacity <= 0)
+ continue;
+ var destination = layer.Fill ? bounds : layer.Destination;
+ if (destination.Width <= 0 || destination.Height <= 0)
+ continue;
+ using var options = context.PushRenderOptions(new RenderOptions { BitmapInterpolationMode = layer.Interpolation });
+ using var opacity = context.PushOpacity(layer.Opacity);
+ context.DrawImage(bitmap, new Rect(bitmap.Size), destination);
+ }
+ }
+
+ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ base.OnAttachedToVisualTree(e);
+ _topLevel = TopLevel.GetTopLevel(this);
+ if (_topLevel is not null)
+ _topLevel.ScalingChanged += OnScalingChanged;
+ ReportSize();
+ }
+
+ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ base.OnDetachedFromVisualTree(e);
+ if (_topLevel is not null)
+ _topLevel.ScalingChanged -= OnScalingChanged;
+ _topLevel = null;
+ }
+
+ private void OnScalingChanged(object? sender, EventArgs e) => ReportSize();
+
+ protected override void OnSizeChanged(SizeChangedEventArgs e)
+ {
+ base.OnSizeChanged(e);
+ ReportSize();
+ }
+
+ private void ReportSize() => Handler?.OnResize(Bounds.Width, Bounds.Height, Scaling);
+
+ private static int Buttons(PointerPointProperties p) =>
+ (p.IsLeftButtonPressed ? 1 : 0) | (p.IsRightButtonPressed ? 2 : 0) | (p.IsMiddleButtonPressed ? 4 : 0) |
+ (p.IsXButton1Pressed ? 8 : 0) | (p.IsXButton2Pressed ? 16 : 0);
+
+ private static int ButtonOf(PointerUpdateKind kind) => kind switch
+ {
+ PointerUpdateKind.LeftButtonPressed or PointerUpdateKind.LeftButtonReleased => 1,
+ PointerUpdateKind.RightButtonPressed or PointerUpdateKind.RightButtonReleased => 2,
+ PointerUpdateKind.MiddleButtonPressed or PointerUpdateKind.MiddleButtonReleased => 3,
+ PointerUpdateKind.XButton1Pressed or PointerUpdateKind.XButton1Released => 4,
+ PointerUpdateKind.XButton2Pressed or PointerUpdateKind.XButton2Released => 5,
+ _ => 0,
+ };
+
+ private bool Send(int kind, PointerPoint point, int button, KeyModifiers modifiers, int clickCount = 0, Vector delta = default)
+ {
+ if (Handler is not { } handler)
+ return false;
+ var p = point.Position;
+ var hr = handler.OnPointer(kind, p.X, p.Y, button, Buttons(point.Properties), (int)modifiers, clickCount, delta.X, delta.Y, out var handled);
+ return hr >= 0 && handled != 0;
+ }
+
+ protected override void OnPointerPressed(PointerPressedEventArgs e)
+ {
+ base.OnPointerPressed(e);
+ if (Handler is null)
+ return;
+ Focus();
+ var point = e.GetCurrentPoint(this);
+ e.Pointer.Capture(this);
+ if (Send(0, point, ButtonOf(point.Properties.PointerUpdateKind), e.KeyModifiers, e.ClickCount))
+ e.Handled = true;
+ }
+
+ protected override void OnPointerMoved(PointerEventArgs e)
+ {
+ base.OnPointerMoved(e);
+ if (Handler is null)
+ return;
+ var handled = false;
+ foreach (var point in e.GetIntermediatePoints(this))
+ handled |= Send(1, point, 0, e.KeyModifiers);
+ if (handled)
+ e.Handled = true;
+ }
+
+ protected override void OnPointerReleased(PointerReleasedEventArgs e)
+ {
+ base.OnPointerReleased(e);
+ if (Handler is null)
+ return;
+ var point = e.GetCurrentPoint(this);
+ var button = e.InitialPressMouseButton switch
+ {
+ MouseButton.Left => 1,
+ MouseButton.Right => 2,
+ MouseButton.Middle => 3,
+ MouseButton.XButton1 => 4,
+ MouseButton.XButton2 => 5,
+ _ => 0,
+ };
+ if (Send(2, point, button, e.KeyModifiers))
+ e.Handled = true;
+ if (Buttons(point.Properties) == 0 && e.Pointer.Captured == this)
+ e.Pointer.Capture(null);
+ }
+
+ protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
+ {
+ base.OnPointerWheelChanged(e);
+ if (Send(3, e.GetCurrentPoint(this), 0, e.KeyModifiers, 0, e.Delta))
+ e.Handled = true;
+ }
+
+ protected override void OnPointerEntered(PointerEventArgs e)
+ {
+ base.OnPointerEntered(e);
+ Send(4, e.GetCurrentPoint(this), 0, e.KeyModifiers);
+ }
+
+ protected override void OnPointerExited(PointerEventArgs e)
+ {
+ base.OnPointerExited(e);
+ Send(5, e.GetCurrentPoint(this), 0, e.KeyModifiers);
+ }
+
+ protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e)
+ {
+ base.OnPointerCaptureLost(e);
+ Handler?.OnPointer(6, 0, 0, 0, 0, 0, 0, 0, 0, out _);
+ }
+
+ protected override void OnKeyDown(KeyEventArgs e)
+ {
+ base.OnKeyDown(e);
+ if (Handler is { } handler && handler.OnKey(0, (int)e.Key, (int)e.KeyModifiers, out var handled) >= 0 && handled != 0)
+ e.Handled = true;
+ }
+
+ protected override void OnKeyUp(KeyEventArgs e)
+ {
+ base.OnKeyUp(e);
+ if (Handler is { } handler && handler.OnKey(1, (int)e.Key, (int)e.KeyModifiers, out var handled) >= 0 && handled != 0)
+ e.Handled = true;
+ }
+
+ protected override void OnTextInput(TextInputEventArgs e)
+ {
+ base.OnTextInput(e);
+ if (Handler is { } handler && !string.IsNullOrEmpty(e.Text) && handler.OnTextInput(e.Text, out var handled) >= 0 && handled != 0)
+ e.Handled = true;
+ }
+}
diff --git a/host/Com/AvnShellTray.cs b/host/Com/AvnShellTray.cs
new file mode 100644
index 0000000..1939b04
--- /dev/null
+++ b/host/Com/AvnShellTray.cs
@@ -0,0 +1,164 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices.Marshalling;
+using Avalonia.Controls;
+using Avalonia.Rust;
+using Avalonia.Threading;
+
+namespace Avalonia.Host.Com;
+
+/// Stage 34: creates tray icons from Rust.
+public partial class AvnApplication
+{
+ public int CreateTrayIcon(IAvnShellTrayHandler? handler, out IAvnShellTray? tray)
+ {
+ tray = null;
+ try
+ {
+ Dispatcher.UIThread.VerifyAccess();
+ var application = Application.Current
+ ?? throw new InvalidOperationException("A tray icon needs a running application.");
+ var created = new AvnShellTray(application, handler);
+ _trays.Add(created);
+ tray = created;
+ return HResults.S_OK;
+ }
+ catch (Exception e)
+ {
+ return AbiError.Capture(e);
+ }
+ }
+}
+
+///
+/// An Avalonia whose Rust builds
+/// item by item. Avalonia only shows icons in the application's
+/// , so the icon is added there and removed on
+/// (or by the application's shutdown).
+///
+[GeneratedComClass]
+internal sealed partial class AvnShellTray : IAvnShellTray, IDisposable
+{
+ private const int KindCommand = 0;
+ private const int KindCheckBox = 1;
+ private const int KindSeparator = 2;
+ private const int KindSubmenu = 3;
+ private const int FlagChecked = 1;
+ private const int FlagDisabled = 2;
+
+ private readonly Application _application;
+ private readonly Dictionary _items = new();
+ private readonly NativeMenu _menu = new();
+ private IAvnShellTrayHandler? _handler;
+ private TrayIcon? _icon;
+
+ public AvnShellTray(Application application, IAvnShellTrayHandler? handler)
+ {
+ _application = application;
+ _handler = handler;
+ _icon = new TrayIcon { Menu = _menu, IsVisible = true };
+ _icon.Clicked += (_, _) => _handler?.OnActivate();
+ var icons = TrayIcon.GetIcons(application);
+ if (icons is null)
+ {
+ icons = new TrayIcons();
+ TrayIcon.SetIcons(application, icons);
+ }
+ icons.Add(_icon);
+ }
+
+ public int SetIcon(string? source) => Run(icon => icon.Icon = string.IsNullOrWhiteSpace(source) ? null : RustTrayAttachment.LoadIcon(source));
+
+ public int SetToolTip(string? text) => Run(icon => icon.ToolTipText = text);
+
+ public int SetVisible(int visible) => Run(icon => icon.IsVisible = visible != 0);
+
+ public int ClearMenu() => Run(_ =>
+ {
+ _menu.Items.Clear();
+ _items.Clear();
+ });
+
+ public int AddMenuItem(int parent, int id, int kind, string? header, int flags)
+ {
+ if (id <= 0 || kind is < KindCommand or > KindSubmenu)
+ return HResults.E_INVALIDARG;
+ return Run(_ =>
+ {
+ if (_items.ContainsKey(id))
+ throw new ArgumentException($"Menu item {id} already exists.", nameof(id));
+ NativeMenu target;
+ if (parent == 0)
+ target = _menu;
+ else if (_items.TryGetValue(parent, out var owner) && owner is NativeMenuItem { Menu: { } submenu })
+ target = submenu;
+ else
+ throw new ArgumentException($"Menu item {parent} is not a submenu.", nameof(parent));
+
+ NativeMenuItemBase item;
+ if (kind == KindSeparator)
+ {
+ item = new NativeMenuItemSeparator();
+ }
+ else
+ {
+ var menuItem = new NativeMenuItem(header ?? string.Empty);
+ if (kind == KindSubmenu)
+ menuItem.Menu = new NativeMenu();
+ else
+ menuItem.Click += (_, _) => _handler?.OnMenuItem(id);
+ if (kind == KindCheckBox)
+ menuItem.ToggleType = MenuItemToggleType.CheckBox;
+ Apply(menuItem, flags);
+ item = menuItem;
+ }
+ target.Items.Add(item);
+ _items.Add(id, item);
+ });
+ }
+
+ public int UpdateMenuItem(int id, string? header, int flags) => Run(_ =>
+ {
+ if (!_items.TryGetValue(id, out var item))
+ throw new ArgumentException($"Menu item {id} does not exist.", nameof(id));
+ if (item is NativeMenuItem menuItem)
+ {
+ if (header is not null)
+ menuItem.Header = header;
+ Apply(menuItem, flags);
+ }
+ });
+
+ private static void Apply(NativeMenuItem item, int flags)
+ {
+ item.IsChecked = (flags & FlagChecked) != 0;
+ item.IsEnabled = (flags & FlagDisabled) == 0;
+ }
+
+ public int Remove() => Run(_ => Dispose());
+
+ public void Dispose()
+ {
+ _handler = null;
+ if (_icon is not { } icon)
+ return;
+ _icon = null;
+ if (TrayIcon.GetIcons(_application) is { } icons)
+ icons.Remove(icon);
+ icon.Dispose();
+ }
+
+ private int Run(Action action)
+ {
+ try
+ {
+ Dispatcher.UIThread.VerifyAccess();
+ action(_icon ?? throw new ObjectDisposedException(nameof(IAvnShellTray)));
+ return HResults.S_OK;
+ }
+ catch (Exception e)
+ {
+ return AbiError.Capture(e);
+ }
+ }
+}
diff --git a/host/Com/IAvnApplication7.cs b/host/Com/IAvnApplication7.cs
new file mode 100644
index 0000000..db34e0b
--- /dev/null
+++ b/host/Com/IAvnApplication7.cs
@@ -0,0 +1,45 @@
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.Marshalling;
+
+namespace Avalonia.Host.Com;
+
+///
+/// Stage 34 code-first desktop pieces. Queried as an optional interface from
+/// ; no published vtable changes.
+///
+///
+///
+/// The projection covers Avalonia's controls, but an image editor, a selection
+/// overlay or a chart needs two things it cannot express: pixels that Rust
+/// produced, and the raw pointer stream (press, move, release with positions).
+/// A pixel surface is a control that draws Rust-owned bitmap layers and reports
+/// its input to a Rust handler; everything else (zoom, hit testing, what to
+/// draw) stays on the Rust side.
+///
+///
+/// Stage 33's tray icons are declared in a view-model schema compiled into the
+/// host, which a code-first application on the prebuilt host does not have.
+/// builds one, and its menu, from Rust instead.
+///
+///
+[GeneratedComInterface]
+[Guid(AvnGuids.IAvnApplication7)]
+public partial interface IAvnApplication7
+{
+ ///
+ /// Creates a pixel surface reporting its input to
+ /// (which may be null for a display-only surface). Place it with
+ /// .
+ ///
+ [PreserveSig]
+ int CreatePixelSurface(IAvnPixelSurfaceHandler? handler, out IAvnPixelSurface? surface);
+
+ ///
+ /// Shows a notification-area icon reporting clicks to
+ /// (null for an icon that does nothing). The icon
+ /// starts visible with no image or menu, and stays until
+ /// or the application shuts down.
+ ///
+ [PreserveSig]
+ int CreateTrayIcon(IAvnShellTrayHandler? handler, out IAvnShellTray? tray);
+}
diff --git a/host/Com/IAvnPixelSurface.cs b/host/Com/IAvnPixelSurface.cs
new file mode 100644
index 0000000..09aae60
--- /dev/null
+++ b/host/Com/IAvnPixelSurface.cs
@@ -0,0 +1,123 @@
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.Marshalling;
+
+namespace Avalonia.Host.Com;
+
+///
+/// A host-owned control drawing up to
+/// bitmap layers over a background, in layer order. Every member must be called
+/// on the UI thread.
+///
+[GeneratedComInterface]
+[Guid(AvnGuids.IAvnPixelSurface)]
+public partial interface IAvnPixelSurface
+{
+ /// The control to put in a layout (a plain Control).
+ [PreserveSig]
+ int GetControl(out IAvnControl? control);
+
+ ///
+ /// Allocates layer as a by
+ /// bitmap, cleared to transparent, or removes it
+ /// when either is 0. is 0 for premultiplied
+ /// RGBA8888 and 1 for premultiplied BGRA8888. Reallocating at the same size
+ /// and format keeps the pixels.
+ ///
+ [PreserveSig]
+ int SetLayer(int layer, int width, int height, int format);
+
+ ///
+ /// Copies a rectangle of pixels into the layer at (,
+ /// ), clipped to the layer, and redraws.
+ /// points at the first row,
+ /// bytes apart, in the layer's format.
+ ///
+ [PreserveSig]
+ int WriteLayer(int layer, int x, int y, int width, int height, nint pixels, int stride);
+
+ ///
+ /// Where the layer is drawn, in device-independent pixels relative to the
+ /// control: the rectangle, or the whole control when
+ /// is non-zero. is Avalonia's
+ /// BitmapInterpolationMode (0 unspecified, 1 none, 2 low, 3 medium,
+ /// 4 high). The default is the whole control at full opacity.
+ ///
+ [PreserveSig]
+ int PlaceLayer(int layer, double x, double y, double width, double height, int fill, int interpolation, double opacity);
+
+ /// Fills the control under the layers with an ARGB color (0 for none).
+ [PreserveSig]
+ int SetBackground(uint argb);
+
+ ///
+ /// Sets the pointer shape: Avalonia's StandardCursorType, or -1 for
+ /// the inherited cursor.
+ ///
+ [PreserveSig]
+ int SetCursor(int cursor);
+
+ ///
+ /// The size the surface asks for in layout, in device-independent pixels;
+ /// 0 asks for nothing and takes what the layout gives (the default).
+ ///
+ [PreserveSig]
+ int SetDesiredSize(double width, double height);
+
+ /// Moves keyboard focus to the surface.
+ [PreserveSig]
+ int Focus();
+
+ ///
+ /// The control's current size in device-independent pixels and its
+ /// top-level's render scaling (physical pixels per DIP).
+ ///
+ [PreserveSig]
+ int GetMetrics(out double width, out double height, out double scaling);
+
+ ///
+ /// Stops reporting input and releases the handler. The control keeps
+ /// drawing its layers.
+ ///
+ [PreserveSig]
+ int Detach();
+}
+
+///
+/// Implemented by Rust: receives a pixel surface's input. Positions are in
+/// device-independent pixels relative to the control.
+///
+[GeneratedComInterface(StringMarshalling = StringMarshalling.Utf16)]
+[Guid(AvnGuids.IAvnPixelSurfaceHandler)]
+public partial interface IAvnPixelSurfaceHandler
+{
+ ///
+ /// A pointer event. : 0 pressed, 1 moved, 2 released,
+ /// 3 wheel, 4 entered, 5 exited, 6 capture lost. is
+ /// the button that changed (0 none, 1 left, 2 right, 3 middle, 4 X1, 5 X2);
+ /// the ones held after the event (bit 0 left,
+ /// 1 right, 2 middle, 3 X1, 4 X2). is
+ /// Avalonia's KeyModifiers. A press captures the pointer until its
+ /// release. Moves include the coalesced intermediate points, one call each.
+ /// Setting stops the event from bubbling.
+ ///
+ [PreserveSig]
+ int OnPointer(int kind, double x, double y, int button, int buttons, int modifiers, int clickCount, double deltaX, double deltaY, out int handled);
+
+ ///
+ /// A key going down ( 0) or up (1) while the surface
+ /// has focus: Avalonia's Key and KeyModifiers.
+ ///
+ [PreserveSig]
+ int OnKey(int kind, int key, int modifiers, out int handled);
+
+ /// Text typed while the surface has focus.
+ [PreserveSig]
+ int OnTextInput(string? text, out int handled);
+
+ ///
+ /// The control's size (device-independent pixels) or render scaling
+ /// changed, and when it is first attached to a window.
+ ///
+ [PreserveSig]
+ int OnResize(double width, double height, double scaling);
+}
diff --git a/host/Com/IAvnShellTray.cs b/host/Com/IAvnShellTray.cs
new file mode 100644
index 0000000..3c1bc47
--- /dev/null
+++ b/host/Com/IAvnShellTray.cs
@@ -0,0 +1,59 @@
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.Marshalling;
+
+namespace Avalonia.Host.Com;
+
+///
+/// A notification-area icon built from Rust (stage 34). Its menu is a tree of
+/// items Rust numbers itself; a click on one is reported to the
+/// by that number. UI thread only.
+///
+[GeneratedComInterface(StringMarshalling = StringMarshalling.Utf16)]
+[Guid(AvnGuids.IAvnShellTray)]
+public partial interface IAvnShellTray
+{
+ /// The image: a file path or an avares:// URI; null removes it.
+ [PreserveSig]
+ int SetIcon(string? source);
+
+ [PreserveSig]
+ int SetToolTip(string? text);
+
+ [PreserveSig]
+ int SetVisible(int visible);
+
+ /// Removes every menu item.
+ [PreserveSig]
+ int ClearMenu();
+
+ ///
+ /// Appends item (positive, unique in this menu) under
+ /// , a submenu's id or 0 for the top level.
+ /// : 0 command, 1 check box, 2 separator, 3 submenu.
+ /// : bit 0 checked, bit 1 disabled.
+ ///
+ [PreserveSig]
+ int AddMenuItem(int parent, int id, int kind, string? header, int flags);
+
+ /// Changes an item's header (when not null) and flags.
+ [PreserveSig]
+ int UpdateMenuItem(int id, string? header, int flags);
+
+ /// Takes the icon down for good.
+ [PreserveSig]
+ int Remove();
+}
+
+/// Implemented by Rust: what was clicked on a tray icon.
+[GeneratedComInterface]
+[Guid(AvnGuids.IAvnShellTrayHandler)]
+public partial interface IAvnShellTrayHandler
+{
+ /// The icon itself was clicked.
+ [PreserveSig]
+ int OnActivate();
+
+ /// Menu item was clicked.
+ [PreserveSig]
+ int OnMenuItem(int id);
+}
diff --git a/rust/COMPATIBILITY.md b/rust/COMPATIBILITY.md
index ad8adec..97c2dad 100644
--- a/rust/COMPATIBILITY.md
+++ b/rust/COMPATIBILITY.md
@@ -131,9 +131,12 @@ integration capability, stage 30's `IAvnRustVmSink4` richer data shapes,
`IAvnRustVmRangeBatch` windowed range payload, `IAvnRustRangeSource` and
`IAvnRustViewModel2`, or stage 31's `IAvnApplication4` clipboard command
capability and its host-owned `IAvnClipboardData` payload builder,
-`IAvnRustVmSink5` for scalar-number collection elements, or stage 33's
+`IAvnRustVmSink5` for scalar-number collection elements, stage 33's
`IAvnApplication6` desktop shell capability: shutdown mode, schema-declared
-tray icons and window handle/topmost/transparency/capture exclusion), then
+tray icons and window handle/topmost/transparency/capture exclusion, or
+stage 34's `IAvnApplication7` code-first desktop capability: pixel surfaces
+(`IAvnPixelSurface` with its Rust-implemented `IAvnPixelSurfaceHandler`) and
+Rust-built tray icons (`IAvnShellTray`, `IAvnShellTrayHandler`)), then
negotiate/query it as optional capability.
A producer or host that predates an optional capability must report
`E_NOINTERFACE` explicitly; silently dropping the affected updates is not an
diff --git a/rust/prepare-release.ps1 b/rust/prepare-release.ps1
index 953020e..0133ee4 100644
--- a/rust/prepare-release.ps1
+++ b/rust/prepare-release.ps1
@@ -80,9 +80,11 @@ if ($PSCmdlet.ParameterSetName -eq 'Version') {
& cargo update --workspace --manifest-path (Join-Path $root 'rust' 'Cargo.toml')
# The in-repository apps use the crates by path with their own lock
# files; only the crates' own entries move (CI builds them --locked).
+ # Lock files without rustolonia (an app's other workspaces) stay.
foreach ($lock in @(& git -C $root ls-files 'apps/*Cargo.lock' 'apps/**/Cargo.lock' | Sort-Object -Unique)) {
$manifest = Join-Path $root (Split-Path -Parent $lock) 'Cargo.toml'
- if (Test-Path -LiteralPath $manifest) {
+ $usesRustolonia = Select-String -LiteralPath (Join-Path $root $lock) -Pattern '^name = "rustolonia"$' -Quiet
+ if ($usesRustolonia -and (Test-Path -LiteralPath $manifest)) {
& cargo update -p rustolonia -p rustolonia-sys --manifest-path $manifest
}
}
diff --git a/rust/rustolonia-sys/src/lib.rs b/rust/rustolonia-sys/src/lib.rs
index 7b98e73..b35aabf 100644
--- a/rust/rustolonia-sys/src/lib.rs
+++ b/rust/rustolonia-sys/src/lib.rs
@@ -28,6 +28,7 @@ mod event_callback;
mod factory;
mod filters;
mod notification;
+mod pixel_surface;
mod popup_placement;
mod selectors;
mod shell;
@@ -59,6 +60,13 @@ pub use hresult::{
E_NOTIMPL, E_POINTER, S_OK,
};
pub use notification::{notification, Notification, NotificationSpec};
+pub use pixel_surface::{
+ pixel_surface_handler, tray_handler, IAvnApplication7, IAvnPixelSurface,
+ IAvnPixelSurfaceHandler, IAvnShellTray, IAvnShellTrayHandler, PixelSurfaceInput, TrayInput,
+ IAVN_APPLICATION7_METHOD_COUNT, IAVN_PIXEL_SURFACE_HANDLER_METHOD_COUNT,
+ IAVN_PIXEL_SURFACE_METHOD_COUNT, IAVN_SHELL_TRAY_HANDLER_METHOD_COUNT,
+ IAVN_SHELL_TRAY_METHOD_COUNT,
+};
pub use popup_placement::{popup_placement, PopupPlacement, PopupPlacementResult};
pub use rust_vm::{
rust_view_model, rust_view_model_with_control, rust_vm_range_batch, rust_vm_update_batch,
diff --git a/rust/rustolonia-sys/src/pixel_surface.rs b/rust/rustolonia-sys/src/pixel_surface.rs
new file mode 100644
index 0000000..a2057ab
--- /dev/null
+++ b/rust/rustolonia-sys/src/pixel_surface.rs
@@ -0,0 +1,794 @@
+//! Stage 34 code-first desktop ABI.
+//!
+//! `IAvnApplication7` is a separately versioned capability queried from
+//! `IAvnApplication`; nothing here is added to an already published vtable. It
+//! creates pixel surfaces: host controls that draw bitmap layers Rust fills
+//! (`IAvnPixelSurface`) and report their raw pointer and keyboard input to a
+//! Rust-implemented `IAvnPixelSurfaceHandler`; and tray icons whose menu Rust
+//! builds (`IAvnShellTray`, reporting to an `IAvnShellTrayHandler`).
+
+use crate::application::IAvnApplication;
+use crate::com::{ComInterface, ComPtr, IUnknown};
+use crate::generated::IAvnControl;
+use crate::guid::Guid;
+use crate::hresult::{self, Result};
+use std::ffi::c_void;
+use std::panic::{catch_unwind, AssertUnwindSafe};
+use std::sync::atomic::{fence, AtomicU32, Ordering};
+use std::sync::Mutex;
+
+const fn stage34_iid(last: u8) -> Guid {
+ Guid {
+ data1: 0x6B2E8F10,
+ data2: 0x4C91,
+ data3: 0x4E3A,
+ data4: [0x9A, 0x77, 0x1F, 0x0C, 0x2B, 0x3A, 0x4D, last],
+ }
+}
+
+const IAVN_APPLICATION7_IID: Guid = stage34_iid(0x90);
+const IAVN_PIXEL_SURFACE_IID: Guid = stage34_iid(0x91);
+const IAVN_PIXEL_SURFACE_HANDLER_IID: Guid = stage34_iid(0x92);
+const IAVN_SHELL_TRAY_IID: Guid = stage34_iid(0x93);
+const IAVN_SHELL_TRAY_HANDLER_IID: Guid = stage34_iid(0x94);
+
+type QueryInterface =
+ unsafe extern "system" fn(*mut IUnknown, *const Guid, *mut *mut c_void) -> i32;
+type AddRef = unsafe extern "system" fn(*mut IUnknown) -> u32;
+
+#[repr(C)]
+struct IAvnApplication7Vtbl {
+ query_interface: QueryInterface,
+ add_ref: AddRef,
+ release: AddRef,
+ create_pixel_surface: unsafe extern "system" fn(
+ *mut IAvnApplication7,
+ *mut IAvnPixelSurfaceHandler,
+ *mut *mut IAvnPixelSurface,
+ ) -> i32,
+ create_tray_icon: unsafe extern "system" fn(
+ *mut IAvnApplication7,
+ *mut IAvnShellTrayHandler,
+ *mut *mut IAvnShellTray,
+ ) -> i32,
+}
+
+#[repr(C)]
+pub struct IAvnApplication7 {
+ vtbl: *const IAvnApplication7Vtbl,
+}
+
+unsafe impl ComInterface for IAvnApplication7 {
+ const IID: Guid = IAVN_APPLICATION7_IID;
+}
+
+impl ComPtr {
+ fn vtbl(&self) -> &IAvnApplication7Vtbl {
+ unsafe { (*self.as_raw()).vtbl.as_ref().unwrap() }
+ }
+
+ /// Creates a pixel surface reporting its input to `handler`, or a
+ /// display-only one.
+ pub fn create_pixel_surface(
+ &self,
+ handler: Option<&ComPtr>,
+ ) -> Result> {
+ unsafe {
+ let mut surface = std::ptr::null_mut();
+ hresult::check((self.vtbl().create_pixel_surface)(
+ self.as_raw(),
+ handler.map_or(std::ptr::null_mut(), |h| h.as_raw()),
+ &mut surface,
+ ))?;
+ ComPtr::from_raw(surface).ok_or(hresult::Error(hresult::E_POINTER))
+ }
+ }
+
+ /// Shows a tray icon reporting clicks to `handler`, or one that does nothing.
+ pub fn create_tray_icon(
+ &self,
+ handler: Option<&ComPtr>,
+ ) -> Result> {
+ unsafe {
+ let mut tray = std::ptr::null_mut();
+ hresult::check((self.vtbl().create_tray_icon)(
+ self.as_raw(),
+ handler.map_or(std::ptr::null_mut(), |h| h.as_raw()),
+ &mut tray,
+ ))?;
+ ComPtr::from_raw(tray).ok_or(hresult::Error(hresult::E_POINTER))
+ }
+ }
+}
+
+impl ComPtr {
+ /// Queries the separately versioned Rust-drawn surfaces capability.
+ pub fn pixel_surfaces(&self) -> Result> {
+ self.query_interface::()
+ }
+}
+
+#[repr(C)]
+struct IAvnPixelSurfaceVtbl {
+ query_interface: QueryInterface,
+ add_ref: AddRef,
+ release: AddRef,
+ get_control: unsafe extern "system" fn(*mut IAvnPixelSurface, *mut *mut IAvnControl) -> i32,
+ set_layer: unsafe extern "system" fn(*mut IAvnPixelSurface, i32, i32, i32, i32) -> i32,
+ write_layer: unsafe extern "system" fn(
+ *mut IAvnPixelSurface,
+ i32,
+ i32,
+ i32,
+ i32,
+ i32,
+ *const u8,
+ i32,
+ ) -> i32,
+ place_layer: unsafe extern "system" fn(
+ *mut IAvnPixelSurface,
+ i32,
+ f64,
+ f64,
+ f64,
+ f64,
+ i32,
+ i32,
+ f64,
+ ) -> i32,
+ set_background: unsafe extern "system" fn(*mut IAvnPixelSurface, u32) -> i32,
+ set_cursor: unsafe extern "system" fn(*mut IAvnPixelSurface, i32) -> i32,
+ set_desired_size: unsafe extern "system" fn(*mut IAvnPixelSurface, f64, f64) -> i32,
+ focus: unsafe extern "system" fn(*mut IAvnPixelSurface) -> i32,
+ get_metrics:
+ unsafe extern "system" fn(*mut IAvnPixelSurface, *mut f64, *mut f64, *mut f64) -> i32,
+ detach: unsafe extern "system" fn(*mut IAvnPixelSurface) -> i32,
+}
+
+/// A host-owned control drawing bitmap layers. UI thread only.
+#[repr(C)]
+pub struct IAvnPixelSurface {
+ vtbl: *const IAvnPixelSurfaceVtbl,
+}
+
+unsafe impl ComInterface for IAvnPixelSurface {
+ const IID: Guid = IAVN_PIXEL_SURFACE_IID;
+}
+
+impl ComPtr {
+ fn vtbl(&self) -> &IAvnPixelSurfaceVtbl {
+ unsafe { (*self.as_raw()).vtbl.as_ref().unwrap() }
+ }
+
+ pub fn control(&self) -> Result> {
+ unsafe {
+ let mut control = std::ptr::null_mut();
+ hresult::check((self.vtbl().get_control)(self.as_raw(), &mut control))?;
+ ComPtr::from_raw(control).ok_or(hresult::Error(hresult::E_POINTER))
+ }
+ }
+
+ /// Allocates (or with a zero size removes) a layer: format 0 is
+ /// premultiplied RGBA8888, 1 premultiplied BGRA8888.
+ pub fn set_layer(&self, layer: i32, width: i32, height: i32, format: i32) -> Result<()> {
+ unsafe {
+ hresult::check((self.vtbl().set_layer)(
+ self.as_raw(),
+ layer,
+ width,
+ height,
+ format,
+ ))
+ }
+ }
+
+ /// Copies `width` by `height` pixels, `stride` bytes per row, into the
+ /// layer at (`x`, `y`).
+ ///
+ /// # Errors
+ ///
+ /// `E_INVALIDARG` when `pixels` is too short for the rectangle.
+ #[allow(clippy::too_many_arguments)]
+ pub fn write_layer(
+ &self,
+ layer: i32,
+ x: i32,
+ y: i32,
+ width: i32,
+ height: i32,
+ pixels: &[u8],
+ stride: i32,
+ ) -> Result<()> {
+ if width < 0 || height < 0 || stride < width.saturating_mul(4) {
+ return Err(hresult::Error(hresult::E_INVALIDARG));
+ }
+ if height > 0 && width > 0 {
+ let needed = (height as usize - 1) * stride as usize + width as usize * 4;
+ if pixels.len() < needed {
+ return Err(hresult::Error(hresult::E_INVALIDARG));
+ }
+ }
+ unsafe {
+ hresult::check((self.vtbl().write_layer)(
+ self.as_raw(),
+ layer,
+ x,
+ y,
+ width,
+ height,
+ pixels.as_ptr(),
+ stride,
+ ))
+ }
+ }
+
+ #[allow(clippy::too_many_arguments)]
+ pub fn place_layer(
+ &self,
+ layer: i32,
+ x: f64,
+ y: f64,
+ width: f64,
+ height: f64,
+ fill: bool,
+ interpolation: i32,
+ opacity: f64,
+ ) -> Result<()> {
+ unsafe {
+ hresult::check((self.vtbl().place_layer)(
+ self.as_raw(),
+ layer,
+ x,
+ y,
+ width,
+ height,
+ fill as i32,
+ interpolation,
+ opacity,
+ ))
+ }
+ }
+
+ pub fn set_background(&self, argb: u32) -> Result<()> {
+ unsafe { hresult::check((self.vtbl().set_background)(self.as_raw(), argb)) }
+ }
+
+ pub fn set_cursor(&self, cursor: i32) -> Result<()> {
+ unsafe { hresult::check((self.vtbl().set_cursor)(self.as_raw(), cursor)) }
+ }
+
+ pub fn set_desired_size(&self, width: f64, height: f64) -> Result<()> {
+ unsafe { hresult::check((self.vtbl().set_desired_size)(self.as_raw(), width, height)) }
+ }
+
+ pub fn focus(&self) -> Result<()> {
+ unsafe { hresult::check((self.vtbl().focus)(self.as_raw())) }
+ }
+
+ /// The control's size in device-independent pixels and its render scaling.
+ pub fn metrics(&self) -> Result<(f64, f64, f64)> {
+ unsafe {
+ let (mut width, mut height, mut scaling) = (0.0, 0.0, 1.0);
+ hresult::check((self.vtbl().get_metrics)(
+ self.as_raw(),
+ &mut width,
+ &mut height,
+ &mut scaling,
+ ))?;
+ Ok((width, height, scaling))
+ }
+ }
+
+ pub fn detach(&self) -> Result<()> {
+ unsafe { hresult::check((self.vtbl().detach)(self.as_raw())) }
+ }
+}
+
+/// One input notification from a pixel surface, as the host reports it.
+#[derive(Clone, Debug, PartialEq)]
+pub enum PixelSurfaceInput {
+ /// `kind`: 0 pressed, 1 moved, 2 released, 3 wheel, 4 entered, 5 exited,
+ /// 6 capture lost.
+ Pointer {
+ kind: i32,
+ x: f64,
+ y: f64,
+ button: i32,
+ buttons: i32,
+ modifiers: i32,
+ click_count: i32,
+ delta_x: f64,
+ delta_y: f64,
+ },
+ /// `kind`: 0 down, 1 up.
+ Key {
+ kind: i32,
+ key: i32,
+ modifiers: i32,
+ },
+ Text(String),
+ Resize {
+ width: f64,
+ height: f64,
+ scaling: f64,
+ },
+}
+
+type HandlerCallback = Box Result + Send>;
+
+#[repr(C)]
+struct IAvnPixelSurfaceHandlerVtbl {
+ query_interface: QueryInterface,
+ add_ref: AddRef,
+ release: AddRef,
+ on_pointer: unsafe extern "system" fn(
+ *mut IAvnPixelSurfaceHandler,
+ i32,
+ f64,
+ f64,
+ i32,
+ i32,
+ i32,
+ i32,
+ f64,
+ f64,
+ *mut i32,
+ ) -> i32,
+ on_key: unsafe extern "system" fn(*mut IAvnPixelSurfaceHandler, i32, i32, i32, *mut i32) -> i32,
+ on_text_input:
+ unsafe extern "system" fn(*mut IAvnPixelSurfaceHandler, *const u16, *mut i32) -> i32,
+ on_resize: unsafe extern "system" fn(*mut IAvnPixelSurfaceHandler, f64, f64, f64) -> i32,
+}
+
+/// Implemented by Rust ([`pixel_surface_handler`]); the host calls it on the
+/// UI thread.
+#[repr(C)]
+pub struct IAvnPixelSurfaceHandler {
+ vtbl: *const IAvnPixelSurfaceHandlerVtbl,
+}
+
+unsafe impl ComInterface for IAvnPixelSurfaceHandler {
+ const IID: Guid = IAVN_PIXEL_SURFACE_HANDLER_IID;
+}
+
+#[repr(C)]
+struct HandlerObject {
+ vtbl: *const IAvnPixelSurfaceHandlerVtbl,
+ ref_count: AtomicU32,
+ callback: Mutex