EdoardoTona.Rustino 0.4.2

dotnet add package EdoardoTona.Rustino --version 0.4.2
                    
NuGet\Install-Package EdoardoTona.Rustino -Version 0.4.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="EdoardoTona.Rustino" Version="0.4.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="EdoardoTona.Rustino" Version="0.4.2" />
                    
Directory.Packages.props
<PackageReference Include="EdoardoTona.Rustino" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add EdoardoTona.Rustino --version 0.4.2
                    
#r "nuget: EdoardoTona.Rustino, 0.4.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package EdoardoTona.Rustino@0.4.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=EdoardoTona.Rustino&version=0.4.2
                    
Install as a Cake Addin
#tool nuget:?package=EdoardoTona.Rustino&version=0.4.2
                    
Install as a Cake Tool

Rustino

Cross-platform native desktop windows with embedded web views, powered by Rust.

Inspired by Photino.NET.

Rustino replaces Photino's C++ native layer with Rust, using wry for the webview and tao for window management — the same libraries that power Tauri.

Architecture

Your .NET App
    └── RustinoWindow (C#)          ← Rustino.NET
            └── P/Invoke
                └── rustino_native   ← Rustino.Native (Rust cdylib)
                    ├── wry           → WebView2 (Windows)
                    ├── wry           → WKWebView (macOS)
                    └── wry           → WebKitGTK (Linux)

Installation

This fork of Ivy-Interactive/Rustino is published on NuGet as:

Package Contents
EdoardoTona.Rustino RustinoWindow and the native libraries for Windows, macOS and Linux (x64, ARM64)
EdoardoTona.Rustino.Reactive System.Reactive operators for the window events
EdoardoTona.Rustino.Blazor Blazor Hybrid host, replacing Photino.Blazor
dotnet add package EdoardoTona.Rustino

The namespaces stay Rustino.NET, Rustino.NET.Reactive and Rustino.Blazor.

Quick Start

using Rustino.NET;

var window = new RustinoWindow();
window
    .SetTitle("My App")
    .SetUseOsDefaultSize(false)
    .SetSize(1280, 800)
    .SetResizable(true)
    .Center()
    .Load(new Uri("https://example.com"));

window.WaitForClose();

Migrating from Photino

The API is identical. Replace the Photino.NET package with EdoardoTona.Rustino, then change two things:

- using Photino.NET;
+ using Rustino.NET;

- var window = new PhotinoWindow();
+ var window = new RustinoWindow();

All .Set*(), .Center(), .Load(), and .WaitForClose() calls remain the same. The types of ShowMessage follow the same rename: PhotinoDialogButtons, PhotinoDialogIcon and PhotinoDialogResult become RustinoDialogButtons, RustinoDialogIcon and RustinoDialogResult, with the same values.

For Blazor apps, replace Photino.Blazor with Rustino.Blazor:

- using Photino.Blazor;
+ using Rustino.Blazor;

- var builder = PhotinoBlazorAppBuilder.CreateDefault(args);
+ var builder = RustinoBlazorAppBuilder.CreateDefault(args);

API Reference

Configuration (pre-run)

Method Description
SetTitle(string) Set the window title
SetSize(int, int) Set window dimensions in pixels
SetMinSize(int, int) Set minimum window size
SetMaxSize(int, int) Set maximum window size
SetPosition(int, int) Set window position
SetUseOsDefaultSize(bool) Use OS default window size
SetResizable(bool) Allow/prevent window resizing
SetTopMost(bool) Keep window above all others
SetChromeless(bool) Remove window decorations (title bar, borders)
SetTransparent(bool) Enable transparent background
SetMaximized(bool) Start maximized
SetBackgroundColor(r, g, b, a) Set webview background color
SetIconFile(string) Set window icon from .ico/.png file path
SetIcon(Stream) Set window icon from a .NET stream (e.g. embedded resource)
Center() Center window on the primary monitor
SetDevToolsEnabled(bool) Enable the web inspector (right click › Inspect, F12 on Windows, OpenDevTools(); on macOS see Developer tools)
SetContextMenuEnabled(bool) The browser's context menu on right click (on by default)
SetBrowserControlsEnabled(bool) Windows: the browser's shortcuts, such as F5, Ctrl+R, Ctrl+F and Ctrl+P (on by default)
SetScrollBarStyle(ScrollBarStyle) Windows: Default or FluentOverlay, the overlay scroll bars of Windows 11
SetAcceptFirstMouse(bool) macOS: the click that activates the window also reaches the page
SetBackForwardGesturesEnabled(bool) Swipes go back and forward in the history (off by default)
SetFileDropEnabled(bool) Files dropped on the window raise FileDrop with their paths (see Webview)
SetJavascriptClipboardAccessEnabled(bool) Allow JS clipboard access
SetIgnoreCertificateErrorsEnabled(bool) Ignore SSL certificate errors
SetWebSecurityEnabled(bool) Enable/disable web security (CORS, etc.)
SetMediaAutoplayEnabled(bool) Allow media to autoplay
SetZoomHotkeysEnabled(bool) Enable Ctrl+/- zoom hotkeys
SetUserAgent(string) Set custom user agent string
SetUserDataFolder(string) Set webview data folder path
AddInitScript(string) Add JavaScript to run before page loads
RegisterCustomSchemeHandler(string, handler) Serve scheme:// requests from .NET (see Custom Schemes)
Load(Uri) / Load(string) Navigate to a URL or local file
LogVerbosity Set log verbosity (0 = silent)
SetLogger(ILogger) Route native and callback logs to a Microsoft.Extensions.Logging logger
SetApplicationId(string) Windows: the process AppUserModelID (taskbar grouping, pinning; pair it with RegisterNotificationAppId)

Load() creates the native instance. Creation settings other than LogVerbosity can still be changed after Load() and before the first WaitForClose(); Rustino applies them to the native configuration. Set LogVerbosity before Load() because it is passed to the native constructor. Settings that affect window or webview creation throw InvalidOperationException if changed after WaitForClose() starts. Runtime commands such as SetMenu(), ExecuteScript() and SendWebMessage() can be sent before WaitForClose() and are delivered in order once the window and webview exist.

Runtime (post-run)

Method Description
Minimize() Minimize the window
Maximize() Maximize the window
Restore() Restore from minimized/maximized
SetFullscreen(bool) Enter/exit fullscreen
SetVisible(bool) Show/hide the window
Focus() Bring focus to the window
Activate(string? activationToken = null) Show and restore the window, then request focus. On Linux, pass the launcher activation token when available.
Close() Close the window
RustinoWindow.OpenExternal(string) Open an http, https or mailto URL in the system's default application
ExecuteScript(string) Evaluate JavaScript in the webview
SendWebMessage(string) Post a message to the webview
SetZoom(double) Set webview zoom factor
Print() Open the system print dialog for the page
Reload() Reload the page (a page loaded from an HTML string is loaded again)
OpenDevTools() / CloseDevTools() Open or close the web inspector (needs SetDevToolsEnabled(true); closing isn't supported on Windows; on macOS see Developer tools)
ClearBrowsingData() Delete the cookies, the cache and the storage of every site
GetCookies(string?) / SetCookie(RustinoCookie) / DeleteCookie(RustinoCookie) Read and change the webview's cookies (see Webview)
SetBadgeCount(int?, string?, string?) Set taskbar/dock badge with optional bg/fg hex colors
ClearBadge() Remove the taskbar/dock badge
WaitForClose() Block until the window is closed; throws RustinoException if native window or webview creation fails
Dispose() Release native resources (RustinoWindow implements IDisposable)

Single Instance

Acquire the app id at the start of Main. The first process becomes primary; later processes forward their arguments and return with Status == Forwarded after the primary queues the launch:

using var single = RustinoSingleInstance.Acquire("com.company.product", new SingleInstanceOptions
{
    Arguments = args,
});
if (single.Status == SingleInstanceStatus.Forwarded) return;
if (single.Status == SingleInstanceStatus.ForwardFailed)
{
    Console.Error.WriteLine("The running instance did not respond.");
    return;
}

var window = new RustinoWindow().Load("wwwroot/index.html");
single.SecondInstanceStarted += (_, e) => OpenFiles(e.Args, e.WorkingDirectory);
single.MainWindow = window; // restores and activates after each forwarded launch
window.WaitForClose();
SingleInstanceOptions Default Description
Arguments the arguments of Main Sent to the primary by a later process
ForwardAutomatically true false returns Status == Secondary without forwarding: call ForwardToPrimary(args?, timeout?) yourself
Timeout 5 s How long to wait for a primary that is still starting (at most 5 minutes)
ActivateMainWindow true Activate MainWindow after each forwarded launch
Logger null Receives listener and handler failures

SecondInstanceEventArgs carries Args, WorkingDirectory, ProcessId and ActivationToken (Linux) of the later process. Exceptions thrown by the handlers are logged and raise UnhandledException. Disposing the instance stops listening, lets the handlers finish the launches already acknowledged, then releases the app id.

SecondInstanceStarted runs sequentially on a background thread, so UI frameworks must dispatch UI work to their own thread. Messages received before a handler is attached are held in a bounded queue. Set ActivateMainWindow to false in a handler when that launch should not activate the configured window. Linux launchers may provide XDG_ACTIVATION_TOKEN or DESKTOP_STARTUP_ID; Rustino forwards it to Activate() when it activates the window. On macOS, Finder and LaunchServices may reactivate a bundled app without starting another process; this API also covers direct executable launches.

After registering a custom scheme with the operating system or declaring it in the macOS app bundle, use AttachDeepLinks() to route URLs from the initial command line and later single-instance launches through UrlsOpened on the window thread:

window.UrlsOpened += (_, urls) =>
{
    foreach (var url in urls) HandleAfterValidation(url);
};
window.AttachDeepLinks(single, "notes");

RustinoDeepLinks.GetCurrent("notes") returns matching absolute URLs from the current process arguments. The helper filters only by the supplied scheme allow-list; it does not register the scheme or validate URL hosts, paths, OAuth state, or tokens. Treat every URL as untrusted input.

Splashscreen

A borderless window showing an image while the app starts; it closes on Close() or Dispose():

using (var splash = new RustinoSplashscreen("splash.png", width: 400, height: 400))
{
    LoadSettings(); // slow startup work
}

// From an embedded resource
using var splash = RustinoSplashscreen.FromImage(stream, 400, 300);

Custom Schemes

Serve content for a custom URL scheme directly from .NET (same signature as Photino). Register handlers before WaitForClose(); returning null produces a 404:

window.RegisterCustomSchemeHandler("app", (sender, scheme, url, out contentType) =>
{
    contentType = "text/html";
    return new MemoryStream("<h1>Hello from .NET</h1>"u8.ToArray());
});
window.Load("app://localhost/");

On Windows, WebView2 sees custom schemes as http://<scheme>.localhost/; handlers still receive the original <scheme>://... URLs.

Dialogs

Native cross-platform file dialogs (powered by rfd):

// Open file (single)
string[]? files = window.ShowOpenFileDialog(
    title: "Select an image",
    filters: [new FileFilter("Images", "jpg", "png", "gif")]);

// Open files (multi-select)
string[]? files = window.ShowOpenFileDialog(
    title: "Select files",
    multiSelect: true);

// Save file
string? path = window.ShowSaveFileDialog(
    title: "Save as",
    defaultPath: "document.pdf",
    filters: [new FileFilter("PDF", "pdf")]);

// Select folder
string[]? folders = window.ShowSelectFolderDialog(
    title: "Choose output directory");

All dialogs return null when canceled. File filters use the format new FileFilter("Name", "ext1", "ext2", ...).

Message boxes use the same signature as Photino's ShowMessage:

var answer = window.ShowMessage("Unsaved changes", "Save the document before closing?",
    RustinoDialogButtons.YesNoCancel, RustinoDialogIcon.Question);
if (answer == RustinoDialogResult.Yes) Save();

Buttons: Ok, OkCancel, YesNo, YesNoCancel, RetryCancel, AbortRetryIgnore. Icons: Info, Warning, Error, Question (shown as Info on macOS). Closing the box without a button returns Cancel, or Ok when Ok is the only button.

While the window runs, every dialog is modal for it: a sheet on macOS, owned by the window on Windows (which can't be clicked until the dialog closes), transient for it on Linux. They can be called from any thread, including the event handlers; before the window runs they have no parent. Message boxes use MessageBoxW on Windows (localized buttons), NSAlert on macOS and GtkMessageDialog on Linux; before the window runs, macOS shows a system alert instead of an NSAlert. On Linux file dialogs go through the XDG desktop portal.

Notifications

Native cross-platform toast notifications (powered by notify-rust):

// Static — no window instance required
RustinoWindow.ShowNotification("Download Complete", "Your file has been saved.");

// With icon (file path)
RustinoWindow.ShowNotification("Alert", "Something happened", iconPath: "/path/to/icon.png");

// With icon (embedded resource stream)
using var stream = Assembly.GetExecutingAssembly()
    .GetManifestResourceStream("MyApp.notify.png")!;
RustinoWindow.ShowNotification("Alert", "Something happened", stream);

Uses WinRT Toast (Windows), NSUserNotification (macOS), and D-Bus (Linux).

Windows: register your appId

On Windows, a toast is shown under the AppUserModelID passed as appId. Windows only displays toasts for an id it knows (a packaged app, a Start Menu shortcut carrying the id, or a registry registration) and silently drops the others, while ShowNotification still returns true. Without appId, notifications fall back to the Windows PowerShell id and show its name and icon.

Unpackaged apps (a plain .exe, a dotnet tool) should register their id once at startup:

RustinoWindow.RegisterNotificationAppId("MyCompany.MyApp", "My App", iconPath: @"C:\path\to\icon.png");
RustinoWindow.ShowNotification("Hello", "Shown as My App", appId: "MyCompany.MyApp");

This writes HKCU\Software\Classes\AppUserModelId\<appId> (display name and icon; no admin rights, no shortcut). The icon must be an image file (.png/.ico) that stays at that path; registering again without iconPath removes the previous icon. appId must be a valid AppUserModelID such as CompanyName.ProductName (at most 128 characters, no spaces or backslashes); otherwise the call returns false on every platform. On macOS and Linux a valid id is accepted without writing anything.

Native cross-platform application menus and context menus (powered by muda):

// Application menu bar
var menu = new RustinoMenu()
    .AddSubmenu("File", file => file
        .AddItem("new", "New", accelerator: "CmdOrCtrl+N", iconPath: "new.png")
        .AddItem("open", "Open...", accelerator: "CmdOrCtrl+O")
        .AddSeparator()
        .AddItem("exit", "Exit"))
    .AddSubmenu("Edit", edit => edit
        .AddPredefinedItem(PredefinedMenuItem.Undo)
        .AddPredefinedItem(PredefinedMenuItem.Redo)
        .AddSeparator()
        .AddPredefinedItem(PredefinedMenuItem.Copy)
        .AddPredefinedItem(PredefinedMenuItem.Paste, label: "Paste here")
        .AddSeparator()
        .AddCheckItem("wordwrap", "Word Wrap", isChecked: true, accelerator: "Alt+Z"))
    .AddWindowMenu("Window", win => win
        .AddPredefinedItem(PredefinedMenuItem.Minimize)
        .AddPredefinedItem(PredefinedMenuItem.Maximize))
    .AddHelpMenu("Help", help => help
        .AddItem("about", "About"));

window.SetMenu(menu);

// Context menu (right-click)
var ctx = new RustinoMenu()
    .AddItem("cut", "Cut")
    .AddItem("copy", "Copy")
    .AddItem("paste", "Paste");

window.ShowContextMenu(ctx);

// Handle clicks
window.MenuItemClicked += (_, id) => Console.WriteLine($"Clicked: {id}");

// A click toggles a check item: its new state comes after MenuItemClicked
window.MenuItemCheckedChanged += (_, e) => Console.WriteLine($"{e.Id}: {e.IsChecked}");

// Change items without rebuilding the menu
window.SetMenuItemEnabled("open", false);
window.SetMenuItemChecked("wordwrap", false);
window.SetMenuItemText("open", "Open Recent...");

// Remove menu bar
window.RemoveMenu();

The SetMenuItem* methods change every item with that id in the menu bar and in the tray menu, until the menu is set again. Items with the same id stay in sync when the user toggles one of them. A context menu is built from its RustinoMenu at every ShowContextMenu, so it always shows the values of the definition: build it when you show it, with the current values (for a check item, the state received from MenuItemCheckedChanged).

iconPath shows an image next to the label, scaled to the menu's icon size; if it can't be read, the item has no icon.

On macOS, the system lists the open windows in the menu added with AddWindowMenu and adds a search field to the one added with AddHelpMenu; on Windows and Linux both are normal submenus. Until SetMenu, macOS shows a standard menu bar: the application menu, Edit and Window.

Predefined items run a native OS action and don't raise MenuItemClicked. On macOS, a custom menu bar replaces the default one: include the predefined Edit items, otherwise Cmd+C/V/X/A stop working in the webview.

On macOS, SetMenu also prepends the standard application menu (About, Hide, Hide Others, Show All, Quit), shown with the app name, so your first submenu (e.g. File) stays visible. Replace it with AddAppMenu (ignored on Windows and Linux):

var menu = new RustinoMenu()
    .AddAppMenu(app => app
        .AddPredefinedItem(PredefinedMenuItem.About)
        .AddSeparator()
        .AddItem("settings", "Settings…", accelerator: "CmdOrCtrl+,")
        .AddSeparator()
        .AddPredefinedItem(PredefinedMenuItem.Quit))
    .AddSubmenu("File", file => file.AddItem("open", "Open..."));

The predefined About item opens the native About panel with the window icon and the values of SetAboutName, SetAboutVersion, SetAboutCopyright, SetAboutComments, AddAboutAuthor, SetAboutWebsite and SetAboutLicense (the macOS panel shows the last four as credits). Without SetAboutName, macOS uses the app name shown in the menu bar. The values are read when a menu is set: after changing them (or the icon) at runtime, call SetMenu again. For a fully custom About, use a normal item and handle MenuItemClicked.

Support depends on the platform (muda):

Predefined item macOS Windows Linux
About OK OK, no icon OK
Cut, Copy, Paste, SelectAll OK OK X11 only¹
Undo, Redo, Minimize, Maximize, Hide, CloseWindow, Quit OK OK omitted
Fullscreen, HideOthers, ShowAll, Services, BringAllToFront OK shown, does nothing omitted

"Omitted" items are not added to the menu. ¹ Clicking the item sends the shortcut through libxdo; on Wayland it does nothing (the keyboard shortcut still works in the webview).

On Windows, Ctrl+C/X/V/A/Z/Y go to the webview, which handles them natively, unless a custom item uses them: the item then replaces the native shortcut. If the menu also has the predefined item with the same shortcut (Copy, Cut, Paste, Select All, Undo, Redo), the shortcut stays with the webview.

System Tray

Native cross-platform system tray icon with optional context menu (powered by tray-icon):

// Tray icon with tooltip and context menu
var trayMenu = new RustinoMenu()
    .AddItem("show", "Show Window")
    .AddItem("hide", "Hide Window")
    .AddSeparator()
    .AddItem("quit", "Quit");

window.SetTrayIcon("icon.png", tooltip: "My App", menu: trayMenu);

// From embedded resource (Stream)
using var stream = Assembly.GetExecutingAssembly()
    .GetManifestResourceStream("MyApp.tray.png")!;
window.SetTrayIcon(stream, tooltip: "My App", menu: trayMenu);

// macOS: a monochrome template icon that follows the light or dark menu bar, text next to
// the icon, and the menu only on right click
window.SetTrayIcon("tray-template.png", tooltip: "My App", menu: trayMenu,
    title: "3", isTemplateIcon: true, menuOnLeftClick: false);
window.SetTrayTitle("4");

// Handle tray icon clicks
window.TrayIconClicked += (_, e) =>
{
    if (e.Button == TrayMouseButton.Left)
        window.SetVisible(true);
};

// Remove tray icon
window.RemoveTrayIcon();

TrayIconClicked is raised once per click, with the button and the cursor position in physical pixels. It's raised when the button is released, or when it's pressed if the click opens the tray menu (on macOS the menu takes the release). By default both left and right click open the menu: with menuOnLeftClick: false left clicks only raise TrayIconClicked.

A template icon uses only the image's alpha channel, so a colored icon turns into a solid silhouette: use it with a black shape on a transparent background.

macOS Windows Linux
TrayIconClicked OK OK never raised¹
tooltip OK OK not supported
title, SetTrayTitle OK not supported only on some desktops
isTemplateIcon OK ignored ignored
menuOnLeftClick OK OK ignored: any click opens the menu

¹ On Linux the tray icon is an AppIndicator (libayatana-appindicator), which doesn't report clicks: put every action in the tray menu.

Taskbar Badge

Set a numeric badge on the taskbar icon (Windows) or dock icon (macOS, Linux):

// Set badge with default colors (red background, white text)
window.SetBadgeCount(5);

// Set badge with custom colors (Windows only, hex format)
window.SetBadgeCount(5, background: "#4A154B", foreground: "#FFFFFF");

// Clear the badge
window.ClearBadge();

On Windows, this renders an overlay icon on the taskbar button using a 32px anti-aliased circle with bold Segoe UI text. Numbers above 99 display as "99+". The background and foreground parameters accept #RRGGBB hex strings and default to #E01E5A (red) and #FFFFFF (white).

On macOS, the native dockTile.setBadgeLabel API is used — color parameters are ignored as the OS controls badge appearance.

On Linux, the count goes to the dock through the Unity LauncherEntry D-Bus API (Ubuntu Dock, Dash to Dock, KDE Plasma). The dock finds the app by its .desktop file: <executable name>.desktop unless you call SetDesktopFileName("com.example.App.desktop").

Monitors

Enumerate connected displays with position, resolution, and DPI scale factor:

// Get all monitors
MonitorInfo[] monitors = window.GetMonitors();
foreach (var m in monitors)
    Console.WriteLine($"{m.Name}: {m.Width}x{m.Height} at ({m.X},{m.Y}), scale={m.ScaleFactor}, primary={m.IsPrimary}");

// Get the monitor containing this window
MonitorInfo? current = window.GetCurrentMonitor();

// DPI-aware positioning: center window on a specific monitor
var target = monitors.First(m => !m.IsPrimary);
var (w, h) = window.GetSize();
window.SetPosition(
    target.X + (target.Width - w) / 2,
    target.Y + (target.Height - h) / 2);

MonitorInfo properties: Name, X, Y, Width, Height, ScaleFactor, IsPrimary.

Native Window Features

window
    .SetTheme(WindowTheme.Dark)                     // title bar, native controls, prefers-color-scheme
    .SetMacTitleBarStyle(MacTitleBarStyle.Overlay)  // macOS: the page extends under the traffic lights
    .SetMacTrafficLightPosition(14, 12);

window.ThemeChanged += (_, theme) => Console.WriteLine($"Now {theme}");

// Later, while the window runs
window.SetProgressBar(ProgressBarState.Normal, 40);   // taskbar button / Dock icon
window.RequestUserAttention(UserAttentionType.Critical);

The platform limits below hold both before the window runs and while it runs.

Method / property Description
SetTheme(WindowTheme) System, Light or Dark. Windows applies it to the window (dark title bar) and to WebView2; macOS and Linux to the whole app
Theme Current theme, Light or Dark
ScaleFactor Physical pixels per logical pixel on the window's monitor
SetProgressBar(ProgressBarState, int?) / ClearProgressBar() Progress on the taskbar button (Windows), the Dock icon (macOS) or the dock icon (Linux, LauncherEntry: see Taskbar Badge)
RequestUserAttention(UserAttentionType) / CancelUserAttentionRequest() Flashes the taskbar button, bounces the Dock icon or sets the urgency hint, until the app is focused
Beep() Plays the system alert sound (MessageBeep, NSBeep, the GDK display bell)
SetShadow(bool) Window shadow (Windows: chromeless windows; macOS: all windows)
SetSkipTaskbar(bool) No taskbar button, for apps that live in the tray. On macOS the app leaves the Dock and the app switcher
SetContentProtection(bool) Keeps the window out of screenshots and recordings (Windows, macOS)
SetVisibleOnAllWorkspaces(bool) Shows the window on every virtual desktop (macOS, Linux)
SetClosable(bool) / SetMinimizable(bool) / SetMaximizable(bool) Enable the title bar buttons. Linux: only SetClosable, as a request the window manager may ignore; minimize and maximize can't be disabled
SetAlwaysOnBottom(bool) Keeps the window below the others, replacing SetTopMost. Linux: a request to the window manager, not supported on Wayland
SetIgnoreCursorEvents(bool) Mouse clicks go through the window, for overlays
SetMacTitleBarStyle(MacTitleBarStyle) macOS: Default, Transparent or Overlay (no title, the page under the traffic lights, like Slack or VS Code). SetChromeless(false) brings the style back
SetTitleBarOverlayColor(byte, byte, byte, byte) / ResetTitleBarOverlayColor() Color of the overlay title bar buttons, over the page's CSS
SetTitleBarOverlay(bool) Windows, Linux: the page extends to the top of the window, under minimize, maximize and close buttons drawn by Rustino, like VS Code (see below)
SetMacTrafficLightPosition(double, double) macOS: position of the traffic lights, in logical pixels
SetDesktopFileName(string) Linux: .desktop file of the app, for the dock badge and progress
SetDragRegionsEnabled(bool) Pre-run: drag regions and edge resizing from the page (on by default, see below)
DragWindow() / DragResizeWindow(ResizeDirection) Move or resize the window with the mouse (call them while the left button is down)
Event Args Description
ThemeChanged WindowTheme The theme changed, by the system or by SetTheme
ScaleFactorChanged ScaleFactorChangedEventArgs The window moved to a monitor with another scale (.ScaleFactor, .Width, .Height)
UrlsOpened string[] Deep links routed by AttachDeepLinks (all platforms); on macOS also files and URLs opened through the app bundle (file associations and URL schemes)
Reopened bool macOS: Dock icon clicked; false when no window is visible, e.g. hidden in the tray

Each event also has an observable: WhenThemeChanged, WhenScaleFactorChanged, WhenUrlsOpened, WhenReopened.

On Linux with the X11 backend WebKitGTK restyles the page when the theme changes, but doesn't fire the change event of matchMedia('(prefers-color-scheme: dark)'): scripts that need to know should listen to ThemeChanged instead.

Drag regions for chromeless windows

The elements with data-rustino-drag-region move the window, and a double click maximizes it. The attribute applies to the element itself, not to its children, so the buttons of a custom title bar stay clickable:

<header data-rustino-drag-region>
  <span data-rustino-drag-region>My App</span>
  <button onclick="window.ipc.postMessage('close')">✕</button>
</header>

On Windows and Linux the page covers the resize borders of chromeless windows: its outer 6 pixels resize the window instead. macOS keeps its own resize borders. With MacTitleBarStyle.Overlay the page's title bar needs the attribute too.

Overlay title bar on Windows and Linux

SetTitleBarOverlay(true) removes the native title bar and draws the minimize, maximize and close buttons over the page, in the top corner, like VS Code: Windows 11 caption buttons on Windows, GNOME round buttons on Linux (placed and filtered by the GTK gtk-decoration-layout setting). The buttons follow SetMinimizable, SetMaximizable, SetClosable and SetResizable, turn into restore when the window is maximized and go away in fullscreen; close raises Closing like the native button. The window keeps resizing from the page's edges, as a chromeless window.

The page lays out its own title bar in the room left by the buttons, with CSS variables set on <html> only while the buttons show:

header {
  height: var(--rustino-titlebar-height, 32px);   /* 32px on Windows, 40px on Linux */
  padding-left: var(--rustino-window-controls-left, 0px);
  padding-right: var(--rustino-window-controls-right, 0px);
}

The safe area of the title bar, the strip free of buttons, has the variables of the web's Window Controls Overlay (env(titlebar-area-*)): --rustino-titlebar-area-x, --rustino-titlebar-area-y, --rustino-titlebar-area-width and --rustino-titlebar-area-height, for content placed there directly:

.search {
  position: fixed;
  left: var(--rustino-titlebar-area-x, 0px);
  top: var(--rustino-titlebar-area-y, 0px);
  width: var(--rustino-titlebar-area-width, 100%);
  height: var(--rustino-titlebar-area-height, 32px);
}

The buttons are black or white after prefers-color-scheme (the theme of the window, see SetTheme), not after the page's colors: a page with its own palette sets --rustino-window-controls-color in its CSS, and SetTitleBarOverlayColor sets one from the host, over the CSS and across page loads (handy with Blazor Hybrid); ResetTitleBarOverlayColor goes back to the page's. They need drag regions (SetDragRegionsEnabled), and on Windows 11 the Snap Layouts flyout doesn't open from the maximize button (Win+Z still works). On macOS the option is ignored: use SetMacTitleBarStyle(MacTitleBarStyle.Overlay).

The drag region script talks to the native side with window.ipc.postMessage messages that start with __rustino: (like the print script on Windows and macOS, see Webview): they never reach WebMessageReceived, so don't use that prefix for your own messages. Any page loaded in the window can send them, and so move, resize, maximize, minimize or close it: if the window shows untrusted pages, call SetDragRegionsEnabled(false) before WaitForClose() (no script, and the __rustino: messages reach WebMessageReceived like the others, except __rustino:print, which only prints).

Tray apps on macOS

A click on the Dock icon raises Reopened: show the window again when it was hidden in the tray.

window.Reopened += (_, hasVisibleWindows) =>
{
    if (!hasVisibleWindows) window.SetVisible(true).Focus();
};

Webview

Options and events that make the page behave like an app rather than a browser:

window
    .SetContextMenuEnabled(false)                   // no browser menu on right click
    .SetBrowserControlsEnabled(false)               // Windows: no F5, Ctrl+R, Ctrl+F, Ctrl+P
    .SetScrollBarStyle(ScrollBarStyle.FluentOverlay) // Windows 11 scroll bars
    .SetFileDropEnabled(true);

// Files dropped on the window, with their full paths (HTML5 drag and drop only gives the names)
window.FileDrop += (_, e) =>
{
    if (e.Type == FileDropEventType.Drop)
        OpenFiles(e.Paths);
};

// The page's <title> in the title bar
window.DocumentTitleChanged += (_, title) => window.SetTitle(title);

// Downloads: by default the user chooses where to save each file in the native save dialog
window.DownloadStarting += (_, e) =>
{
    if (e.Url.EndsWith(".exe")) e.Cancel = true;                 // refuse it
    else if (saveToDownloads) e.DestinationPath = e.SuggestedPath; // save without asking
};
window.DownloadCompleted += (_, e) => Console.WriteLine(e.Success ? $"Saved {e.Path}" : $"Not saved: {e.Url}");

Without SetContextMenuEnabled(false) right click opens the browser's menu. On macOS and Linux turning it off adds an init script that prevents the default of contextmenu events: the page still gets them, e.g. to call ShowContextMenu. SetBrowserControlsEnabled(false) leaves the menu accelerators and the page's own shortcuts working; WKWebView and WebKitGTK have no browser shortcuts.

File drop. With SetFileDropEnabled(true), FileDrop reports Enter (with the paths), Over, Drop (with the paths) and Leave; positions are in logical (CSS) pixels from the top-left corner of the page. The page doesn't get the dropped files, and the webview doesn't navigate to them. Drags that carry no files (e.g. elements dragged within the page) stay with the page on macOS and Linux; on Windows the webview stops receiving drops altogether, so the page's HTML5 drag and drop doesn't work while file drop is on (a WebView2 limitation of wry).

Downloads. DownloadStarting runs before the file is written: SuggestedPath is where a browser would save it (the Downloads folder, with a name that doesn't replace an existing file). Leave DestinationPath null to let the user choose in the save dialog (modal for the window), set it to save there without asking (an existing file is replaced), or set Cancel. Don't show dialogs from this handler. The file is downloaded to a temporary folder and moved to its destination when complete, so the save dialog can stay open while it downloads. DownloadCompleted reports every download that DownloadStarting let through: Path is null when the download failed or the user canceled the dialog. Without these events WKWebView wouldn't download at all; WebView2 no longer shows its download bar, nor asks before a page downloads more than one file. WKWebView and WebKitGTK start only the last of several downloads that the same script starts (e.g. consecutive click() calls on <a download> links): let a moment pass between them, e.g. with setTimeout.

Cookies. GetCookies() returns all the cookies, GetCookies(url) those sent to that URL; SetCookie adds or replaces a cookie (same name, domain and path), DeleteCookie removes it. RustinoCookie has Name, Value, Domain, Path, Expires, Secure, HttpOnly and SameSite (Strict, Lax, None):

window.SetCookie(new RustinoCookie("session", token) { Domain = "example.com", Path = "/", Secure = true, HttpOnly = true });
var session = window.GetCookies("https://example.com/").FirstOrDefault(c => c.Name == "session");

On Windows, WebView2 applies SetCookie and DeleteCookie a moment later: a GetCookies right after them can still return the previous cookies. On Windows, GetCookies also can't run within a webview event (WebMessageReceived, Navigating, PageLoaded, ...), where it throws InvalidOperationException: WebView2 answers only after the event returns. Call it afterwards, e.g. var cookies = await Task.Run(() => window.GetCookies()); in an async handler.

Printing. Print() and the page's window.print() open the system print dialog on every platform. On Windows and macOS an init script sends window.print() to it: WebView2 would show its print preview inside the window, cut by small windows (the system dialog of Windows has no preview), and WKWebView would do nothing.

Developer tools. With SetDevToolsEnabled(true), right click › Inspect (and F12 on Windows) open the web inspector, and so does OpenDevTools(): in its own window on Windows, docked in the window on Linux. On macOS the inspector inside the app needs a private WebKit API, which can get an app rejected from the Mac App Store, so the native library includes it only when built with the devtools feature (see Building from Source). Without it, SetDevToolsEnabled(true) makes the page inspectable from Safari's Develop menu (macOS 13.3 or later), while OpenDevTools() and CloseDevTools() do nothing.

State Queries

Property Description
IsMinimized Whether the window is minimized
IsMaximized Whether the window is maximized
IsFullscreen Whether the window is in fullscreen
GetPosition() Returns (X, Y) position
GetSize() Returns (Width, Height) size
GetMonitors() Returns all connected MonitorInfo[]
GetCurrentMonitor() Returns MonitorInfo? for the monitor containing the window

Events

Event Args Description
WindowClosing CancelEventArgs Fired before close (set Cancel = true to prevent)
WindowClosed EventArgs Fired after the window is destroyed
UnhandledCallbackException NativeCallbackExceptionEventArgs A handler, observable observer, or logger threw while Rustino.Native was calling into .NET (.CallbackName, .Exception)
SizeChanged SizeEventArgs Fired on resize (.Width, .Height)
LocationChanged PointEventArgs Fired on move (.X, .Y)
FocusChanged bool Fired on focus/blur
WebMessageReceived string Fired when JS calls window.ipc.postMessage(msg)
WebMessageReceivedWithSource WebMessageEventArgs Same messages with the sending page's URL (.Message, .SourceUrl): check it if the webview can navigate to other sites
PageLoaded PageLoadEventArgs Fired on page load start/finish (.IsStarted, .Url)
Navigating NavigationEventArgs Fired before navigation (.Url, set Cancel = true to block)
MenuItemClicked string Fired when a menu item is clicked (the item's ID)
MenuItemCheckedChanged MenuItemCheckedEventArgs Fired after MenuItemClicked when the user toggles a check item (.Id, .IsChecked)
TrayIconClicked TrayIconClickedEventArgs Fired once per click on the tray icon (.Button, .X, .Y); never on Linux
FileDrop FileDropEventArgs Files dragged over the window and dropped, with their full paths (.Type, .Paths, .X, .Y); needs SetFileDropEnabled(true)
DocumentTitleChanged string The page's <title> changed
DownloadStarting DownloadStartingEventArgs A download starts (.Url, .SuggestedPath; set .DestinationPath or .Cancel)
DownloadCompleted DownloadCompletedEventArgs A download ended (.Url, .Path, .Success)

Exceptions thrown from a native callback are caught before they can cross the Rust/.NET boundary. Rustino logs them through the configured ILogger and raises UnhandledCallbackException; exceptions thrown by that event are also caught. A closing-handler exception allows the window to close, a navigation-handler exception allows the navigation, and a download-starting exception cancels the download. A custom-scheme handler exception leaves the response empty, which Rustino returns as HTTP 404.

Observable Streams (IObservable<T>)

All events are also available as IObservable<T> properties for reactive programming (no System.Reactive dependency required):

Property Type Description
WhenSizeChanged IObservable<(int Width, int Height)> Size change stream
WhenLocationChanged IObservable<(int X, int Y)> Position change stream
WhenFocusChanged IObservable<bool> Focus/blur stream
WhenWebMessageReceived IObservable<string> JS message stream
WhenWebMessageReceivedWithSource IObservable<WebMessageEventArgs> JS message stream with the sending page's URL
WhenPageLoaded IObservable<PageLoadEventArgs> Page load stream
WhenNavigating IObservable<NavigationEventArgs> Navigation stream
WhenWindowClosed IObservable<EventArgs> Window closed stream
WhenMenuItemClicked IObservable<string> Menu item click stream
WhenMenuItemCheckedChanged IObservable<MenuItemCheckedEventArgs> Check item toggle stream
WhenTrayIconClicked IObservable<TrayIconClickedEventArgs> Tray icon click stream
WhenFileDrop IObservable<FileDropEventArgs> File drag and drop stream
WhenDocumentTitleChanged IObservable<string> Document title stream
WhenDownloadStarting IObservable<DownloadStartingEventArgs> Download start stream
WhenDownloadCompleted IObservable<DownloadCompletedEventArgs> Download end stream

All streams complete automatically when the window closes or is disposed.

Rustino.NET.Reactive (companion package)

For System.Reactive operators, add the EdoardoTona.Rustino.Reactive package:

using System.Reactive.Linq;
using Rustino.NET.Reactive;

// Throttled resize
window.WhenSizeChangedThrottled(TimeSpan.FromMilliseconds(200))
    .Subscribe(size => Console.WriteLine($"{size.Width}x{size.Height}"));

// Message routing by prefix
window.WhenWebMessageWithPrefix("cmd:")
    .Subscribe(cmd => HandleCommand(cmd));

// Page load completion only
window.WhenPageLoadCompleted()
    .Subscribe(e => Console.WriteLine($"Loaded: {e.Url}"));

// Throttled move events
window.WhenLocationChangedThrottled(TimeSpan.FromMilliseconds(100))
    .Subscribe(pos => Console.WriteLine($"Moved to {pos.X},{pos.Y}"));

// Distinct focus changes
window.WhenFocusChangedDistinct()
    .Subscribe(focused => Console.WriteLine($"Focus: {focused}"));

Blazor Hybrid (Rustino.Blazor)

The EdoardoTona.Rustino.Blazor package hosts Razor components in a Rustino window, like Photino.Blazor. Components run in .NET and render into the native webview; the app is served from app://localhost/.

using Rustino.Blazor;

var builder = RustinoBlazorAppBuilder.CreateDefault(args);
builder.Services.AddSingleton<MyService>();   // regular dependency injection
builder.RootComponents.Add<App>("#app");

var app = builder.Build();
app.MainWindow.SetTitle("My Blazor App");     // the underlying RustinoWindow
app.Run();

Use the Microsoft.NET.Sdk.Razor SDK and put a host page in wwwroot/index.html:

<!DOCTYPE html>
<html>
<head>
    <base href="/" />
</head>
<body>
    <div id="app">Loading…</div>
    <script src="_framework/blazor.webview.js"></script>
</body>
</html>

Inside components:

  • @inject RustinoWindow Window gives access to the whole native API (dialogs, notifications, menus, window state, …).
  • Native events (SizeChanged, MenuItemClicked, …) are raised outside Blazor's dispatcher: update state with InvokeAsync(StateHasChanged).
  • IJSRuntime and [JSInvokable] work as in any Blazor app.
  • @inject HttpClient Http reads app files (e.g. Http.GetStringAsync("data.json"); missing files are 404 Not Found) and forwards other requests to the network.
  • Only the app's own pages (app://localhost/) can talk to the components: messages from other sites the webview navigates to, or from frames of other origins embedded in the app, are ignored.

Static files come from wwwroot next to the executable (published apps) or from the project during development. Pass an IFileProvider to CreateDefault to serve them from somewhere else (e.g. embedded resources).

See src/Rustino.Samples.Blazor for a complete example.

Building from Source

Prerequisites

  • Rust toolchain (1.80+)
  • .NET SDK (10.0+)
  • Windows: WebView2 runtime (pre-installed on Windows 10/11)
  • macOS: Xcode Command Line Tools
  • Linux: libgtk-3-dev libwebkit2gtk-4.1-dev

Build

# Build the Rust native library
cd src/Rustino.Native
cargo build --release
# ...or, on macOS, with the web inspector inside the app (private WebKit API, see Developer tools)
cargo build --release --features devtools

# Build the .NET wrapper
cd ../Rustino.NET
dotnet build

# Run a sample
cd ../Rustino.Samples
dotnet run

# Run the Blazor sample
cd ../Rustino.Samples.Blazor
dotnet run

Samples

  • src/Rustino.Samples: a tabbed showcase of the window API — window state and chrome, JS interop, dialogs and message boxes, menus and tray, notifications, badges, monitors, reactive streams, native window features, webview (downloads, file drop, cookies, printing, devtools) and, in the Launch tab, single instance, deep links, Activate() and OpenExternal(). Run it a second time, e.g. dotnet run -- rustino-sample://open?id=42, to see the launch forwarded to the first process.
  • src/Rustino.Samples.Blazor: Blazor Hybrid with the app:// custom scheme, dependency injection and HttpClient.

Interop contract tests

The .NET tests compare every Rust C export with the P/Invoke imports, including callback signatures and platform gating. They also compare the #[repr(C)] layouts with the Rust-generated snapshot and check that each import exists in the built native library for the current platform:

dotnet test src/Rustino.NET.Tests/Rustino.NET.Tests.csproj -c Release

Build src/Rustino.Native with cargo build --release first to enable the binary export check. The Rust ABI snapshot is checked by cargo test --release; after an intentional ABI layout change, update it with:

cd src/Rustino.Native
UPDATE_ABI_SNAPSHOT=1 cargo test --release abi_layout

Releasing

.github/workflows/release.yml builds the native library for the six platforms, packs the three packages and pushes them to NuGet.org through trusted publishing (a short-lived key obtained with GitHub OIDC; the NUGET_USER secret holds the nuget.org profile name). It runs when a GitHub release is published (the version is the tag without the leading v, e.g. v0.4.0 → 0.4.0) and attaches the packages to the release; run it manually to publish the <Version> of src/Directory.Build.props.

Cross-Platform Support

Platform WebView Engine Native Library
Windows x64/ARM64 WebView2 (Chromium) rustino_native.dll
macOS x64/ARM64 WKWebView (WebKit) librustino_native.dylib
Linux x64/ARM64 WebKitGTK librustino_native.so

License

MIT — see LICENSE. Based on Rustino by Ivy Interactive.

Inspired by and API-compatible with Photino, originally created by TryPhotino (Apache-2.0).

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on EdoardoTona.Rustino:

Package Downloads
EdoardoTona.Rustino.Blazor

Blazor Hybrid for Rustino.NET: host Razor components in a native Rustino window. Drop-in replacement for Photino.Blazor.

EdoardoTona.Rustino.Reactive

System.Reactive extensions for Rustino.NET window events.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.4.2 134 9/29/2026
0.4.1 121 9/28/2026
0.4.0 110 9/28/2026