Files
GacUI/.github/KnowledgeBase/KB_GacUI_Design_ImplementingIGuiGraphicsElement.md
2025-10-30 22:39:15 -07:00

14 KiB
Raw Blame History

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

  1. IGuiGraphicsElement

    • Declares GetRenderer() and GetOwnerComposition() plus the protected SetOwnerComposition(compositions::GuiGraphicsComposition*) (invoked only by GuiGraphicsComposition as its friend).
    • Every concrete element inherits (indirectly) from this interface through GuiElementBase<TElement>.
  2. 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 via Render(Rect bounds).
    • Supplies layout info through GetMinSize().
  3. IGuiGraphicsRendererFactory

    • Creates renderers. Registered in the global GuiGraphicsResourceManager through RegisterRendererFactory() indirectly by GuiElementRendererBase<...>::Register().
  4. 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).
  5. 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 ElementTypeName literal (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 (see GuiSolidLabelElement::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 through InvokeOnElementStateChanged() 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 like UpdateMinSize() in label renderers after layout recomputation).

Examples:

  • Direct2D: GuiSolidLabelElementRenderer caches ID2D1SolidColorBrush, Direct2DTextFormatPackage, IDWriteTextLayout, re-building in CreateTextLayout() when font, text, wrapping, or max width changes; updates min size with UpdateMinSize().
  • GDI: GuiSolidLabelElementRenderer caches WinFont and updates min width/height similarly.
  • Shared brush helpers: GuiSolidBrushElementRendererBase and GuiGradientBrushElementRendererBase unify 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): uses IWindowsDirect2DRenderTarget, ID2D1RenderTarget, DirectWrite text formatting, gradient/radial brushes, effects (e.g., focus rectangle effect in GuiFocusRectangleElementRenderer).
  • GDI (elements_windows_gdi): uses IWindowsGDIRenderTarget, 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

  1. Application selects backend (e.g., Direct2D or GDI), calling RendererMainDirect2D() or RendererMainGDI().
  2. Inside these functions each renderers Register() is invoked (e.g., GuiSolidLabelElementRenderer::Register()).
  3. Register() calls GetGuiGraphicsResourceManager()->RegisterRendererFactory(ElementTypeName, factory) linking element type to a factory.
  4. When an element instance is created via GuiElementBase<T>::Create(), the resource manager looks up the factory and constructs a matching renderer, calls Initialize(element).
  5. 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 GuiGraphicsComposition holds at most one IGuiGraphicsElement. On attach it calls the elements protected SetOwnerComposition.
  • Rendering pipeline (GuiGraphicsHost::Render()):
    1. Host invokes windowComposition->Render(offset) recursively.
    2. Each composition pushes clipping, then (if it has an element) obtains element->GetRenderer()->Render(bounds).
    3. Render target clipping stack managed by GuiGraphicsRenderTarget::PushClipper / PopClipper ensures nested composition visibility.
    4. After traversal StopRendering() finalizes; any RenderTargetFailure is processed (e.g., lost device or resize triggers re-creation of render target and re-run of render).

7. State Change Propagation and Invalidation Chain

  1. Setter in element detects a change and calls InvokeOnElementStateChanged() (provided by GuiElementBase<T>).
  2. That method calls the bound renderers OnElementStateChanged() so it can drop caches (brushes, layouts) or lazily refresh on next Render.
  3. InvokeOnElementStateChanged() also raises composition invalidation causing GuiGraphicsHost to mark needRender = true.
  4. The main loop / timer triggers GuiGraphicsHost::GlobalTimer(), which if needRender calls Render().
  5. 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 triggers UpdateMinSize() 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 in RenderTargetChangedInternal() when a new target arrives.
  • On target loss (device lost / resize reported via RenderTargetFailure), host re-acquires target; each renderer releases old target objects in RenderTargetChangedInternal(old, nullptr) then recreates after new target available.

10. Adding a New Lightweight Element (Checklist)

Element Class:

  • Add class GuiXxxElement : public GuiElementBase<GuiXxxElement> with static 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 call InvokeOnElementStateChanged() on change only.

Renderer (per backend):

  • Derive GuiXxxElementRenderer from GuiElementRendererBase<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 to presentation::elements::GuiXxxElement or directly create in C++ via GuiXxxElement::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 DocumentModel with paragraphs, runs, hyperlink packages, embedded objects instead of flat properties.
  • Has nested renderer (GuiDocumentElementRenderer) implementing IGuiGraphicsParagraphCallback and paragraph caching (ParagraphCache array) with lazy IGuiGraphicsParagraph creation.
  • Maintains selection, caret, inline objects, per-paragraph measurement arrays, and dynamic caret rendering utilities (OpenCaret, CloseCaret, SetSelection etc.).
  • 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 when RenderTargetChangedInternal(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)

  1. Declare element with properties + static name.
  2. Implement setters calling InvokeOnElementStateChanged() only on real changes.
  3. Add renderer (per backend) caching required native resources.
  4. In renderer OnElementStateChanged() mark internal dirty flags, rebuild brushes / layouts next use (or immediately) and update min size.
  5. Register renderer in backend startup.
  6. 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 / FinalizeInternal implemented.
  • RenderTargetChangedInternal re/creates target-bound resources.
  • Render draws respecting bounds & element properties.
  • OnElementStateChanged invalidates & 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.