Skip to content
 
 

Repository files navigation

PhotinoX Logo

PhotinoX

NuGet Version Build License NuGet Downloads

Lightweight .NET wrapper for building desktop applications with native OS WebViews:

  • Windows: WebView2
  • macOS: WKWebView
  • Linux: WebKitGTK 4.1

PhotinoX is a maintained fork of Photino.NET positioned between Electron/Tauri-style WebView desktop runtimes and traditional .NET UI frameworks such as WPF, Avalonia, and .NET MAUI. It focuses on predictable cross-platform desktop behavior, native runtime stability, and a cleaner managed API surface.

What is PhotinoX?

PhotinoX builds on the original Photino design: native desktop windows hosted by modern Web UI technologies (Blazor, React, Vue, Angular, etc.), without bundling a full Chromium runtime.
It relies entirely on OS-native WebView engines, keeping apps small and efficient.

The core model is native-first: PhotinoX.Native owns the platform windowing and WebView integration, while the .NET layer exposes that native runtime through PhotinoApplication, PhotinoDispatcher, and PhotinoWindow.

.NET managed API
    ↓
PhotinoApplication / PhotinoDispatcher / PhotinoWindow
    ↓
PhotinoX.Native
    ↓
OS-native WebView
    ├── Windows: WebView2
    ├── macOS: WKWebView
    └── Linux: WebKitGTK 4.1

Note: PhotinoX is an independent fork of tryphotino/photino.NET under the Apache-2.0 license and is not affiliated with the original project or organization.

How PhotinoX differs from Photino.NET

Compared with the original Photino.NET managed API, PhotinoX introduces an explicit PhotinoApplication model, centralized UI-thread dispatching through PhotinoDispatcher, application-owned notifications, simplified window event names, native-driven window state tracking, and explicit application and window lifecycle APIs.

Application model

PhotinoApplication is the explicit application lifetime object. Window creation, shutdown behavior, and UI-thread dispatching are coordinated through the application and its dispatcher instead of implicit global state. The application also tracks open windows and exposes MainWindow and Windows.

Previous model New model
PhotinoWindow.WaitForClose() creates the window and starts the message loop. PhotinoApplication.Run(window) owns application lifetime and message-loop execution.
Window creation and message-loop state are controlled from PhotinoWindow. PhotinoApplication.Run(window) shows the main window; explicit window creation/showing is available through PhotinoWindow.Show().
Window lifetime is centered around individual PhotinoWindow instances. PhotinoApplication tracks open windows through MainWindow and Windows.
PhotinoWindow.Invoke(...) dispatches through window-level invoke helpers. UI-thread dispatching is centralized through PhotinoApplication.Dispatcher, including CheckAccess, Invoke, TryInvoke, BeginInvoke, and InvokeAsync.
Notification display is tied to window-level APIs. Notifications are exposed through PhotinoApplication, with application-level enabled state and notification events.
Shutdown behavior is implicit around the native message loop. Shutdown behavior is controlled by PhotinoShutdownMode, PhotinoApplication.Shutdown(...), and ShutdownRequested.

Notable application lifecycle APIs:

Area API
Lifecycle Run, Shutdown, Startup, ShutdownRequested, Exit
Shutdown behavior ShutdownMode, ShutdownRequestedEventArgs, PhotinoShutdownRequestReason
Notifications ShowNotification, NotificationsEnabled, notification activation/dismissal/failure events
var app = new PhotinoApplication();

var window = new PhotinoWindow()
    .SetTitle("PhotinoX")
    .Load("index.html")
    .RegisterClosingHandler((_, e) => e.Cancel = true);

return app.Run(window);

On Windows, PhotinoApplication.Run() performs native window initialization and runs the message loop on an STA thread when the calling thread is not an STA thread.

PhotinoDispatcher provides application-level UI-thread dispatching. Invoke(...) executes work on the dispatcher thread and throws when scheduling fails, while TryInvoke(...) returns false for scheduling failures. BeginInvoke(...) reports scheduling success as bool, and InvokeAsync(...) returns a task that completes when the dispatched callback completes, is canceled, or faults if scheduling fails. Cancellation-aware overloads accept CancellationToken, and async callback overloads use ValueTask / ValueTask<TResult> for allocation-friendly completion paths. State-based overloads are available to avoid closure captures.

ShutdownRequested is raised for cancellable shutdown requests. ShutdownRequestedEventArgs.Reason identifies application requests, Windows session logoff, Windows system shutdown/restart, or an unknown platform shutdown source. Canceling Windows session shutdown requests may prevent logoff or system shutdown.

PhotinoApplication also owns native notification integration. Notifications are shown through the application surface instead of individual windows, with enable/disable state, optional user state, and notification events for activation, action activation, input activation, dismissal, and asynchronous failure.

ShowNotification(...) returns a positive notification identifier when the request is accepted. It returns 0 when the notification is skipped by policy or application state, -1 for invalid or untracked requests, -2 when native notification backend initialization fails, and -3 when native notification display fails synchronously.

