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: 2 additions & 2 deletions apps/archive-tool/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions apps/archive-tool/generated/.avalonia-viewmodel.owned.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions apps/archive-tool/generated/RustViewRegistry.g.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ internal static class RustViewRegistry
_ => throw new global::System.ArgumentOutOfRangeException(nameof(viewId)),
};

internal static IDisposable CreateTray(int trayId, IAvnRustViewModel model) => trayId switch
{
_ => throw new global::System.ArgumentOutOfRangeException(nameof(trayId)),
};

private static TWindow CreateDynamic<TWindow>(
IAvnRustViewModel model,
RustViewModelDescriptor descriptor)
Expand Down
1 change: 1 addition & 0 deletions apps/archive-tool/generated/generated_view_models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,7 @@ impl<T: MainViewModel> crate::view_model::DynamicViewModel for MainViewModelDisp
}

pub fn mount_main_window(scope: &crate::AppScope, model: impl MainViewModel) -> crate::Result<()> { scope.mount_dynamic_view_model(1, MainViewModelDispatch { model }) }
pub fn mount_main_window_hidden(scope: &crate::AppScope, model: impl MainViewModel) -> crate::Result<()> { scope.mount_dynamic_view_model_hidden(1, MainViewModelDispatch { model }).map(drop) }

#[derive(Clone, Debug)]
pub struct EntryRowViewModelSink(crate::view_model::ViewModelSink);
Expand Down
4 changes: 2 additions & 2 deletions apps/hash-calculator/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions apps/hash-calculator/generated/.avalonia-viewmodel.owned.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions apps/hash-calculator/generated/RustViewRegistry.g.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ internal static class RustViewRegistry
_ => throw new global::System.ArgumentOutOfRangeException(nameof(viewId)),
};

internal static IDisposable CreateTray(int trayId, IAvnRustViewModel model) => trayId switch
{
_ => throw new global::System.ArgumentOutOfRangeException(nameof(trayId)),
};

private static TWindow CreateDynamic<TWindow>(
IAvnRustViewModel model,
RustViewModelDescriptor descriptor)
Expand Down
1 change: 1 addition & 0 deletions apps/hash-calculator/generated/generated_view_models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ impl<T: MainViewModel> crate::view_model::DynamicViewModel for MainViewModelDisp
}

pub fn mount_main_window(scope: &crate::AppScope, model: impl MainViewModel) -> crate::Result<()> { scope.mount_dynamic_view_model(1, MainViewModelDispatch { model }) }
pub fn mount_main_window_hidden(scope: &crate::AppScope, model: impl MainViewModel) -> crate::Result<()> { scope.mount_dynamic_view_model_hidden(1, MainViewModelDispatch { model }).map(drop) }

#[derive(Clone, Debug)]
pub struct FileRowViewModelSink(crate::view_model::ViewModelSink);
Expand Down
4 changes: 2 additions & 2 deletions apps/pdf-viewer/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions apps/pdf-viewer/generated/.avalonia-viewmodel.owned.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions apps/pdf-viewer/generated/RustViewRegistry.g.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ internal static class RustViewRegistry
_ => throw new global::System.ArgumentOutOfRangeException(nameof(viewId)),
};

internal static IDisposable CreateTray(int trayId, IAvnRustViewModel model) => trayId switch
{
_ => throw new global::System.ArgumentOutOfRangeException(nameof(trayId)),
};

private static TWindow CreateDynamic<TWindow>(
IAvnRustViewModel model,
RustViewModelDescriptor descriptor)
Expand Down
1 change: 1 addition & 0 deletions apps/pdf-viewer/generated/generated_view_models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ impl<T: MainViewModel> crate::view_model::DynamicViewModel for MainViewModelDisp
}

pub fn mount_main_window(scope: &crate::AppScope, model: impl MainViewModel) -> crate::Result<()> { scope.mount_dynamic_view_model(1, MainViewModelDispatch { model }) }
pub fn mount_main_window_hidden(scope: &crate::AppScope, model: impl MainViewModel) -> crate::Result<()> { scope.mount_dynamic_view_model_hidden(1, MainViewModelDispatch { model }).map(drop) }

#[derive(Clone, Debug)]
pub struct OutlineItemViewModelSink(crate::view_model::ViewModelSink);
Expand Down
2 changes: 1 addition & 1 deletion apps/system-monitor/.avalonia-viewmodel.owned.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ internal static class RustViewRegistry
_ => throw new global::System.ArgumentOutOfRangeException(nameof(viewId)),
};

internal static IDisposable CreateTray(int trayId, IAvnRustViewModel model) => trayId switch
{
_ => throw new global::System.ArgumentOutOfRangeException(nameof(trayId)),
};

private static TWindow CreateDynamic<TWindow>(
IAvnRustViewModel model,
RustViewModelDescriptor descriptor)
Expand Down
4 changes: 2 additions & 2 deletions apps/system-monitor/NeoHtop.App/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions apps/system-monitor/generated_view_models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,7 @@ impl<T: MainViewModel> crate::view_model::DynamicViewModel for MainViewModelDisp
}

pub fn mount_main_window(scope: &crate::AppScope, model: impl MainViewModel) -> crate::Result<()> { scope.mount_dynamic_view_model(1, MainViewModelDispatch { model }) }
pub fn mount_main_window_hidden(scope: &crate::AppScope, model: impl MainViewModel) -> crate::Result<()> { scope.mount_dynamic_view_model_hidden(1, MainViewModelDispatch { model }).map(drop) }

