Skip to content

feat(zui): ZUI system — docking, text editing, scroll, Doxygen docs + cleanup - #679

Draft
JeanPhilippeKernel wants to merge 164 commits into
developfrom
feature/zui
Draft

feat(zui): ZUI system — docking, text editing, scroll, Doxygen docs + cleanup#679
JeanPhilippeKernel wants to merge 164 commits into
developfrom
feature/zui

Conversation

@JeanPhilippeKernel

@JeanPhilippeKernel JeanPhilippeKernel commented Aug 27, 2026

Copy link
Copy Markdown
Owner

Summary

  • Text editing — selection (mouse drag + Shift+Arrow/Home/End), Ctrl+A, 8-level undo/redo (Ctrl+Z/Y), paste replaces selection, clipboard ops on selection
  • Tab bar — horizontal scroll (mouse wheel + arrows), stable width (close button always reserved), overline as tab child, vertical centering via FramePadding.y
  • Scroll regions — horizontal scrollbar when MaxScrollX > 1; layout solver and interaction pass handle X-axis scrollable boxes
  • Docking — SizePx storage model (absolute px, no drift, 60 px min), drag-split positions persist across restarts via title matching in ZUIDockLoad, orphan leaf cleanup, re-insert pass, self-dock guard, CommitDrop correctness fixes, drop zone FloatPos corrected, VS Code-style tab insertion bar, combo ArrowDown flicker fixed
  • Font — replaced stb_truetype with FreeType (FT_LOAD_FORCE_AUTOHINT); atlas always baked at 2× logical size with FontScale=0.5 for cross-DPI sharpness
  • UI polish — pixel snapping in vertex shader, drawn cursor caret, drag ghost drop shadow, 12 px divider hover band, tab scroll arrows, SelectionBg theme color, empty panel placeholders
  • Memory — UIContextArena moved to Engine::Initialize via MemoryBudgetConfig (128 MB budgeted); LocalArena removed from AppRenderPipeline
  • Cleanup — dead code (ZUIDockAnimate, TargetPct/AnimT), 6 dead shaders, 187 divider comments, using namespace removed from headers, ArenaAllocator fully qualified, Doxygen docs on ZUIDockspace.h / ZUIPanel.h / ZUIWidgets.h

Test plan

  • Default 4-panel layout loads correctly; drag-split positions survive restart
  • Close panel → sibling fills; reopen → 50/50 split
  • All panels merged → edge zone proposals visible when dragging
  • Tab bar: overflow shows < > arrows; wheel scrolls; insertion bar tracks between tabs
  • Text field: mouse drag selects, Shift+Arrow extends, caret blinks as 1.5 px rect
  • Ctrl+Z/Y undo/redo; Ctrl+A/C/X/V; Ctrl+V replaces selection
  • Combo: Up/Down nav, Enter selects, no flicker
  • Divider: 12 px tinted band on hover; EW/NS cursor; 60 px min enforced
  • Empty panels show placeholder text; drag ghost has drop shadow
  • Non-Retina: text sharp (FreeType hinting + 2× atlas + pixel snapping)

Introduces the ZUI custom immediate-mode UI system as a parallel track
alongside ImGui.  No editor panels are wired yet; this commit establishes
the core infrastructure that all subsequent phases build on.

Phase 1 — core types and context
- ZUIBox: box node with size spec, visual spec, tree links, layout output
- ZUIContext: two sub-arenas (frame + persistent) carved from the engine
  ArenaAllocator; open-addressing persistent state hash table
- ZUIPushBox / ZUIPopBox: build API with '##' key splitting and FNV-1a hash
- ZUIInteraction: DFS hot/active key resolution; ZUISignal return-value API;
  exponential animation on HotT / ActiveT

Phase 2 — layout solver (ZUILayout)
- Pass 1 post-order: Pixels, Text (stub), ChildrenSum (sum on layout axis,
  max on cross axis)
- Pass 2 pre-order: ParentPercent, Fill (cross = parent size, layout axis =
  remaining ÷ fill-sibling count); ScreenMin / ScreenMax from sibling flow
  or FloatPos when ZUI_FloatX / ZUI_FloatY is set
- MaxBoxesPerFrame stored in ZUIContext; used by both layout and interaction

Phase 3 — font system (ZUIFont)
- stb_truetype + stb_rect_pack atlas bake (2× oversampling, ASCII 32-127)
- Single-channel alpha → RGBA8 expansion; upload via RenderResourceManager
- ZUIMeasureText wired into ZUISizeKind::Text in the layout solver
- ZUIContext::Font pointer; ZUIFontBake takes separate persistent / temp arenas

