Skip to content

ui::NativeHost

Header: winui/render/NativeHost.hpp.

The interface between a NativeControl, a leaf wrapping a real Win32 child window such as RichEdit, a shell control or WebView2, and whatever owns the real HWND.

Call it from a NativeControl. The library implements it, and a test installs TestNativeHost.

Every other control draws itself without a window of its own. NativeControl hosts what libwinui doesn't reimplement. The control never calls Win32 itself. It calls this interface from Element::sync_native(), keyed by its VisualId, and the host creates, moves, clips and hides the child window. The window installs its host with Window::set_native_host() (non-owning). The host must outlive every NativeControl synced against it, since a NativeControl's destructor calls back into it.

Example:

// An app never calls a NativeHost. It puts a NativeControl in the tree, and the window's host
// creates the child window, places it and keeps it in step with the element:
auto edit = std::make_unique<ui::NativeControl>(
    ui::NativeControlDesc{.class_name = "EDIT", .initial_text = "hello"}, ui::Size{100, 20});
window.set_content(std::move(edit));

~NativeHost

virtual ~NativeHost() = default

ensure_created

virtual void ensure_created(VisualId id, const NativeControlDesc& desc) = 0

Creates the native control for id if it doesn't exist yet, and does nothing if it does. A NativeControl calls this once per sync_native(), whether or not it already has a live handle.

set_bounds

virtual void set_bounds(VisualId id, const Rect& bounds) = 0

Moves and resizes the native control to bounds, in DIPs like every other Element. The host converts to device pixels.

set_visible

virtual void set_visible(VisualId id, bool visible) = 0

Shows or hides the control without destroying its window, for example while it is inactive. Does nothing if id has no created control yet.

destroy

virtual void destroy(VisualId id) = 0

Destroys the native control. Does nothing if id has none.

set_clip

virtual void set_clip(VisualId, const std::optional<Rect>&)

Cuts the native control down to visible (DIPs, the same space as set_bounds()), or shows all of it for nullopt. A child window ignores the clips the rest of the window is drawn with, so a control half scrolled out of a Scroll would otherwise paint over whatever sits outside the viewport. NativeControl::sync_native() sends the part its scrolling ancestors leave in view, through place() whenever the control also moved, so a host may keep the region relative to the window. The default does nothing, for a host with nothing to cut.

place

virtual void place(VisualId id, const Rect& bounds, const std::optional<Rect>& visible)

Moves the control and clips it in one step, so a host that repaints on a move paints the new position with the new clip, not the one left over from the old position. NativeControl::sync_native() calls this whenever the bounds changed; the default is set_bounds() then set_clip().

native_handle

virtual NativeHandle native_handle(VisualId id) const = 0

The raw platform handle (an HWND on Windows), for control-specific messages a generic NativeControl can't express, such as WM_SETTEXT or WM_GETTEXT to a RichEdit. nullptr if id has no created control yet.

set_spell_checker

void set_spell_checker(SpellChecker* checker)

Publishes the window's spell checker, or null when the window has none. Window::sync_native() calls it, and NativeControl::sync_native() hands the checker to the control through Element::sync_spell_checker().

Other controls get the checker when they take focus. A NativeControl never becomes the window's focused element, because keyboard focus inside it belongs to the child window, so it gets the checker through this sync pass instead.

spell_checker

SpellChecker* spell_checker() const

The checker set_spell_checker() last published, or null. Non-owning.

begin_sync

void begin_sync()

Window::sync_native() brackets its tree walk with these. Every NativeControl the walk reaches reports through present() whether it should show. end_sync() then hides every control the walk didn't reach, such as one on a Tabs page or NavigationShell entry that isn't selected, and present() shows it again the next time a pass reaches it. Only end_sync() hides anything, so a host whose controls are synced directly never hides one this way. This one starts a pass.

end_sync

void end_sync()

Ends a pass: hides (set_visible(false)) every shown control present() wasn't called for since begin_sync().

present

void present(VisualId id, bool visible)

Called by NativeControl::sync_native() on every sync: visible is whether any of the control shows. Calls set_visible() only on a change; a control starts out shown.

presented

bool presented(VisualId id) const

Whether present()/end_sync() last left the control shown (true for one never presented).

forget

void forget(VisualId id)

Drops the control's presence record; NativeControl's destructor calls it before destroy().

notify_host_lost

static void notify_host_lost(NativeControl& control) noexcept

Tells control, a NativeControl registered through set_owner(), that this host is going away while it lives on (its window was closed; the app still owns the Window and its tree). The control forgets the host and its native window, and gets a new one from the next sync_native(). A host calls it from its destructor for every owner still registered.

set_owner

virtual void set_owner(VisualId, NativeControl*)

Records which NativeControl owns id, so a host that receives platform events addressed to the child window can route them back to its element. On Windows that is a child window's WM_CONTEXTMENU: a right-click inside a native control goes to the child window and never reaches Window::handle_context_menu(), so the host looks the owner up by its handle and calls Element::handle_context_menu() on it directly.

NativeControl::sync_native() calls it right after ensure_created(), so the owner is registered for exactly as long as the control exists. destroy() drops it again, and NativeControl is non-movable, so the pointer can't go stale. The default does nothing, for a host with no platform events to route. A host implements it, and only NativeControl calls it.