#[derive(Clone, Debug)]
pub struct ProcessRowViewModelSink(crate::view_model::ViewModelSink);
Expand Down
134 changes: 134 additions & 0 deletions host/Com/AvnApplication.Shell.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Host.Generated.ViewModels;
using Avalonia.Rust.Interop;
using Avalonia.Threading;

namespace Avalonia.Host.Com;

/// <summary>
/// Stage 33 desktop shell integration: the shutdown mode, tray icons and
/// window-level settings. Avalonia's own <c>ShutdownMode</c>, <c>TrayIcon</c>,
/// <c>Topmost</c> and <c>TransparencyLevelHint</c> do the work; only the
/// capture exclusion calls the platform directly, because Avalonia has no
/// equivalent.
/// </summary>
public partial class AvnApplication : IAvnApplication6
{
private ShutdownMode _shutdownMode = ShutdownMode.OnLastWindowClose;
private readonly List<IDisposable> _trays = new();

public int SetShutdownMode(int mode)
{
if (mode is < (int)ShutdownMode.OnLastWindowClose or > (int)ShutdownMode.OnExplicitShutdown)
return HResults.E_INVALIDARG;
try
{
_shutdownMode = (ShutdownMode)mode;
if (_lifetime is { } lifetime)
lifetime.ShutdownMode = _shutdownMode;
return HResults.S_OK;
}
catch (Exception e)
{
return AbiError.Capture(e);
}
}

public int CreateRustVmTray(int trayId, IAvnRustViewModel? model)
{
if (model is null)
return HResults.E_POINTER;
try
{
Dispatcher.UIThread.VerifyAccess();
_trays.Add(RustViewRegistry.CreateTray(trayId, model));
return HResults.S_OK;
}
catch (Exception e)
{
return AbiError.Capture(e);
}
}

public int GetPlatformHandle(IAvnWindow? window, out long handle)
{
long found = 0;
var hr = WithWindow(window, managed => found = managed.TryGetPlatformHandle()?.Handle ?? 0);
handle = found;
return hr;
}

public int SetWindowTopmost(IAvnWindow? window, int topmost) =>
WithWindow(window, managed => managed.Topmost = topmost != 0);

public int SetWindowTransparent(IAvnWindow? window, int transparent) =>
WithWindow(window, managed => managed.TransparencyLevelHint = transparent != 0
? [WindowTransparencyLevel.Transparent]
: []);

public int SetWindowExcludedFromCapture(IAvnWindow? window, int excluded)
{
if (!OperatingSystem.IsWindows())
return HResults.E_NOTIMPL;
var result = 0;
var hr = WithWindow(window, managed =>
{
var hwnd = managed.TryGetPlatformHandle()?.Handle ?? 0;
if (hwnd == 0)
throw new InvalidOperationException("The window has no native handle yet; show it first.");
if (!Win32.SetWindowDisplayAffinity(hwnd, excluded != 0 ? Win32.WDA_EXCLUDEFROMCAPTURE : Win32.WDA_NONE))
result = Marshal.GetHRForLastWin32Error();
});
return hr < 0 ? hr : result;
}

/// <summary>Runs <paramref name="action"/> on the managed window, on the UI thread.</summary>
private static int WithWindow(IAvnWindow? window, Action<Window> action)
{
if (window is null)
return HResults.E_POINTER;
try
{
Dispatcher.UIThread.VerifyAccess();
var managed = (Window?)ProjectionRuntime.Unwrap(window)
?? throw new ObjectDisposedException(nameof(window));
action(managed);
return HResults.S_OK;
}
catch (Exception e)
{
return AbiError.Capture(e);
}
}

/// <summary>Removes the tray icons; Avalonia also disposes them as the dispatcher shuts down.</summary>
private void ReleaseTrays()
{
foreach (var tray in _trays)
{
try
{
tray.Dispose();
}
catch (Exception)
{
// Shutdown keeps going; the platform removes the icon with the process.
}
}
_trays.Clear();
}

private static partial class Win32
{
public const uint WDA_NONE = 0x00;
public const uint WDA_EXCLUDEFROMCAPTURE = 0x11;

[LibraryImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool SetWindowDisplayAffinity(nint hwnd, uint affinity);
}
}
4 changes: 3 additions & 1 deletion host/Com/AvnApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ public int Run(IAvnAppHandler? handler)
var startupArguments = SnapshotStartupArguments();
var lifetime = new ClassicDesktopStyleApplicationLifetime
{
ShutdownMode = ShutdownMode.OnLastWindowClose,
// OnLastWindowClose unless IAvnApplication6.SetShutdownMode said otherwise.
ShutdownMode = _shutdownMode,
Args = startupArguments,
};
_lifetime = lifetime;
Expand All @@ -58,6 +59,7 @@ public int Run(IAvnAppHandler? handler)
}
finally
{
ReleaseTrays();
ReleaseDesktopFileIntegration();
}
}
Expand Down
5 changes: 5 additions & 0 deletions host/Com/AvnGuids.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,9 @@ internal static class AvnGuids
// 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";

// Stage 33 desktop shell integration: shutdown mode, tray icons and the
// 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";
}
Loading
Loading