Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/avalonia-rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ on:
- '**'
- '.github/workflows/avalonia-rust.yml'
push:
branches: [main]
# The default branch is `master`; targeting `main` (the fork's default) meant the
# upstream repository never ran this gate on its own default branch.
branches: [master]
paths:
- '**'
- '.github/workflows/avalonia-rust.yml'
Expand Down
50 changes: 50 additions & 0 deletions host/Com/AvnApplication.Dialogs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Host.Desktop;

namespace Avalonia.Host.Com;

/// <summary>
/// Stage 32 modal dialog activation. <c>Window.ShowDialog</c> returns a
/// <see cref="Task{TResult}"/> and never completes synchronously, so it composes onto the
/// shared async operation registry the same way the storage pickers and clipboard do:
/// exactly one completion, cancellable, and the UI thread is never blocked waiting on the
/// dialog.
/// </summary>
public partial class AvnApplication : IAvnApplication5
{
public int StartShowDialog(
IAvnWindow? owner,
IAvnWindow? dialog,
IAvnAsyncCompletion? completion,
out long operationId)
{
operationId = 0;
if (owner is null || dialog is null)
return HResults.E_POINTER;
try
{
Avalonia.Threading.Dispatcher.UIThread.VerifyAccess();
var ownerWindow = (Window?)ProjectionRuntime.Unwrap(owner)
?? throw new ObjectDisposedException(nameof(owner));
var dialogWindow = (Window?)ProjectionRuntime.Unwrap(dialog)
?? throw new ObjectDisposedException(nameof(dialog));

return _asyncOperations.Start(
completion,
async cancellation =>
{
var result = await global::Avalonia.Host.Desktop.Dialogs
.ShowAsync<object?>(dialogWindow, ownerWindow, cancellation)
.ConfigureAwait(true);
return AvnAsyncValue.FromString(global::Avalonia.Host.Desktop.Dialogs.ResultToString(result));
},
out operationId);
}
catch (Exception e)
{
return AbiError.Capture(e);
}
}
}
5 changes: 5 additions & 0 deletions host/Com/AvnGuids.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,9 @@ internal static class AvnGuids
// file entries back, and nothing below is added to a published vtable.
public const string IAvnApplication4 = "6B2E8F10-4C91-4E3A-9A77-1F0C2B3A4D60";
public const string IAvnClipboardData = "6B2E8F10-4C91-4E3A-9A77-1F0C2B3A4D61";

// Stage 32 modal dialog activation. Another separately versioned capability;
// the completion rides the shared async operation registry, and nothing below
// is added to a published vtable.
public const string IAvnApplication5 = "6B2E8F10-4C91-4E3A-9A77-1F0C2B3A4D70";
}
27 changes: 27 additions & 0 deletions host/Com/IAvnDialogs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Marshalling;

namespace Avalonia.Host.Com;

/// <summary>
/// The separately versioned dialog capability, queried from <c>IAvnApplication</c>.
/// Nothing here is ever added to an already published vtable; a consumer that predates
/// the capability reads <c>E_NOINTERFACE</c> from the query instead.
/// </summary>
[GeneratedComInterface(StringMarshalling = StringMarshalling.Utf16)]
[Guid(AvnGuids.IAvnApplication5)]
public partial interface IAvnApplication5
{
/// <summary>
/// Shows <paramref name="dialog"/> modally over <paramref name="owner"/> and completes
/// through the shared async operation registry: exactly one completion, cancellable
/// through <c>CancelAsyncOperation</c>, never blocking the UI thread. The result string
/// is the dialog result converted by the host (null when the dialog returned no result).
/// </summary>
[PreserveSig]
int StartShowDialog(
IAvnWindow? owner,
IAvnWindow? dialog,
IAvnAsyncCompletion? completion,
out long operationId);
}
68 changes: 68 additions & 0 deletions host/Desktop/Dialogs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Threading;

namespace Avalonia.Host.Desktop;

/// <summary>
/// Runs <see cref="Window.ShowDialog{TResult}(Window)"/> from an ABI request.
/// </summary>
/// <remarks>
/// A modal dialog owns its owner while it is open: the dialog may outlive the originating
/// call, so the caller's window tokens are resolved to managed windows up front and the
/// task is awaited without blocking the UI thread. A cancellation request closes the
/// dialog rather than abandoning the task, because an abandoned <c>ShowDialog</c> task
/// still holds the owner.
/// </remarks>
internal static class Dialogs
{
public static async Task<TResult?> ShowAsync<TResult>(
Window dialog,
Window owner,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(dialog);
ArgumentNullException.ThrowIfNull(owner);
cancellationToken.ThrowIfCancellationRequested();

var task = dialog.ShowDialog<TResult>(owner);
if (!cancellationToken.CanBeCanceled)
return await task.ConfigureAwait(true);

using var registration = cancellationToken.Register(() =>
{
if (Dispatcher.UIThread.CheckAccess())
TryClose(dialog);
else
Dispatcher.UIThread.Post(() => TryClose(dialog));
});
return await task.ConfigureAwait(true);
}

/// <summary>
/// Converts a dialog result to its ABI string form: <c>null</c> stays null (the dialog
/// returned no result), a string crosses as-is, and anything else uses its invariant
/// <see cref="object.ToString"/> form.
/// </summary>
public static string? ResultToString(object? result) => result switch
{
null => null,
string text => text,
_ => Convert.ToString(result, System.Globalization.CultureInfo.InvariantCulture),
};

private static void TryClose(Window dialog)
{
try
{
dialog.Close();
}
catch
{
// The dialog may already be closing or detached from its owner; a cancellation
// that races the natural close is satisfied either way.
}
}
}
Loading