PhotinoApplication.GetRuntimeInfo() exposes runtime diagnostics for the current application process, including OS, .NET, PhotinoX native version, WebView engine, WebView runtime version, and platform-specific runtime details. See Samples/RuntimeDiagnostics for a minimal diagnostics sample.

Window events

Window event names are simplified to remove redundant Window prefixes and align better with common .NET event naming. Closing now uses standard CancelEventArgs, focus events are exposed as Activated and Deactivated, and window state events are driven by actual native state transitions instead of transient resize messages.

Photino.NET API New API
WindowCreating Creating
WindowCreated Created
WindowClosing Closing
- Closed
WindowLocationChanged LocationChanged
WindowSizeChanged SizeChanged
WindowFocusIn Activated
WindowFocusOut Deactivated
WindowMaximized Maximized
WindowRestored Restored
WindowMinimized Minimized
- FullScreenEntered
- FullScreenExited
- StateChanged
- NavigationStarting
- NewWindowRequested
- ContentLoading
- ContentLoaded
- InitialContentLoaded

Closing now uses EventHandler<CancelEventArgs>; set CancelEventArgs.Cancel to cancel the close operation.

LocationChanged, SizeChanged, and WebMessageReceived now use strongly typed event payload records instead of raw Point, Size, and string values. WebMessageReceivedEventArgs includes both the message and the top-level WebView URI at the time the message was received.

NavigationStarting is raised before the WebView starts navigating to top-level content. Set NavigationStartingEventArgs.Cancel to cancel the current-window navigation.

NewWindowRequested is raised when WebView content requests opening content in a new window, such as through target="_blank" links or window.open(...). PhotinoX does not create browser-controlled popup windows. Applications can handle this event and open the requested URI externally if needed.

ContentLoading, ContentLoaded, and InitialContentLoaded are available for observing top-level WebView content loading. ContentLoading is raised when top-level content starts loading after navigation has committed. ContentLoaded is raised after each completed top-level content load, while InitialContentLoaded is raised once after the initial top-level content load completes. These events do not indicate that a JavaScript framework, SPA route, Blazor component tree, or all asynchronous page work has finished rendering.

Previous registration helper New registration helper
RegisterWindowCreatingHandler(...) RegisterCreatingHandler(...)
RegisterWindowCreatedHandler(...) RegisterCreatedHandler(...)
RegisterWindowClosingHandler(...) RegisterClosingHandler(...)
RegisterFocusInHandler(...) RegisterActivatedHandler(...)
RegisterFocusOutHandler(...) RegisterDeactivatedHandler(...)
- RegisterClosedHandler(...)
- RegisterFullScreenEnteredHandler(...)
- RegisterFullScreenExitedHandler(...)
- RegisterStateChangedHandler(...)
- RegisterNavigationStartingHandler(...)
- RegisterNewWindowRequestedHandler(...)
- RegisterContentLoadingHandler(...)
- RegisterContentLoadedHandler(...)
- RegisterInitialContentLoadedHandler(...)

Window API

PhotinoWindow now uses explicit Show()-based window creation, unified WindowState tracking, explicit lifecycle state, and simplified lifecycle events.

Previous API New API / direction
WaitForClose() PhotinoApplication.Run(window) for application startup; PhotinoWindow.Show() for explicit window creation/showing.
LoadRawString(...) LoadString(...)
Windows-only WindowHandle Platform-specific handle: HWND, GtkWidget*, or NSWindow*
No explicit closed state IsClosed
No explicit initialization state IsInitialized
FullScreen, Maximized, and Minimized properties Unified native-driven WindowState with Normal, Minimized, Maximized, and FullScreen
TemporaryFilesPath UserDataFolder
SetTemporaryFilesPath(...) SetUserDataFolder(...)

Notable window lifecycle and API changes in PhotinoX:

Area API
Window lifecycle Show, Activate, BringToFront
Window state model WindowState, StateChanged
Window state commands Maximize, Minimize, Restore, SetWindowState
Existing state helpers SetFullScreen, SetMaximized, SetMinimized
Chromeless window helpers BeginWindowDrag, BeginWindowResize
Linux chromeless native hit-test settings SetLinuxChromelessDragRegion, SetLinuxChromelessDragRegions, SetLinuxChromelessResizeBorderThickness, LinuxChromelessSettings
Window/platform state IsInitialized, IsClosed, cross-platform WindowHandle

WindowState replaces the previous FullScreen, Maximized, and Minimized properties with a single state model. It supports Normal, Minimized, Maximized, and FullScreen, and is also used for startup state configuration.

MainMonitor represents the monitor that currently contains the native window. Monitors enumerates all available monitors and does not define which monitor is considered current.

Window state tracking is native-driven: StateChanged, Maximized, Minimized, Restored, FullScreenEntered, and FullScreenExited are raised from actual state transitions, not from transient resize messages. This avoids duplicate or misleading Restored notifications during operations such as resizing or minimizing from fullscreen.

Maximize(), Minimize(), and Restore() are new command-style APIs. Existing helpers such as SetFullScreen(...), SetMaximized(...), and SetMinimized(...) remain available and update the same underlying native state.

Chromeless windows can use BeginWindowDrag() and BeginWindowResize(...) for custom title bar and resize implementations on Windows and macOS. On Linux, native chromeless drag and resize use GTK hit testing because Wayland requires the originating native pointer event.

Use LinuxChromelessSettings or SetLinuxChromelessDragRegion(...) to configure the initial drag region. SetLinuxChromelessDragRegion(...) can also replace the current drag region after native window initialization.

For dynamic or complex title bars, use SetLinuxChromelessDragRegions(...) to replace all drag and no-drag regions:

window.SetLinuxChromelessDragRegions(
    dragRegions:
    [
        new LayoutRegion(
            width: 0,
            height: 44,
            horizontalAlignment: HorizontalAlignment.Stretch)
    ],
    noDragRegions:
    [
        new LayoutRegion(
            width: 120,
            height: 32,
            margin: new Thickness(left: 12, top: 6, right: 0, bottom: 0))
    ]);

No-drag regions take precedence over drag regions. Alignment and margins are resolved against the current WebView client area, allowing regions to adapt when the window is resized. Each call replaces all previously configured drag and no-drag regions.

Use SetLinuxChromelessResizeBorderThickness(...) to change the native resize border independently without replacing the current drag regions. These Linux-specific APIs are ignored on Windows and macOS.

Custom schemes and startup content

Custom scheme registration is stricter and more predictable. Scheme names are validated, and reserved schemes such as http, https, and file are rejected.

Startup content selection is explicit: Load(...) sets URL content and clears raw string content, while LoadString(...) sets raw string content and clears URL content.

Load(string) treats only explicit URI strings such as http://, https://, file://, and registered custom schemes as URI navigation. Other strings are resolved as local file paths.

Area Behavior
RegisterCustomSchemeHandler(...) Validates scheme names and rejects reserved schemes.
Managed custom scheme responses Response data is backed by native-owned memory for safer managed/native interop.
Load(...) Sets startup URL content and clears raw string content.
LoadString(...) Sets raw string content and clears startup URL content.

WebView runtime behavior

On Windows, UserDataFolder specifies the WebView2 user data folder used by the WebView2 runtime. It is used for browser profile data such as cookies, permissions, cache, local storage, IndexedDB, and related WebView2 state.

TemporaryFilesPath and SetTemporaryFilesPath(...) were renamed to UserDataFolder and SetUserDataFolder(...) to match the actual WebView2 behavior.

PhotinoX reuses an existing WebView2 environment when the requested Windows WebView2 configuration is compatible with an environment that has already been created.

StatusBarEnabled controls the embedded WebView status bar where supported. On Windows, it maps to WebView2 IsStatusBarEnabled and can disable the bottom-left link hover URL overlay. On macOS and Linux, the option is stored but currently has no native effect.

Compatibility

These changes may require source-level updates for applications that use older Photino.NET event names, WaitForClose()-based startup, focus-in/focus-out event handlers, old bool-returning close handlers, direct Point / Size / string event payloads, older WebMessageReceived handlers that only expected a message string, or the previous separate fullscreen/maximized/minimized state model now replaced by WindowState.

Native runtime foundation

The managed API is built on the updated PhotinoX.Native runtime, including safer native memory ownership, clearer platform isolation, improved interop layout, an application-oriented message-loop and dispatch model, native notification integration, and unified native window state tracking.

On Windows, fullscreen is handled as a native restore-aware state transition: the previous window style and placement are preserved before entering fullscreen and restored when leaving fullscreen. Startup state is synchronized without raising user callbacks before window creation completes.

On Linux Wayland, top-level window position is compositor-controlled. Move notifications and position restore are best-effort; state and size tracking remain supported. Chromeless drag and resize use native GTK event-driven hit testing configured from the managed Linux chromeless settings.

Core (ecosystem)


Install

dotnet add package PhotinoX

PhotinoX.Native provides the native WebView host binaries and must be available for the target runtime identifier.

Package targets net8.0; net9.0; net10.0.

Samples

See real, working examples here:

Original Photino concept docs: https://docs.tryphotino.io/

Requirements

Build from source

dotnet restore Photino.NET/PhotinoX.csproj
dotnet build   Photino.NET/PhotinoX.csproj -c Release
dotnet pack    Photino.NET/PhotinoX.csproj -c Release -o artifacts

CI: see .github/workflows/build.yml (build + pack + upload .nupkg/.snupkg).

Contributing

Issues and PRs are welcome. Keep PRs focused, minimal, and consistent with the rest of PhotinoX.

License

PhotinoX is licensed under Apache-2.0.

About

Lightweight .NET wrapper for building cross-platform desktop apps with native OS WebViews. Maintained fork of Photino.NET.

Topics

Resources

Stars

31 stars

Watchers

1 watching

Forks

Used by

Contributors

Languages