14 KiB
Implementing IGuiGraphicsElement
This document explains the end-to-end design and required implementation steps for adding a new graphics element to GacUI (a lightweight element similar to GuiSolidLabelElement, not a complex document-like element). It is organized around the involved interfaces, lifecycle, renderer abstraction, platform integration, and the event / rendering pipeline.
1. Core Interfaces and Their Responsibilities
-
IGuiGraphicsElement
- Declares
GetRenderer()andGetOwnerComposition()plus the protectedSetOwnerComposition(compositions::GuiGraphicsComposition*)(invoked only byGuiGraphicsCompositionas its friend). - Every concrete element inherits (indirectly) from this interface through
GuiElementBase<TElement>.
- Declares
-
IGuiGraphicsRenderer
- Owns the platform-dependent drawing logic for exactly one bound element (lifecycle:
Initialize(element),Finalize()). - Responds to state changes:
OnElementStateChanged(). - Receives a render target with
SetRenderTarget(IGuiGraphicsRenderTarget*)and draws viaRender(Rect bounds). - Supplies layout info through
GetMinSize().
- Owns the platform-dependent drawing logic for exactly one bound element (lifecycle:
-
IGuiGraphicsRendererFactory
- Creates renderers. Registered in the global
GuiGraphicsResourceManagerthroughRegisterRendererFactory()indirectly byGuiElementRendererBase<...>::Register().
- Creates renderers. Registered in the global
-
IGuiGraphicsRenderTarget (and concrete subclasses per backend)
- Encapsulates drawing surface, clip stack (
PushClipper,PopClipper,GetClipper,IsClipperCoverWholeTarget). - Manages rendering phases (
StartRendering,StopRendering, hosted variants) and reports device failures (RenderTargetFailure).
- Encapsulates drawing surface, clip stack (
-
GuiGraphicsRenderTarget (base implementation)
- Implements clip stack logic and rendering phase orchestration, deferring platform specifics to derived classes (e.g., Direct2D / GDI targets override native start/stop and resource creation behavior).
2. Element Class Pattern (GuiElementBase<TElement>) and Example
A lightweight element (e.g., GuiSolidLabelElement) follows this template pattern provided by GuiElementBase<T>:
- Static
ElementTypeNameliteral (e.g.,L"SolidLabel"). - A protected constructor initializing default property values.
- Public getters/setters; setters compare old vs new and call
InvokeOnElementStateChanged()only when a change occurs (seeGuiSolidLabelElement::SetText,SetColor, etc.). - Shape / format or content properties stored as POD or small structs (e.g.,
Color,FontProperties,Alignment,WString). - No rendering code appears in the element itself; it only stores state.
Typical additional element examples in GuiGraphicsElement.h / .cpp (all using the same pattern):
GuiSolidBorderElement,Gui3DBorderElement,Gui3DSplitterElement(two-color, directional),GuiSolidBackgroundElement,GuiGradientBackgroundElement,GuiInnerShadowElement,GuiImageFrameElement,GuiPolygonElement. All rely on property change notification throughInvokeOnElementStateChanged()to invalidate caches in their renderers.
3. Renderer Abstraction (GuiElementRendererBase<TElement, TRenderer, TRenderTarget>)
Renderer bases implement a consistent lifecycle with overridable hooks:
InitializeInternal()/FinalizeInternal()for creating / releasing persistent resources (e.g., brushes, text formats, cached geometries).RenderTargetChangedInternal(old, new)for resources tied to the render target (re-create when target switches or becomes available; also release on null).Render(Rect bounds)does immediate drawing each frame.OnElementStateChanged()invalidates or rebuilds caches when properties changed.GetMinSize()supplied by the base (often updated via specific helper likeUpdateMinSize()in label renderers after layout recomputation).
Examples:
- Direct2D:
GuiSolidLabelElementRenderercachesID2D1SolidColorBrush,Direct2DTextFormatPackage,IDWriteTextLayout, re-building inCreateTextLayout()when font, text, wrapping, or max width changes; updates min size withUpdateMinSize(). - GDI:
GuiSolidLabelElementRenderercachesWinFontand updates min width/height similarly. - Shared brush helpers:
GuiSolidBrushElementRendererBaseandGuiGradientBrushElementRendererBaseunify brush caching logic in Direct2D path.
4. Cross-Platform Renderer Families
Per backend a parallel renderer hierarchy exists sharing naming but differing resource types:
- Direct2D (
elements_windows_d2d): usesIWindowsDirect2DRenderTarget,ID2D1RenderTarget, DirectWrite text formatting, gradient/radial brushes, effects (e.g., focus rectangle effect inGuiFocusRectangleElementRenderer). - GDI (
elements_windows_gdi): usesIWindowsGDIRenderTarget,WinDC,WinFont,WinPen,WinBrush, Uniscribe text shaping for complex paragraphs or colorized text. - Remote / Hosted modes: (not shown here) follow analogous patterns; Hosted leverages whichever native renderer factories are active.
Each concrete renderer is registered exactly once through its static Register() created by the CRTP base and invoked inside platform bootstrap functions (RendererMainDirect2D, RendererMainGDI, or remote manager initialization).
5. Registration Flow
- Application selects backend (e.g., Direct2D or GDI), calling
RendererMainDirect2D()orRendererMainGDI(). - Inside these functions each renderer’s
Register()is invoked (e.g.,GuiSolidLabelElementRenderer::Register()). Register()callsGetGuiGraphicsResourceManager()->RegisterRendererFactory(ElementTypeName, factory)linking element type to a factory.- When an element instance is created via
GuiElementBase<T>::Create(), the resource manager looks up the factory and constructs a matching renderer, callsInitialize(element). - When a composition later receives a render target, it propagates to bound elements’ renderers through
SetRenderTarget(under control of composition tree traversal / host initialization).
6. Composition Ownership and Rendering Invocation
- A
GuiGraphicsCompositionholds at most oneIGuiGraphicsElement. On attach it calls the element’s protectedSetOwnerComposition. - Rendering pipeline (
GuiGraphicsHost::Render()):- Host invokes
windowComposition->Render(offset)recursively. - Each composition pushes clipping, then (if it has an element) obtains
element->GetRenderer()->Render(bounds). - Render target clipping stack managed by
GuiGraphicsRenderTarget::PushClipper/PopClipperensures nested composition visibility. - After traversal
StopRendering()finalizes; anyRenderTargetFailureis processed (e.g., lost device or resize triggers re-creation of render target and re-run of render).
- Host invokes
7. State Change Propagation and Invalidation Chain
- Setter in element detects a change and calls
InvokeOnElementStateChanged()(provided byGuiElementBase<T>). - That method calls the bound renderer’s
OnElementStateChanged()so it can drop caches (brushes, layouts) or lazily refresh on nextRender. InvokeOnElementStateChanged()also raises composition invalidation causingGuiGraphicsHostto markneedRender = true.- The main loop / timer triggers
GuiGraphicsHost::GlobalTimer(), which ifneedRendercallsRender(). - Min size recalculation done inside renderer (e.g.,
GuiSolidLabelElementRenderer::UpdateMinSize()) influences subsequent layout passes.
8. Min Size Calculation Strategy
- Renderers compute intrinsic size based on current element state and cached layout objects.
- Example: label renderer (Direct2D or GDI) measures text (wrapping, font, multiline) and stores result so
GetMinSize()returns consistent values until invalidated. - Property changes that affect geometry (text, font, wrap flags) call
InvokeOnElementStateChanged()which triggersUpdateMinSize()inside the renderer on next render/state change handling.
9. Resource Lifetime and Render Target Changes
- Persistent resources independent of target (e.g., cached last element values) retained across target switches.
- Target-bound resources (Direct2D brushes, text layouts, GDI pens/brushes, bitmaps) created in
InitializeInternal()if target already set, or inRenderTargetChangedInternal()when a new target arrives. - On target loss (device lost / resize reported via
RenderTargetFailure), host re-acquires target; each renderer releases old target objects inRenderTargetChangedInternal(old, nullptr)then recreates after new target available.
10. Adding a New Lightweight Element (Checklist)
Element Class:
- Add class
GuiXxxElement : public GuiElementBase<GuiXxxElement>withstatic constexpr const wchar_t* ElementTypeName = L"Xxx". - Define private/protected property fields and defaults in constructor.
- Implement getters and setters in
.cpp; setters compare old/new and callInvokeOnElementStateChanged()on change only.
Renderer (per backend):
- Derive
GuiXxxElementRendererfromGuiElementRendererBase<GuiXxxElement, GuiXxxElementRenderer, IWindowsDirect2DRenderTarget>(and analog for GDI / Remote). - Implement hooks:
InitializeInternal(),FinalizeInternal(),RenderTargetChangedInternal(old,new),Render(bounds),OnElementStateChanged(). - Cache necessary graphics resources; rebuild only when affected element properties change.
- Compute / update min size (helper method like
UpdateMinSize()).
Registration:
- Insert
GuiXxxElementRenderer::Register()in each backend initialization (Direct2D:RendererMainDirect2D, GDI:RendererMainGDI, Remote: remote resource manager initialization).
Testing:
- Instantiate via XML
<Xxx>mapping topresentation::elements::GuiXxxElementor directly create in C++ viaGuiXxxElement::Create(). - Verify property mutations trigger re-render (breakpoint or visual change) and min size recomputation.
11. Differences From Complex Elements (e.g., GuiDocumentElement)
GuiDocumentElement adds a multi-layer model / cache system beyond the lightweight pattern:
- Manages
DocumentModelwith paragraphs, runs, hyperlink packages, embedded objects instead of flat properties. - Has nested renderer (
GuiDocumentElementRenderer) implementingIGuiGraphicsParagraphCallbackand paragraph caching (ParagraphCachearray) with lazyIGuiGraphicsParagraphcreation. - Maintains selection, caret, inline objects, per-paragraph measurement arrays, and dynamic caret rendering utilities (
OpenCaret,CloseCaret,SetSelectionetc.). - Invalidation granularity per paragraph (
NotifyParagraphUpdated). - Rendering enumerates paragraphs, reflows only needed ones (width-sensitive caching via
lastMaxWidth). - Thus: far more complex state synchronization path vs. single-primitive elements where all state sits directly on the element and renderer only depends on a small fixed property set.
12. Common Pitfalls
- Forgetting to compare old vs new value in setter: causes redundant invalidations and potential performance issues.
- Allocating target-bound resources in constructor instead of
InitializeInternal()/RenderTargetChangedInternal()leads to null target usage or leaks. - Not releasing resources in
FinalizeInternal()or whenRenderTargetChangedInternal(new == nullptr)=> leaks on device reset. - Failing to update min size after relevant property change (text/font/wrap) => layout flickers or stale size.
- Omitting
Register()call => element silently renders nothing (renderer never created). - Using element state directly inside
Render()without caching expensive conversions (e.g., text layout) => per-frame overhead.
13. End-to-End Flow Summary
Creation: GuiXxxElement::Create() -> element object + renderer via factory -> renderer->Initialize(element).
Attachment: Composition calls element->SetOwnerComposition(this) -> composition tree now owns element.
Render Target Acquisition: Composition tree traversal sets render target on each renderer (SetRenderTarget). RenderTargetChangedInternal() fires; resources created.
Property Change: Setter -> InvokeOnElementStateChanged() -> renderer invalidation -> host scheduled.
Frame Render: Host GuiGraphicsHost::Render() -> composition traversal -> each renderer Render(bounds) with valid clipper.
Shutdown: Renderer Finalize() (invokes FinalizeInternal()), composition releases element, element destructor runs.
14. Quick Implementation Template (Narrative)
- Declare element with properties + static name.
- Implement setters calling
InvokeOnElementStateChanged()only on real changes. - Add renderer (per backend) caching required native resources.
- In renderer
OnElementStateChanged()mark internal dirty flags, rebuild brushes / layouts next use (or immediately) and update min size. - Register renderer in backend startup.
- Test lifecycle: create window, add composition + element, mutate properties, resize window, switch backend if applicable.
15. Lightweight Element Implementation Checklist (Condensed)
- Element class added with
ElementTypeName. - Constructor sets defaults.
- All setters compare and call
InvokeOnElementStateChanged(). - Renderer(s) created for every required backend.
InitializeInternal/FinalizeInternalimplemented.RenderTargetChangedInternalre/creates target-bound resources.Renderdraws respecting bounds & element properties.OnElementStateChangedinvalidates & triggers min size recalculation.- Registration calls inserted in each backend entry point.
- Remote protocol enums / serialization (if remote supported) implemented.
- Manual / unit tests cover property changes & rendering.
This pattern ensures every simple element integrates uniformly into the composition, rendering, and resource management infrastructure provided by GacUI.