LWS is a compact C++26 windowing layer with explicit platform ownership. It provides portable window lifecycle, input, events, immutable cursors and icons, timers, clipboard, drag-and-drop, and bitmap presentation while keeping native backends private.
| Platform | Backend | Status |
|---|---|---|
| Windows | Win32 | Supported |
| Linux | Wayland | Supported when the Wayland development packages and protocols are available |
| Linux | X11 | Source scaffold only; it is not reported as an available backend |
PlatformContext::GetAvailableBackends() reports compiled complete backends. Context-scoped Supports() queries
optional behavior without consulting global state.
cmake -S . -B build -DLWS_BUILD_TESTS=ON
cmake --build build
ctest --test-dir build --output-on-failureEmbed LWS with add_subdirectory and link LWSLib.
LWSLib is static by default. Set -DLWS_BUILD_SHARED=ON to build a shared library
(LWSLib.dll and its import library on Windows). The target name and include paths
stay the same. Windows builds export the compiled API automatically. LWSUI keeps
its existing static-library configuration. Deploy the shared library alongside the
application, or place it on the platform's library search path.
On Win32, PlatformConfig::dispatchMode selects DispatchMode::OwnedLoop by default, retaining
event-based wakeups serviced by RunMessageLoop() or ProcessMessages(). A frontend running under a
host loop selects DispatchMode::ExternalLoop. Its context-owned message-only window delivers
coalesced task notifications through the host's normal dispatch, including native modal loops. Both
implementations compile into the same library and are selected once during context initialization.
External-loop contexts reject explicit LWS loop pumping. The owned-loop fast path remains unchanged,
with shared task storage and windowing contracts. Other backends reject external-loop mode until a
backend-specific integration is implemented.
Both builds dispatch timers through their target windows, keeping DLL callback addresses out of queued timer messages. Destroy all bound windows and services and shut down the context on its UI thread before unloading the module. Quit requests affect the LWS context only. Foreign parenting and host DPI adoption remain explicit Win32 operations and are independent of dispatcher selection.
#include <LWS/Platform.hpp>
#include <LWS/Window.hpp>
#ifdef LWS_HAS_WIN32_BACKEND
#include <LWS/Win32/Platform.hpp>
#endif
int main()
{
#ifdef LWS_HAS_WIN32_BACKEND
if (LWS::Win32::BootstrapProcess() != LWS::Result::Success)
return 1;
#endif
LWS::PlatformContext platform;
const LWS::PlatformConfig platformConfig{
#ifdef LWS_HAS_WIN32_BACKEND
.backend = LWS::BackendId::Win32,
#else
.backend = LWS::BackendId::Wayland,
#endif
};
if (platform.Init(platformConfig) != LWS::Result::Success)
return 1;
{
LWS::Window window(platform);
auto connection = window.Listen(
[&](const LWS::AnyEvent& event)
{
if (std::holds_alternative<LWS::EventWindowDestroyed>(event))
platform.RequestQuit();
return LWS::EventResponse::Unhandled;
});
const LWS::WindowConfig config{
.clientSize = {800, 600},
.styles = LWS::WindowStyleFlags(LWS::WindowStyle::Caption | LWS::WindowStyle::CloseButton),
.visible = true,
};
if (!connection.has_value() || window.Create(config) != LWS::Result::Success)
return 1;
platform.RunMessageLoop();
std::ignore = window.Destroy();
}
return platform.Shutdown() == LWS::Result::Success && !platform.GetFailure().has_value() ? 0 : 1;
}- One
PlatformContextowns one backend, one UI thread, one task queue, and one message loop. - Failed context initialization may retry. Successful shutdown is terminal.
- A
Windowpermanently borrows an active context and has at most one successful native lifetime. - Destroy every bound window and persistent service before shutting its context down.
- Context-bound operations require the context thread.
PostTask(),RequestQuit(), andIsCurrentThread()are the cross-thread-safe instance operations. - User callbacks execute synchronously on the context thread. Exceptions are reported through the installed non-throwing context handler and never unwind through native callbacks.
Window::PresentBitmap() borrows the caller's pixels only for the duration of the call. The caller may reuse or
release that storage on return. The bitmap must match the current framebuffer dimensions; Wayland accepts tightly
packed, top-down, premultiplied BGRA pixels.
Wayland copies each accepted frame directly into reusable shared memory. At most three presentation buffers are submitted to the compositor at once, with one additional unsubmitted buffer holding the newest pending frame. Repeated pending frames overwrite that idle buffer. When a submitted buffer is released, LWS submits the pending buffer itself, without another pixel copy. Released buffers are reused, and obsolete idle sizes are discarded.
The copying API is an intentional simplicity tradeoff. An optional acquire/write/present API could eliminate the remaining copy for clients that render directly into LWS shared memory. It was not selected because it would expose exclusive buffer access and compositor release coordination to clients, including buffer exhaustion, resize, cancellation, window destruction, and background-render completion. LWS keeps that coordination internal so clients can present ordinary pixel storage without managing presentation-buffer leases. Such an additive API remains a future option if a measured client workload justifies the additional contract.
Shared code uses Window, AnyEvent, ClientAreaMetrics, and logical client coordinates. WindowConfig::clientSize
and WindowPlacementRequest::clientSize describe the drawable client area with LogicalSize; native title bars,
borders, shadows, and other outer decorations are outside that size contract. ClientAreaMetrics::pixels is an
optional, non-convertible PixelSize and is authoritative for native rendering when present.
Logical size is the stable layout size an application aims to preserve when display scaling changes. On Win32, logical units are normalized to 96 DPI; on Wayland, they are surface coordinates. A width of 600 logical units uses 600 pixels at 100% scaling, 900 at 150%, and 1200 at 200%. Logical units do not measure inches or millimetres, and resizing, native window constraints, or integer rounding can still change the reported logical dimensions.
GetClientAreaMetrics() returns a ClientAreaMetrics value directly. When native metrics are available, it pairs
logical and pixel dimensions from the last published backend update. Size listeners observe that pair before
running; show-state listeners can run earlier and observe the previous pair. Nested dispatch can publish a newer
pair before an outer listener resumes. Otherwise, logical comes from the backend while created or
the stored configuration outside that state, and pixels is empty. Logical size remains useful for startup layout.
Pixels are unavailable before initial configuration, during Wayland remapping, and after native teardown or backend
failure. They can remain available during orderly cleanup even though IsConfigured() is false. An available
zero-sized client area is distinct from missing native metrics. On Wayland, later metrics updates can also result
from LWS applying size requests without a new compositor acknowledgment. Before initial configuration or while
remapping, size and scale requests update backend layout state without publishing paired metrics.
Use one snapshot for rendering dimensions and coordinate conversion so both use the same scale:
const auto metrics = window.GetClientAreaMetrics();
if (const auto scale = metrics.Scale(); scale && metrics.pixels->x > 0 && metrics.pixels->y > 0)
{
ResizeRenderTarget(*metrics.pixels);
const auto logicalPosition = window.GetMousePosition();
const double pixelX = logicalPosition.x * scale->x;
const double pixelY = logicalPosition.y * scale->y;
HandlePointerInPixels(pixelX, pixelY);
}ResizeRenderTarget() and HandlePointerInPixels() above represent application code. Scale() computes
pixels / logical on each axis, returning nullopt if pixels are absent or either logical dimension is zero.
Rounding can make these effective ratios
differ between axes and from nominal display scale; use the reported pixel size for render targets. On Win32,
pixel dimensions come from GetClientRect() or native resize notifications.
Wayland scale is compositor-provided per-surface state, not physical-monitor DPI. With wp_fractional_scale_v1 and
wp_viewporter, LWS allocates each pixel dimension with ceil(logical * preferredScale), uses buffer scale one, and
sets the viewport destination to the logical size. Without those protocols, LWS uses the maximum integer scale of the
outputs containing the surface. A scale change that alters the paired pixel size publishes
EventClientAreaSizeChanged even when logical size is unchanged; clients must use that pixel size rather than monitor
scale or reconstructed dimensions.
RequestPlacement() accepts optional position and logical client size: omitted fields stay unchanged, and an empty
request or nonpositive supplied size is invalid. Combined Win32 movement/resizing uses one native geometry operation;
redraw and callbacks retain their own timing. Placement does not issue a show-state request or override one made
by a geometry listener. Wayland top-level positions cannot be set or queried, so an explicit
position returns NotSupported without applying an accompanying resize. Wayland child position and content can take
effect on separate parent/child commits. GetPlacement() returns optional position and logical client size; it does
not promise an atomic native observation. Win32 top-level position follows the restored placement in DPI-normalized
screen coordinates, matching position requests, while child positions are relative to their parent's client area.
GetClientSizeLimits() and SetClientSizeLimits() exchange a ClientSizeLimits with minimum and maximum logical
dimensions. Zero means no application-specified limit on that axis. Values must be nonnegative, and a nonzero maximum
must be at least its minimum. Native constraints may still apply. Creation uses the same type through
WindowConfig::clientSizeLimits.
RequestShowState(WindowShowState::Maximized) requests a windowed, maximized top-level window even from fullscreen.
Win32 retains the current monitor and saved normal client size where it fits. Wayland sends both leaving-fullscreen
and maximization requests, even when earlier configuration events are in flight. Success means the request was
issued; the compositor controls the resulting state and timing. Child maximization returns NotSupported.
SetMouseCursor(Cursor::FromShape(CursorShape::Arrow)) resets the cursor while preserving its visibility.
SetWindowIcon(std::nullopt) clears the custom icon. Both preserve the previous reset operations' lifecycle and
platform support; a moved-from cursor or engaged moved-from icon remains invalid, and repeated immutable resources
retain native handle reuse.
Platform translation units may opt into typed extensions:
#ifdef LWS_HAS_WIN32_BACKEND
#include <LWS/Win32/WindowExtensions.hpp>
auto connection = LWS::Win32::Listen(
window,
[](const LWS::Win32::PlatformEvent& event) -> std::optional<LRESULT>
{
if (const auto* paint = std::get_if<LWS::Win32::PaintEvent>(&event))
{
DrawSidebar(paint->deviceContext, paint->invalidRect);
return 0;
}
return std::nullopt;
});
#endifTyped native handles are borrowed from successful creation until native teardown begins, including orderly
EventWindowDestroying notification. Release dependent swap chains, EGL surfaces, wl_egl_window objects, and native
registrations in that notification. Backend failure invalidates public handle access immediately.
-
File-dialog filters are a
ListFileDialogFilterscollection ofFileDialogFiltervalues. Store/pass that collection directly instead of constructing aFileDialogFilterBuilderand callingGetFilters(). -
Win32::GetHwnd()andWayland::GetSurface()takeconst Window&; mutable-window calls continue to work. -
Portable
AnyEventno longer includes the unusedEventRawPlatform; use the existing typed platform listeners. -
PlatformContext::AssertCurrentThread()is private; clients can queryIsCurrentThread(). -
Read backend identity through
window.GetPlatformContext().GetBackendId();Window::GetBackendId()was removed. The context getter returns an optional because contexts can exist before initialization; a window is permanently bound to an initialized context. -
Replace
Window::GetClientSize()/GetLogicalClientSize()withGetClientAreaMetrics().logical, andGetClientAreaSize()withGetClientAreaMetrics(). RenameClientAreaSizetoClientAreaMetrics; check its optionalpixelsandScale()instead of checking an outerexpectedresult. -
Replace
SetPosition(),RequestClientSize(), andSetPlacement()withRequestPlacement()supplying only the desired fields. UseGetPlacement().positioninstead ofGetPosition(). -
Replace
GetMinClientSize(),GetMaxClientSize(), andSetMinMaxClientSize()withGetClientSizeLimits()andSetClientSizeLimits(). ReplaceWindowConfig::minClientSize/maxClientSizewithclientSizeLimits.minimum/clientSizeLimits.maximum. -
Replace
ResetMouseCursor()withSetMouseCursor(Cursor::FromShape(CursorShape::Arrow)), andResetWindowIcon()withSetWindowIcon(std::nullopt). -
Replace
RequestMaximize()withRequestShowState(WindowShowState::Maximized). The latter now also leaves fullscreen and rejects child windows; restore and minimize behavior is unchanged. -
Boolean queries are
IsVisible(),IsTransparent(),IsAlwaysOnTop(), andIsBackgroundErasureEnabled(); drag-and-drop configuration isSetDragAndDropEnabled(bool). These replace the correspondingGet...()methods andEnableDragAndDrop(bool). These API migrations have no compatibility aliases; rebuild clients together. -
RunMessageLoop()andProcessMessages()returnLoopResult. A host loop must compare explicitly withContinue,Quit, orFailed; the old Boolean use ofProcessMessages()requires a source update.RunMessageLoop()never returnsContinue. Quit is sticky, including across repeated loop calls, and failure takes precedence. -
GetFailure()returns the first backend diagnostic, owned by the context and retained throughShutdown(). Failure rejects new posts and suppresses ordinary callbacks, including remaining listeners after nested pumping. Unstarted tasks are discarded; their captures are released after active dispatch unwinds or at later explicit shutdown. -
Failure does not destroy clients' C++ objects.
IsCreated()/IsConfigured()and handle access become unavailable; clients destroy their object graph normally. Cleanup receivesEventWindowDestroying{false}when the backend is already lost. No application exit, restart, or backend migration is performed by LWS. -
EventWindowDestroyingis appended toAnyEvent. Update exhaustive visitors and rebuild clients and libraries together; this change does not claim binary compatibility with earlier headers. Embedded LLUtils must include the accompanyingEvent::RaiseWhile()addition used to stop notification listeners after failure. -
On orderly destruction, the parent is notified before its children, then its native window is destroyed and the final destroyed event is delivered. Pre-destruction cannot be consumed or cancelled. Unexpected cleanup exceptions are reported and later eligible cleanup listeners continue if the handler returns; without a handler LWS terminates.
-
Recursive destruction of a destroying window returns
InvalidState. Destroying descendants during an ancestor's pre-destruction notification, or an ancestor while a descendant is destroying, is rejected. Supported sibling removal remains valid once ancestor pre-destruction notification has completed. -
Native
Destroy()may run from callbacks, but the executing C++ Window must survive all active native/event dispatch. Deleting it, or the active context, is a debug-diagnosed precondition violation. Deferring deletion by posting a task alone is insufficient if nested pumping can execute the task early. Existing supported timer self-deletion remains. -
Normal
Shutdown()rejects active dispatch or bound objects. Otherwise it closes task acceptance and drains accepted tasks. New posts during the drain are rejected. Resources left bound by those tasks keep a healthy context active and reopen acceptance for cleanup/retry. Successful shutdown is terminal. Failure during the drain discards the rest. -
Active scheduling remains unchanged. Do expensive work on background threads and post brief, nonblocking result application callbacks. The queue is unbounded; callers own workload discipline.
All operations retain their documented context-thread precondition. A failed context overrides native access in every phase. Typed native handles must not be used to destroy LWS-owned windows directly.
On Wayland, style flags describe requests and local-frame policy. With server-side decorations, the compositor controls
caption buttons; GetWindowStyles() does not claim every requested decoration was enforced by the compositor.
| Property/operation | Pre-create | Created | Orderly cleanup notification | Destroyed or backend failed |
|---|---|---|---|---|
| Context/backend identity | Available | Available | Available | Available until C++ destruction |
| Title, optional position, logical size, placement, min/max, styles, show state, visibility, transparency, always-on-top, erase flag | Stored defaults | Current backend values | Last stored request/observed value | Last stored request/observed value |
| Window mode | Backend's retained logical mode | Current mode | Retained mode | Retained mode |
| Client-area metrics | Logical defaults; pixels absent | Logical size always available; paired pixels after configuration | Last configured pair before native teardown | Stored logical size; pixels absent |
| Parent | None until successful creation | Bound parent | Relationship remains during notification | Cleared after final destroyed dispatch; retained until cleanup on failure |
| Focus/pointer-in-client | False | Current observation | False | False |
| Mouse position | Zero | Current observation | Zero | Zero |
| Typed handles | InvalidState | Borrowed | Borrowed until native teardown starts | InvalidState |
| Cursor/icon selection, visibility and typed app-ID/menu configuration | Allowed where supported | Allowed where supported | InvalidState | InvalidState |
| Other window mutations, presentation, drag/resize/lock requests | InvalidState | Validated operation | InvalidState | InvalidState |
| Listen | Allowed | Allowed | InvalidState | InvalidState |
| Disconnect existing listener | Allowed | Allowed | Allowed | Allowed |
These are retained logical values, not a promise to query a disconnected native system. Construction events stay
suppressed and failed Create() remains retryable; successful native lifetime is one-shot. Unsupported requests return
NotSupported without changing retained/native state. Malformed arguments use InvalidArgument, and lifecycle errors
use InvalidState. Service APIs with legacy non-Result signatures preserve their failure conventions: timers cannot
rearm after failure, clipboard operations return empty/error results, and notification creation reports invalid state.
Ordinary portable and typed dispatch read stable vectors without allocating a snapshot per event. Additions made during any nested listener traversal on a window accumulate in a pending list; it is published when the outermost traversal completes. Publication precedes retired-capture destruction, so events reentered from those destructors see the published list; additions made during that cleanup are deferred until the cleanup finishes. This intentionally changes nested ordinary-event visibility: a newly registered listener does not receive events in the current dispatch chain. Its connection is immediately queryable/disconnectable, and disconnection immediately suppresses later invocation. Destruction notifications include pending listeners, preserving cleanup for resources and timers attached just before a callback destroys its window. Window rejects further registration during that cleanup phase.
Retired callbacks are released outside vector mutation, and Close cannot revive pending registrations. Listener operations remain context-thread-affine; this adds no listener locks or atomic publication. The tradeoff is extra per-window pending state and more work on registration changes in exchange for removing steady-state vector allocation/copying.
Reapplying the same immutable custom cursor or icon to a created Win32 window reuses that window's native allocation. Cursor visibility and WM_SETICON application still run. Creation/retry and replacement realize resources normally; each window retains independent native resources. Custom cursor bitmaps/hotspots are pixel data and do not depend on window DPI. Wayland's focus/serial/theme/scale-driven cursor application is unchanged.
When wayland-server development files are available, LWS tests build a private protocol server for deterministic seat
capability/removal/promotion, cursor focus/serial, and integer/fractional-scale checks. This dependency is test-only. Win32
DPI scenarios run in separate child processes because process DPI initialization is irreversible.
| Path | Purpose |
|---|---|
src/LWS/include/LWS |
Portable public headers and typed extension headers |
src/LWS/source |
Portable implementation and private native backends |
tests |
Unit and platform integration tests |
The optional LWSUI::LWSUI target provides controls, layout, text rendering, popups,
focus/capture and bitmap presentation without a JSON dependency. LWS_BUILD_UI defaults
on for Windows/Wayland and off for the unavailable X11 scaffold. LWSUI_BUILD_DEMO
controls the simple example below and the multi-window control showcase,
with fixed and resizable native child panes and a complete control catalog. Dependency builds compile LWSUI only when linked
(or explicitly requested); standalone builds include it. LWSSettings is a separate consumer.
Start with simple.cpp for a complete, single-window example using only public APIs. A height-limited split panel contains two multiline text boxes, followed by a name field, greeting label, and Clear button in a vertical stack. Event connections are retained in local variables. Typing in the name field previews the greeting, Enter commits the name, Escape restores the last committed name, and Clear resets the name and greeting. Tab moves between the controls.
cmake -S . -B build -DLWS_BUILD_UI=ON -DLWSUI_BUILD_DEMO=ON
cmake --build build --target LWSUISimpleExampleRun build/LWSUISimpleExample.exe on Windows or build/LWSUISimpleExample on
Wayland. Multi-configuration generators place the executable in the selected
configuration directory, for example build/Debug/LWSUISimpleExample.exe.
Closing the window exits the example; it does not read or write any files.
Every control supports optional maximum dimensions in logical pixels:
splitPanel->SetMaxHeight(120); // Leaves room for following StackPanel children.
control.SetMaxWidth(300);
control.SetMaxWidth(std::nullopt); // Restore unconstrained width.Limits apply before measuring content (so text can wrap at the limited width), to
its requested size, and to its arranged bounds. A control remains at the origin
assigned by its parent and can be smaller when less space is assigned. Limits
must be finite and nonnegative; zero is allowed, and invalid values throw
std::invalid_argument. Setters automatically invalidate layout. MaxWidth() and
MaxHeight() return the current optional limits; both are unset by default.
A maximum does not reserve space for siblings or add scrolling. The parent still
needs enough room for all children; use DockPanel when a child should fill the
space remaining after other controls have been allocated.
UIHost coalesces deferred work, layout and painting through PlatformContext::PostTask. Numeric holds arm their own timers; idle hosts have no periodic tick or tree traversal. Wayland timers use timerfd in the context wait loop, and clipboard transfers wait on pipe readiness and their actual timeout. Portal dialogs still use a nested timed D-Bus pump; converting that path remains a separate asynchronous-dialog design.
UI hosts borrow a context-bound Window; editors borrow the caller's PlatformContext. Use upstream Listen/EventConnection and ClientAreaMetrics, with logical input/layout and exact pixel buffers. Rendering pauses while pixels are unavailable or zero-sized. Canvas accepts per-axis ContentScale so fractional-scale rounding stays aligned.
AnyEvent additionally carries EventTextInput (committed UTF-8), EventMouseLeave and
EventMouseCaptureLost. Explicit capture is a Win32 extension:
LWS::Win32::SetMouseCapture(window, capture) in LWS/Win32/WindowExtensions.hpp.
UIHost uses it on Windows; Wayland retains implicit button-drag capture. The existing EventResponse and non-cancellable cleanup events
keep their upstream semantics. Windows retained bitmap presentation borrows caller
pixels only for the call and copies them for repaint.
TextClipboard.hpp provides UI-thread SetClipboardText/RequestClipboardText helpers using a Window's context. Text is valid UTF-8 without embedded NUL. Reads may complete immediately and are cancelled when the native owner or context becomes unavailable. Wayland uses the context's data device alongside URI drag/drop and requires an input serial for writing. Portal dialogs retain the existing Window& owner and filter APIs; until xdg-foreign parenting exists they enforce owner modality within LWS.
Wayland SetWindowIcon uses upstream WindowIcon values and the optional xdg-toplevel-icon-v1 protocol. Unsupported compositors still return NotSupported; UI headers provide an always-visible icon. No public bitmap enlargement API is added.
LWSUI::Event is the UI-layer notification type. Owned connections disconnect safely on destruction and may outlive their publisher; dispatch retains stable callables and new registrations start with the next notification. This isolates UI requirements from the pinned LLUtils Event lifetime contract without changing that dependency.
LWSUI controls can supply flat command menus through SetContextMenuProvider.
Right-click or Shift+F10 requests the nearest provider on the target or its ancestors,
within the active popup. A registered provider returning no items suppresses the menu;
clearing the provider restores ancestor lookup. TextBox supplies Cut, Copy, Paste, and
Select All by default, including disabled editing actions for read-only text.
control.SetContextMenuProvider([](LWSUI::Control&, const LWSUI::ContextMenuRequest&) {
return std::vector<LWSUI::ContextMenuItem>{
{"Refresh", "F5", [](LWSUI::Control& owner) { owner.Invalidate(); }},
LWSUI::ContextMenuItem::Separator(),
{"Unavailable action", "", {}, false}
};
});Providers run on request; item availability and optional checked states are snapshots.
Shortcut hints are descriptive, not key bindings, and checkmarks do not toggle themselves.
UIHost::ShowContextMenu and CloseContextMenu support programmatic invocation.
Menus retain editor focus and drafts, including inside a modal color picker. Up/Down,
Home/End, Enter/Space navigate and activate; Escape/Tab or an outside click dismiss.
Outside clicks are consumed. Commands execute through an owner-checked Post after
closing the menu; detached or unavailable owners are skipped. Callbacks must respect
the existing structural-edit lifetime rules and must not capture shorter-lived objects.
Long menus scroll using existing menu theme metrics. Submenus, icons, native menus,
and automatic shortcut registration are intentionally deferred.
TextBox remains single-line by default. Use the same control for wrapped notes:
auto& notes = panel.Emplace<LWSUI::TextBox>("First line\nSecond line", LWSUI::TextBoxMode::Multiline);
notes.SetVisibleLines(4);The mode is fixed at construction. Visible lines set the preferred height; the parent supplies the actual allocation. Multiline text wraps and gets a vertical scrollbar when needed. Enter inserts a newline, Ctrl+Enter commits, Escape cancels the current edit, and Tab changes focus. Home/End navigate visual lines; Ctrl+Home/End navigate the document. Up/Down retain the preferred caret column, and PageUp/PageDown move by the viewport. Shift extends selection.
Assigned and pasted multiline text normalizes CRLF/CR to LF. Existing OnEdit, validation, read-only, clipboard, placeholder, borderless, and highlighting APIs apply. Scrollbar interaction keeps the draft and does not add a Tab stop. These are plain-text notes, without rich text, syntax highlighting, or undo history.
Each native dropdown has its own UIHost for layout, rendering, scrolling, and mouse capture. The menu session coordinates keyboard navigation and submenu lifetimes. Switching top menus on Windows retains the root popup window; closed levels remain alive until their current input dispatch has returned.
Captured control gestures and active modal/context popups take priority over the main menu. On Wayland, keyboard focus may move between the owner and its dropdowns without dismissing the menu; leaving that group or compositor dismissal closes it.
Opt in with WindowConfig::snappingMode when creating a window, or call
window.SetSnappingMode(LWS::WindowSnappingMode::ScreenEdgesAndOtherWindows)
after creation. GetSnappingMode() returns the selected mode. The modes are
Disabled (the default), ScreenEdges, OtherWindows, and
ScreenEdgesAndOtherWindows.
On Win32, dragging a top-level window aligns its visible edges with monitor work
area edges or visible neighboring windows within 12 logical units, scaled for
DPI. This also applies to BeginWindowDrag(WindowDragOperation::Move) and custom
menu-bar dragging. Snapping preserves window size and releases when the pointer
moves beyond the threshold. Hidden, minimized, cloaked, and tool windows are
excluded as targets. Window targets snap only opposite edges (left to right or
top to bottom) with positive overlap along the other axis. In the same LWS
context, both windows must enable OtherWindows or ScreenEdgesAndOtherWindows for
geometric snapping. Disabled and ScreenEdges targets do not attract other
windows, regardless of role. External windows remain geometric targets.
Enabled modes return NotSupported on Wayland and for
child or popup windows.
Attachment is a separate, explicit role. WindowConfig::snappingRole defaults
to WindowSnappingRole::Independent. Select Leader or Follower at creation,
or call SetSnappingRole() after creation. Leader and Follower roles support
only Win32 top-level windows.
Dragging either a Leader or an unattached Follower can attach them in the same platform context. Both must enable snapping to other windows, and their adjacent visible edges must touch when the drag ends. A leader can carry multiple followers, each follower belongs to one leader, and attachments never form chains. Matching roles, Independent windows, and other applications still align without attaching.
Moving a leader carries its followers by the same displacement, including programmatic movement. Direct follower movement detaches only that follower, leaving the leader and siblings stationary. The former leader remains an eligible snap target, so the follower can pull free and return to it during the same drag. Dropping against either the same leader or a different leader reattaches on release.
GetSnappingState() returns the role, attachedToLeader, and followerCount.
Subscribe to EventSnappingStateChanged and query current state when notified.
The event follows reciprocal state updates and may reenter native dispatch.
Register a listener before querying initial state. No window identities or
borrowed attachment pointers are exposed.
ReleaseSnappingAttachments() detaches a follower or releases a leader's
followers while retaining roles and modes. It is idempotent and cancels attachment
for the current drag. Actual role changes, disabling other-window snapping,
resizing (including DPI-induced resizing), hiding, minimizing, maximizing,
fullscreen, and destruction also release attachments. No-op role or mode setters
preserve attachments. Outside a live window, state queries retain the configured
role with no attachments. All operations require the context thread.
Horizontal Top/Bottom menu bars on top-level windows can opt into caption buttons and native dragging. Other layouts retain their ordinary menu behavior.
bar->SetWindowControls({
.minimize = true,
.maximize = true,
.draggable = true,
.requestClose = [&] { platform.RequestQuit(); }
});The close callback enables the Close button and runs outside input dispatch. The
application decides whether to quit, hide the window, or decline closing. Minimize
and maximize/restore call RequestShowState; dragging unused bar space calls
BeginWindowDrag(Move) synchronously with the pointer press. Wayland Minimize is
visible but disabled. Defaults leave all window controls and dragging off.
For a resizable captionless window, use WindowStyle::ResizableBorder without
native caption or caption-button flags: the Win32 backend adds a caption for
CloseButton, MinimizeButton, and MaximizeButton. The controls-only showcase's
main Workspace demonstrates this configuration while its companion windows keep
native captions.
Close uses red hover and darker red pressed backgrounds with a white X. Applications
can customize Theme::closeHoverBackground, closePressedBackground, and
closeActiveForeground. Normal/disabled Close appearance and other caption buttons
continue to use the ordinary theme colors. These are C++ theme properties; this
change does not extend LWSSettings JSON definitions or its theme catalog.
MenuBarWindowControls::doubleClickMaximize defaults to false. Explicitly enable it
for title-bar-style maximize/restore on a left double-click in blank caption space,
the icon, or passive hosted text. It does not depend on native caption styles,
draggable, or a visible Maximize button. Interactive/disabled controls, menu items,
and caption buttons keep their normal behavior. Only horizontal Top/Bottom bars on
top-level windows support caption gestures. State requests remain deferred and
cancel when the bar detaches or the option is disabled before execution.
EventMouseButton::clickCount and Input::clickCount report a double press as 2 on
the second Down event. Windows uses native double-click recognition; Wayland uses
the same 500 ms / five-logical-pixel policy as its client-side caption.
UIHost::SetRedrawOnResize(true) opts into synchronous layout and presentation on
each client-size notification. The default is false; the application decides
whether its rendering workload can afford live resizing. No hardware detection or
frame-rate cap is applied. This also works inside Windows' native resize loop,
where queued updates may otherwise wait until resizing finishes.
The live path does not execute posted application work or reap retired controls. Nested size/layout changes retry a current frame without recursive rendering; repeated invalidation is bounded and remaining work stays queued. Hidden, minimized, unconfigured, or destroyed windows are not presented. Normal deferred updates keep processing callbacks and cleanup outside the resize event.
MenuBar::SetIcon(BitmapSharedPtr) adds an optional leading icon (left in a
horizontal bar, top in a vertical one). Icon() returns the retained bitmap;
passing null removes it and its space. The icon keeps its aspect ratio in a
24-logical-pixel box, clamped to the bar thickness. It starts a native window move
when window dragging is enabled for the bar. It is not a menu item and does not
change the native window icon.
SetContent(std::unique_ptr<Control>, minimumWidth) owns one optional control tree
between menu items and window buttons in horizontal Top/Bottom bars. Content()
returns that tree, and SetContentMinimumWidth() updates its minimum. The child
reports its preferred size through measurement. MinimumWidth() combines required
icon, menu, content, and window-button widths; OnMinimumWidthChanged lets the
application defer updates to native size limits. Structural content replacements
must be deferred from control callbacks, just like other tree mutations.
Content participates in focus, capture, Tab traversal, context menus, popups, and edit completion. Its hit-test result identifies occupied regions; disabled controls still occupy their rectangles. Containers that intend gaps to be draggable should return null from HitTest for those gaps or passive text intended as a drag region. Labels remain occupied unless their content container passes hits through; the settings header does this for its title and saved-state indicator. Content is clipped to its slot and never intercepts window buttons. Other bar layouts keep the content attached but inactive, preserving control state for returning to a horizontal dock. Existing bars without content retain their previous behavior.
The settings editor demonstrates this with its icon, title, theme selector, and saved-state indicator. The bar remains generic and has no dependency on settings documents, JSON, or theme persistence. A TODO in its layout code records the deferred decision about reserving a guaranteed drag strip for combined-content bars.
LWSUI::MakeTheme(ThemePreset::Dark/Light/Warm) returns a complete palette with the same
font and geometry defaults. Theme{} and unconfigured hosts/controls default to Light. Explicit Dark retains the original palette. Apply a palette through the
existing UIHost theme update path; close transient popups first so they reopen with the
new style. The connected showcase Theme menus demonstrate live switching.
Settings-specific sections, JSON profiles, Save/Revert, and the F6 editor remain in
LWSSettings; LWSUI has no persistence or theme registry.
Popup replacement returns a success flag: callers must only use the new popup's controls when OpenPopup succeeds. Rejected validation preserves the current popup, and window resizing cancels transient popups through their normal rollback callback. TextBox uses native point hit-testing for bidirectional text and measures only the active caret and visual extent, avoiding an all-character caret map on every edit.
The Windows LWSLib target exports UNICODE, _UNICODE, and NOMINMAX as public
usage requirements, so core-only consumers see the same string ABI and compatible
Windows headers as the library. LWSUI inherits these requirements.
TreeView::Row is a fixed property row, constructed with owned name/editor controls
and an optional trailing action. Use NameControl(), EditorControl(), and ActionControl()
to access those roles. It no longer exposes Grid column configuration; measurement and
arrangement share one column calculation. As with other composites, callers must not
mutate the owned structure through a base-container cast. Derived controls retain the
protected capture helpers; host detachment/capture plumbing and handle lifetime records
are implementation-private. Logical focus may survive hiding/disabling a parent, but
input cannot reach that unavailable subtree, and Focus rejects foreign-host controls.
When LWSUI is enabled, LWSTests also covers event connection mutation and lifetime, UTF-8 editing and validation, popup cancellation, deferred context-menu commands, and scrolling without losing editor drafts. Core-only builds omit these UI tests.
SplitPanel owns exactly two controls and its divider. Nest instances for larger
layouts; Orientation::Horizontal places panes beside each other, and Vertical
stacks them. First(), Second(), and Divider() expose the owned roles.
SetRatio() selects the initial proportion, SetMinimumSizes() constrains panes,
and SetDefaultTrailingSize(size, maximumFraction) supports a preferred details
area. A user drag may exceed that initial fraction cap. When the available extent
is too small, pane minima scale proportionally. Dragging preserves editor focus;
Escape, detachment, or resizing cancels a drag. Tab/arrow keys resize a focused
divider; Home/double-click restores its configured default. OnEdit reports
preview/commit/cancel. Layout state is in memory only.
auto split = std::make_unique<LWSUI::SplitPanel>(
std::move(settings), std::move(description));
split->SetDefaultTrailingSize(180, .45f);
split->SetMinimumSizes(100, 80);
host.SetRoot(std::move(split));Panel owns one content control and supplies background, borders, padding and a
minimum height. DockPanel::AddDocked() consumes top/bottom/left/right space in
insertion order, then assigns Fill children the remaining rectangle. Ownership
order controls keyboard traversal independently of docking. Zero extent uses measured content. Take() removes
a docked child. FlowPanel::AddItem() lays out intrinsic widths, wraps rows, and
optionally grows an item to fill spare space. firstLeading and alignEnd support
leading Reset plus trailing action buttons. These role-owning controls must not
be structurally mutated through a base Container cast.
MeasureContent() requests intrinsic width; horizontal StackPanel::sizeToContent
uses those widths, and clipPartialChildren hides actions that cannot fit fully.
Labels support verticalAlignment, TextTrimming::Ellipsis, and minimumText
for stable intrinsic widths. GlyphButton supplies reusable chevrons; Image
presents a shared bitmap. Controls may set SetTooltip() and an optional cursor;
the host presents them. hitTestVisible=false makes a subtree pointer-transparent,
and Container::hitTestBackground=false passes hits through empty container space.
OnAttach() runs after children attach and complements OnDetach() for queued work.
The existing Slider has optional SliderValueMode::Hidden (default), ReadOnly,
and Editable presentation. SetValueDisplay() adds a normalized double readout
without changing continuous dragging. Value() and OnEdit remain normalized.
For a typed range, include <LWSUI/Composites.hpp> and configure the same slider:
auto& amount = slider.ConfigureValue<int64_t>({0, 100, 1}, 25,
LWSUI::SliderValueMode::Editable);
auto connection = amount.OnEdit.Connect([](int64_t value, LWSUI::EditPhase phase) {
// Preview, commit, or roll back the application's typed value.
});
amount.SetValue(30); // External writes cancel pending interaction without notifications.The returned model belongs to the slider and is valid until reconfiguration or
slider destruction. Reconfiguration is structural: post it through UIHost::Post
when requested from a callback. The numeric editor retains exact typed values;
integer slider bounds and span must fit within 2^53-1, while ordinary NumericEdit
keeps its wider integer support. Steps must be positive and bounds finite/distinct.
TrackBounds() excludes the value field; ValueField() returns the current visible
field or null. ScrollBar retains its ordinary normalized behavior.
LWSUI::GridView provides typed editable tables, full-row and multiple-row selection,
column movement/resizing/sorting, both-edge pinned columns and virtual scrolling.
It supports owned or external models and custom cell renderers/editors.
See GridView for configuration, events and model contracts.
Build LWSUIGridDemo with LWSUI_BUILD_DEMO=ON. A layout ComboBox and right-side
feature checkboxes exercise nine presets, including a single-column list and a
million-row external model. Single-click mode commits checkbox toggles and custom
progress-bar value actions immediately through normal validation and events.
--smoke exercises all presets, while --snapshot
also writes grid-<preset>.ppm. Run LWSTests "[grid]" for the focused tests.
LWS::Win32::CreateChild(window, hwnd, config) creates an owned child under a same-thread external HWND.
The host is not wrapped or subclassed. The child uses the normal LWS lifecycle, metrics, input and timer APIs,
and its destruction never destroys the external parent. The host sizes the child returned by the plugin.
Native parent destruction ends the child's lifetime through its ordinary window procedure.
config.parent must be null. For LWS-owned parents, use ordinary WindowConfig::parent instead so managed
family teardown remains authoritative. GetParent() returns only managed LWS parents and is null for an
external parent. Parent centering therefore requires a managed parent. Child fullscreen, activation, maximizing
and window dragging remain restricted. An external child may be reparented into a managed LWS family.
LWS deliberately has no borrowed-window ownership mode. Hosting needs an owned child, not read-only wrappers, rejected setters and special detachment semantics throughout the portable window API. The Win32 boundary adopts the external parent's DPI context during creation, while native events on the owned child provide its geometry and lifetime. The host must run its UI loop, and release all plugin resources before unloading their module.
Linux standalone remains Wayland-based. Linux lister hosting needs a GTK/Qt frontend adapter using the host's existing widget contract. A borrowed Wayland surface would not supply allocation, lifetime or event dispatch by itself, so no generic foreign-surface attachment API is provided.
The core Wayland tests use a private compositor. Portal scenarios additionally need the dbus-daemon
test utility and a private session bus with no service activation. From the LWS directory, run:
dbus-run-session --config-file=tests/portal-session.conf -- env LWS_TEST_PORTAL_BUS=1 build/LWSTestsThe fake portal checks selection, multiple paths, filters, default extensions, cancellation, service failure
and native owner destruction without displaying desktop dialogs. It does not replace production functions.
Applications using the real file picker require xdg-desktop-portal and an appropriate desktop backend.