mirror of
https://github.com/vczh-libraries/Release.git
synced 2026-09-21 14:13:55 +08:00
Update prompt
This commit is contained in:
@@ -549,6 +549,24 @@ It provides a comprehensive testing framework, XML-to-C++ compilation, and integ
|
||||
|
||||
### Choosing APIs
|
||||
|
||||
#### Remote Protocol Unit Test Framework
|
||||
|
||||
Testing GacUI applications without real OS windows or rendering, using the remote protocol architecture with a mock renderer (`UnitTestRemoteProtocol`) that captures rendering snapshots and simulates user input.
|
||||
|
||||
- Use `GacUIUnitTest_Initialize` and `GacUIUnitTest_Finalize` for global test executable setup and teardown.
|
||||
- Use `GacUIUnitTest_SetGuiMainProxy` to register frame-based test callbacks per test case.
|
||||
- Use `GacUIUnitTest_LinkGuiMainProxy` for decorator-style proxy chaining to compose setup layers.
|
||||
- Use `GacUIUnitTest_StartFast_WithResourceAsText<Theme>` for the most common entry point that compiles XML resources, registers themes, creates windows, and runs the application.
|
||||
- Use `GacUIUnitTest_Start` and `GacUIUnitTest_StartAsync` for synchronous and async protocol stack tests.
|
||||
- Use `OnNextIdleFrame(name, callback)` on `UnitTestRemoteProtocol` to register frame callbacks; the name describes the rendering result, not the upcoming action.
|
||||
- Use `LocationOf(controlOrComposition)` to compute absolute screen coordinates for input simulation.
|
||||
- Use `LClick`, `RClick`, `MClick`, `LDBClick`, `MouseMove` for mouse input simulation.
|
||||
- Use `KeyPress`, `KeyDown`, `KeyUp`, `TypeString` for keyboard input simulation.
|
||||
- Use `TryFindObjectByName<T>(window, name)` to look up named controls from GacUI XML resources.
|
||||
- Use `GetApplication()->InvokeInMainThread` to defer IO actions that would trigger blocking functions like `ShowDialog`.
|
||||
|
||||
[API Explanation](./KB_GacUI_RemoteProtocolUnitTestFramework.md)
|
||||
|
||||
### Design Explanation
|
||||
|
||||
#### Platform Initialization and Multi-Platform Architecture
|
||||
@@ -637,6 +655,46 @@ It provides a comprehensive testing framework, XML-to-C++ compilation, and integ
|
||||
|
||||
[Design Explanation](./KB_GacUI_Design_AddingNewControl.md)
|
||||
|
||||
#### Hosted Mode Window Management
|
||||
|
||||
- Hosted mode runs the entire GUI application inside a single OS native window, virtualizing all sub-windows, dialogs, popups, and menus as graphics within that window.
|
||||
- `GuiHostedController` is the central class wrapping a real native controller, implementing its own window manager, input dispatching, and service delegation while replacing the global `INativeController`.
|
||||
- The window manager template `hosted_window_manager::WindowManager<T>` maintains z-ordered lists (ordinary and top-most), a parent-child tree, and handles activation, focus, hit testing, dragging, and resizing.
|
||||
- `GuiHostedWindow` implements `INativeWindow` with a proxy pattern: `PlaceholderProxy` for unassigned windows, `MainProxy` delegating to the real native window, and `NonMainProxy` operating purely through the window manager.
|
||||
- Input dispatching uses `HandleMouseCallback` templates with pluggable PreAction/GetSelectedWindow/PostAction strategies, and `HandleKeyboardCallback` routes to the active window.
|
||||
- The rendering pipeline renders all hosted windows in a single begin/end session via `StartHostedRendering`/`StopHostedRendering`, with per-window offset via `GetRenderingOffset()`.
|
||||
- Remote mode inherently requires hosted mode, with `GuiHostedController` wrapping `GuiRemoteController` in the same architecture.
|
||||
|
||||
[Design Explanation](./KB_GacUI_Design_HostedModeWindowManagement.md)
|
||||
|
||||
#### Remote Protocol Core Architecture
|
||||
|
||||
- Remote protocol mode separates GacUI into a core side (application logic) and a renderer side (rendering and OS services), communicating through `IGuiRemoteProtocol`.
|
||||
- Messages flow core → renderer via `IGuiRemoteProtocolMessages`; events and responses flow renderer → core via `IGuiRemoteProtocolEvents`.
|
||||
- `GuiRemoteMessages` provides synchronous batched request-response with auto-incrementing IDs and blocking `Submit()`.
|
||||
- `GuiRemoteController` implements `INativeController` and all sub-services as virtual stubs: single window only, intentionally null clipboard/dialog services, synchronous key state queries.
|
||||
- Connection lifecycle: `SetupRemoteNativeController` creates a layered stack (`GuiRemoteController` → `GuiHostedController` → resource managers), with connect/disconnect/reconnection handling that re-sends all window state.
|
||||
- Rendering pipeline: element lifecycle via ID allocation, diff-based element updates, frame rendering flow (`StartRenderingOnNativeWindow` → traversal → `StopRenderingOnNativeWindow`), and measurement feedback loop (font heights, min sizes, image metadata, inline object bounds).
|
||||
- `GuiRemoteGraphicsParagraph` handles rich text with run property system, incremental diff synchronization, delegated layout queries, and caret bounds caching.
|
||||
- DOM diff layer (`GuiRemoteProtocolDomDiffConverter`) converts per-frame command streams into diffed tree structures.
|
||||
- Protocol combinator and filter layers enable composable transformations and traffic optimization via `[@DropRepeat]`/`[@DropConsecutive]` annotations.
|
||||
- Channel layer (`IGuiRemoteProtocolChannel`, `GuiRemoteProtocolAsyncChannelSerializer`) supports real remote deployment with async IO on a separate thread.
|
||||
|
||||
[Design Explanation](./KB_GacUI_Design_RemoteProtocolCoreArchitecture.md)
|
||||
|
||||
#### Remote Protocol Renderer and Serialization
|
||||
|
||||
- `GuiRemoteRendererSingle` is the renderer-side implementation that bridges `IGuiRemoteProtocol` to a real native window with actual graphics rendering, relying on an existing platform provider (e.g., Windows Direct2D).
|
||||
- It implements `IGuiRemoteProtocol` to receive protocol messages and translates them into native element operations, and implements `INativeWindowListener`/`INativeControllerListener` to forward OS events back as protocol events.
|
||||
- Rendering pipeline: receives `RequestRendererBeginRendering` with `OrdinaryElementDescVariant` updates, applies them to real graphics elements, renders the DOM tree in `GlobalTimer()`, and returns measurement feedback via `RespondRendererEndRendering`.
|
||||
- Event forwarding coalesces high-frequency events (mouse move, wheel, key auto-repeat) and sends discrete events immediately; hit testing is performed locally by traversing the rendering DOM tree.
|
||||
- Layered channel architecture for protocol serialization: `IGuiRemoteProtocol` ↔ `JsonObject` (via `GuiRemoteProtocolFromJsonChannel`/`GuiRemoteJsonChannelFromProtocol`) ↔ `WString` (via `JsonToStringSerializer`) ↔ user-implemented transport.
|
||||
- JSON envelope format with `semantic`, `id`, `name`, `arguments` fields; protocol types code-generated from `Protocol/*.txt` with `JsonHelper<T>` specializations.
|
||||
- `GuiRemoteProtocolAsyncChannelSerializer` provides thread separation (channel thread for IO, UI thread for application logic) with queued event delivery and connection-safe request matching.
|
||||
- Demo project pair (`RemotingTest_Core` and `RemotingTest_Rendering_Win32`) demonstrates full protocol stack assembly for both core and renderer sides with named-pipe/HTTP transport.
|
||||
|
||||
[Design Explanation](./KB_GacUI_Design_RemoteProtocolRendererAndSerialization.md)
|
||||
|
||||
## Experiences and Learnings
|
||||
|
||||
# Copy of Online Manual
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
Hosted Mode Window Management
|
||||
|
||||
Hosted mode is a GacUI operating mode in which the entire GUI application runs inside a single OS native window. Instead of creating one native window per `GuiWindow`, all sub-windows, dialogs, popups, and menus are rendered as graphics within the sole native window. The hosted mode implements its own window manager to handle z-ordering, hit testing, focus, activation, dragging, resizing, and input dispatching — functions normally provided by the OS.
|
||||
|
||||
The implementation lives in `Source/PlatformProviders/Hosted/` with these files:
|
||||
- `GuiHostedController.h/.cpp` — the core controller wrapping a native controller
|
||||
- `GuiHostedWindow.h/.cpp` — virtual window implementing `INativeWindow`
|
||||
- `GuiHostedWindowManager.h` — template-based window management algorithm
|
||||
- `GuiHostedGraphics.h/.cpp` — graphics resource manager wrapper
|
||||
- `GuiHostedApplication.h/.cpp` — global hosted application accessor
|
||||
- `GuiHostedWindowProxy_Main.cpp` — proxy for the main window
|
||||
- `GuiHostedWindowProxy_NonMain.cpp` — proxy for non-main windows
|
||||
- `GuiHostedWindowProxy_Placeholder.cpp` — proxy for windows before role assignment
|
||||
|
||||
## Entry Points and Activation
|
||||
|
||||
Hosted mode is activated through specific setup functions:
|
||||
- `SetupHostedWindowsDirect2DRenderer()` calls `SetupWindowsDirect2DRendererInternal(true, false)`
|
||||
- `SetupHostedWindowsGDIRenderer()` calls `SetupWindowsGDIRendererInternal(true, false)`
|
||||
- `SetupRemoteNativeController(protocol)` creates a `GuiHostedController` wrapping `GuiRemoteController` — remote mode inherently requires hosted mode
|
||||
|
||||
The internal setup functions (`SetupWindowsDirect2DRendererInternal`, `SetupWindowsGDIRendererInternal`) take a `bool hosted` parameter. When `hosted` is true:
|
||||
1. `StartWindowsNativeController(hInstance)` creates the real native controller
|
||||
2. A `GuiHostedController` is created wrapping the native controller
|
||||
3. `SetNativeController(hostedController)` replaces the global native controller
|
||||
4. `SetHostedApplication(hostedController->GetHostedApplication())` sets the hosted application global
|
||||
5. `RendererMainDirect2D` / `RendererMainGDI` is called with the hosted controller
|
||||
6. Inside the renderer main, `GuiHostedGraphicsResourceManager` wraps the native resource manager
|
||||
7. `hostedController->Initialize()` creates the single native window
|
||||
8. `GuiApplicationMain()` runs the application
|
||||
9. `hostedController->Finalize()` tears down
|
||||
|
||||
For remote mode in `SetupRemoteNativeController`:
|
||||
1. `GuiRemoteController` wraps the protocol
|
||||
2. `GuiHostedController` wraps the `GuiRemoteController`
|
||||
3. Both remote and hosted resource managers are created in a chain
|
||||
4. The same initialize/run/finalize sequence applies
|
||||
|
||||
## GuiHostedController Architecture
|
||||
|
||||
`GuiHostedController` is the central class of hosted mode. It inherits from:
|
||||
- `Object` — the GacUI base class
|
||||
- `hosted_window_manager::WindowManager<GuiHostedWindow*>` — the window management algorithm
|
||||
- `INativeWindowListener` — to listen to events on the single real native window
|
||||
- `INativeControllerListener` — to receive global timer, clipboard, and shortcut events
|
||||
- `INativeController` — to replace the real native controller for the rest of GacUI
|
||||
- `INativeAsyncService` — to redirect async operations through the real native controller
|
||||
- `INativeScreenService` / `INativeScreen` — to provide a virtual single screen
|
||||
- `INativeWindowService` — to create, destroy, and manage virtual windows
|
||||
- `IGuiHostedApplication` — to expose the native window host
|
||||
|
||||
### Service Delegation
|
||||
|
||||
The controller delegates most services to the underlying native controller:
|
||||
- `ResourceService()` → `nativeController->ResourceService()`
|
||||
- `ClipboardService()` → `nativeController->ClipboardService()`
|
||||
- `ImageService()` → `nativeController->ImageService()`
|
||||
- `InputService()` → `nativeController->InputService()`
|
||||
- `DialogService()` → returns `nullptr` (replaced by `FakeDialogServiceBase`)
|
||||
|
||||
Services it implements itself:
|
||||
- `CallbackService()` → local `SharedCallbackService` instance
|
||||
- `AsyncService()` → itself (delegates internally to native async service, translates window references)
|
||||
- `ScreenService()` → itself (provides a single virtual screen)
|
||||
- `WindowService()` → itself (manages virtual hosted windows)
|
||||
|
||||
### Virtual Screen
|
||||
|
||||
`GuiHostedController` acts as both `INativeScreenService` and `INativeScreen`:
|
||||
- `GetScreenCount()` returns 1
|
||||
- `GetScreen()` returns itself
|
||||
- Screen bounds are derived from the native window's client size
|
||||
- DPI scaling is forwarded from the real screen
|
||||
|
||||
### Native Window Lifecycle
|
||||
|
||||
`Initialize()` creates one real native OS window via `nativeController->WindowService()->CreateNativeWindow()` and installs itself as `INativeWindowListener` on it.
|
||||
|
||||
`Finalize()` destroys the native window and uninstalls listeners.
|
||||
|
||||
### Window Frame Configuration
|
||||
|
||||
`GuiHostedController` provides two frame configs:
|
||||
- `GetMainWindowFrameConfig()` — delegates to the native controller (the main window gets the real OS frame)
|
||||
- `GetNonMainWindowFrameConfig()` — returns a static config with `MaximizedBoxOption = AlwaysFalse`, `MinimizedBoxOption = AlwaysFalse`, `CustomFrameEnabled = AlwaysTrue` (non-main windows are always custom-frame rendered)
|
||||
|
||||
## Window Manager Template
|
||||
|
||||
### WindowManager<T> (GuiHostedWindowManager.h)
|
||||
|
||||
The core window management algorithm is template class `hosted_window_manager::WindowManager<T>` parameterized by window ID type. It maintains:
|
||||
- `registeredWindows` — dictionary of all registered windows
|
||||
- `ordinaryWindowsInOrder` — z-ordered list of normal windows (front to back)
|
||||
- `topMostedWindowsInOrder` — z-ordered list of top-most windows (front to back)
|
||||
- `mainWindow` — the root window
|
||||
- `activeWindow` — currently focused window
|
||||
- `needRefresh` — dirty flag for rendering
|
||||
|
||||
Pure virtual callbacks used by `GuiHostedController`:
|
||||
- `OnOpened` / `OnClosed` — fire `Opened` / `Closed` events on hosted window listeners
|
||||
- `OnEnabled` / `OnDisabled` — fire `Enabled` / `Disabled` events
|
||||
- `OnGotFocus` / `OnLostFocus` — fire `GotFocus` / `LostFocus` events
|
||||
- `OnActivated` / `OnDeactivated` — fire `Activated` / `Deactivated` events
|
||||
|
||||
### Window<T>
|
||||
|
||||
Each `Window<T>` holds:
|
||||
- `parent` — parent window pointer forming a tree structure
|
||||
- `children` — child windows list
|
||||
- `bounds` — position and size in the parent's client coordinate space
|
||||
- `topMost`, `visible`, `enabled`, `active`, `renderedAsActive` — state flags
|
||||
|
||||
Key operations:
|
||||
- `SetParent(value)` — reparents the window; null parent defaults to main window; updates z-order via `FixWindowInOrder`
|
||||
- `SetVisible(bool)` — shows or hides; triggers `FixWindowInOrder` and `OnOpened`/`OnClosed` callbacks
|
||||
- `SetTopMost(bool)` — toggles top-most status; moves window between ordinary and top-most z-order lists
|
||||
- `SetEnabled(bool)` — enables or disables; triggers callbacks
|
||||
- `BringToFront()` — moves window and its visible subtree to the front of z-order within its priority level
|
||||
- `Activate()` — sets focus; finds common parent with previously active window; updates `renderedAsActive` chain; calls `BringToFront`
|
||||
- `Deactivate()` — walks up parent chain to find next enabled window to activate
|
||||
- `Show()` — shorthand for `SetVisible(true)` + `Activate()`
|
||||
|
||||
### Z-Ordering
|
||||
|
||||
Two separate lists (`ordinaryWindowsInOrder` and `topMostedWindowsInOrder`) track window order. `IsEventuallyTopMost()` checks if a window or any ancestor is top-most and visible. `FixWindowInOrder` ensures windows are placed in the correct list based on their top-most status. `EnsureChildrenMovedInFrontOf` maintains the invariant that children appear before (in front of) their parent in the z-order list.
|
||||
|
||||
### HitTest
|
||||
|
||||
`WindowManager::HitTest(position)` iterates `topMostedWindowsInOrder` then `ordinaryWindowsInOrder`, returning the first visible window whose bounds contain the position — giving top-most windows priority.
|
||||
|
||||
### Start and Stop
|
||||
|
||||
`WindowManager::Start(mainWindow)` makes orphan windows children of the main window. `WindowManager::Stop()` clears all state and fires deactivation/close events.
|
||||
|
||||
## GuiHostedWindow — Virtual INativeWindow
|
||||
|
||||
`GuiHostedWindow` implements `INativeWindow` and inherits from `GuiHostedWindowData`, which holds all window properties (title, icon, cursor, size state, border options, etc.). Each `GuiHostedWindow` contains a `hosted_window_manager::Window<GuiHostedWindow*> wmWindow` embedded in its data.
|
||||
|
||||
### Proxy Pattern
|
||||
|
||||
`GuiHostedWindow` delegates behavior to an `IGuiHostedWindowProxy`. There are three proxy types:
|
||||
1. **PlaceholderProxy** (`GuiHostedWindowProxy_Placeholder.cpp`) — initial no-op proxy for newly created windows before role assignment
|
||||
2. **MainProxy** (`GuiHostedWindowProxy_Main.cpp`, `GuiMainHostedWindowProxy`) — for the main window; delegates title, icon, size, border properties, maximized/minimized/show/hide to the real native window
|
||||
3. **NonMainProxy** (`GuiHostedWindowProxy_NonMain.cpp`, `GuiNonMainHostedWindowProxy`) — for all other windows; operates purely through the window manager; disallows maximized/minimized boxes; enforces custom frame mode when system borders are used
|
||||
|
||||
`BecomeMainWindow()` and `BecomeNonMainWindow()` switch the proxy. This happens in `SettingHostedWindowsBeforeRunning()` when the main window is known, and in `CreateNativeWindow()` for windows created after the main window.
|
||||
|
||||
### Coordinate System
|
||||
|
||||
- `GetRenderingOffset()` returns the window's top-left position in the native window's client space as `NativeSize`
|
||||
- `GetBounds()` returns `wmWindow.bounds` directly (position is in parent's client space)
|
||||
- `GetClientSize()` equals `GetBounds().GetSize()` (no frame for hosted windows)
|
||||
- `GetClientBoundsInScreen()` equals `GetBounds()` (screen = client area of native window)
|
||||
- DPI conversion functions delegate to the real native window
|
||||
|
||||
### Capture and Cursor
|
||||
|
||||
- `RequireCapture()` sets `controller->capturingWindow` and calls `nativeWindow->RequireCapture()`
|
||||
- `ReleaseCapture()` clears `controller->capturingWindow` and calls `nativeWindow->ReleaseCapture()`
|
||||
- `SetWindowCursor()` only updates the real native window cursor if this is the currently hovering window
|
||||
- `SetCaretPoint()` adjusts by `GetRenderingOffset()` and sets on the real native window if this is the active window
|
||||
|
||||
## Window Creation and Destruction
|
||||
|
||||
### CreateNativeWindow(windowMode)
|
||||
|
||||
1. Creates a `GuiHostedWindow` with placeholder proxy
|
||||
2. Adds to `createdWindows` sorted list
|
||||
3. Registers with `wmManager`
|
||||
4. Fires `InvokeNativeWindowCreated` callback
|
||||
5. If main window already exists, calls `BecomeNonMainWindow()`
|
||||
|
||||
### DestroyNativeWindow(window)
|
||||
|
||||
1. Clears references (`enteringWindow`, `hoveringWindow`, `capturingWindow`, and WM-tracking pointers)
|
||||
2. Fires `Destroying` on listeners
|
||||
3. Fires `InvokeNativeWindowDestroying` callback
|
||||
4. Unregisters from `wmManager` (which reparents children)
|
||||
5. Removes from `createdWindows`
|
||||
|
||||
### Run(window)
|
||||
|
||||
1. Sets `mainWindow`
|
||||
2. Calls `SettingHostedWindowsBeforeRunning()` which:
|
||||
- Switches all windows to their proper proxies (main/non-main)
|
||||
- Centers native window on screen
|
||||
- Calls `wmManager->Start(&mainWindow->wmWindow)`
|
||||
3. Delegates to `nativeController->WindowService()->Run(nativeWindow)` for the message loop
|
||||
4. Handles exceptions in unit test mode
|
||||
5. Calls `DestroyHostedWindowsAfterRunning()` to tear down all hosted windows
|
||||
|
||||
## Input Event Dispatching
|
||||
|
||||
### Mouse Events
|
||||
|
||||
`GuiHostedController` listens to mouse events on the single native window. Mouse dispatching uses a template-based system:
|
||||
|
||||
`HandleMouseCallback<PreAction, GetSelectedWindow, PostAction, Callback>` is the core template:
|
||||
1. Calls `PreAction` (may trigger window manager operations)
|
||||
2. If not in a window-manager operation (`wmWindow == nullptr`), calls `GetSelectedWindow` to find the target
|
||||
3. Adjusts mouse coordinates by subtracting the target window's `wmWindow.bounds` top-left
|
||||
4. Dispatches to the target window's listeners
|
||||
5. Calls `PostAction`
|
||||
|
||||
Three `GetSelectedWindow` strategies:
|
||||
- `GetSelectedWindow_MouseDown` — closes popup windows not in the hovering chain, returns `capturingWindow` or `hoveringWindow`
|
||||
- `GetSelectedWindow_MouseMoving` — updates `hoveringWindow` via `UpdateHoveringWindow` and `enteringWindow` via `UpdateEnteringWindow`
|
||||
- `GetSelectedWindow_Other` — returns `capturingWindow` or `hoveringWindow`
|
||||
|
||||
Individual mouse event methods (`LeftButtonDown`, `MouseMoving`, etc.) are wired to the right combination of PreAction/GetSelectedWindow/PostAction via `IMPLEMENT_MOUSE_CALLBACK` macros.
|
||||
|
||||
### Window Manager Dragging and Resizing
|
||||
|
||||
`PreAction_LeftButtonDown` checks non-main enabled windows for hit test results (Title, BorderLeft, BorderRight, etc.). If a border/title hit is detected:
|
||||
- Sets `wmOperation` to the appropriate `WindowManagerOperation` enum value
|
||||
- Stores `wmWindow` pointer and `wmRelative` offset
|
||||
- Captures the native window
|
||||
|
||||
`PreAction_MouseMoving` handles ongoing drag/resize when `wmWindow` is set: recalculates bounds based on mouse position and operation type, calls `Moving` and `Moved` listeners.
|
||||
|
||||
`PostAction_LeftButtonUp` checks for `ButtonClose` hit test to close windows, and ends window manager operations.
|
||||
|
||||
### Mouse Hovering and Entering
|
||||
|
||||
- `UpdateHoveringWindow(location)` stores mouse position and calls `HitTestInClientSpace` to find the window under cursor
|
||||
- `UpdateEnteringWindow(window)` fires `MouseLeaved` on the old entering window and `MouseEntered` on the new one
|
||||
- `MouseLeaved` on the native window calls `UpdateEnteringWindow(nullptr)`
|
||||
|
||||
### Mouse-Down Activation
|
||||
|
||||
`PreAction_MouseDown` activates the hovering window when a mouse button is pressed (if the window is enabled and activation-enabled).
|
||||
|
||||
### Popup Closure on Click
|
||||
|
||||
`GetSelectedWindow_MouseDown` implements the pattern of closing popup windows (non-`Normal` mode windows) that are not ancestors of the clicked window.
|
||||
|
||||
### Keyboard Events
|
||||
|
||||
`HandleKeyboardCallback` dispatches keyboard events (`KeyDown`, `KeyUp`, `Char`) to the active window's listeners.
|
||||
|
||||
## Rendering Pipeline
|
||||
|
||||
### Hosted Rendering in GlobalTimer
|
||||
|
||||
`GuiHostedController::GlobalTimer()` drives the rendering cycle. On each global timer tick:
|
||||
1. Skip if the native window is not visible or already in hosted rendering
|
||||
2. Check all visible windows' listeners for `NeedRefresh()` — if any returns true, set `needRefresh`
|
||||
3. If no refresh is needed and nothing was updated last frame, skip rendering
|
||||
4. Enter rendering loop:
|
||||
- Call `renderTarget->StartHostedRendering()` on the native resource manager's render target
|
||||
- Iterate ordinary windows (back to front, reversed list order) then top-most windows
|
||||
- For each window, call each listener's `ForceRefresh(false, updated, failureByResized, failureByLostDevice)`
|
||||
- Call `renderTarget->StopHostedRendering()`
|
||||
- Handle failures: `LostDevice` → `RecreateRenderTarget`; `ResizeWhileRendering` → `ResizeRenderTarget`
|
||||
- On success, call `nativeWindow->RedrawContent()` and break
|
||||
|
||||
### Hosted Rendering Protocol (GuiGraphicsRenderTarget)
|
||||
|
||||
`GuiGraphicsRenderTarget` (base class for D2D/GDI render targets) supports hosted rendering:
|
||||
- `StartHostedRendering()` sets `hostedRendering = true` and calls `StartRenderingOnNativeWindow()` once
|
||||
- During hosted rendering, individual `StartRendering()` / `StopRendering()` pairs do NOT call `StartRenderingOnNativeWindow()` / `StopRenderingOnNativeWindow()` — they just toggle the `rendering` flag
|
||||
- `StopHostedRendering()` calls `StopRenderingOnNativeWindow()` once
|
||||
- This means all hosted windows render within a single begin/end rendering session on the native render target
|
||||
|
||||
### Per-Window Rendering Offset (GuiGraphicsHost)
|
||||
|
||||
`GuiGraphicsHost::Render()` (called from `ForceRefresh`) applies the rendering offset:
|
||||
1. `hostRecord.renderTarget->StartRendering()`
|
||||
2. `auto nativeOffset = hostRecord.nativeWindow->GetRenderingOffset()` — for hosted windows this returns the window position
|
||||
3. `auto localOffset = hostRecord.nativeWindow->Convert(nativeOffset)` — converts to logical pixels
|
||||
4. `windowComposition->Render(localOffset)` — renders the composition tree at the offset
|
||||
5. `hostRecord.renderTarget->StopRendering()`
|
||||
|
||||
The rendering offset mechanism is the key: in non-hosted mode, `GetRenderingOffset()` returns `(0,0)` because each window has its own render target. In hosted mode, it returns the window's position in the shared native window coordinate space, so all window content is rendered at the correct position within the single render target.
|
||||
|
||||
### GuiHostedGraphicsResourceManager
|
||||
|
||||
`GuiHostedGraphicsResourceManager` wraps the native resource manager:
|
||||
- `GetRenderTarget(window)` always returns the native render target for the single native window, regardless of which hosted window asks
|
||||
- `RecreateRenderTarget(window)` and `ResizeRenderTarget(window)` are no-ops (managed by the global timer loop)
|
||||
- All other methods (element registration, renderer factories, layout provider) delegate to the native manager
|
||||
|
||||
## Native Window Event Forwarding
|
||||
|
||||
`GuiHostedController` as `INativeWindowListener` on the single real native window forwards events to hosted windows:
|
||||
- `HitTest(location)` — if main window is enabled and cursor is on it, performs hit test via main window's listeners
|
||||
- `Moving(bounds)` — adjusts bounds to main window's coordinate space and forwards to main window's listeners
|
||||
- `Moved()` — syncs main window bounds to native window client size
|
||||
- `DpiChanged(preparing)` — recreates render target and broadcasts to all hosted windows
|
||||
- `GotFocus()` — reactivates last focused window (or main window)
|
||||
- `LostFocus()` — stores last focused window and deactivates all
|
||||
- `BeforeClosing(cancel)` / `AfterClosing()` — forwards to main window's listeners
|
||||
- `Opened()` — forwards to main window's listeners
|
||||
|
||||
## Dialog Service
|
||||
|
||||
`GuiHostedController::DialogService()` returns `nullptr`, causing GacUI to use `FakeDialogServiceBase` — GacUI-implemented dialogs rendered inside the hosted environment. This is critical because OS-native dialogs would create separate windows outside the hosted environment.
|
||||
|
||||
`FakeDialogServiceBase` provides view models (`IMessageBoxDialogViewModel`, `IColorDialogViewModel`, `IFontDialogViewModel`, `IOpenFileDialogViewModel`, `ISaveFileDialogViewModel`) and creates corresponding GacUI windows that appear as hosted sub-windows.
|
||||
|
||||
## Remote Mode Integration
|
||||
|
||||
Remote mode (`SetupRemoteNativeController`) always uses hosted mode:
|
||||
- `GuiRemoteController` implements `INativeController` through protocol communication
|
||||
- `GuiHostedController` wraps `GuiRemoteController` just as it wraps Windows native controllers
|
||||
- `GuiRemoteGraphicsResourceManager` wraps `GuiRemoteController`, then `GuiHostedGraphicsResourceManager` wraps that
|
||||
- The single native window in remote mode is the `GuiRemoteWindow`, which sends rendering commands through the protocol rather than to a real screen
|
||||
@@ -0,0 +1,322 @@
|
||||
Remote Protocol Core Architecture
|
||||
|
||||
Remote protocol mode separates GacUI into two sides: the **core side** runs all application logic and framework code, while the **renderer side** (client) handles actual rendering and OS services. They communicate through `IGuiRemoteProtocol`. The core side is always wrapped in hosted mode — `GuiHostedController` wraps `GuiRemoteController`.
|
||||
|
||||
Implementation lives in `Source/PlatformProviders/Remote/`.
|
||||
|
||||
## Motivation
|
||||
|
||||
In native modes (Windows Direct2D, GDI), GacUI couples directly to OS window management, rendering APIs, and input handling. Remote protocol decouples all of this: the core side becomes platform-independent and only speaks a protocol. This enables:
|
||||
- Running GacUI applications in a web browser, terminal, or any custom renderer
|
||||
- Unit testing without a real window system
|
||||
- Reconnection support — the core side can survive renderer disconnection and restore full visual state when a new renderer connects
|
||||
|
||||
## Protocol Design: Messages, Events, and Responses
|
||||
|
||||
The protocol has a strict directional split defined by three interface layers in `GuiRemoteProtocol.h`:
|
||||
|
||||
### IGuiRemoteProtocolMessages (Core Side → Renderer Side)
|
||||
|
||||
Messages are requests sent from the core side to the renderer side. They cover controller configuration queries, window operations, rendering commands, and image management. Some messages expect responses (indicated by `_RES` tag in the protocol definition).
|
||||
|
||||
### IGuiRemoteProtocolEvents (Renderer Side → Core Side)
|
||||
|
||||
Events are notifications sent from the renderer side to the core side. They include connection lifecycle (`OnControllerConnect`, `OnControllerDisconnect`, `OnControllerRequestExit`), user input (`OnIOKeyDown`, `OnIOKeyUp`, `OnIOMouseMoving`, etc.), and window state changes (`OnWindowBoundsUpdated`).
|
||||
|
||||
Responses are also delivered through `IGuiRemoteProtocolEvents` via `RespondNAME(id, arguments)` methods, matched to their originating request by request ID.
|
||||
|
||||
### Protocol Definition Files
|
||||
|
||||
Protocol messages and events are defined in `Protocol/*.txt` files (e.g., `Protocol_Controller.txt`, `Protocol_Renderer.txt`, `Protocol_IO.txt`) and code-generated into `GuiRemoteProtocolSchema.h`. The generated macros `GACUI_REMOTEPROTOCOL_MESSAGES` and `GACUI_REMOTEPROTOCOL_EVENTS` carry drop annotations (`[@DropRepeat]`, `[@DropConsecutive]`) used by the filter layer for traffic optimization.
|
||||
|
||||
## Request/Response Synchronization (GuiRemoteMessages)
|
||||
|
||||
`GuiRemoteMessages` wraps `IGuiRemoteProtocol` to provide a synchronous request-response model with batching:
|
||||
|
||||
1. **Queue requests**: Each `RequestNAME(...)` call enqueues a message. If the message expects a response, an auto-incrementing request ID (`++id`) is assigned and returned.
|
||||
2. **Submit**: `Submit()` flushes all queued messages to the renderer side via `IGuiRemoteProtocol::Submit()`. This call is **blocking** — it waits until all responses have been received.
|
||||
3. **Retrieve responses**: `RetrieveNAME(id)` fetches the response for a given request ID.
|
||||
|
||||
The core side operates synchronously from its own perspective despite the underlying protocol potentially being asynchronous. Batching multiple requests before a single `Submit()` is the primary mechanism for reducing round-trip overhead.
|
||||
|
||||
`GuiRemoteEvents` implements `IGuiRemoteProtocolEvents` on the core side. It receives both responses (stored for later retrieval by `GuiRemoteMessages`) and events (dispatched to `GuiRemoteController`, `GuiRemoteWindow`, etc.).
|
||||
|
||||
## GuiRemoteController: Virtual Native Controller
|
||||
|
||||
`GuiRemoteController` implements `INativeController` and all sub-services as virtual stubs that communicate via protocol. It inherits from:
|
||||
- `Object`
|
||||
- `INativeController`
|
||||
- `INativeResourceService`
|
||||
- `INativeInputService`
|
||||
- `INativeScreenService` / `INativeScreen`
|
||||
- `INativeWindowService`
|
||||
|
||||
### Service Design Decisions
|
||||
|
||||
- **Single window only**: `CreateNativeWindow` can only be called once (enforced by `CHECK_ERROR`). Multiple sub-windows are managed by `GuiHostedController` through hosted mode.
|
||||
- **Intentionally null services**: `ClipboardService()` and `DialogService()` return `nullptr`, causing GacUI to fall back to built-in fakes (`FakeDialogServiceBase`). This is intentional — the core side cannot access real OS clipboard or dialogs.
|
||||
- **Synchronous key state queries**: `IsKeyPressing()` and `IsKeyToggled()` perform synchronous request-submit-retrieve round-trips. This is expensive but rarely called.
|
||||
- **Global shortcut keys**: Tracked core-side and batch-sent to the renderer side on update.
|
||||
|
||||
## Connection Lifecycle
|
||||
|
||||
### Startup (SetupRemoteNativeController)
|
||||
|
||||
`SetupRemoteNativeController(IGuiRemoteProtocol* protocol)` creates a layered stack:
|
||||
1. `GuiRemoteController` — implements `INativeController`, wraps the protocol
|
||||
2. `GuiHostedController` — wraps `GuiRemoteController` for hosted mode sub-window support
|
||||
3. `GuiRemoteGraphicsResourceManager` — implements `GuiGraphicsResourceManager` and `IGuiGraphicsLayoutProvider`
|
||||
4. `GuiHostedGraphicsResourceManager` — wraps the above for hosted mode rendering
|
||||
|
||||
Initialization order: remote → resource manager → hosted controller. Finalization order: reverse. Then `GuiApplicationMain()` runs.
|
||||
|
||||
### Connection Establishing
|
||||
|
||||
A connection begins when the renderer side fires `OnControllerConnect(ControllerGlobalConfig)`. The core side responds:
|
||||
1. Acknowledges via `RequestControllerConnectionEstablished()`
|
||||
2. Fetches `FontConfig` and `ScreenConfig` in one batched `Submit()`
|
||||
3. Propagates the connection event to `remoteWindow`, `imageService`, and `resourceManager`, each of which restores its state
|
||||
|
||||
`GuiRemoteWindow::OnControllerConnect()` re-sends all current window styles when `applicationRunning` is true, enabling seamless reconnection — the renderer side experiences a fresh complete window state setup.
|
||||
|
||||
### Disconnection
|
||||
|
||||
The renderer side fires `OnControllerDisconnect()`. Each subsystem (`GuiRemoteWindow`, `GuiRemoteGraphicsImageService`, `GuiRemoteGraphicsResourceManager`) marks itself disconnected and suspends protocol communication until reconnection.
|
||||
|
||||
### Graceful Exit
|
||||
|
||||
The renderer side fires `OnControllerRequestExit()`:
|
||||
1. `remoteWindow.Hide(true)` triggers the close sequence
|
||||
2. `BeforeClosing` / `AfterClosing` listener chain fires
|
||||
3. `DestroyNativeWindow` is scheduled
|
||||
|
||||
`connectionForcedToStop` bypasses the `BeforeClosing` cancellation check for forced shutdown.
|
||||
|
||||
### Run Loop (RunOneCycle)
|
||||
|
||||
`GuiRemoteController::RunOneCycle()` drives the core side:
|
||||
1. Process incoming events via `IGuiRemoteEventProcessor::ProcessRemoteEvents()`
|
||||
2. `Submit()` flushes queued messages
|
||||
3. If timer enabled: `InvokeGlobalTimer()` — triggers hosted controller rendering
|
||||
4. Execute async tasks
|
||||
|
||||
The timer-driven rendering is crucial: `InvokeGlobalTimer()` causes `GuiHostedController` to decide whether to render, which calls into `GuiRemoteGraphicsRenderTarget`.
|
||||
|
||||
## GuiRemoteWindow: Virtual Native Window
|
||||
|
||||
`GuiRemoteWindow` implements `INativeWindow` for one virtual window.
|
||||
|
||||
### Timer-Driven Rendering
|
||||
|
||||
`IsActivelyRefreshing()` returns `true`, forcing hosted mode to render every cycle. `RedrawContent()` is a no-op because rendering is driven by the timer in `RunOneCycle`, not on-demand.
|
||||
|
||||
### Size and Bounds
|
||||
|
||||
Bounds and sizing use `remoteWindowSizingConfig` obtained from the renderer side. A `sizingConfigInvalidated` flag triggers a synchronous re-fetch when bounds are read — this ensures that style changes that affect the window frame are reflected before the next layout pass.
|
||||
|
||||
### Style Change Protocol
|
||||
|
||||
Two macros manage window style synchronization:
|
||||
- `SET_REMOTE_WINDOW_STYLE(STYLE, VALUE)` — checks for value change, then sends `RequestWindowNotifySetSTYLE` if changed
|
||||
- `SET_REMOTE_WINDOW_STYLE_INVALIDATE(STYLE, VALUE)` — same as above, but also marks `sizingConfigInvalidated = true` for styles that affect window sizing (e.g., border, title bar)
|
||||
|
||||
Managed properties: Title, CustomFrameMode, ShowInTaskBar, MaximizedBox, MinimizedBox, Border, SizeBox, IconVisible, TitleBar, TopMost.
|
||||
|
||||
### Reconnection
|
||||
|
||||
On reconnection (`OnControllerConnect`), `GuiRemoteWindow` re-sends all current window styles to the renderer side, so the renderer side can reconstruct the window exactly as it was.
|
||||
|
||||
## Rendering Pipeline
|
||||
|
||||
The rendering system bridges GacUI's composition-tree rendering model and the remote protocol's message-based communication. In native mode, rendering is immediate: traverse the composition tree, call graphics APIs. In remote mode, each element exists as an ID on both sides. The core side must track changes, batch element updates, send rendering commands, and receive measurement feedback.
|
||||
|
||||
### Element Lifecycle
|
||||
|
||||
Every visual element has a core-side renderer implementing `IGuiRemoteProtocolElementRender`, with a base class template `GuiRemoteProtocolElementRenderer<TElement, TRenderer, _RendererType>`. The concrete renderer classes include:
|
||||
- `GuiFocusRectangleElementRenderer`
|
||||
- `GuiRawElementRenderer`
|
||||
- `GuiSolidBorderElementRenderer`, `Gui3DBorderElementRenderer`, `Gui3DSplitterElementRenderer`
|
||||
- `GuiSolidBackgroundElementRenderer`, `GuiGradientBackgroundElementRenderer`
|
||||
- `GuiInnerShadowElementRenderer`
|
||||
- `GuiSolidLabelElementRenderer`
|
||||
- `GuiImageFrameElementRenderer`
|
||||
- `GuiPolygonElementRenderer`
|
||||
|
||||
Element lifecycle:
|
||||
1. **Creation**: When a renderer's render target is set (`RenderTargetChangedInternal`), it allocates an ID via `AllocateNewElementId()` and registers with `GuiRemoteGraphicsRenderTarget`. The ID is added to `createdRenderers`.
|
||||
2. **Notification**: `EnsureRequestedRenderersCreated()` (called at the start of each frame) batches pending creates and destroys into `RequestRendererCreated` / `RequestRendererDestroyed` messages to the renderer side.
|
||||
3. **Destruction**: `FinalizeInternal()` calls `UnregisterRenderer()`, moving the ID to `destroyedRenderers`.
|
||||
|
||||
### Frame Rendering Flow
|
||||
|
||||
A single rendering frame follows this sequence:
|
||||
|
||||
**StartRenderingOnNativeWindow()**:
|
||||
1. Capture current canvas size from `remoteWindow.GetClientSize()`
|
||||
2. Increment `renderingBatchId` (tracks which elements rendered this frame)
|
||||
3. `EnsureRequestedRenderersCreated()` — flush element create/destroy batches
|
||||
4. Build `ElementBeginRendering` with updated element descriptions:
|
||||
- **Normal mode**: iterate all renderers, collect only those where `IsUpdated()` returns true
|
||||
- **Reconnect mode** (`needFullElementUpdateOnNextFrame`): collect ALL renderers with `fullContent=true`, ensuring the new renderer side gets complete state
|
||||
5. Each renderer's `SendUpdateElementMessages()` serializes its current state into the update list
|
||||
6. For renderers needing min size from cache (`NeedUpdateMinSizeFromCache()`), add to `renderersAskingForCache`
|
||||
7. Send `RequestRendererBeginRendering` with the frame ID and all updates
|
||||
|
||||
**Composition tree traversal**:
|
||||
- Each element's `Render(bounds)` sends `RequestRendererRenderElement` with the element ID, bounds, and clipped area
|
||||
- Clipper push/pop sends `RequestRendererBeginBoundary` / `RequestRendererEndBoundary` with hit test results and cursor types per composition
|
||||
|
||||
**StopRenderingOnNativeWindow()**:
|
||||
1. Send `RequestRendererEndRendering`, `Submit()`, retrieve `ElementMeasurings` response
|
||||
2. Process four categories of measurement feedback (see below)
|
||||
3. If any min sizes changed, call `hostedController->RequestRefresh()` to trigger another frame
|
||||
4. Return `ResizeWhileRendering` if canvas size changed during rendering
|
||||
|
||||
### Element Update Mechanism (Diff-Based)
|
||||
|
||||
Each element renderer tracks its own dirty state:
|
||||
- `updated` flag: set by `OnElementStateChanged()` when element properties change
|
||||
- `renderTargetChanged` flag: set when the render target is first assigned
|
||||
|
||||
`SendUpdateElementMessages(fullContent, updatedElements)` serializes the element's current visual state. The `fullContent` parameter controls whether to include all properties or just changed ones.
|
||||
|
||||
For example, `GuiSolidLabelElementRenderer::SendUpdateElementMessages()` only includes font and text when they actually changed (comparing `lastFont`/`lastText`), unless `fullContent` or `renderTargetChanged` is true. This diff-based update minimizes protocol traffic — for a typical frame, most elements are unchanged.
|
||||
|
||||
### Measurement Feedback Loop
|
||||
|
||||
The core side cannot measure text or images locally — it must ask the renderer side. This creates a feedback loop processed in `StopRenderingOnNativeWindow()`:
|
||||
|
||||
1. **Font heights**: `GuiSolidLabelElementRenderer` includes a `measuringRequest` field (either `FontHeight` or `TotalSize`). The renderer side measures and returns `fontHeights` in `ElementMeasurings`, cached in `renderTarget->fontHeights` keyed by `(fontFamily, fontSize)`.
|
||||
|
||||
2. **Min sizes**: The renderer side returns `minSizes` per element ID. `StopRenderingOnNativeWindow()` calls `UpdateMinSize()` on each renderer.
|
||||
|
||||
3. **Image metadata**: Newly created images return metadata (dimensions, format) via `createdImages`. `GuiRemoteGraphicsImage::UpdateFromImageMetadata()` stores this.
|
||||
|
||||
4. **Inline object bounds**: For `DocumentParagraph` elements with inline objects, the renderer returns layout bounds per callback ID via `inlineObjectBounds`.
|
||||
|
||||
**Convergence**: If any min size changed, `hostedController->RequestRefresh()` schedules another rendering frame. The layout engine uses the new min sizes, potentially repositioning elements, requiring another render. This converges quickly because min sizes stabilize after one or two iterations.
|
||||
|
||||
**Font height caching**: `TryFetchMinSizeFromCache()` in `GuiSolidLabelElementRenderer` checks whether the needed font height is already in `renderTarget->fontHeights`. If cached, it uses the value directly and skips the measurement request, avoiding redundant round-trips.
|
||||
|
||||
### Rendering Boundary Tracking
|
||||
|
||||
During composition tree traversal, clipper operations emit boundary messages to enable renderer-side hit testing and cursor display:
|
||||
- `AfterPushedClipper()`: inspects the composition for hit test results and cursor types. If either is set and differs from the current stack top, sends `RequestRendererBeginBoundary` with the composition's `remoteId`, bounds, and clipped area.
|
||||
- `AfterPoppedClipper()`: sends `RequestRendererEndBoundary` for the matching boundary.
|
||||
|
||||
This allows the renderer side to know which composition each pixel belongs to without needing the full composition tree.
|
||||
|
||||
## GuiRemoteGraphicsParagraph (IGuiGraphicsParagraph)
|
||||
|
||||
`GuiRemoteGraphicsParagraph` is the remote implementation of `IGuiGraphicsParagraph`, handling rich text layout for document elements. It is the most protocol-intensive part of the rendering system.
|
||||
|
||||
### Why Paragraphs Are Special
|
||||
|
||||
Unlike ordinary elements (borders, backgrounds, labels) that send their visual state and let the renderer side draw them, paragraphs need **bidirectional communication**:
|
||||
- The core side defines text content, styling, and inline objects
|
||||
- The renderer side performs text layout (line breaking, glyph positioning)
|
||||
- The core side needs layout results (sizes, caret positions, inline object positions) for hit testing, caret navigation, and selection
|
||||
|
||||
### Lifecycle
|
||||
|
||||
Paragraphs are created by `GuiRemoteGraphicsResourceManager::CreateParagraph()`. Each paragraph gets its own element ID (shared ID space with element renderers), registered via `RegisterParagraph()` instead of `RegisterRenderer()`. Creations are batched in `pendingParagraphCreations` alongside element creates in `EnsureRequestedRenderersCreated()`.
|
||||
|
||||
### Run Property System
|
||||
|
||||
Paragraph styling uses a run-based system with three layers:
|
||||
1. **`DocumentTextRunPropertyMap textRuns`**: text-level properties (font, size, color, style) by `CaretRange`
|
||||
2. **`DocumentInlineObjectRunPropertyMap inlineObjectRuns`**: inline objects (images, controls embedded in text) by `CaretRange`
|
||||
3. **`DocumentRunPropertyMap stagedRuns` / `committedRuns`**: merged result used for diff computation
|
||||
|
||||
`AddTextRun()` handles overlapping ranges by splitting and merging, supporting incremental property changes. `AddInlineObjectRun()` requires exact range matches (inline objects cannot be split). `MergeRuns()` combines both maps, with inline objects taking priority over text runs.
|
||||
|
||||
### Core Synchronization (EnsureRemoteParagraphSynced)
|
||||
|
||||
This is the core synchronization method, called before any query:
|
||||
1. `EnsureRequestedRenderersCreated()` — ensures the paragraph element exists on the renderer side
|
||||
2. Merge text and inline object runs into `stagedRuns`
|
||||
3. `DiffRuns(committedRuns, stagedRuns, desc)` — compute diff between last committed and current state
|
||||
4. Send `RequestRendererUpdateElement_DocumentParagraph(desc)` with the diff
|
||||
5. `Submit()` synchronously — the response includes the new `documentSize`
|
||||
6. Store `cachedSize` from response, swap `stagedRuns` to `committedRuns`
|
||||
|
||||
The first sync sends full text content; subsequent syncs send only run property diffs. This is efficient for scenarios like syntax highlighting where only styling changes between frames.
|
||||
|
||||
### Layout Queries (Delegated to Renderer Side)
|
||||
|
||||
All layout-dependent operations require synchronous round-trips or cached data:
|
||||
- **`GetSize()`**: returns `cachedSize` from the last sync
|
||||
- **`GetCaret(comparingCaret, position, preferFrontSide)`**: sends `RequestDocumentParagraph_GetCaret` — the renderer side performs text layout-aware caret navigation
|
||||
- **`GetCaretBounds(caret, frontSide)`**: uses `GetCaretBoundsInternal()` which lazily fetches ALL caret bounds (front and back arrays) in one request via `RequestDocumentParagraph_GetCaretBounds` and caches them in `cachedCaretBounds`. Subsequent calls use the cache.
|
||||
- **`GetCaretFromPoint(point)`**: iterates all caret positions locally using cached bounds from `GetCaretBoundsInternal()`, finding the nearest by Manhattan distance — no additional remote call if bounds are cached
|
||||
- **`GetInlineObjectFromPoint(point)`**: sends `RequestDocumentParagraph_GetInlineObjectFromPoint` for hit-test, then looks up `inlineObjectProperties` locally
|
||||
- **`GetNearestCaretFromTextPos(textPos, frontSide)`**: sends `RequestDocumentParagraph_GetNearestCaretFromTextPos`
|
||||
- **`IsValidCaret(caret)`**: sends `RequestDocumentParagraph_IsValidCaret`
|
||||
- **`IsValidTextPos(textPos)`**: purely local — checks bounds against `text.Length()`
|
||||
|
||||
### Caret Display
|
||||
|
||||
- `EnableCaret()` sends `RequestDocumentParagraph_OpenCaret` with position, color, and front/back side
|
||||
- `DisableCaret()` sends `RequestDocumentParagraph_CloseCaret`
|
||||
- `BlinkCaret()` returns false — blinking is handled renderer-side
|
||||
|
||||
### Paragraph Rendering
|
||||
|
||||
`GuiRemoteGraphicsParagraph::Render(bounds)`:
|
||||
1. `EnsureRemoteParagraphSynced()` — ensure current state sent to renderer side
|
||||
2. For each inline object with cached bounds, call `callback->OnRenderInlineObject()` — if the inline object's size changed, update `inlineObjectRuns` and mark dirty
|
||||
3. Send `RequestRendererRenderElement` — same as ordinary elements
|
||||
|
||||
### Dirty Tracking
|
||||
|
||||
`MarkParagraphDirty(invalidateSize, invalidateCaretBoundsCache)` sets:
|
||||
- `needUpdate = true` always
|
||||
- `cachedSize = {0,0}` if `invalidateSize` is true (forcing re-measurement from renderer side)
|
||||
- `needUpdateCaretBoundsCache = true` if `invalidateCaretBoundsCache` is true (forcing caret bounds refetch)
|
||||
|
||||
Size-affecting changes (font, size, text content, wrap line, max width) set both invalidation flags. Color-only changes skip size invalidation because they don't affect layout.
|
||||
|
||||
## DOM Diff Layer
|
||||
|
||||
### Motivation
|
||||
|
||||
Without DOM diff, each frame sends per-element rendering commands (`RenderElement`, `BeginBoundary`, `EndBoundary`). For a typical GacUI application with hundreds of elements, most of which don't move between frames, this is wasteful. The DOM diff layer converts the per-frame command stream into a tree structure and diffs against the previous frame, sending only structural changes.
|
||||
|
||||
### How It Works
|
||||
|
||||
`GuiRemoteProtocolDomDiffConverter` extends `GuiRemoteProtocolCombinator_PassingThrough<GuiRemoteEventDomDiffConverter>` and wraps `IGuiRemoteProtocol`:
|
||||
- During rendering, `RenderingDomBuilder` builds a `RenderingDom` tree from the intercepted boundary and element commands
|
||||
- On `RequestRendererEndRendering`:
|
||||
- First frame: sends `RequestRendererRenderDom` with the full tree
|
||||
- Subsequent frames: `DiffDom(lastDom, lastDomIndex, newDom, newDomIndex, diffs)` computes structural diffs, sends `RequestRendererRenderDomDiff`
|
||||
- `lastDom` is stored for next frame; `OnControllerConnect` clears it to force a full DOM send on reconnection
|
||||
|
||||
DOM node IDs encode their type: element IDs use `(elementId << 2) + 0`, hit test compositions use `(compositionId << 2) + 2`, with parent variants at `+1` and `+3`.
|
||||
|
||||
## Protocol Combinator Layer
|
||||
|
||||
The combinator pattern allows composing protocol transformations as a pipeline. `GuiRemoteProtocolCombinator<TEvents>` (in `GuiRemoteProtocol_Shared.h`) wraps an `IGuiRemoteProtocol*` and interposes a `TEvents` event combinator. This creates a chain where messages flow core → combinator → target protocol, and events flow target protocol → event combinator → core.
|
||||
|
||||
`GuiRemoteProtocolCombinator_PassingThrough` (specialization with `void` events plus `GuiRemoteEventCombinator_PassingThrough`) forwards everything unchanged, allowing subclasses to selectively override only the messages they care about. Both the filter layer and DOM diff layer use this pattern.
|
||||
|
||||
## Protocol Filter Layer
|
||||
|
||||
`GuiRemoteProtocolFilter` wraps `IGuiRemoteProtocol` via `GuiRemoteProtocolCombinator<GuiRemoteEventFilter>` and optimizes traffic by dropping redundant messages/events:
|
||||
- `[@DropRepeat]`: drop messages or events with identical arguments to the previous send, regardless of what happened in between
|
||||
- `[@DropConsecutive]`: drop events with identical arguments to the immediately preceding event of the same type
|
||||
|
||||
The filter queues messages internally and applies drop logic in `ProcessRequests()` before `Submit()`.
|
||||
|
||||
## Channel Layer
|
||||
|
||||
For real remote deployment, `IGuiRemoteProtocolChannel<T>` provides a bidirectional channel abstraction. The typical stack:
|
||||
1. `GuiRemoteProtocolFromJsonChannel` adapts a JSON channel to `IGuiRemoteProtocol`
|
||||
2. `GuiRemoteProtocolAsyncChannelSerializer<Ptr<JsonObject>>` (aliased as `GuiRemoteProtocolAsyncJsonChannelSerializer`) runs channel IO on a separate thread
|
||||
|
||||
The async serializer's design: the UI thread batches messages and blocks on `Submit()` until the channel thread completes the round-trip. Events from the channel thread are queued and processed on the UI thread during `ProcessRemoteEvents()`. Connection/disconnection is tracked via a `connectionCounter` to handle race conditions when the channel thread delivers events after disconnection.
|
||||
|
||||
## Image Service
|
||||
|
||||
`GuiRemoteGraphicsImageService` implements `INativeImageService` and creates `GuiRemoteGraphicsImage` objects with core-side binary copies. Image metadata (dimensions, format) is lazily fetched from the renderer side via `ImageCreated` messages in the `ElementMeasurings` response.
|
||||
|
||||
On reconnection (`OnControllerConnect`), all existing images are re-registered with the renderer side so it can recreate its local image objects. On disconnection (`OnControllerDisconnect`), the service suspends sending and resets metadata that may have become stale.
|
||||
@@ -0,0 +1,190 @@
|
||||
#### Remote Protocol Renderer and Serialization
|
||||
|
||||
The remote protocol renderer side receives protocol messages from the core side and translates them into native window operations and graphics rendering. The serialization and channel infrastructure provides composable layers that convert between typed protocol calls and transport-ready strings, enabling GacUI applications to run across process boundaries over any user-provided transport (named pipe, HTTP, WebSocket, etc.).
|
||||
|
||||
## GuiRemoteRendererSingle
|
||||
|
||||
`GuiRemoteRendererSingle` (in `Source/PlatformProviders/RemoteRenderer/`) is the renderer-side implementation that bridges `IGuiRemoteProtocol` to a real native window with actual graphics rendering. It is not a full `INativeController` — it relies on a real platform provider (e.g., Windows Direct2D via `SetupRawWindowsDirect2DRenderer()`) already running, and registers itself as a listener on a native window.
|
||||
|
||||
### Class Hierarchy
|
||||
|
||||
`GuiRemoteRendererSingle` inherits from:
|
||||
- `Object` — standard base
|
||||
- `IGuiRemoteProtocol` (virtual) — receives all protocol messages from the core side
|
||||
- `INativeWindowListener` (protected virtual) — listens to native window events (mouse, keyboard, window lifecycle)
|
||||
- `INativeControllerListener` (protected virtual) — listens to global timer and shortcut key events
|
||||
|
||||
### Key Members
|
||||
|
||||
- `window` / `screen` / `events`: The native window handle, current screen, and `IGuiRemoteProtocolEvents` callback for firing events to the core side.
|
||||
- `availableElements`: `Dictionary<vint, Ptr<IGuiGraphicsElement>>` mapping element IDs to actual graphics elements created by the real rendering engine (e.g., `GuiSolidBorderElement`, `GuiSolidLabelElement`).
|
||||
- `availableImages`: `Dictionary<vint, Ptr<INativeImage>>` mapping image IDs to loaded native images.
|
||||
- `renderingDom` / `renderingDomIndex`: The rendering DOM tree received from the core side, used for on-screen rendering and hit testing.
|
||||
- `solidLabelMeasurings` / `fontHeightMeasurings`: Caches and measurement tracking for text elements, supporting the measurement feedback loop.
|
||||
- `pendingMouseMove`, `pendingHWheel`, `pendingVWheel`, `pendingKeyAutoDown`, `pendingWindowBoundsUpdate`: Accumulation fields for coalescing high-frequency events before sending.
|
||||
|
||||
### Source File Organization
|
||||
|
||||
- `GuiRemoteRendererSingle.cpp`: Construction, destruction, main window registration, connection lifecycle (`Opened`, `BeforeClosing`, `Closed`), screen/config management.
|
||||
- `GuiRemoteRendererSingle_Controller.cpp`: Controller-level requests — `RequestControllerGetFontConfig`, `RequestControllerGetScreenConfig`, `RequestControllerConnectionEstablished`, `RequestControllerConnectionStopped`.
|
||||
- `GuiRemoteRendererSingle_MainWindow.cpp`: Window style notifications — `RequestWindowNotifySetBounds`, `RequestWindowNotifySetTitle`, `RequestWindowNotifySetEnabled`, `RequestWindowNotifyShow`, etc.
|
||||
- `GuiRemoteRendererSingle_IO.cpp`: IO requests (global shortcuts, mouse capture, key state queries) and native-to-protocol input event conversion. Contains `SendAccumulatedMessages()` for batching high-frequency events.
|
||||
- `GuiRemoteRendererSingle_Rendering.cpp`: Core rendering pipeline — element creation/destruction (`RequestRendererCreated`, `RequestRendererDestroyed`), begin/end rendering, DOM rendering (`Render` recursive traversal), hit testing, and `GlobalTimer`/`Paint` driven refresh.
|
||||
- `GuiRemoteRendererSingle_Rendering_Elements.cpp`: Property updates on ordinary graphics elements (solid border, sink border, splitter, background, gradient, inner shadow, polygon).
|
||||
- `GuiRemoteRendererSingle_Rendering_Label.cpp`: Solid label measurement and property updates.
|
||||
- `GuiRemoteRendererSingle_Rendering_Image.cpp`: Image creation, metadata, and image frame element updates.
|
||||
- `GuiRemoteRendererSingle_Rendering_Document.cpp`: Complex document paragraph element rendering via `GuiRemoteDocumentParagraphElement` inner class.
|
||||
|
||||
### Rendering Pipeline
|
||||
|
||||
When the core sends `RequestRendererBeginRendering(ElementBeginRendering)`, the `updatedElements` field contains a list of `OrdinaryElementDescVariant`. Each variant is dispatched via `Apply(Overloading(...))` to the corresponding `RequestRendererUpdateElement_*` helper, which looks up the element by ID in `availableElements` and sets its properties.
|
||||
|
||||
The core sends `RequestRendererRenderDom` or `RequestRendererRenderDomDiff` to update the rendering DOM tree. The legacy command-based rendering path (`RequestRendererBeginBoundary`/`RequestRendererEndBoundary`/`RequestRendererRenderElement`) is disabled with `CHECK_FAIL` — the DOM-diff approach is required.
|
||||
|
||||
Actual painting happens in `GlobalTimer()`: if `needRefresh` is true, it calls `Render(renderingDom, rt)` which recursively traverses the DOM tree, rendering each element in order with clipping. After rendering, it checks if label measurements changed and populates `elementMeasurings`. The response is returned via `RespondRendererEndRendering(id, elementMeasurings)`.
|
||||
|
||||
### Event Forwarding
|
||||
|
||||
`GuiRemoteRendererSingle` implements all `INativeWindowListener` mouse/keyboard callbacks to forward OS events as protocol events:
|
||||
- **Discrete events** (button clicks, key presses): Sent immediately via `events->OnIOMouseLeft`, `events->OnIOKeyDown`, etc.
|
||||
- **High-frequency events** (mouse move, wheel, key auto-repeat): Accumulated and coalesced:
|
||||
- `pendingMouseMove`: Only the latest mouse position is kept.
|
||||
- `pendingHWheel` / `pendingVWheel`: Wheel deltas are summed across frames.
|
||||
- `pendingKeyAutoDown`: Only the latest auto-repeat key is kept.
|
||||
- `SendAccumulatedMessages()` is called from `GlobalTimer()` to flush these accumulated events.
|
||||
- **Window lifecycle events** (`Opened`, `BeforeClosing`, `Moved`, `DpiChanged`): Translated to protocol events like `OnControllerConnect`, `OnControllerRequestExit`, `OnWindowBoundsUpdated`.
|
||||
|
||||
### Hit Testing
|
||||
|
||||
Hit testing is performed locally in the renderer by traversing the rendering DOM tree via `HitTestInternal`. Each DOM node may have `hitTestResult` and `cursor` attributes set by the core side. The renderer walks the tree and finds the matching node for a given point. This avoids round-trips — hit testing stays entirely renderer-side.
|
||||
|
||||
## Protocol Serialization and Channel Architecture
|
||||
|
||||
The serialization system uses a layered channel architecture to convert between typed protocol calls and transport-ready data. Each layer is a composable building block.
|
||||
|
||||
### Core Interfaces
|
||||
|
||||
- `IGuiRemoteProtocolChannel<TPackage>`: A bidirectional channel that can `Write` packages and receives packages via `IGuiRemoteProtocolChannelReceiver<TPackage>::OnReceive`.
|
||||
- `IGuiRemoteProtocol`: The high-level typed protocol interface with named methods (`RequestNAME`, `OnNAME`, `RespondNAME`).
|
||||
|
||||
### Layer 1: IGuiRemoteProtocol ↔ JsonObject Channel
|
||||
|
||||
Two adapter classes handle bidirectional conversion between typed protocol calls and JSON objects:
|
||||
|
||||
- `GuiRemoteProtocolFromJsonChannel`: Wraps `IGuiRemoteProtocolChannel<Ptr<JsonObject>>` and implements `IGuiRemoteProtocol`. When `RequestNAME(arguments)` is called, it serializes arguments to JSON via `ConvertCustomTypeToJson()`, packs them into a JSON envelope with `JsonChannelPack()`, and calls `channel->Write(package)`. When it receives a JSON package via `OnReceive`, it unpacks with `JsonChannelUnpack()`, dispatches by name, deserializes via `ConvertJsonToCustomType()`, and calls `events->OnNAME()` or `events->RespondNAME()`.
|
||||
- `GuiRemoteJsonChannelFromProtocol`: The reverse — wraps `IGuiRemoteProtocol` and implements `IGuiRemoteProtocolChannel<Ptr<JsonObject>>`. Converts incoming JSON packages to protocol calls, and outgoing events/responses to JSON packages.
|
||||
|
||||
### Layer 2: JsonObject ↔ WString Channel
|
||||
|
||||
`JsonToStringSerializer` handles JSON-to-string conversion:
|
||||
- `Serialize`: Converts `Ptr<JsonObject>` to `WString` using `JsonToString` with compact formatting.
|
||||
- `Deserialize`: Parses `WString` to `Ptr<JsonObject>` using `JsonParse`.
|
||||
- Type aliases: `GuiRemoteJsonChannelStringSerializer` and `GuiRemoteJsonChannelStringDeserializer` for `GuiRemoteProtocolChannelSerializer<JsonToStringSerializer>` and `GuiRemoteProtocolChannelDeserializer<JsonToStringSerializer>`.
|
||||
|
||||
### Layer 3: WString ↔ Transport (User-Implemented)
|
||||
|
||||
The user provides an `IGuiRemoteProtocolChannel<WString>` implementation that sends and receives strings over any transport mechanism (named pipe, HTTP, WebSocket, etc.). GacUI does not provide transport implementations directly.
|
||||
|
||||
### Layer 4: UTF String Conversion (Optional)
|
||||
|
||||
`GuiRemoteUtfStringChannelSerializer` / `GuiRemoteUtfStringChannelDeserializer` can convert between `WString` and other UTF string types if the transport requires a specific encoding.
|
||||
|
||||
### Channel Transformer Pattern
|
||||
|
||||
`GuiRemoteProtocolChannelTransformerBase<TFrom, TTo>` bridges two channel types:
|
||||
- `GuiRemoteProtocolChannelSerializer<TSerialization>`: Calls `TSerialization::Serialize` on `Write` and `TSerialization::Deserialize` on `OnReceive`.
|
||||
- `GuiRemoteProtocolChannelDeserializer<TSerialization>`: The reverse — calls `Deserialize` on `Write` and `Serialize` on `OnReceive`.
|
||||
|
||||
The `TSerialization` concept requires: `SourceType`, `DestType`, `ContextType`, and static `Serialize`/`Deserialize` methods.
|
||||
|
||||
### JSON Envelope Format
|
||||
|
||||
Every package is a JSON object with fields:
|
||||
- `"semantic"`: One of `"Message"`, `"Request"`, `"Response"`, `"Event"`.
|
||||
- `"id"`: (optional) An integer request/response ID, present only for `Request` and `Response`.
|
||||
- `"name"`: The message/event name (e.g., `"ControllerGetFontConfig"`, `"IOMouseMoving"`).
|
||||
- `"arguments"`: (optional) The serialized arguments as a JSON value.
|
||||
|
||||
Messages (no response expected) use `"Message"`. Requests (response expected) use `"Request"` with an ID. Responses use `"Response"` with the matching ID. Events use `"Event"`.
|
||||
|
||||
### JSON Serialization of Protocol Types
|
||||
|
||||
Protocol types are code-generated from `Protocol/*.txt` files into `GuiRemoteProtocolSchema.h`/`.cpp`. Each struct gets a `JsonHelper<T>` specialization with `ToJson` and `FromJson` methods. The shared infrastructure in `GuiRemoteProtocolSchemaShared.h` provides:
|
||||
- Primitive type serializers: `bool`, `vint`, `float`, `double`, `WString`, `wchar_t`, `VKEY`, `Color`, `Ptr<MemoryStream>` (Base64-encoded).
|
||||
- Generic container serializers: `Nullable<T>`, `Ptr<T>`, `List<T>`, `ArrayMap<K,V,F>`, `Dictionary<K,V>`.
|
||||
- `Variant` types are serialized with a type discriminator field.
|
||||
- `ConvertCustomTypeToJsonField` adds a named field to a JSON object.
|
||||
|
||||
### Async Channel
|
||||
|
||||
`GuiRemoteProtocolAsyncChannelSerializer<TPackage>` provides thread separation for real remote deployment:
|
||||
- A **channel thread** handles all `Write`/`OnReceive` calls on the underlying channel (IO operations).
|
||||
- A **UI thread** runs the GacUI application logic.
|
||||
- `Start(channel, uiMainProc, startingProc)` launches both threads. The `startingProc` is responsible for creating the threads.
|
||||
- Events received asynchronously are queued and dispatched on the UI thread during `Submit()`.
|
||||
- Responses are matched to pending requests using a `PendingRequestGroup` with `connectionCounter` for safe handling of disconnection/reconnection races.
|
||||
- `ExecuteInChannelThread()` allows queueing work from any thread to the channel thread.
|
||||
|
||||
## Demo Project Pair
|
||||
|
||||
Two projects in `Test/GacUISrc/` demonstrate a full remote protocol deployment. They are paired: one is the core side (console application) and the other is the renderer side (Windows application).
|
||||
|
||||
### RemotingTest_Core (Console Application)
|
||||
|
||||
Located at `Test/GacUISrc/RemotingTest_Core/`. Accepts `/Pipe` or `/Http` arguments to start either a named-pipe server or HTTP server.
|
||||
|
||||
**Protocol stack setup** (`StartServer<TServer>` in `GuiMain.cpp`):
|
||||
1. Creates `CoreChannel` wrapping the transport server — implements `IGuiRemoteProtocolChannel<WString>`.
|
||||
2. Creates `GuiRemoteJsonChannelStringSerializer` for JSON ↔ String conversion.
|
||||
3. Creates `GuiRemoteProtocolAsyncJsonChannelSerializer` for async channel/UI thread separation.
|
||||
4. Starts via `asyncChannelSender.Start()` with a UI main proc that builds the protocol stack: `GuiRemoteProtocolFromJsonChannel` → `GuiRemoteProtocolFilter` → `GuiRemoteProtocolDomDiffConverter` → `SetupRemoteNativeController`.
|
||||
5. Waits for a renderer client to connect, then waits for the async channel to stop, then waits for disconnection.
|
||||
|
||||
**CoreChannel** bridges async channel and network transport:
|
||||
- `Write(WString)`: Accumulates messages in a pending list.
|
||||
- `Submit()`: Dispatches accumulated messages via `networkProtocol->SendStringArray()`.
|
||||
- `OnReadStringThreadUnsafe()`: Called when strings arrive from the network, queues them for channel thread processing via `asyncChannel->ExecuteInChannelThread()`.
|
||||
- Detects `ControllerConnect` event JSON to track connection state.
|
||||
- On reconnection (`OnReconnectedUnsafe`), injects a `ControllerDisconnect` event.
|
||||
|
||||
### RemotingTest_Rendering_Win32 (Windows Application)
|
||||
|
||||
Located at `Test/GacUISrc/RemotingTest_Rendering_Win32/`. Accepts `/Pipe` or `/Http` arguments to start as a named-pipe or HTTP client.
|
||||
|
||||
**Protocol stack setup** (`StartClient<TClient>` in `GuiMain.cpp`):
|
||||
1. Creates `GuiRemoteRendererSingle` — the renderer implementing `IGuiRemoteProtocol`.
|
||||
2. Creates `GuiRemoteJsonChannelFromProtocol` wrapping the renderer — converts protocol calls to JSON.
|
||||
3. Creates `GuiRemoteJsonChannelStringDeserializer` for String ↔ JSON conversion.
|
||||
4. Creates `RendererChannel` connecting renderer, transport, and channel.
|
||||
5. Hooks `BeforeWrite`/`BeforeOnReceive` events for request/response caching in `RendererChannel`.
|
||||
6. Calls `SetupRawWindowsDirect2DRenderer()` to run the native window event loop.
|
||||
|
||||
**RendererChannel** bridges network transport and JSON channel:
|
||||
- `OnReadStringThreadUnsafe()`: Receives strings from network, dispatches to UI thread via `InvokeInMainThread()`. Handles error strings (prefixed with `!`) by displaying a `MessageBox` and calling `renderer->ForceExitByFatelError()`.
|
||||
- `OnReceive(WString)`: Uses a caching mechanism (`isCaching`/`cachedPackages`) to batch responses — when a `Request` semantic is detected (via `BeforeWrite`), caching is enabled; when a `Response` semantic is detected (via `BeforeOnReceive`), caching is disabled and all cached packages are sent as a batch.
|
||||
|
||||
### Protocol Stack Direction
|
||||
|
||||
**Core side** (messages flow outward):
|
||||
```
|
||||
SetupRemoteNativeController
|
||||
→ GuiRemoteProtocolDomDiffConverter
|
||||
→ GuiRemoteProtocolFilter
|
||||
→ GuiRemoteProtocolFromJsonChannel
|
||||
→ GuiRemoteProtocolAsyncJsonChannelSerializer
|
||||
→ GuiRemoteJsonChannelStringSerializer
|
||||
→ CoreChannel (IGuiRemoteProtocolChannel<WString>)
|
||||
→ Network transport (user-implemented)
|
||||
```
|
||||
|
||||
**Renderer side** (messages flow inward):
|
||||
```
|
||||
Network transport (user-implemented)
|
||||
→ RendererChannel
|
||||
→ GuiRemoteJsonChannelStringDeserializer
|
||||
→ GuiRemoteJsonChannelFromProtocol
|
||||
→ GuiRemoteRendererSingle (IGuiRemoteProtocol)
|
||||
→ Native window rendering
|
||||
```
|
||||
|
||||
Events flow in the reverse direction through the same stack.
|
||||
@@ -0,0 +1,313 @@
|
||||
# Remote Protocol Unit Test Framework
|
||||
|
||||
The remote protocol unit test framework allows testing GacUI applications without real OS windows or rendering. It reuses the same remote protocol architecture as production remote deployment, replacing the renderer side with a mock implementation (`UnitTestRemoteProtocol`) that captures rendering results as snapshots and feeds simulated user input through the protocol pipeline. The GacUI core side runs identically to production.
|
||||
|
||||
Implementation lives in `Source/UnitTestUtilities/`.
|
||||
|
||||
## Comprehensive Example
|
||||
|
||||
Below is a complete example demonstrating the key concepts of the framework: test executable setup, GacUI XML resource definition, frame-based interaction with named snapshots, control lookup, and mouse input simulation.
|
||||
|
||||
### Test Executable Entry Point
|
||||
|
||||
The test executable's `main` function initializes the framework once, runs all test cases, and finalizes:
|
||||
|
||||
```cpp
|
||||
int UnitTestMain(int argc, T* argv[])
|
||||
{
|
||||
UnitTestFrameworkConfig config;
|
||||
config.snapshotFolder = GetTestSnapshotPath();
|
||||
config.resourceFolder = GetTestDataPath();
|
||||
|
||||
GacUIUnitTest_Initialize(&config);
|
||||
int result = UnitTest::RunAndDisposeTests(argc, argv);
|
||||
GacUIUnitTest_Finalize();
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
### A Test Case: Clicking a Button
|
||||
|
||||
This example defines a GacUI XML resource with a window containing an OK button, then tests hovering, pressing, and releasing the button in separate frames. Each frame callback describes the action it performs; the frame name is assigned to the **rendering snapshot that results from the previous frame's action**.
|
||||
|
||||
```cpp
|
||||
const auto resource = LR"GacUISrc(
|
||||
<Resource>
|
||||
<Instance name="MainWindowResource">
|
||||
<Instance ref.Class="gacuisrc_unittest::MainWindow">
|
||||
<Window ref.Name="self" Text="Hello, world!" ClientSize="x:320 y:240">
|
||||
<Button ref.Name="buttonOK" Text="OK">
|
||||
<att.BoundsComposition-set AlignmentToParent="left:5 top:5 right:-1 bottom:-1"/>
|
||||
<ev.Clicked-eval><![CDATA {
|
||||
Application::GetApplication().InvokeInMainThread(self, func():void{
|
||||
// changing UI structures that affects the sender of an event
|
||||
// is supposed to be put in InvokeInMainThread
|
||||
// Otherwise followed up events on the same control may gone
|
||||
self.Close();
|
||||
});
|
||||
}]]></ev.Clicked-eval>
|
||||
</Button>
|
||||
</Window>
|
||||
</Instance>
|
||||
</Instance>
|
||||
</Resource>
|
||||
)GacUISrc";
|
||||
|
||||
TEST_CASE(L"Click Button")
|
||||
{
|
||||
GacUIUnitTest_SetGuiMainProxy([](UnitTestRemoteProtocol* protocol, IUnitTestContext*)
|
||||
{
|
||||
// Frame 0: snapshot named "Ready" — shows the initial rendering after the window opens.
|
||||
// Callback action: move the mouse on the OK button.
|
||||
protocol->OnNextIdleFrame(L"Ready", [=]()
|
||||
{
|
||||
auto window = GetApplication()->GetMainWindow();
|
||||
auto buttonOK = TryFindObjectByName<GuiButton>(window, L"buttonOK");
|
||||
// MouseMove(location) + LClick() could be merged into one operation
|
||||
// LClick(location) will perform both
|
||||
// but when the side effect of MouseMove is important
|
||||
// you are given choices
|
||||
protocol->MouseMove(protocol->LocationOf(buttonOK));
|
||||
});
|
||||
// Frame 1: snapshot named "Hover" — shows the button in hover state (result of MouseMove above).
|
||||
// Callback action: click the OK button, triggering Clicked → window closes.
|
||||
protocol->OnNextIdleFrame(L"Hover", [=]()
|
||||
{
|
||||
protocol->LClick();
|
||||
});
|
||||
// If the user interaction above does not cause the application to exit,
|
||||
// an additional frame is needed to close the window explicitly:
|
||||
//
|
||||
// protocol->OnNextIdleFrame(L"Frame name describing what the previous frame does", [=]()
|
||||
// {
|
||||
// auto window = GetApplication()->GetMainWindow();
|
||||
// window->Hide();
|
||||
// });
|
||||
});
|
||||
GacUIUnitTest_StartFast_WithResourceAsText<darkskin::Theme>(
|
||||
WString::Unmanaged(L"UnitTestFramework/WindowWithOKButton"),
|
||||
WString::Unmanaged(L"gacuisrc_unittest::MainWindow"),
|
||||
resource
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
### What This Example Demonstrates
|
||||
|
||||
- **`GacUIUnitTest_SetGuiMainProxy`**: Registers the test's frame callback function. It receives `UnitTestRemoteProtocol*` (for input simulation and snapshot access) and `IUnitTestContext*`.
|
||||
- **`OnNextIdleFrame(name, callback)`**: Registers a callback to run after the next rendering frame settles. The `name` is attached to the snapshot that was just captured (the rendering result), not to what the callback will do.
|
||||
- **`TryFindObjectByName<T>(window, name)`**: Looks up a named control from the GacUI XML resource by its `ref.Name`.
|
||||
- **`protocol->LocationOf(control)`**: Computes the absolute screen coordinate of the center of a control, for use with mouse input methods.
|
||||
- **`LClick(location)`**: Simulates a full left mouse click (mouse move + button down + button up) at the given location. Use this for simple clicks instead of separate `_LDown`/`_LUp` calls.
|
||||
- **`GacUIUnitTest_StartFast_WithResourceAsText<Theme>`**: Compiles the XML resource, registers the theme, creates the window, runs the application, and captures snapshots — all in one call.
|
||||
- **Frame name semantics**: `"Ready"` names the initial rendering. Each snapshot file (`frame_0.json`) records the full rendering DOM at that point.
|
||||
- **Closing the window**: The button's click handler uses `InvokeInMainThread` to defer `self.Close()`. This is necessary because `Close()` would otherwise be called synchronously inside the IO event dispatch, which could block the frame callback (see the blocking function caveat below). If user interaction does not cause the application to exit, an extra frame must be added at the end to call `window->Hide()` explicitly.
|
||||
|
||||
## Test Executable Initialization and Finalization
|
||||
|
||||
### Global Setup (GacUIUnitTest_Initialize)
|
||||
|
||||
Each test executable's `main` calls `GacUIUnitTest_Initialize(&config)` with a `UnitTestFrameworkConfig` containing `snapshotFolder` and `resourceFolder`. This function:
|
||||
- Sets `GACUI_UNITTEST_ONLY_SKIP_THREAD_LOCAL_STORAGE_DISPOSE_STORAGES` and `GACUI_UNITTEST_ONLY_SKIP_TYPE_AND_PLUGIN_LOAD_UNLOAD` to `true`, allowing the type manager and plugin system to be loaded once and reused across all test cases.
|
||||
- Calls `GetGlobalTypeManager()->Load()` and `GetPluginManager()->Load(true, false)` once.
|
||||
|
||||
### Global Teardown (GacUIUnitTest_Finalize)
|
||||
|
||||
Called after all tests complete. Tears down the type manager and plugin manager.
|
||||
|
||||
## Per-Test-Case Setup and the Proxy Chaining Pattern
|
||||
|
||||
### Registering Frame Callbacks
|
||||
|
||||
Each test case follows a standard pattern:
|
||||
1. Call `GacUIUnitTest_SetGuiMainProxy(callback)` to register a `UnitTestMainFunc` (signature: `void(UnitTestRemoteProtocol*, IUnitTestContext*)`). This is the test's frame-based action callback that registers idle frame handlers via `OnNextIdleFrame`.
|
||||
2. Optionally call `GacUIUnitTest_LinkGuiMainProxy(linkCallback)` to wrap additional setup around the test proxy.
|
||||
3. Call one of the start functions to launch the test.
|
||||
|
||||
### Proxy Chaining (GacUIUnitTest_LinkGuiMainProxy)
|
||||
|
||||
`GacUIUnitTest_LinkGuiMainProxy` captures the current proxy as `previousMainProxy` and installs a new proxy that calls the link function with the previous proxy as a parameter. This enables decorator-style composition — each link layer can perform setup before delegating to the inner proxy and cleanup after.
|
||||
|
||||
### GacUIUnitTest_StartFast_WithResourceAsText
|
||||
|
||||
This template function is the most commonly used entry point. It internally calls `GacUIUnitTest_LinkGuiMainProxy` to inject:
|
||||
1. Sending `OnControllerConnect` with `ControllerGlobalConfig` to the protocol.
|
||||
2. Registering a theme (e.g., `DarkSkin`).
|
||||
3. Compiling the GacUI XML resource via `GacUIUnitTest_CompileAndLoad`.
|
||||
4. Creating the main window from the resource via `Value::Create(windowTypeFullName)`.
|
||||
5. Calling `previousMainProxy(protocol, context)` to let the test register its frame callbacks.
|
||||
6. Running the application via `GetApplication()->Run(window)`.
|
||||
7. Unregistering the theme on cleanup.
|
||||
|
||||
It also saves the compiled Workflow script text as a snapshot file (`[x64].txt` or `[x86].txt`) for Workflow generation stability verification.
|
||||
|
||||
### Start Functions
|
||||
|
||||
- `GacUIUnitTest_Start(appName, config)` — synchronous in-process test with optional serialization channel.
|
||||
- `GacUIUnitTest_StartAsync(appName, config)` — async test with core and renderer on separate threads.
|
||||
- `GacUIUnitTest_Start_WithResourceAsText(appName, config, resourceText)` — wraps resource compilation via `GacUIUnitTest_LinkGuiMainProxy`, then delegates to `GacUIUnitTest_Start` or `GacUIUnitTest_StartAsync` depending on `config.useChannel`.
|
||||
|
||||
## Protocol Stack Construction
|
||||
|
||||
### Synchronous Mode (GacUIUnitTest_Start)
|
||||
|
||||
`GacUIUnitTest_Start` constructs the full protocol stack in-process:
|
||||
|
||||
**Renderer side (deserialization direction):**
|
||||
- `UnitTestRemoteProtocol` — mock `IGuiRemoteProtocol` implementation.
|
||||
- `GuiRemoteJsonChannelFromProtocol` — converts protocol calls to JSON.
|
||||
- `GuiRemoteJsonChannelStringDeserializer` — JSON to String.
|
||||
- `GuiRemoteUtfStringChannelDeserializer<wchar_t, char8_t>` — String to UTF-8.
|
||||
|
||||
**Core side (serialization direction, mirrors back):**
|
||||
- `GuiRemoteUtfStringChannelSerializer<wchar_t, char8_t>` — UTF-8 to String.
|
||||
- `GuiRemoteJsonChannelStringSerializer` — String to JSON.
|
||||
- `GuiRemoteProtocolFromJsonChannel` — JSON to typed protocol calls.
|
||||
|
||||
**Protocol filter layers on core side:**
|
||||
- `GuiRemoteProtocolFilterVerifier` — validates repeat-filtering invariants.
|
||||
- `GuiRemoteProtocolFilter` — filters redundant messages.
|
||||
- `GuiRemoteProtocolDomDiffConverter` — (optional, when `useDomDiff` is true) converts full DOM to DOM diffs.
|
||||
|
||||
When `useChannel == UnitTestRemoteChannel::None`, the verifier directly wraps `UnitTestRemoteProtocol`'s `IGuiRemoteProtocol`, bypassing the serialization layers for speed. Otherwise, the full serialization channel is used for testing round-trip fidelity.
|
||||
|
||||
Finally, `SetupRemoteNativeController(protocol)` creates the runtime stack: `GuiRemoteController` → `GuiHostedController` → resource managers, then calls `GuiApplicationMain()` → `GuiMain()` → the registered test proxy.
|
||||
|
||||
### Async Mode (GacUIUnitTest_StartAsync)
|
||||
|
||||
`GacUIUnitTest_StartAsync` inserts `GuiRemoteProtocolAsyncJsonChannelSerializer` into the channel, placing the core and renderer on separate threads. Two threads are spawned via `RunInNewThread`: a channel thread for serialization I/O and a UI thread for the GacUI application. The call waits for `asyncChannelSender.WaitForStopped()` before writing snapshots.
|
||||
|
||||
## UnitTestRemoteProtocol Class Hierarchy
|
||||
|
||||
`UnitTestRemoteProtocol` inherits from `UnitTestRemoteProtocolFeatures` and `IGuiRemoteEventProcessor`. Its base classes are:
|
||||
- `UnitTestRemoteProtocolBase` — holds `IGuiRemoteProtocolEvents*`, `UnitTestScreenConfig`, default `Impl_*` stubs, `Initialize`, `GetExecutablePath`.
|
||||
- `UnitTestRemoteProtocol_MainWindow` — simulates window management (bounds, sizing config, styles).
|
||||
- `UnitTestRemoteProtocol_IO` — simulates controller connection and IO event forwarding.
|
||||
- `UnitTestRemoteProtocol_Rendering` — simulates element creation/destruction, rendering begin/end, DOM capture.
|
||||
- `UnitTestRemoteProtocol_IOCommands` — provides high-level input simulation methods.
|
||||
- `UnitTestRemoteProtocolFeatures` — combines all feature bases and implements `LogRenderingResult()`, the core frame capture mechanism.
|
||||
|
||||
`UnitTestRemoteProtocol` adds:
|
||||
- `processRemoteEvents` — a list of `(Nullable<WString>, Func<void()>)` pairs representing named frame callbacks.
|
||||
- `OnNextIdleFrame(callback)` and `OnNextIdleFrame(name, callback)` — register callbacks to execute after specific rendering frames settle.
|
||||
- `ProcessRemoteEvents()` — the main event processing method called each cycle.
|
||||
|
||||
## The Main Event Loop
|
||||
|
||||
`GuiRemoteController::RunOneCycle()` drives the core side per cycle:
|
||||
1. `remoteProtocol->GetRemoteEventProcessor()->ProcessRemoteEvents()` — this invokes `UnitTestRemoteProtocol::ProcessRemoteEvents()`.
|
||||
2. `remoteMessages.Submit(disconnected)` — flushes pending protocol messages.
|
||||
3. `callbackService.InvokeGlobalTimer()` — triggers `GuiHostedController::GlobalTimer()`, which performs layout and rendering, generating new DOM/element data.
|
||||
4. `asyncService.ExecuteAsyncTasks()`.
|
||||
|
||||
The cycle repeats until `connectionStopped` is true (application closes).
|
||||
|
||||
## Frame Capture and Snapshot Mechanism
|
||||
|
||||
### Passive Rendering Capture (UnitTestRemoteProtocol_Rendering)
|
||||
|
||||
Each time the GacUI core calls `RequestRendererBeginRendering`, `UnitTestRemoteProtocol_Rendering::Impl_RendererBeginRendering` creates a new `UnitTestLoggedFrame` and records the `frameId` from the core's monotonic counter. When `Impl_RendererEndRendering` is called, the frame's `renderingDom` is captured from the rendering commands.
|
||||
|
||||
### Active Frame Logging (LogRenderingResult)
|
||||
|
||||
`LogRenderingResult()` is called at the start of each `ProcessRemoteEvents()` cycle in `UnitTestRemoteProtocolFeatures`:
|
||||
1. `TryGetLastRenderingFrameAndReset()` checks if a rendering frame completed since the last check. If yes, it becomes the `candidateFrame`.
|
||||
2. On the next cycle where no new rendering occurred (the UI has settled), the `candidateFrame` is committed to `loggedTrace.frames` with a copy of `lastElementDescs` and `sizingConfig`. This ensures the snapshot captures the final stable rendering state after all layout passes.
|
||||
3. `LogRenderingResult()` returns `true`, signaling that a frame is ready.
|
||||
4. In `ProcessRemoteEvents()`, the `frameName` is set on the just-committed frame (from the callback's registered name), and then the callback function executes.
|
||||
5. If 100 consecutive cycles pass without any rendering change after previously rendering, the test fails with an error.
|
||||
|
||||
### Frame Name Semantics
|
||||
|
||||
The frame name is assigned to the **already-committed snapshot** (the rendering result), then the callback executes. This means:
|
||||
- The `frameName` describes what **led to** the current visual state, not what the callback will do next.
|
||||
- The first frame is conventionally named `"Ready"`, representing the initial rendering state after the window opens.
|
||||
- Subsequent frame names describe the action taken in the previous callback (e.g., `"Hover"` means the previous callback moved the mouse, and this snapshot shows the hover state).
|
||||
|
||||
The sequence is:
|
||||
1. Application renders → DOM/elements captured as `candidateFrame`.
|
||||
2. UI settles (no more rendering) → `candidateFrame` committed to `loggedTrace.frames`.
|
||||
3. `frameName` is set on the committed frame from the callback registration.
|
||||
4. Frame callback executes (may trigger events → triggering more rendering).
|
||||
5. Return to step 1.
|
||||
|
||||
### Snapshot File Structure
|
||||
|
||||
For a test named `Controls/Basic/GuiButton/ClickOnMouseUp`:
|
||||
- **Main trace file**: `{snapshotFolder}/Controls/Basic/GuiButton/ClickOnMouseUp.json` — `UnitTest_RenderingTrace` with `createdElements`, `imageCreations`, `imageMetadatas`, and `frames` (each frame only has `frameId` and `frameName`, detail stripped).
|
||||
- **Per-frame files**: `{snapshotFolder}/Controls/Basic/GuiButton/ClickOnMouseUp/frame_0.json`, `frame_1.json`, etc. — full `UnitTest_RenderingFrame` with `frameId`, `frameName`, `windowSize`, `elements` (all rendered element descriptions keyed by ID), and `root` (the rendering DOM tree).
|
||||
- **Workflow snapshot**: `ClickOnMouseUp[x64].txt` or `[x86].txt` — compiled Workflow script text for generation stability verification.
|
||||
- **Commands log**: `ClickOnMouseUp[commands].txt` — rendering command logs per frame (when not using DOM diff).
|
||||
- **Diffs log**: `ClickOnMouseUp[diffs].txt` — DOM diff logs per frame.
|
||||
|
||||
### Write Strategy (GacUIUnitTest_LogUI)
|
||||
|
||||
`GacUIUnitTest_LogUI` performs the following:
|
||||
1. Serializes the full `loggedTrace` to JSON and verifies round-trip fidelity (serialize → deserialize → re-serialize must match).
|
||||
2. Writes each frame as an individual `frame_N.json` file via `GacUIUnitTest_WriteSnapshotFileIfChanged`, which only writes if content differs from the existing file (enabling clean `git diff`).
|
||||
3. Strips frame detail from the in-memory trace (keeping only `frameId` and `frameName`) and writes the main trace file.
|
||||
4. Deletes any leftover `frame_*.json` files that no longer correspond to actual frames.
|
||||
|
||||
## User Interaction Simulation
|
||||
|
||||
### UnitTestRemoteProtocol_IOCommands
|
||||
|
||||
`UnitTestRemoteProtocol_IOCommands` provides methods to simulate all user input. It maintains virtual state:
|
||||
- `mousePosition` — current mouse position (`Nullable<NativePoint>`, initially unset; first `MouseMove` triggers `OnIOMouseEntered`).
|
||||
- `pressingKeys` — `SortedList<VKEY>` of currently pressed keys.
|
||||
- `leftPressing`, `middlePressing`, `rightPressing` — mouse button states.
|
||||
- `capslockToggled` — capslock toggle state.
|
||||
|
||||
Each method constructs the appropriate `NativeWindowMouseInfo`, `NativeWindowKeyInfo`, or `NativeWindowCharInfo` struct (via `MakeMouseInfo()`, `MakeKeyInfo()`, `MakeCharInfo()`) and calls the corresponding event on `IGuiRemoteProtocolEvents` (obtained via `UseEvents()`).
|
||||
|
||||
### Location Calculation (LocationOf)
|
||||
|
||||
`LocationOf(controlOrComposition, ratioX, ratioY, offsetX, offsetY)` computes the target point in absolute screen coordinates:
|
||||
1. Gets the composition's `GetGlobalBounds()` (logical coordinates).
|
||||
2. Converts to native coordinates using `nativeWindow->Convert()`.
|
||||
3. Applies the ratio (default 0.5 = center) and offset.
|
||||
4. Adds the window's screen position `nativeWindow->GetBounds().LeftTop()`.
|
||||
|
||||
Overloads accept either `GuiGraphicsComposition*` or `GuiControl*`.
|
||||
|
||||
### Mouse Input Methods
|
||||
|
||||
- `MouseMove(location)` — sends `OnIOMouseMoving`. If mouse was previously unset, first sends `OnIOMouseEntered`.
|
||||
- `_LDown(location)` / `_LUp(location)` — low-level left button down/up. `_LDown` calls `MouseMove` if position changed, then `UseEvents().OnIOButtonDown({Left, ...})`.
|
||||
- `LClick(location)` — `_LDown` followed by `_LUp`.
|
||||
- `LDBClick(location)` — two `LClick` calls (the framework detects double click from timing).
|
||||
- Analogous `RClick`, `MClick`, `RDBClick`, `MDBClick` for right and middle buttons.
|
||||
- `WheelUp(jumps)` / `WheelDown(jumps)` — `OnIOVWheel` with delta scaled by 120 per jump.
|
||||
- `HWheelLeft(jumps)` / `HWheelRight(jumps)` — `OnIOHWheel` similarly.
|
||||
|
||||
### Key and Character Input Methods
|
||||
|
||||
- `KeyDown(key)` / `KeyUp(key)` — sends `OnIOKeyDown` / `OnIOKeyUp`. Tracks `pressingKeys` state. Special handling for `VKEY::KEY_CAPITAL` toggles `capslockToggled`.
|
||||
- `KeyPress(key)` — `KeyDown` followed by `KeyUp`.
|
||||
- `KeyPress(key, ctrl, shift, alt)` — wraps the key press with modifier key down/up events.
|
||||
- `TypeString(text)` — sends a sequence of `OnIOChar` events for each character via `MakeCharInfo`, without synthesizing key down/up events.
|
||||
|
||||
### Event Flow Through the Pipeline
|
||||
|
||||
When a test calls `protocol->LClick(location)`:
|
||||
1. `_LDown(location)` → `MouseMove(location)` if position changed → `UseEvents().OnIOButtonDown({Left, MakeMouseInfo()})`.
|
||||
2. `_LUp(location)` → `UseEvents().OnIOButtonUp({Left, MakeMouseInfo()})`.
|
||||
3. These events reach `GuiRemoteEvents` (the `IGuiRemoteProtocolEvents` implementation in `GuiRemoteController`).
|
||||
4. `GuiRemoteEvents::OnIOButtonDown()` dispatches to `listener->LeftButtonDown(info)` on `remoteWindow`.
|
||||
5. `GuiGraphicsHost` receives the call as `INativeWindowListener`.
|
||||
6. `GuiGraphicsHost::LeftButtonDown()` calls `OnMouseInput()`, which converts native coordinates to logical coordinates, performs hit-testing via `FindVisibleComposition()`, creates `GuiMouseEventArgs`, and dispatches via `RaiseMouseEvent()`.
|
||||
7. The event bubbles through the composition tree, triggering control-level handlers.
|
||||
|
||||
All of this happens **synchronously within a single `ProcessRemoteEvents()` call**, so by the time the frame callback returns, all side effects of the interaction have been processed.
|
||||
|
||||
### Blocking Function Caveat
|
||||
|
||||
If an IO action triggers a blocking function (like `ShowDialog`), the frame callback would never return. The unit test framework detects this via the `frameExecuting` flag and fails. The solution is to wrap the IO action in `GetApplication()->InvokeInMainThread(window, [=]() { protocol->LClick(location); })`, which defers the action to the next event loop iteration.
|
||||
|
||||
## Running Multiple Test Applications
|
||||
|
||||
Each call to `GacUIUnitTest_Start` runs one complete application lifecycle (from `GuiMain()` to window close). Typically each `TEST_CASE` calls the full `GacUIUnitTest_SetGuiMainProxy` → `GacUIUnitTest_Start` sequence independently.
|
||||
|
||||
Within a single application instance, multiple windows can be created inside frame callbacks using `Value::Create(L"namespace::ClassName")` and shown with `Show()`, `ShowModal()`, or `ShowDialog()`.
|
||||
|
||||
`GacUIUnitTest_LinkGuiMainProxy` can be called multiple times before `GacUIUnitTest_Start`, chaining multiple setup layers (e.g., one for resources, one for theme, one for additional initialization).
|
||||
Reference in New Issue
Block a user