CMakeLists: ZENGINE_SOURCES_UI glob picks up all UI/*.cpp automatically
ZUIRenderer — Vulkan backend that walks the ZUIBox tree and emits
draw calls, running as a parallel overlay pass alongside ImGui.

ZUIRenderer::Initialize
- Creates a RenderGraph + RenderPass ("ZUI Pass") using the "zui"
  shader, same vertex layout as ImGui (UIDrawVert: pos2+uv2+col4)
- Allocates 3× HOST_VISIBLE vertex (2 MB) and index (1 MB) buffers

ZUIRenderer::PreparePayload
- Collects boxes in pre-order via DFS into frame_arena arrays
- ZUI_DrawBackground: emits a solid-color quad (texId sentinel 0xFFFFFFFF)
- ZUI_DrawText: emits per-glyph quads using the ZUIFont atlas UVs
- Batches consecutive same-texture draws into one ZUIDrawCmd
- Output arrays (ZUIVertex, uint32_t indices, ZUIDrawCmd) are
  arena-allocated into ZUIContext::FrameArena — no heap allocation

ZUIRenderer::Submit
- Uploads vertex/index data via RenderResourceManager::UpdateBuffer
- Records a secondary CommandBuffer (slot ZUICommandBufferIndex=2)
  with SetViewport, BindPipeline, BindVertexBuffer/IndexBuffer,
  per-cmd SetScissor + PushConstants + DrawIndexed
- Executes the secondary CB into the provided primary command buffer

zui.vert / zui.frag
- Identical vertex stage to imgui.vert (scale+translate push constants)
- Fragment stage adds a sentinel check: texId==0xFFFFFFFF outputs
  vertex color directly (solid backgrounds), otherwise samples
  TextureArray[texId] and multiplies by vertex color (text/images)

ZUIPushConstant defined in ZUIRenderer.h to avoid taking a
dependency on ImGUIRenderer.h for its PushConstantData struct
ZUILayer (Tetragrama/Layers/)
- Routes GLFW mouse position, button press/release, wheel, and text input
  events into ZUIContext each frame
- Keyboard stubs return false (text-field support comes in Phase 7 widgets)
- Render() is intentionally empty — ZUI panels are ported in Phase 7

Input-state timing fix (ZUIContext.cpp)
- GLFW events fire in window->PollEvent() before BeginOverlayFrame().
  Clearing MousePressed/MouseReleased/ScrollDelta at the start of
  BeginFrame would discard events before the interaction pass sees them.
  Edge states are now cleared at the END of ZUIEndFrame instead, after
  ZUIInteractionPass has consumed them.

ZUIRenderer::PreparePayload — per-mailbox-slot arena
- The output vertex/index/cmd arrays must outlive the update thread's frame
  (the render thread may consume them 1-2 frames later with triple
  buffering). Changed signature to take an explicit payload_arena so the
  caller controls lifetime. AppRenderPipeline passes one of three
  ZUIPayloadArenas[], one per mailbox slot.

AppRenderPipeline
- Owns ZUIContext* ZUICtx (allocated in LocalArena, init via ZUIContextInit)
- Owns ZUIPayloadArenas[3] (per-slot sub-arenas for render-safe payloads)
- Owns ZUIRendererPtr ZUIRenderer (arena-allocated alongside ImguiRenderer)
- BeginOverlayFrame(float dt): calls ZUIBeginFrame with the real delta time
  now that Engine.cpp passes raw_dt to it
- EndOverlayFrame: calls ZUIEndFrame (layout + interaction passes)
- FillOverlayPayload(RenderPayload&): fills both ImGui and ZUI payloads;
  signature widened from RenderOverlayPayload& to RenderPayload&
- RenderOverlay(const RenderPayload&): renders ImGui pass then, if ZUI has
  vertices, calls ZUIRenderer::Submit for the ZUI pass;
  signature widened to match FillOverlayPayload

Engine.cpp
- Passes raw_dt to BeginOverlayFrame (dt flows to ZUIContext::DeltaTime
  for hot/active animation)
- FillOverlayPayload and RenderOverlay updated to full RenderPayload

Editor
- Creates and initializes ZUILayer alongside ImguiLayer
- Bakes OpenSans-Regular 17 px font atlas (512×512, ASCII 32–127) using
  a scratch scope on MainArena for temp bake data
- Routes OnEvent and OnRenderUI to ZUILayer
ZUIWidgets.h/.cpp — higher-level building blocks on top of ZUIPushBox/ZUIPopBox:

Size helpers (inline, zero-overhead):
  ZPx(v)   Pixels    ZFill()  Fill      ZText()  Text content
  ZPct(v)  ParentPercent      ZFit()   ChildrenSum

Layout containers:
  ZUIBeginColumn / ZUIEndColumn — vertical stack (LayoutAxis::Y)
  ZUIBeginRow    / ZUIEndRow    — horizontal stack (LayoutAxis::X)
  Both accept optional ZUISize overrides for w/h; return ZUIBox* for
  further customisation before children are added.

Leaf widgets:
  ZUILabel(ctx, text, color)     — static text, ZText sizing
  ZUIButton(ctx, label)          — background + text + clickable;
                                   returns ZUISignal; ZUI_SignalClicked
                                   set on the frame the user releases
  ZUISeparator(ctx)              — 1 px horizontal fill bar
  ZUISpacer(ctx, px)             — empty box for explicit spacing
  ZUITreeNode(ctx, label, *open) — disclosure row ("> "/"v " indicator
                                   + label); toggles *open on click;
                                   returns signal from the row box

ZUIBox.h: added operator|(ZUIBoxFlags, ZUIBoxFlags) so flag combinations
compile without explicit casts (ZUI_DrawBackground | ZUI_Clickable etc.)
ZUIComponent (Tetragrama/Components/ZUI/)
- Base struct for ZUI panels: virtual BuildUI(ZUIContext*), Name, Visible,
  ParentLayer pointer. Separate from the ImGui-based UIComponent hierarchy
  so both can coexist during the migration window.

ZUILogComponent (Tetragrama/Components/ZUI/)
- Owns its own 512-entry ring buffer and Logger::AddEventHandler cookie,
  independent of LogUIComponent so both panels run in parallel during
  transition — verified working, then the ImGui version is removed.
- BuildUI: floated panel (420×310 px, anchored bottom-left of root box).
  Header row: "Console" label + "Clear" button (clears ring buffer on
  ZUI_SignalClicked). Separator. Last kVisibleLines=12 entries rendered
  via ZUILabel with per-entry color from LogMessage::Color.
  No scroll widget needed: always shows the most-recent 12 lines, which
  matches the scroll-to-bottom behaviour of the ImGui version.

ZUILayer
- Render() now builds a transparent full-screen root column (ZFill×ZFill,
  BgColor alpha=0) and calls BuildUI(ctx) on each registered ZUIComponent.
  Floating child panels anchor their FloatPos relative to this root.
- AddComponent(ZUIComponent*) appends to a fixed 32-slot array (arena-free,
  no heap allocation needed for panel registration).

Editor
- Creates ZUILogComponent via ZPushStructCtor into MainArena, initializes
  it, and registers it with ZUIUILayer before the font bake.
Ports the core of HierarchyViewUIComponent to ZUI. ImGui counterpart
keeps running in parallel (gizmo rendering stays there until Phase 9+).

Panel: floated 280×700 at (480,80), dark background.

Header row
  "Hierarchy" label | "Add" button | "Del" button
  Add: creates actor with NameComponent("Actor") + TransformComponent,
       sets SelectedActorHandle to the new handle.
  Del: destroys selected actor (removes MeshInstance from RenderScene
       first if one exists), clears selection.

DFS tree — identical tree build to HierarchyViewUIComponent (pure arena
code, no ImGui dependency):
  - ForEach all actors → OutlinerNode array (scratch arena)
  - Build parent/child/sibling index arrays
  - DFS stack, depth-first from roots

Scene root row
  Disclosure arrow (">"/v") toggles m_root_open.
  Shows scene name + "World" type label.

Actor rows
  Indented (14 px per depth level + 1 for scene root offset).
  If has_children: disclosure arrow box (ZUI_DrawText|ZUI_Clickable)
    toggles ToggleCollapsed(eid) on ZUI_SignalClicked.
  If leaf: 14 px alignment spacer.
  Actor name label + dim type string (Coll/Light/Camera/Mesh/Actor).
  Row box is ZUI_DrawBackground|ZUI_Clickable; BgColor set to selection
    colour (0.26, 0.44, 0.70, 0.50) when selected.
  Row ZUI_SignalClicked → sets SelectedActorHandle.
  Expanded children pushed onto DFS stack in reverse order.

Status bar
  Separator + label: "N actors" or "N actors  M selected".

Deferred for later: type icons, three-column layout, drag-and-drop,
context menu, inline rename.
…ector

Ports InspectorViewUIComponent to ZUI in display-only mode. Editable
DragFloat / InputText widgets are deferred to Phase 10.

Panel: floated 280×600 at (760, 80) — right of the hierarchy panel.

No selection: "No actor selected" label in dim colour.

Actor header card (42 px, darker bg)
  - Actor name from NameComponent::Value (read-only label)
  - "Actor" type label in dim colour

Transform section (ZUITreeNode — toggled by m_transform_open)
  XYZRow "Location": "x.xxx  y.xxx  z.xxx" from Position
  XYZRow "Rotation": degrees (x*180/π, y*180/π, z*180/π) from Rotation
  XYZRow "Scale":    from Scale

Mesh section (ZUITreeNode — toggled by m_mesh_open; only shown when
  actor has MeshComponent)
  PropRow "UUID": uuids::to_string(MeshUUID)

PropRow / XYZRow helpers
  PropRow: fixed-width (72 px) dim label + right-side value label in a row.
  XYZRow:  snprintf("%.3f  %.3f  %.3f") → PropRow.
  Both use ZUIBeginRow/ZUIEndRow, ZUIPushBox for the label, ZUILabel for
  the value — no heap allocation; all labels land in FrameArena.
…Inspector

ZUIContext
  PrevMousePos[2]: saved at end of ZUIEndFrame; used by ZUISignalFromBox to
    compute per-frame drag delta = CurrentMousePos - PrevMousePos.
  BackspacePressed: set by ZUILayer::OnKeyPressed when ZENGINE_KEY_BACKSPACE
    fires; cleared at end of ZUIEndFrame with the rest of the edge states.
  TextInputLen cleared only in ZUIEndFrame — it was also being cleared in
    ZUIBeginFrame (bug), which discarded text-input events that arrived in
    PollEvent() before BeginFrame. Fixed by removing the BeginFrame clear.

ZUIInteraction — ZUISignalFromBox
  When ZUI_SignalHeld: DragDelta[0/1] = MousePos - PrevMousePos.
  Previous value was always {0,0}; this enables continuous value editing
  while the mouse button is held.

ZUILayer::OnKeyPressed
  Detects ZENGINE_KEY_BACKSPACE and sets ctx->BackspacePressed = true.

ZUIWidgets — ZUIDragFloat(ctx, key, *value, speed, width_px)
  Box with DrawBackground + DrawText + Clickable.
  Label is regenerated each frame via snprintf("%.3f") + ZUIPushStr into
  FrameArena — no heap allocation.
  On ZUI_SignalHeld: *value += DragDelta[0] * speed. Returns true if changed.

ZUIWidgets — ZUITextField(ctx, key, buf, buf_size, width_px)
  Box with DrawBackground + DrawText + DrawBorder + Clickable.
  On ZUI_SignalClicked: ctx->FocusKey = box->Key (grants keyboard focus).
  When focused: appends ctx->TextInput chars; handles BackspacePressed.
  Display string adds a "|" cursor when focused. Returns true if buf changed.

ZUIInspectorViewComponent
  Actor header: ZUITextField for NameComponent::Value (live rename).
  Transform section: XYZDragRow helper replaces read-only XYZRow.
    Location / Scale: speed 0.05 / 0.01 units per pixel.
    Rotation: speed 0.01 rad/px (~0.57°/px) — stored and displayed in
    radians for Phase 10; degree conversion deferred to Phase 11+.
  Mesh section: PropRow UUID read-only display unchanged.
Infrastructure additions (apply to all phases)
  ZUIContext: ScreenW/H — set by AppRenderPipeline::BeginOverlayFrame each
    frame from SwapchainImageWidth/Height; read by panels for bottom/center
    anchoring and by the dockspace for layout calculations.
  ZUIComponent: RegionX/Y/W/H — dockspace assigns these before each BuildUI
    call; panels use region values when RegionW > 0, fall back to hardcoded
    defaults when 0 (enables standalone debugging without a dockspace).
  ZUIBox: TextureIndex — when ZUI_DrawBackground is set and Index != 0xFFFFFF,
    the renderer emits a UV [0,1] quad sampling TextureArray[Index] with white
    vertex colour instead of a solid colour quad.
  ZUIRenderer: image branch in PreparePayload — handles TextureIndex != sentinel
    by emitting full-UV textured quad; reuses the existing flush/begin-cmd path.
  ZUIWidgets: ZUIImage(ctx, key, texture_index, w, h) — thin wrapper that
    pushes a ZUI_DrawBackground box with TextureIndex set.

Phase 11 — ZUIStatusBarComponent (ZUIStatusBarComponent.h/.cpp)
  Bottom-anchored row (kBarH=28 px). Reads DeltaTime from ZUIContext for a
  rolling 32-sample FPS / frame-time average. Buttons toggle
  ShowConsole / ShowContentBrowser / ShowImporter on the EditorConfiguration.
  Scene name, camera XYZ, and FPS label on the right side.

Phase 12 — ZUIProjectViewComponent (ZUIProjectViewComponent.h/.cpp)
  File-browser panel using IVFSContext::List() each frame into a scratch arena.
  Directory entries shown as clickable rows ([D] prefix in blue); clicking a
  directory updates m_current_path. "Up" button calls VFSPath::Parent().
  Arena sub-allocated from ZUILayer::LocalArena during Initialize.

Phase 13 — ZUISceneViewportComponent (ZUISceneViewportComponent.h/.cpp)
  Calls SceneRenderer::GetFrameOutput() every frame to get the latest render
  texture handle, then emits a ZUIImage that fills the viewport region.
  Enqueues a RenderTargetResizeRequest when the assigned region dimensions
  change, keeping the scene render resolution in sync.

Phase 14 — ZUIDockspaceComponent (ZUIDockspaceComponent.h/.cpp)
  Registered FIRST with ZUILayer so it runs before the panels it manages.
  Calculates a fixed 5-region layout from ScreenW/H (matching the ImGui
  DockBuilder default: 18% left / 22% right / 25% bottom / center viewport)
  and writes RegionX/Y/W/H onto each panel pointer. Renders a full-screen
  dark background column and a 26 px menu bar row with: app name, scene name,
  and per-panel visibility toggle buttons (click to show/hide each panel).

Existing panels updated
  ZUILogComponent, ZUIHierarchyViewComponent, ZUIInspectorViewComponent:
  all now read RegionX/Y/W/H when RegionW > 0 instead of hardcoded floats.
  ZUISceneViewportComponent and the new panels follow the same convention.

Editor.cpp
  Creates all seven ZUI components, wires dockspace panel pointers, and
  registers them in the correct order (dockspace first, panels after).
Dear ImGui and ImGuizmo are fully removed from the codebase.
ZUI is now the sole UI system.

dependencies.cmake
  Removed FetchContent_Declare blocks for imgui and imguizmo.
  Removed both from FetchContent_MakeAvailable().
  Removed the add_library(imgui STATIC) and add_library(imguizmo STATIC)
    blocks that compiled their source trees.
  Removed imguizmo from External_libs target_link_libraries.

IRenderer.h
  Removed ScissorCmd, IndexedCmd, RenderOverlayPayload (all ImGui-only).
  UIDrawVert is kept — it is the shared vertex layout for ZUIRenderer
    and the zui.vert / zui.frag shaders.

AppRenderPipeline.h
  Removed #include <ImGUIRenderer.h>, ImGUIRendererPtr member,
    UICommandBufferIndex, and RenderOverlayPayload UIOverlay from RenderPayload.

AppRenderPipeline.cpp
  Initialize: creates only SceneRenderer + ZUIRenderer (ImguiRenderer gone).
  Shutdown: deinitialises only ZUIRenderer.
  BeginOverlayFrame: sets ScreenW/H and calls ZUIBeginFrame only.
  EndOverlayFrame: calls ZUIEndFrame only.
  FillOverlayPayload: calls ZUIRenderer::PreparePayload only.
  RenderOverlay: single line — ZUIRenderer::Submit.

Engine.cpp
  Removed r_payload.UIOverlay.DrawDataIndex = 0 reset.

Editor.h/.cpp
  Removed ImguiLayer field; UILayer replaced by ZUIUILayer throughout.
  OnUpdate, OnEvent, OnRenderUI all route exclusively through ZUIUILayer.

Deleted files (git rm)
  ZEngine/ZEngine/Rendering/Renderers/ImGUIRenderer.h/.cpp
  Tetragrama/Layers/ImguiLayer.h/.cpp
  Tetragrama/Components/UIComponent.h
  Tetragrama/Components/{Dockspace,Hierarchy,Inspector,Log,Project,
    SceneViewport,StatusBar,AssetImporter}UIComponent.h/.cpp
  Tetragrama/Components/AboutUIComponent.h
  Tetragrama/Components/DemoUIComponent.h
  Tetragrama/Components/ContentBrowserIcons.h
  Tetragrama/Helpers/UIComponentDrawerHelper.h/.cpp
  Tetragrama/Messengers/Messenger.h: removed orphaned UIComponent.h include

Known gaps (future phases)
  Gizmo / ImGuizmo — no ZUI gizmo system yet
  Viewport drag-and-drop asset assignment
  Viewport-focus-gated camera input
…reposition

Gap 2 — Viewport drag-and-drop

ZUIContext: DragSourceKey, DragPayload[512], DragPayloadLen — set by
  ZUIBeginDragSource when a held box moves; DragDropFired + DragTargetKey —
  set by ZUIInteractionPass on mouse release when a drag is active.

ZUIInteraction: when MouseReleased[0] fires and DragSourceKey != 0, the
  interaction pass records the drop (DragDropFired=true, DragTargetKey=HotKey)
  and clears the source. The drop result is cleared at the START of ZUIEndFrame
  the next frame so BuildUI can consume it via ZUIAcceptDrop.

ZUIWidgets — ZUIBeginDragSource(ctx, box, payload, len)
  Reads ctx->ActiveKey + MousePos/PrevMousePos directly (no second
  ZUISignalFromBox call) to activate drag when a box is held and moves.

ZUIWidgets — ZUIAcceptDrop(ctx, box, out_buf, out_size) → bool
  Returns true once (the BuildUI frame after the drop) when DragDropFired
  and DragTargetKey matches the box. Copies payload to out_buf.

ZUIProjectViewComponent: file rows call ZUIBeginDragSource with the VFS
  path CStr() as payload; directory rows are not draggable.

ZUISceneViewportComponent: the scene image box accepts drops from the project
  view via ZUIAcceptDrop. Dispatches .zescene → OPENSCENE message,
  .zemesh → OPENMESH message, .glb/.gltf/.fbx/.obj → sets PendingImportPath /
  PendingImportName on EditorConfiguration and opens the importer panel.

Gap 3 — Viewport-focus camera routing

ZUIContext: ViewportHovered bool — reset at start of ZUIEndFrame, written
  true by ZUISceneViewportComponent::BuildUI when the image box is hovered.

GameApplication::ProcessEvent made virtual so Editor can override it.

Editor::ProcessEvent override: routes events to window + ZUILayer + camera
  as before, except mouse events (Pressed / Released / Moved / Wheel) are
  only forwarded to the camera controller when ViewportHovered is true.
  Keyboard events always reach the camera controller.

Gap 4 — Panel drag-to-reposition

ZUIComponent: Detached bool — when true, ZUIDockspaceComponent::AssignRegion
  skips this panel so the user-set RegionX/Y are preserved across frames.

ZUIDockspaceComponent: AssignRegion returns early when cmp->Detached.

ZUIWidgets — ZUIPanelDragHeader(ctx, title, &x, &y, &detached)
  Utility row that accumulates DragDelta into *x/*y; sets *detached on drag,
  clears it on double-click. (Available as a standalone helper; panels below
  implement drag inline on their existing header rows instead.)

ZUILogComponent / ZUIHierarchyViewComponent / ZUIInspectorViewComponent /
ZUIProjectViewComponent: existing header rows gain ZUI_Clickable flag +
  ZUISignalFromBox call. On ZUI_SignalHeld + DragDelta: RegionX/Y accumulate
  the delta, Detached = true, panel->FloatPos updated immediately so the
  layout solver positions the panel at the new location this frame.
  On ZUI_SignalDoubleClicked: Detached = false (snaps back to dockspace).
  All panels initialise RegionX/Y/W/H from defaults when RegionW == 0
  (standalone debug mode with no dockspace).
… VFS cache

ZUIRenderer — VertexOffset must be 0 (root cause of black screen)
  ZUI emits absolute vertex indices via EmitQuad (base = vert_count at
  emission time). vkCmdDrawIndexed's vertexOffset is ADDED to each index,
  so a non-zero value caused the GPU to read VB[absIdx + N] instead of
  VB[absIdx], sampling wrong vertices for every draw command after the first.
  Fix: cmd.VertexOffset = 0 always in FlushAndBeginCmd.
  All panels and glyph quads now render at their correct screen positions.

ZUILayer::LocalArena not initialized
  ZUIHierarchyViewComponent and ZUIProjectViewComponent carved sub-arenas
  from ZUILayer::LocalArena in their Initialize() calls. LocalArena was
  zero-initialized (no backing memory), causing an EXC_BREAKPOINT assert
  in ArenaAllocator::CreateSubArena on startup.
  Fix: arena->CreateSubArena(ZMega(4), &LocalArena) in ZUILayer::Initialize.

ZUICommandBufferIndex 2 → 1
  The engine allocates: index 0 = primary, index 1 = secondary. With ImGui
  gone, slot 1 is free for ZUI. Slot 2 didn't exist → BeginSecondary assert.

ZUIProjectViewComponent — cache VFS listing
  vfs->List() was called every frame from BuildUI — heavy disk I/O that also
  raced with background VFSScanner threads causing SIGBUS. Replaced with a
  CachedEntry[] array (arena-allocated into LocalArena sub-arena) that is
  only refreshed when m_current_path != m_listed_path.

ZUISceneViewportComponent — remove std::filesystem heap allocation
  Drop handler used std::filesystem::path().extension().string() — heap
  allocation on every asset drop. Replaced with strrchr + strcmp.

Panel background colors lifted to visible range
  All panel backgrounds were 10-12% linear gray — indistinguishable from
  the black scene clear color on-screen. Raised to 22-28% gray.

Root box ZPx instead of ZFill
  ZUILayer::Render pushed the root box with ZFill×ZFill. ZFill on a box
  with no parent resolves to 0 in the layout solver, collapsing the root.
  Children using ZFill against the root also got 0 size. Fixed to
  ZPx(ScreenW)×ZPx(ScreenH) so the root is correctly sized each frame.
…rdinate fix

Renderer — PreparePayload
  Hover / active blending: clickable boxes with ZUI_DrawBackground read
    ZUIPersistentState::HotT/ActiveT from the persistent store and lift
    the RGB channels by hot_t×0.12 − active_t×0.06. Transparent rows
    (BgColor[3]==0) fade in to 18% alpha on hover — gives list rows
    interactive visual feedback without requiring an explicit background.
  Implicit hover for Clickable boxes without DrawBackground: header rows
    and navigation items that omit ZUI_DrawBackground get a 15% alpha
    neutral highlight on hover via a separate solid quad.
  Border rendering: ZUI_DrawBorder emits four thin solid quads (top /
    bottom / left / right) using BorderColor and BorderThickness.
  Text vertical centering: baseline now computed as
    by0 + (box_h − LineHeight)×0.5 + Ascent, so text is vertically
    centred in its box regardless of row height.
  Text indent 4 px: cx = bx0 + 4 for a small left breathing margin.
  Text clip rect full-framebuffer: per-box scissor was cutting off the
    indent-shifted glyphs on tightly-sized (ZText()) boxes. Changed to
    {0, 0, fb_w, fb_h}; per-panel clipping via ZUI_ClipChildren later.

ZUIWidgets — visual palette overhaul
  Buttons: ZUI_DrawBorder, 28 px tall, brighter background (0.32 → 0.38),
    1 px border (k_button_bdr), BorderThickness=1.
  Separator: 2 px thick, brighter color.
  DragFloat / TextField: ZUI_DrawBorder, input-style dark background,
    1 px subtle border; focused TextField gets accent-blue border.

Panel borders
  All floating panel containers (Hierarchy, Inspector, Log, Project,
  StatusBar) now carry ZUI_DrawBorder + BorderColor {0.40,0.42,0.50,1}
  + BorderThickness=1 — each panel has a clear outline that separates it
  from the viewport.

Hierarchy list rows
  Scene root row: visible tinted background (18% alpha dark-blue).
  Actor rows: neutral transparent base {0.42, 0.42, 0.48, 0}; on hover
    the renderer fades them to ~18% alpha giving clear row hover feedback.
  Row height raised to 24 px for better touch targets.

AppRenderPipeline — logical window coordinates
  ctx->ScreenW/H now reads Device->CurrentWindow->GetWidth/Height()
  (glfwGetWindowSize = logical pixels) instead of SwapchainImageWidth
  (physical Retina pixels). Mouse positions from GLFW cursor callbacks
  are also in logical pixels, so all panels and hit-testing are now
  in the same coordinate space. ZUIRenderer::PreparePayload uses the
  same logical size for the NDC scale.
  Added #include <ZEngine/Windows/CoreWindow.h> to both call sites.

Font size 28 px, 1024² atlas
  17 px on Retina appeared as ~8pt. 28 px gives comfortable reading
  size across display configurations.
ZUIBox — Padding[4] (left, top, right, bottom)
  Layout solver Pass 1: ChildrenSum includes padding on both ends.
  Layout solver Pass 2: first child starts at parent origin + pad-start;
    cross-axis children offset by cross-start pad; Fill subtracts both
    padding ends from available space before distributing to Fill children.

ZUILayout — scroll offset
  When a ZUI_Scrollable box is the parent, the first child's layout-axis
  position is shifted by -PersistentState::ScrollY (for Y-scroll) so the
  entire content block shifts up, revealing content below the fold.

ZUIInteraction — scroll accumulation
  ZUIInteractionPass now tracks the nearest ZUI_Scrollable box under the
  cursor. When ScrollDelta != 0, updates state->ScrollY (24 px per unit,
  clamped to >= 0). Works alongside the existing hot/active logic.

ZUIRenderer — scissor stack for ZUI_ClipChildren
  A depth-8 scissor stack is maintained during PreparePayload. When a box
  has ZUI_ClipChildren, its screen bounds are intersected with the current
  clip rect and pushed. The IsAncestor() check pops stale entries in
  pre-order. All FlushAndBeginCmd calls inside the loop now use the
  dynamic clip_x/y/w/h instead of the full framebuffer — content that
  overflows a clipped container is scissored out.

ZUIWidgets — new APIs
  ZUIPadding(box, all) / ZUIPaddingXY(box, h, v) — inline helpers.
  ZUIBeginScrollRegion / ZUIEndScrollRegion — ZUI_Scrollable | ZUI_ClipChildren
    box; default fill-fill, Y-axis layout.
  All widget colors now read from ctx->Theme instead of static constants.
  SetBgArr / SetBdrArr helpers added to set colors from float[4] arrays.

ZUITheme (new, in ZUIContext.h)
  Single struct with all UI colors: WindowBg, PanelBg, PanelBgAlt,
  HeaderBg, RowHoverBg, RowSelectedBg, RowRootBg, InputBg, ButtonBg,
  TextDefault, TextDim, TextAccent, PanelBorder, ButtonBorder,
  InputBorder, InputFocusBorder, Separator.
  ctx->Theme is the live instance; swap the whole struct to retheme.

Editor panels updated
  ZUILogComponent: removed kVisibleLines cap — all entries shown in a
    scroll region; header gets HeaderBg; all k_* replaced with ctx->Theme.
  ZUIHierarchyViewComponent: actor list wrapped in scroll region; header
    gets DrawBackground + HeaderBg; status bar moved outside scroll.
  ZUIProjectViewComponent: directory entries wrapped in scroll region;
    all k_* replaced with ctx->Theme.
  ZUIInspectorViewComponent, ZUIDockspaceComponent, ZUIStatusBarComponent:
    all local k_dim/k_text/k_dir palette constants replaced with ctx->Theme.
…undation

Root cause of the 'all panels transparent' regression:
  ZPushStruct arena-allocates via memset(0) — it does NOT call constructors.
  ZUITheme uses C++ default member initializers (= {0.22f, ...}) which only
  apply when the constructor is called. Zero-filled theme → all BgColor[3]=0
  → every background transparent → panels invisible.
  Fix: ZPushStructCtor calls placement-new, applying all default initializers.

Scissor stack correctness:
  Previous implementation used 0xFFFFFFFFu+1 (overflows to 0) as an invalid
  sentinel to force cmd flush after clip change. 0 is a valid bindless texture
  index — if the font atlas lands at slot 0, text boxes skip FlushAndBeginCmd
  and inherit the wrong clip rect, corrupting the cmd stream.
  Fix: use kInvalidTex = 0xFFFFFFFEu (safe sentinel that can't be a real index
  or the solid-colour sentinel 0xFFFFFFFF). Clip change tracking consolidated
  into a single clip_changed bool before the draw-flag checks.

Tier 1 — Padding (ZUIBox.Padding[4])
  Layout Pass 1: ChildrenSum includes padding on both ends.
  Layout Pass 2: first child starts at parent + pad-start; cross-axis children
    offset by cross pad; Fill subtracts padding from available space.

Tier 1 — Scroll regions (ZUIBeginScrollRegion / ZUIEndScrollRegion)
  ZUI_Scrollable | ZUI_ClipChildren box. Scroll offset stored in
  PersistentState::ScrollY, applied to first child's layout-axis start.
  ZUIInteractionPass accumulates ScrollDelta into the nearest scrollable
  ancestor under the cursor (24 px per wheel unit, >= 0 clamp).

Tier 1 — ZUITheme
  Single struct with all UI colours; ctx->Theme is the live instance.
  Widgets read ctx->Theme.* instead of local static constexpr constants.
  All component files updated to use theme colours.

Tier 1 — ZUIPadding / ZUIPaddingXY helpers in ZUIWidgets.

ZUILogComponent, ZUIHierarchyViewComponent, ZUIProjectViewComponent:
  Content wrapped in scroll regions; all k_* palette constants replaced
  with ctx->Theme.*; header rows given DrawBackground + HeaderBg colour.
ZUIButton(ctx, label, w=ZText(), h=ZPx(28))
  Added explicit w/h overrides with the previous defaults as defaults.
  Respects ctx->Disabled: skips ZUI_Clickable and dims BgColor/TextColor.

ZUISmallButton(ctx, label)
  22 px tall, no border, tight fit — safe inside toolbar rows.

ZUIInvisibleButton(ctx, key, w, h)
  Hit area only, no draw flags. Used for custom-drawn interactive regions.

ZUIToggleButton(ctx, label, *active, w, h) → bool
  Active state renders with +0.14 brightness bg and InputFocusBorder accent.
  Returns true the frame *active flips.

ZUIImageButton(ctx, key, texture_index, w, h)
  Full-UV image quad with Clickable hit area. Alpha=0.38 when disabled.

ZUIBeginDisabled(ctx) / ZUIEndDisabled(ctx)
  Nestable disabled scope. ctx->Disabled + ctx->DisabledDepth track depth.
  All button variants (and future widgets) call ApplyDisabledDim() to
  reduce alpha to 38% and skip ZUI_Clickable when ctx->Disabled is true.
…, combo, modal)

ZUIContext additions
  OpenPopupKey — set by ZUIOpenPopup; promoted to ActivePopupKey at the
    end of ZUIEndFrame so the popup is never visible on the frame it was
    requested (avoids open-then-immediately-close on right-click).
  PopupPos[2] — screen position; defaults to ctx->MousePos if not given.
  ActivePopupKey — persists across frames; cleared by close-on-outside.
  ActivePopupBox — pointer to the live popup ZUIBox; rebuilt each frame.
  PopupSavedParent — ctx->Current is saved/restored around popup content.

ZUIOpenPopup(ctx, key, pos_x=-1, pos_y=-1)
  Sets OpenPopupKey + PopupPos. pos_x/y default to current mouse position.

ZUIBeginPopup(ctx, key) → bool
  Returns true when this popup is active. Saves ctx->Current, switches it
  to ctx->Root, then pushes a floated ZFit×ZFit column box as the LAST
  child of root — pre-order DFS visits it last → renders on top of all
  other panels. Styling: PanelBg + PanelBorder + ZUI_ClipChildren.

ZUIEndPopup(ctx)
  Pops the popup box and restores ctx->Current to PopupSavedParent.

ZUIClosePopup(ctx)
  Clears ActivePopupKey immediately.

ZUIBeginPopupContextItem(ctx, key, signal) → bool
  If item is hovered and MousePressed[1] (right-click), calls ZUIOpenPopup.
  Then returns ZUIBeginPopup — so right-clicking any item opens a context
  menu without extra bookkeeping in the call site.

ZUIMenuItem(ctx, label, enabled) → bool
  Transparent-background full-width row; hover fade-in via renderer.
  Calls ZUIClosePopup on click and returns true.

ZUIInteractionPass
  Closes popup when any mouse button is pressed outside ActivePopupBox.
  Uses the safe timing: OpenPopupKey only promotes at end-of-frame, so
  a right-click to open a popup does not immediately close it.
…apsingHeader

ZUICheckbox(ctx, label, *checked) → bool
  Row of [16×16 tick box with border] + [label]. Tick box shows "v" in
  InputFocusBorder colour when checked. Returns true on toggle. Disabled-aware.

ZUIRadioButton(ctx, label, *selected, index) → bool
  Same structure as Checkbox; shows "*" dot when selected == index.
  Returns true when the selection changes.

ZUIProgressBar(ctx, key, fraction, w, h, overlay_text)
  Outer track box (InputBg + InputBorder). Inner fill box at ZPct(fraction)
  with InputFocusBorder colour. Optional overlay_text drawn on the fill bar.

ZUISetTooltip(ctx, sig, text)
  When sig contains ZUI_SignalHovered: escapes to root (same parent-swap
  as ZUIBeginPopup), pushes a ZFit×ZFit floated box near the cursor with
  an 14 px offset and screen-edge clamping.

ZUICollapsingHeader(ctx, label, *open) → bool
  Full-width HeaderBg row with "v" / ">" indicator. Toggles *open on click.
  Returns current *open value so callers can guard child content.

ZUISelectable(ctx, label, *selected, h) → bool
  Full-width clickable row. RowSelectedBg when *selected, transparent-base
  (fade-in hover) when not. Returns true on toggle.

ZUISeparatorText(ctx, text)
  8 px left line, dim label, full-width right line — section divider.
ZUIBeginContextMenu / ZUIEndContextMenu
  Opens on right-click anywhere in the caller's scope (MousePressed[1]).
  Thin wrapper over ZUIBeginPopup / ZUIEndPopup.

ZUIBeginCombo(ctx, key, preview_label, w) / ZUIEndCombo
  Collapsed: bordered preview row + "v" arrow. Click opens a popup
  positioned near the mouse; popup rows are filled with ZUISelectable.
  ZUIEndCombo calls ZUIEndPopup.

ZUIBeginMenuBar / ZUIEndMenuBar
  Pushes a ZFill HeaderBg horizontal row — minimal menu bar scaffold.

ZUIBeginMenu(ctx, label, enabled) / ZUIEndMenu
  Menu button inside a menu bar; opens a popup on click. ZUIMenuItem
  (Phase 2) fills it. Returns true while the menu is open.

ZUIOpenModal / ZUIBeginModal(ctx, key, title) / ZUIEndModal
  Modal state stored in ctx->ActiveModalKey (not in OpenPopupKey —
  modals cannot be closed by clicking outside).
  ZUIBeginModal escapes to root and pushes:
    1. A full-screen dim overlay (rgba 0,0,0,0.55) — always drawn on top.
    2. A centred fixed-size (480×280) panel with HeaderBg title bar.
  ZUIEndModal pops the modal panel and restores ctx->Current via
  PopupSavedParent (shared with the popup mechanism).
ZUITextAlign enum (Left/Center/Right) on ZUIBox
  Renderer: for Center/Right calls ZUIMeasureText to get text width then
  offsets cx accordingly. Left remains the fast path (bx0+4).

ZUISameLine(ctx, spacing)
  Changes ctx->Current->LayoutAxis to X so the immediately following
  ZUIPushBox call is placed horizontally next to the previous sibling.
  Lightweight and arena-free — no new state needed.

ZUIBeginTable / ZUITableNextRow / ZUITableSetColumn / ZUIEndTable
  Arena-based: ZUIContext holds TableColumns, TableCurrentCol,
  TableColWidths (FrameArena float array), TableRowBox.
  Outer column container wraps the table; each row is a ZUIBeginRow;
  each cell is a ZUIBeginColumn with the specified pixel width.
  widths[] defaults to 80 px per column when nullptr.
  Single-level only (no nested tables).

ZUISetTextAlign(box, align) — inline helper to set alignment on any box.
ZUIFontSize enum: Small=0, Body=1, Header=2 on ZUIBox.
ZUIBox.FontSize field (default Body) selects the atlas for this box.
ZUIContext: FontSmall + Font (Body) + FontHeader + GetFont(size) helper.
ZUILabel gains optional ZUIFontSize size parameter.
ZUILayout / ZUIRenderer use ctx->GetFont(box->FontSize) instead of ctx->Font.
Editor: bakes all 3 atlases at init — 18 px/512², 28 px/1024², 36 px/1024².
  Uses a single scratch scope for all three bakes (each bake releases
  intermediates before the next starts).
…olorPicker

ZUIBeginTabBar / ZUIBeginTabItem / ZUIEndTabItem / ZUIEndTabBar
  Selected tab index stored in PersistentState::ScrollY (int cast) keyed
  by the tab bar hash. BeginTabItem returns true only for the active tab;
  when true it closes the button row and pushes a content column.
  EndTabItem pops the content column. EndTabBar closes remaining row/outer col.

ZUIBeginListBox / ZUIEndListBox
  Bordered InputBg frame containing a ZUIBeginScrollRegion — callers add
  ZUISelectable items inside.

ZUISliderFloat(ctx, key, *value, v_min, v_max, w, h)
  Track box (InputBg + InputBorder). Drag horizontally to change value
  (range / 120 px scale). Click on track jumps to that position (uses
  previous frame's ScreenMin/Max after layout resolves them).

ZUIInputInt(ctx, key, *value, v_min, v_max, w)
  Bordered text field restricted to digits + optional leading "-".
  Internal static buf keyed by box hash; also supports drag-to-change
  (±1 per 2 px). Displays "|" cursor when focused.

ZUIInputTextMultiline(ctx, key, buf, buf_size, w, h)
  Bordered frame + scroll region. Full text editing (append + backspace).
  Rendering via ZUILabel inside scroll region (text wrapping in a later pass).

ZUIColorPicker4(ctx, key, color[4])
  Color swatch (full-width DrawBackground using live color[]) +
  four ZUISliderFloat rows for R/G/B/A channels [0..1].

ZUIContext: TabBarKey, TabBarSelectedIdx, TabBarCurrentIdx, TabItemWasSelected,
  TabBarOuterBox, TabBarRowBox added for single-level tab state.
…dering

Phase 8 — Interaction
  ZUIContext: CtrlDown, ShiftDown, AltDown — set by ZUILayer::OnKeyPressed/
    Released on all Ctrl/Shift/Alt variants; persist until key release.
  ZUIContext: BackspaceHeld + KeyRepeatTimer — ZUILayer::OnKeyPressed sets
    BackspaceHeld=true and resets the timer; OnKeyReleased clears both.
    ZUIEndFrame advances KeyRepeatTimer each frame; after kRepeatDelay (0.45 s)
    fires BackspacePressed at kRepeatRate (0.04 s) intervals while held and
    a text field is focused.
  Clipboard paste: Ctrl+V in ZUILayer::OnKeyPressed calls glfwGetClipboardString
    on the native window and injects the result into ctx->TextInput.
  ZUILayer::OnKeyReleased now clears modifier + backspace hold state.

Phase 9 — Rendering polish
  ZUIBox: BgColorB[4] — when BgColorB[3] > 0, the background quad uses
    EmitQuadGradient with top vertices = BgColor, bottom = BgColorB.
  ZUIBox: ShadowColor[4] + ShadowOffset[2] — when ShadowColor[3] > 0, an
    extra offset quad is emitted before the main background quad so it
    appears as a drop shadow.
  EmitQuadGradient helper added to ZUIRenderer.
  ZUIWidgets: ZUISetGradient(box, r,g,b,a) and
    ZUISetShadow(box, alpha, offset_x, offset_y) inline helpers.
…croll-to-bottom

Rounded corners (ZUIRenderer)
  EmitRoundedRect(x0,y0,x1,y1, r, color, N=4) — fan-triangulated convex
  polygon from centre. 4 arc segments per corner = 20 boundary points =
  20 fan triangles. Falls back to EmitQuad when r<=0 or rect too small.
  Background emission now uses EmitRoundedRect when box->CornerRadius > 0.
  ZUIButton gets CornerRadius=4; DragFloat/TextField get CornerRadius=3.

Scroll clamping (ZUILayout pass 3 + ZUIInteraction)
  ZUIPersistentState gains MaxScrollY (set by layout) and UserData fields.
  Layout Pass 3: for every ZUI_Scrollable box, sums child heights on the
  layout axis and stores max(0, content-height - visible-height) as MaxScrollY.
  ZUIInteractionPass: clamps ScrollY to [0, MaxScrollY] after each wheel event.

ZUIScrollToBottom(ctx, key) + ZUIGetScrollY(ctx, key)
  ZUIScrollToBottom sets ScrollY=1e9 (layout pass clamps to MaxScrollY on
  the next frame — correct scroll-to-bottom with one frame latency).
…ns, search, toolbars

ZUIComboItem(ctx, label, selected) — combo-specific selectable that closes
  the combo popup on click.

ZUIHierarchyViewComponent — complete rewrite
  Type icons: 14×14 colored boxes before each actor name (W/L/C/M/+).
  Context menu: right-click → Rename (starts inline rename) / Delete /
    Duplicate Actor via ZUIBeginPopupContextItem.
  Inline rename: double-click or context Rename → ZUITextField replaces
    ZUILabel; commit on focus loss; writes back to NameComponent::Value.
  Drag-drop reparent: ZUIBeginDragSource on each row broadcasts ActorHandle
    bytes; ZUIAcceptDrop on each row receives and sets pending_reparent,
    applied via ParentComponent after the DFS loop.

ZUIInspectorViewComponent
  ZUICollapsingHeader replaces ZUITreeNode for Transform + Mesh sections.
  ZUIBeginTable (2 cols: 80 px label | stretch value) for property rows.
  Rotation displayed in degrees (×57.2957); writes back (/57.2957).
  LightComponent section: Intensity DragFloat + LightType PropRow.
  panel->CornerRadius = 6 for softer panel edges.

ZUILogComponent
  Search ZUITextField + level-filter ZUIBeginCombo (Trace/Info/Warn/Error/
    Critical/All) in a toolbar row below the header.
  Entry filtering: level != m_filter_level OR !strstr(text, search) → skip.
  Auto-scroll: PushEntry sets m_scroll_to_bottom; ZUIScrollToBottom fires
    after the mutex is released, one frame after the entry arrives.

ZUIProjectViewComponent
  Search ZUITextField above the directory listing.
  strstr filter hides entries that don't match.
  Right-click context menu per entry: directories → Open (navigate);
    files → Import (sets PendingImportPath + ShowImporter).

ZUIDockspaceComponent
  Old flat panel-toggle buttons replaced with proper drop-down menus:
    File (New/Open/Save/Save As.../Quit stubs)
    Edit (Undo+Redo disabled stubs, Select All)
    View (ZUIToggleButton per panel — Viewport/Hierarchy/Inspector/Console/Project)
  AssignRegion Detached check restored (was accidentally dropped by agent).

ZUISceneViewportComponent
  Toolbar row (28 px): T/R/S gizmo-mode small-button stubs + smoothed FPS
    (exponential moving average, static float, ~1-second time constant).
  ZUISeparator between toolbar and scene image.
  Scene image restored as full interactive box with ViewportHovered tracking
    and all drag-drop asset-dispatch logic.
ZUIContext: UIScale float (default 1.0) set each frame in
  AppRenderPipeline::BeginOverlayFrame via glfwGetWindowContentScale.
  On a 2x Retina/HiDPI display this is 2.0; on standard screens 1.0.

Editor font baking: base logical sizes (Small=16, Body=22, Header=30)
  multiplied by UIScale at init. Atlas size grows automatically for larger
  glyphs. Result: fonts are readable at any DPI without changing the
  coordinate system.

ZUIWidgets: ZSPx(ctx, n) helper = ZPx(n * ctx->UIScale). Applied to all
  core widget heights: Button(28), SmallButton(22), Separator(2),
  TreeNode row(22), DragFloat(24), TextField(28).

AppRenderPipeline: includes GLFW header and calls glfwGetWindowContentScale
  on the native GLFWwindow* obtained from Device->CurrentWindow.
Font sizing — stable physical-pixel approach
  Fixed sizes (Small=24, Body=32, Header=42 px) work on the ~3024px-wide
  display without dynamic scale math that caused garbled text. UIScale is
  still queried from glfwGetWindowContentScale and stored in ctx->UIScale
  so ZSPx() can scale widget heights proportionally.

Pixel snapping (matches ImGui's IM_TRUNC behaviour)
  Pen X and text_top Y are now floored (not rounded) before glyph emission,
  landing glyphs on exact pixel boundaries. Horizontal centering and
  right-alignment also floored.

ZUITheme — ImGui StyleColorsDark() palette
  WindowBg:   (0.06,0.06,0.06,0.94) — ImGui exact
  PanelBg:    (0.10,0.10,0.10,0.96)
  HeaderBg:   (0.14,0.14,0.14,1.00)
  ButtonBg:   (0.26,0.59,0.98,0.40) — ImGui Button exact
  InputBg:    (0.16,0.29,0.48,0.54) — ImGui FrameBg exact
  TextDefault:(1.00,1.00,1.00,1.00) — pure white
  TextDim:    (0.50,0.50,0.50,1.00) — ImGui TextDisabled exact
  Borders:    (0.43,0.43,0.50,0.50) — ImGui Border exact

Hover blending — ImGui button effect
  Alpha now expands toward 1.0 on hover in addition to brightness lift:
  bg[3] += HotT * (1-bg[3]) * 0.65. A blue button at 0.40 alpha becomes
  ~0.76 on hover, matching ImGui's ButtonHovered visual weight.

Dockspace background uses ctx->Theme.WindowBg (no hardcoded values).
…tion

- ZUIRectInst instanced renderer: per-corner colors/radii, edge_softness,
  border_thickness — SDF shader matching RAD Debugger quality
- Single shared font atlas (ZUIFontAtlasBake): all 3 font sizes in one
  texture, white pixel at (0,0) for solid draws, OversampleH=2 OversampleV=1
- ZUIKey + ZUIFeedXxx input API: platform-agnostic event feed functions,
  no CoreEvent dependency inside ZUI lib
- ZUIDockspace: arena-based split panel tree (split_axis + pct_of_parent),
  ZUIDockLayout/ZUIDockRectForKey/ZUIDockResize
- ZUIBox: per-corner Colors[4][4], CornerRadii[4], EdgeSoftness; all legacy
  BgColor/BgColorB/ShadowColor fields removed, helpers ZUIBoxSetColor/
  ZUIBoxSetColorArr/ZUIBoxSetGradientV/ZUIBoxSetCornerRadius added
- ZUIPushBox uses ZPushStructCtor so TextureIndex defaults to 0xFFFFFFFF
- RendererPipeline: dynamic vertex attribute array capacity (was hardcoded 5)
- Opaque root box covers scene bleed from render pass LOAD_OP_LOAD
…, status bar

- ZUITheme: all backgrounds fully opaque (no scene bleed), StatusBarBg blue,
  TitleBarBg/MenuBarBg/TabActiveBg added for visual hierarchy
- ZUIBox border fix: enforce min 0.5 edge softness on border instances so SDF
  correctly discards interior (border was rendering as solid fill before)
- Buttons: 3px corner radius + 0.5 edge softness for polished rounded look
- ZUIDockspace: panel tree wired (ZUIDockLayout + AssignFromDock per panel)
- ZUIContext: UIScaleLogged flag — content scale logged once not every frame
- MenuBar: MenuBarBg color + bottom separator border
- ZUIWidgets: ZSPx() for row heights throughout
ZUIMeasureText already applies font->FontScale internally.
Multiplying ts[0] by FontScale again halved the position,
placing the caret at the text midpoint instead of the cursor.
Replaced the ZUI_FloatX arrow (broken position on fixed-width combos,
shared key causing state collision) with a plain flow child:
- Preview label uses ZFill() width so it expands and the arrow sits flush at the right edge
- Arrow uses a per-combo unique key (##carrow_<key>)
- No FloatPos math, no edge-case for ZFill vs ZPx width
ZUI_DrawTriArrow now supports three UserData modes:
  0.f  = filled ► (tree node collapsed)
  1.f  = filled ▼ (tree node expanded)
  2.f  = stroked ∨ VS Code chevron (combo dropdown)

Chevron uses two ZUIDrawListAddLine calls matched to VS Code codicon
chevron-down proportions (hw=FontSize*0.275, hh=FontSize*0.15, 1.5px stroke).
Tree nodes and collapsing headers keep their filled triangle style.
hw = FontSize*0.327 (4.25px), hh = FontSize*0.212 (2.75px)
Derived from the actual codicon SVG: apex at 63% height,
chevron spanning 50% width and 29% height of the arrow box.
- Add ZUIDrawListAddChevronDown: single PathStroke through 3 points
  (avoids the junction gap from two separate AddLine calls)
- Switch chevron dimensions from FontSize-relative to box-relative:
  hw = box_w * 0.25, hh = box_h * 0.143 (codicon proportions, scale
  correctly regardless of font size or arrow box dimensions)
ZUICollapsingHeader:
- Drag handle strip: 2px teal bar at top of header on hover (drag indicator)
- Thin 1px teal border on focus/click (VS Code selection indicator)
- Neutral TitleBarBg background, no teal tint at rest
- Focus retained on header after click for keyboard nav
- VS Code chevrons: ∨ expanded (UserData 2) / › collapsed (UserData 3)
- Dim arrow color, thin 1px stroke, 90° opening angle (hw=2×hh)

ZUIDrawListAddChevronRight: 90°CW rotation of ChevronDown

InspectorPanel — three collapsible sections with real widgets:
- Transform: Position / Rotation / Scale (DragFloat3)
- Mesh Renderer: Cast Shadows / Receive Shadows (Checkbox)
- Rigid Body: Mass / Use Gravity / Kinematic
ZUICollapsingHeader:
- out_drag_dy replaces resize_height: widget reports DragDelta[1], caller handles cascade
- Drag strip only visible on hover (0.30 rest, 0.70 when grabbed) — hidden at rest
- Border clears on blur (FocusKey check unchanged, was already correct)

InspectorPanel:
- ApplyResize(boundary, delta_y): redistributes heights between adjacent sections
  · drag UP   → above shrinks, below grows (section covers section above)
  · drag DOWN → above grows, below shrinks
  · cascade: when a section hits kMinSectionH (30px) excess spills to its neighbour
- Mesh Renderer header drag → boundary 0 (Transform ↕ Mesh)
- Rigid Body header drag    → boundary 1 (Mesh ↕ RigidBody)
- All section content in ZUIBeginScrollRegion of their stored height
Add ZUICollapsingHeader (click-only toggle, VS Code chevron, transparent bg,
1px focus border that clears on blur) and ZUIPaneSash (4px drag handle with
greedy cascade resize matching VS Code SplitView algorithm).

InspectorPanel BuildContent rewrites to use one parent ZUIBeginScrollRegion
wrapping all 6 sections, with alternating header/fixed-height-column/sash
layout. Clear keyboard focus on click-empty-space in ZUIInteractionPass.
Header press + 8px drag threshold activates reorder. Source slot shows a
teal-bordered ghost placeholder; a 2px teal drop indicator appears at the
target boundary. On release, m_order/m_h/m_open are all rotated by the same
permutation so sash cascade resize stays correct after reorder. Ghost header
floated after ZUIEndScrollRegion to paint on top of scroll content.
Read ScrollY from the scroll region's persistent state (bg->Key) and
subtract it from rect[1] so header Y-range detection stays accurate
regardless of scroll position.
Replace 2px hairline drop indicator with VS Code interaction model:
the header of the section at the drop slot receives a teal bg fill
via ZUICollapsingHeader bg_color parameter. Source slot keeps its
teal border placeholder. Ghost header floats at cursor unchanged.
Mirrors panel BuildDividers architecture: transparent hit zone for grab
area, separate 1px floating visual box for the separator line. Uses full
Separator theme color at rest, teal at hover/drag.
Drop slot now uses actual section heights + upper/lower half split:
- Expanded target, cursor upper half: 2px teal divider at TOP of section
- Expanded target, cursor lower half: 2px teal divider at BOTTOM of section
- Collapsed target: teal header highlight

Sash only rendered after expanded sections. Source uses thin border
placeholder. Ghost header floats at cursor.
…e bug

Extract ZUIDropZoneFill, ZUIDockDividerH, ZUIDockGhostHeader into shared
helpers — ZUIPanel.cpp drop zones and ghost now use the same code as the
section stack, making visuals identical.

InspectorPanel: drop zones float as siblings in scroll region with
running_y tracking (correct Z-order). m_drop_is_bot flag ensures exactly
one zone indicator per boundary (top OR bottom, never both). kMinSectionH
= 80px enforced via ContentH(di).
…oords

Root cause: Engine::Initialize overrides GameWindow's GLFW scroll callback,
so MouseButtonWheelEvent never fires. Fix: ZUILayer::Initialize registers
after Engine and chains both InputManager (FlyCamera) + ctx->ScrollDelta.

Scrollbar FloatPos was using absolute screen coords (sx1-kBarW, thumb_y)
as if it were a ##pm_bg child. Since it is a scroll-region child, coords
must be parent-relative: (sx1-kBarW-sx0, thumb_y-sy0). Inspector scrollbar
was rendering 1000px off-screen on any right-side panel.

Also: smooth scroll (RAD model), keyboard scroll (5×FrameHeight snap),
scrollbar timer (0.5s visibility), ±1 clamp on wheel events, ZUIBeginScrollRegion
advances ScrollY toward ScrollYTarget with 2px snap.
…lags

BuildDividers now uses ZUISignalFromBox on the ZUI_Clickable hit zone,
mirroring ZUIPaneSash and ImGui/RAD ActiveId ownership model:
  - Hit zone sets ctx->ActiveKey on press (ZUI_Clickable)
  - ZUI_SignalPressed starts drag, ZUI_SignalHeld.DragDelta drives resize
  - Section headers: ActiveKey != header_key → ZUI_SignalHeld = false → safe

Removes manual in_rect cursor check and AnyDockInteractionActive flag.
Input ownership is now implicit via ctx->ActiveKey — no flags needed.
- ZUIEndScrollRegion / ZUIRadioButton: drop scroll_sig / dot_sig variables
  that were only assigned to be voided; call ZUISignalFromBox directly
- EditorPanels.h: remove unused kHdrH_fallback constant; remove stale
  keyboard-shortcut hint rows from ConsolePanel
- ZUILayer: OnMouseButtonWheelMoved is unreachable after the ZUILayer::Initialize
  GLFW callback wins the scroll slot; replace body with documented no-op stub
…itecture

HierarchyPanel: 9-level scene tree (World→Hand R depth 9) using ZUITreeNode
with VS Code chevron built-in (∨/›). Indentation via column Padding[0].

ZUITreeNode: chevron is now the default (UserData 2.f/3.f instead of 1.f/0.f).

Panel dividers split into two clean passes matching ImGui/RAD architecture:
  BuildDividerHitZones — input pass, first in ##pm_bg, LIFO-last-processed,
    always wins ctx->HotKey over panel content (tree nodes, section headers)
  BuildDividerVisuals  — render pass, last in ##pm_bg, always drawn on top
    of panels; uses in_rect for immediate hover highlight (no frame lag)

No extra fields, no flags — correct by construction.
ZUITreeView TV_BuildRow: UserData 1.f/0.f → 2.f/3.f (VS Code chevron).
ZUITreeNode was already fixed. Now all ZUI_DrawTriArrow boxes use
the chevron shape: ZUITreeNode, ZUICollapsingHeader, ZUIBeginCombo,
ZUITreeView.
Replaces the single ActivePopupKey with an 8-entry popup stack,
matching ImGui/RAD input-ownership architecture:
  - ZUIBeginPopup checks PopupStack[PopupBuildDepth] and increments depth
  - ZUIEndPopup decrements depth and restores ctx->Current per stack entry
    (SavedParent stored in ZUIPopupEntry — no more single shared field that
    nested popups overwrite each other)
  - Interaction pass allows hover inside ANY open popup; close-on-press
    pops from innermost outward
  - ZUIOpenPopup queues push at current PopupBuildDepth; applied in EndFrame

ZUIBeginSubMenu / ZUIEndSubMenu: new submenu widget with right-aligned
› chevron; opens fly-out to parent popup's right edge on hover.

Window menu in menu bar: Panels submenu → Inspector/Console/Hierarchy/Viewport.
ZUIBeginSubMenu:
  - Add ZUI_DrawBackground so hover color renders (was missing)
  - Close submenu when cursor leaves both the row AND the popup,
    using prev-frame ScreenRect hit-test (same pattern as divider hover)

ZUIBeginMenu:
  - Open on hover when any other menu popup is already open (standard
    menu bar UX: click once, slide across menus). Uses direct ScreenRect
    check to bypass can_hover popup restriction.

ZUIPanel BuildMenuBar (Window menu):
  - Replace inline 'Panels' group header with ZUIBeginSubMenu('Panels')
    now that the popup stack supports nested popups.
1. Triangle heuristic: MenuTriangleContains() suppresses submenu close
   when cursor moves diagonally toward the popup (apex=PrevMousePos,
   base=submenu left edge ±8px padding)

2+3. ZUIMenuItemEx(label, shortcut, selected, enabled): row with
   checkmark slot (✓/empty for column alignment), label, fill spacer,
   right-aligned shortcut, drag-selection (release-while-hovering)

4. Arrow-Right opens focused/hovered submenu row; Arrow-Left in
   ZUIEndFrame closes innermost submenu (PopupStackSize--)

6. Drag selection: ZUIMenuItemEx activates on mouse-release while
   cursor hovers a different item than was pressed

7. One-frame yield: hover-switch zeroes PopupStackSize before
   ZUIOpenPopup so old menu closes cleanly this frame
File: Ctrl/Cmd+N/O/S/Shift+S, Cmd+Q / Alt+F4 (platform-aware via #ifdef __APPLE__)
Edit: Ctrl/Cmd+Z/Y/A with disabled state on Undo/Redo
View: ZUIMenuItemEx selected=cmp->Visible replaces [x]/[ ] text hack
Window→Panels: ZUIMenuItemEx selected=visible replaces [x]/[ ] text hack
All shortcut strings computed from kMod/kModShift/kQuitShortcut constants
On the first frame a submenu popup exists, sub_ps->ScreenMaxX == 0
because the layout pass hasn't run yet. Both cursor_in(popup_hash) and
the triangle heuristic read 0-valued rects and incorrectly fired the
close, collapsing the submenu as soon as the cursor left the row.

Guard the entire close block with sub_ps->ScreenMaxX > 0.f so the popup
is invulnerable on its first frame. From frame 2 onward, both the rect
check and triangle heuristic use valid coordinates.
Menu bar: kMenuH 26 → 28px for better vertical breathing room.
Status bar: ZEngine + Editor labels with dim styling, fill spacer,
UIScale | FPS right-aligned. FPS color: white ≥55fps, yellow ≥30fps,
red <30fps for quick visual performance feedback.
InspectorViewUIComponent.cpp deleted in feature/zui (replaced by
ZUIInspectorViewComponent.cpp); keep deletion, accept develop's
ComponentReflectionRegistry capacity assert.
@JeanPhilippeKernel
JeanPhilippeKernel force-pushed the feature/zui branch 2 times, most recently from 44be85c to e00cd88 Compare August 28, 2026 13:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant