Skip to content

Latest commit

 

History

History
141 lines (114 loc) · 7.44 KB

File metadata and controls

141 lines (114 loc) · 7.44 KB

Window embedding

FL Automate hosts its UI inside FL Studio's own window chrome — a plugin window sits in a real FL editor host form, next to FL's VST/plugin editors, rather than floating as a detached OS window. The IFlWindowHost capability (exposed on IPluginContext as Windows) lets your plugin do the same with any Win32 HWND.

If you host Avalonia UI, use FruityLink.Ui.Avalonia.Hosting instead of driving the raw HWND embed yourself. If you host WPF UI, use FruityLink.Ui.Wpf.Hosting (below) — a raw WPF window will embed, and will then hit three known Win32/WPF bugs that package exists to fix.

The surface (see the interface's XML docs for the fine print):

public interface IFlWindowHost
{
    bool   IsBridgeAvailable();              // is the native bridge loaded in-process?
    bool   TryEmbed(IntPtr childHwnd, bool show);  // create FL host form + reparent your window
    bool   IsHostVisible();
    void   SetVisible(bool visible);
    void   Close();
    void   SetStatusHint(string text);       // write FL's status/hint bar
    string LastEmbedReply { get; }           // raw bridge reply of the last embed (diagnostics)
    int    LastInsetX { get; }               // content-area insets of the host form
    int    LastInsetY { get; }
}

See also: Avalonia UI · Menus and toolbar · Native bridge.

What it does

The host creates a native FL host form and reparents your window's HWND into it as a WS_CHILD. Your window then behaves like a first-class FL sub-window: it carries FL's border/close chrome, can be shown and hidden (drive that from a View-menu toggle or a toolbar toggle — see Menus and toolbar), and is resized to fit the host form's content area.

Under the hood this rides the native bridge's window-host messages (embed, show/hide, close, minimize, maximize, dock) — the host marshals each to the correct thread for you (see below).

The embed threading contract

Reparenting a window across process-internal UI threads is the part that goes wrong if you improvise. The rules the host follows, and that you must respect:

  • Phase A — the FL host form is created on FL's main thread. TryEmbed first asks the bridge (a blocking call marshalled to FL's main thread) to create and show the host form. This phase never touches your window, so it cannot deadlock against your UI thread.
  • Phase B — SetParent + child restyle happen on the CALLER's thread. Call TryEmbed from the thread that owns your window (its UI thread). Windows only reparents a window cleanly from its owning thread, so the actual SetParent(child, hostContent) and the WS_CHILD style flip run right there on your thread — never on FL's.
  • Never issue a blocking bridge call while parented, from the thread that owns the window. A synchronous bridge round-trip blocks your UI thread; if FL's thread is simultaneously waiting on your window to pump (as it can be once you are its child), that is a deadlock. Do FL control calls off the UI thread once embedded.

If you host Avalonia via the hosting package, all of this is handled for you; the rules above are what that package implements.

Rendering note

A window reparented as a WS_CHILD of FL's (non-.NET) host form hits the classic DWM "airspace" problem — a GPU-composited child's redirection surface is not presented inside the foreign parent and goes blank until something invalidates it. The fix is software rendering for the embedded child plus a forced repaint after embed/re-show. The Avalonia hosting package does this for you; if you embed your own HWND, render it in software while it is parented. This is covered in detail for Avalonia in avalonia-ui.md.

WPF windows

FruityLink.Ui.Wpf.Hosting is the WPF counterpart of the Avalonia hosting package:

  • WpfUiThread — provides the WPF Dispatcher to host your window on: it reuses the host process's WPF UI thread when one exists (inside FL Studio it does — the FruityLink host is a WPF app), else spins up a private STA dispatcher thread.
  • EmbeddedWpfView — wraps any Window and applies the live-verified workarounds a WPF window needs to survive as a WS_CHILD of FL's non-WPF host form:
    1. Software rendering with an opaque backdrop. WPF's DWM/hardware composition hits the airspace bug inside a foreign parent (blank until input); and without an opaque backdrop, empty child areas render transparent, letting FL's own content bleed through.
    2. A WM_WINDOWPOSCHANGING position pin. Once it's a child, WPF keeps re-applying Window.Left/Top and moves the window off the host (the classic WPF-window-as-child coordinate bug); the pin forces it to fill the host's content area, inset below FL's titlebar.
    3. Re-present after resize/re-show. In software mode inside a foreign parent WPF does not re-present on its own after the host is resized, maximized, docked, or re-shown — the view stays blank until an input event. The wrapper forces a synchronous repaint (and, on real resizes, posts a synthetic mouse-move that kicks WPF's full render+present cycle — posted, so it never moves the OS cursor or clicks).

The whole flow, on the window's UI thread:

var ui = new WpfUiThread();
ui.Start();
ui.Dispatcher.Invoke(() =>
{
    var window = new MyPluginWindow();
    var view   = new EmbeddedWpfView(window);          // backdrop defaults to the window's Background

    view.PrepareForEmbedding();                        // borderless, off-screen, no taskbar/activation
    window.Show();                                     // force a WPF layout/render pass before reparenting
    bool embedded = context.Windows.TryEmbed(view.EnsureNativeHandle(), show: true);
    if (embedded)
    {
        view.PinToHostContent(context.Windows.LastInsetX, context.Windows.LastInsetY);
        view.ForceRerender();                          // synchronous first paint (else blank until a click)
    }
    else
    {
        view.RestoreExternalChrome();                  // external top-level fallback (always works)
        window.Show();
        window.Activate();
    }
});

Call view.ForceRerender() after every re-show (context.Windows.SetVisible(true)), for the same reason as workaround 3.

Status hint bar

FL's status/hint bar (the strip that shows the name/tooltip of whatever is under the mouse) is readable through the control surface via INativeFlControl.GetStatusAsync(), and the host can write a message to it (FL Automate uses this for its "loading…" indicator during boot, cleared once the UI is embedded). Use it for brief, transient status — it is FL's shared hint bar, not a private log.

Fail-soft when the host is absent

Window embedding depends on the native bridge being present and FL's host form existing. Treat it as a capability that may not be available (older/unpatched host, bridge not injected, an FL build where the host form couldn't be resolved). Design your plugin to fail soft:

  • Check availability and fall back to an ordinary top-level window (an external OS window) when embedding isn't possible, rather than failing to show any UI at all.
  • Keep the content of your window independent of whether it ended up embedded or external, so the same UI works both ways.

This external-window fallback is also the simplest way to develop your UI before wiring up the embed.