Reuse panel frame composition in native XCUI shell

This commit is contained in:
2026-04-05 17:41:31 +08:00
parent 63b5f12b93
commit 3db09ea5d0
10 changed files with 651 additions and 954 deletions

View File

@@ -1,6 +1,9 @@
#include "Application.h"
#include "XCUIBackend/XCUINativeShellLayout.h"
#include "XCUIBackend/NativeWindowUICompositor.h"
#include "XCUIBackend/XCUIPanelCanvasHost.h"
#include "panels/XCUIDemoPanel.h"
#include "panels/XCUILayoutLabPanel.h"
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/UI/DrawData.h>
@@ -41,54 +44,64 @@ std::uint64_t MakeFrameTimestampNanoseconds() {
.count());
}
class ForwardingNativePanelCanvasHost final : public ::XCEngine::Editor::XCUIBackend::IXCUIPanelCanvasHost {
public:
explicit ForwardingNativePanelCanvasHost(::XCEngine::Editor::XCUIBackend::NativeXCUIPanelCanvasHost& host)
: m_host(host) {}
const char* GetDebugName() const override {
return m_host.GetDebugName();
}
auto BeginCanvas(
const ::XCEngine::Editor::XCUIBackend::XCUIPanelCanvasRequest& request)
-> decltype(std::declval<::XCEngine::Editor::XCUIBackend::NativeXCUIPanelCanvasHost&>().BeginCanvas(request))
override {
return m_host.BeginCanvas(request);
}
void DrawFilledRect(
const ::XCEngine::UI::UIRect& rect,
const ::XCEngine::UI::UIColor& color,
float rounding) override {
m_host.DrawFilledRect(rect, color, rounding);
}
void DrawOutlineRect(
const ::XCEngine::UI::UIRect& rect,
const ::XCEngine::UI::UIColor& color,
float thickness,
float rounding) override {
m_host.DrawOutlineRect(rect, color, thickness, rounding);
}
void DrawText(
const ::XCEngine::UI::UIPoint& position,
std::string_view text,
const ::XCEngine::UI::UIColor& color,
float fontSize) override {
m_host.DrawText(position, text, color, fontSize);
}
void EndCanvas() override {
m_host.EndCanvas();
}
bool TryGetLatestFrameSnapshot(
::XCEngine::Editor::XCUIBackend::XCUIPanelCanvasFrameSnapshot& outSnapshot) const override {
return m_host.TryGetLatestFrameSnapshot(outSnapshot);
}
private:
::XCEngine::Editor::XCUIBackend::NativeXCUIPanelCanvasHost& m_host;
};
void AppendDrawData(::XCEngine::UI::UIDrawData& destination, const ::XCEngine::UI::UIDrawData& source) {
for (const ::XCEngine::UI::UIDrawList& drawList : source.GetDrawLists()) {
destination.AddDrawList(drawList);
}
}
bool ContainsKeyTransition(
const std::vector<std::int32_t>& keys,
std::int32_t keyCode) {
return std::find(keys.begin(), keys.end(), keyCode) != keys.end();
}
bool ShouldCaptureKeyboardNavigation(
const ::XCEngine::Editor::XCUIBackend::XCUIPanelCanvasSession& canvasSession,
const ::XCEngine::Editor::XCUIBackend::XCUILayoutLabFrameResult& previousFrame) {
if (!canvasSession.validCanvas) {
return false;
}
return canvasSession.hovered ||
(canvasSession.windowFocused &&
!previousFrame.stats.selectedElementId.empty());
}
void PopulateKeyboardNavigationInput(
::XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState& input,
const ::XCEngine::Editor::XCUIBackend::XCUIInputBridgeFrameDelta& frameDelta,
bool captureKeyboardNavigation) {
if (!captureKeyboardNavigation) {
return;
}
using ::XCEngine::Input::KeyCode;
const auto pressedThisFrame =
[&frameDelta](KeyCode keyCode) {
const std::int32_t code = static_cast<std::int32_t>(keyCode);
return ContainsKeyTransition(frameDelta.keyboard.pressedKeys, code) ||
ContainsKeyTransition(frameDelta.keyboard.repeatedKeys, code);
};
input.navigatePrevious = pressedThisFrame(KeyCode::Up);
input.navigateNext = pressedThisFrame(KeyCode::Down);
input.navigateHome = pressedThisFrame(KeyCode::Home);
input.navigateEnd = pressedThisFrame(KeyCode::End);
input.navigateCollapse = pressedThisFrame(KeyCode::Left);
input.navigateExpand = pressedThisFrame(KeyCode::Right);
}
} // namespace
bool Application::IsNativeWindowHostEnabled() const {
@@ -99,12 +112,35 @@ void Application::InitializeNativeShell() {
m_nativeActivePanel = m_shellChromeState.IsPanelVisible(ShellPanelId::XCUIDemo)
? ShellPanelId::XCUIDemo
: ShellPanelId::XCUILayoutLab;
m_nativeDemoInputBridge.Reset();
m_nativeLayoutInputBridge.Reset();
m_nativeDemoCanvasHost.ClearCanvasSession();
m_nativeLayoutCanvasHost.ClearCanvasSession();
m_nativeDemoReloadSucceeded = m_nativeDemoRuntime.ReloadDocuments();
m_nativeLayoutReloadSucceeded = m_nativeLayoutRuntime.ReloadDocuments();
const auto buildNativePreviewPresenter =
[this](ShellPanelId panelId, bool hostedPreviewEnabled) {
if (!hostedPreviewEnabled || !IsNativeHostedPreviewEnabled(panelId)) {
return std::unique_ptr<::XCEngine::Editor::XCUIBackend::IXCUIHostedPreviewPresenter>();
}
return CreateHostedPreviewPresenter(true);
};
const ShellPanelChromeState* demoState = TryGetShellPanelState(ShellPanelId::XCUIDemo);
const bool demoHostedPreviewEnabled = demoState == nullptr || demoState->hostedPreviewEnabled;
m_demoPanel = std::make_unique<XCUIDemoPanel>(
&m_xcuiInputSource,
buildNativePreviewPresenter(ShellPanelId::XCUIDemo, demoHostedPreviewEnabled),
std::make_unique<ForwardingNativePanelCanvasHost>(m_nativeDemoCanvasHost));
m_demoPanel->SetVisible(demoState != nullptr && demoState->visible);
m_demoPanel->SetHostedPreviewEnabled(demoHostedPreviewEnabled);
const ShellPanelChromeState* layoutState = TryGetShellPanelState(ShellPanelId::XCUILayoutLab);
const bool layoutHostedPreviewEnabled = layoutState == nullptr || layoutState->hostedPreviewEnabled;
m_layoutLabPanel = std::make_unique<XCUILayoutLabPanel>(
&m_xcuiInputSource,
buildNativePreviewPresenter(ShellPanelId::XCUILayoutLab, layoutHostedPreviewEnabled),
std::make_unique<ForwardingNativePanelCanvasHost>(m_nativeLayoutCanvasHost));
m_layoutLabPanel->SetVisible(layoutState != nullptr && layoutState->visible);
m_layoutLabPanel->SetHostedPreviewEnabled(layoutHostedPreviewEnabled);
}
const Application::ShellPanelChromeState* Application::TryGetShellPanelState(ShellPanelId panelId) const {
@@ -683,33 +719,10 @@ bool Application::RenderHostedPreviewOffscreenSurface(
for (const NativeShellPanelLayout& panelLayout : panelLayouts) {
if (panelLayout.panelId == ShellPanelId::XCUIDemo) {
const ShellPanelChromeState* panelState = TryGetShellPanelState(ShellPanelId::XCUIDemo);
const bool nativeHostedPreview =
const bool nativeHostedPreviewRequested =
panelState != nullptr &&
panelState->hostedPreviewEnabled &&
IsNativeHostedPreviewEnabled(ShellPanelId::XCUIDemo);
::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewSurfaceDescriptor hostedSurfaceDescriptor = {};
const bool hasHostedSurfaceDescriptor =
nativeHostedPreview &&
panelState != nullptr &&
!panelState->previewDebugName.empty() &&
m_hostedPreviewSurfaceRegistry.TryGetSurfaceDescriptor(
panelState->previewDebugName.data(),
hostedSurfaceDescriptor);
::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewSurfaceImage hostedSurfaceImage = {};
const bool showHostedSurfaceImage =
nativeHostedPreview &&
panelState != nullptr &&
!panelState->previewDebugName.empty() &&
m_hostedPreviewSurfaceRegistry.TryGetSurfaceImage(
panelState->previewDebugName.data(),
hostedSurfaceImage);
const Application::NativeHostedPreviewConsumption previewConsumption =
Application::ResolveNativeHostedPreviewConsumption(
nativeHostedPreview,
hasHostedSurfaceDescriptor,
showHostedSurfaceImage,
"Native XCUI preview pending",
"Waiting for queued native preview output to publish into the shell card.");
::XCEngine::Editor::XCUIBackend::XCUIPanelCanvasSession canvasSession = {};
canvasSession.hostRect = panelLayout.panelRect;
@@ -720,63 +733,54 @@ bool Application::RenderHostedPreviewOffscreenSurface(
canvasSession.windowFocused = shellSnapshot.windowFocused;
m_nativeDemoCanvasHost.SetCanvasSession(canvasSession);
::XCEngine::Editor::XCUIBackend::XCUIPanelCanvasRequest canvasRequest = {};
canvasRequest.childId = "XCUIDemo.NativeCanvas";
canvasRequest.height = panelLayout.panelRect.height;
canvasRequest.topInset = panelLayout.canvasRect.y - panelLayout.panelRect.y;
canvasRequest.drawPreviewFrame = false;
canvasRequest.showSurfaceImage = previewConsumption.showSurfaceImage;
canvasRequest.surfaceImage = hostedSurfaceImage;
canvasRequest.placeholderTitle =
previewConsumption.placeholderTitle.empty() ? nullptr : previewConsumption.placeholderTitle.data();
canvasRequest.placeholderSubtitle =
previewConsumption.placeholderSubtitle.empty() ? nullptr : previewConsumption.placeholderSubtitle.data();
const ::XCEngine::Editor::XCUIBackend::XCUIPanelCanvasSession resolvedSession =
m_nativeDemoCanvasHost.BeginCanvas(canvasRequest);
const bool wantsKeyboard = panelLayout.active;
const auto panelSnapshot = capturePanelSnapshot(panelLayout, wantsKeyboard);
if (!m_nativeDemoInputBridge.HasBaseline()) {
m_nativeDemoInputBridge.Prime(panelSnapshot);
}
const auto panelFrameDelta = m_nativeDemoInputBridge.Translate(panelSnapshot);
const bool showPanelHud = IsShellViewToggleEnabled(ShellViewToggleId::HostedPreviewHud);
::XCEngine::Editor::XCUIBackend::XCUIDemoInputState input = {};
input.canvasRect = resolvedSession.canvasRect;
input.pointerPosition = panelSnapshot.pointerPosition;
input.pointerInside = panelSnapshot.pointerInside;
input.pointerPressed = panelFrameDelta.pointer.pressed[0];
input.pointerReleased = panelFrameDelta.pointer.released[0];
input.pointerDown = panelSnapshot.pointerButtonsDown[0];
input.windowFocused = panelSnapshot.windowFocused;
input.wantCaptureMouse = panelSnapshot.wantCaptureMouse;
input.wantCaptureKeyboard = panelSnapshot.wantCaptureKeyboard;
input.wantTextInput = panelSnapshot.wantTextInput;
input.events = panelFrameDelta.events;
const auto& frame = m_nativeDemoRuntime.Update(input);
if (previewConsumption.queueRuntimeFrame) {
::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewFrame previewFrame = {};
previewFrame.drawData = &frame.drawData;
previewFrame.canvasRect = resolvedSession.canvasRect;
previewFrame.logicalSize = UI::UISize(
resolvedSession.canvasRect.width,
resolvedSession.canvasRect.height);
previewFrame.debugName =
panelState != nullptr ? panelState->previewDebugName.data() : nullptr;
previewFrame.debugSource =
panelState != nullptr ? panelState->previewDebugSource.data() : nullptr;
m_hostedPreviewQueue.Submit(previewFrame);
} else if (previewConsumption.appendRuntimeDrawDataToShell) {
AppendDrawData(composedDrawData, frame.drawData);
const XCUIDemoPanelFrameComposition* composition = nullptr;
if (m_demoPanel != nullptr) {
composition = &m_demoPanel->ComposeFrame(
XCUIDemoPanelFrameComposeOptions{
panelLayout.panelRect.height,
panelLayout.canvasRect.y - panelLayout.panelRect.y,
panelState == nullptr || panelState->hostedPreviewEnabled,
showPanelHud,
showPanelHud,
panelSnapshot.timestampNanoseconds,
"XCUIDemo.NativeCanvas",
&panelSnapshot,
false,
nativeHostedPreviewRequested ? "Native XCUI preview pending" : nullptr,
nativeHostedPreviewRequested
? "Waiting for queued native preview output to publish into the shell card."
: nullptr,
false,
nullptr,
nullptr,
});
}
if (IsShellViewToggleEnabled(ShellViewToggleId::HostedPreviewHud)) {
const auto& stats = frame.stats;
const Application::NativeHostedPreviewConsumption previewConsumption =
composition != nullptr
? Application::ResolveNativeHostedPreviewConsumption(
composition->nativeHostedPreview,
composition->hasHostedSurfaceDescriptor,
composition->showHostedSurfaceImage,
"Native XCUI preview pending",
"Waiting for queued native preview output to publish into the shell card.")
: Application::NativeHostedPreviewConsumption{};
if (composition != nullptr &&
composition->frame != nullptr &&
previewConsumption.appendRuntimeDrawDataToShell) {
AppendDrawData(composedDrawData, composition->frame->drawData);
}
if (showPanelHud && composition != nullptr && composition->frame != nullptr) {
const auto& stats = composition->frame->stats;
const UI::UIRect hudRect(
resolvedSession.canvasRect.x + 10.0f,
resolvedSession.canvasRect.y + 10.0f,
(std::min)(resolvedSession.canvasRect.width - 20.0f, 360.0f),
composition->canvasSession.canvasRect.x + 10.0f,
composition->canvasSession.canvasRect.y + 10.0f,
(std::min)(composition->canvasSession.canvasRect.width - 20.0f, 360.0f),
62.0f);
if (hudRect.width > 40.0f && hudRect.height > 20.0f) {
m_nativeDemoCanvasHost.DrawFilledRect(
@@ -799,49 +803,28 @@ bool Application::RenderHostedPreviewOffscreenSurface(
stats.statusMessage,
textMuted);
}
if (previewConsumption.drawRuntimeDebugRects) {
const auto drawDebugRect =
[this](const std::string& elementId, const UI::UIColor& color, const char* label) {
if (elementId.empty()) {
return;
}
UI::UIRect rect = {};
if (!m_nativeDemoRuntime.TryGetElementRect(elementId, rect)) {
return;
}
m_nativeDemoCanvasHost.DrawOutlineRect(rect, color, 2.0f, 6.0f);
if (label != nullptr && label[0] != '\0') {
m_nativeDemoCanvasHost.DrawText(
UI::UIPoint(rect.x + 4.0f, rect.y + 4.0f),
label,
color);
}
};
drawDebugRect(
stats.hoveredElementId,
UI::UIColor(1.0f, 195.0f / 255.0f, 64.0f / 255.0f, 1.0f),
"hover");
drawDebugRect(
stats.focusedElementId,
UI::UIColor(64.0f / 255.0f, 214.0f / 255.0f, 1.0f, 1.0f),
"focus");
}
}
m_nativeDemoCanvasHost.EndCanvas();
NativePanelFrameSummary summary = {};
summary.layout = panelLayout;
summary.lineA = m_nativeDemoReloadSucceeded
summary.lineA =
m_demoPanel != nullptr && m_demoPanel->GetLastReloadSucceeded()
? Application::ComposeNativeHostedPreviewStatusLine(
previewConsumption,
frame.stats.statusMessage)
composition != nullptr && composition->frame != nullptr
? composition->frame->stats.statusMessage
: std::string_view("XCUI demo frame unavailable"))
: "Document reload failed; showing last retained runtime state.";
summary.lineB =
std::string(panelLayout.active ? "Active" : "Passive") +
" | " + std::to_string(frame.stats.elementCount) +
" elements | " + std::to_string(frame.stats.commandCount) +
" | " + std::to_string(
composition != nullptr && composition->frame != nullptr
? composition->frame->stats.elementCount
: 0u) +
" elements | " + std::to_string(
composition != nullptr && composition->frame != nullptr
? composition->frame->stats.commandCount
: 0u) +
" cmds";
summary.overlay = extractCanvasOverlay(m_nativeDemoCanvasHost);
panelSummaries.push_back(std::move(summary));
@@ -850,21 +833,22 @@ bool Application::RenderHostedPreviewOffscreenSurface(
}
const ShellPanelChromeState* panelState = TryGetShellPanelState(ShellPanelId::XCUILayoutLab);
const bool nativeHostedPreview =
const bool nativeHostedPreviewRequested =
panelState != nullptr &&
panelState->hostedPreviewEnabled &&
IsNativeHostedPreviewEnabled(ShellPanelId::XCUILayoutLab);
::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewSurfaceDescriptor hostedSurfaceDescriptor = {};
const bool hasHostedSurfaceDescriptor =
nativeHostedPreview &&
nativeHostedPreviewRequested &&
panelState != nullptr &&
!panelState->previewDebugName.empty() &&
m_hostedPreviewSurfaceRegistry.TryGetSurfaceDescriptor(
panelState->previewDebugName.data(),
hostedSurfaceDescriptor);
::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewSurfaceImage hostedSurfaceImage = {};
const bool showHostedSurfaceImage =
nativeHostedPreview &&
nativeHostedPreviewRequested &&
panelState != nullptr &&
!panelState->previewDebugName.empty() &&
m_hostedPreviewSurfaceRegistry.TryGetSurfaceImage(
@@ -872,7 +856,7 @@ bool Application::RenderHostedPreviewOffscreenSurface(
hostedSurfaceImage);
const Application::NativeHostedPreviewConsumption previewConsumption =
Application::ResolveNativeHostedPreviewConsumption(
nativeHostedPreview,
nativeHostedPreviewRequested,
hasHostedSurfaceDescriptor,
showHostedSurfaceImage,
"Native layout preview pending",
@@ -902,50 +886,53 @@ bool Application::RenderHostedPreviewOffscreenSurface(
const bool wantsKeyboard = panelLayout.active;
const auto panelSnapshot = capturePanelSnapshot(panelLayout, wantsKeyboard);
if (!m_nativeLayoutInputBridge.HasBaseline()) {
m_nativeLayoutInputBridge.Prime(panelSnapshot);
}
const auto panelFrameDelta = m_nativeLayoutInputBridge.Translate(panelSnapshot);
::XCEngine::Editor::XCUIBackend::XCUILayoutLabInputState input = {};
input.canvasRect = resolvedSession.canvasRect;
input.pointerPosition = panelSnapshot.pointerPosition;
input.pointerInside = panelSnapshot.pointerInside;
input.pointerPressed = input.pointerInside && panelFrameDelta.pointer.pressed[0];
PopulateKeyboardNavigationInput(
input,
panelFrameDelta,
panelLayout.active && ShouldCaptureKeyboardNavigation(resolvedSession, m_nativeLayoutRuntime.GetFrameResult()));
const auto& frame = m_nativeLayoutRuntime.Update(input);
if (previewConsumption.queueRuntimeFrame) {
::XCEngine::Editor::XCUIBackend::XCUIHostedPreviewFrame previewFrame = {};
previewFrame.drawData = &frame.drawData;
previewFrame.canvasRect = resolvedSession.canvasRect;
previewFrame.logicalSize = UI::UISize(
resolvedSession.canvasRect.width,
resolvedSession.canvasRect.height);
previewFrame.debugName =
panelState != nullptr ? panelState->previewDebugName.data() : nullptr;
previewFrame.debugSource =
panelState != nullptr ? panelState->previewDebugSource.data() : nullptr;
m_hostedPreviewQueue.Submit(previewFrame);
} else if (previewConsumption.appendRuntimeDrawDataToShell) {
AppendDrawData(composedDrawData, frame.drawData);
const XCUILayoutLabFrameComposition* composition = nullptr;
if (m_layoutLabPanel != nullptr) {
composition = &m_layoutLabPanel->ComposeFrame(
XCUILayoutLabFrameCompositionRequest{
resolvedSession,
panelSnapshot });
}
m_nativeLayoutCanvasHost.EndCanvas();
const Application::NativeHostedPreviewConsumption composedPreviewConsumption =
composition != nullptr
? Application::ResolveNativeHostedPreviewConsumption(
composition->nativeHostedPreview,
composition->hasHostedSurfaceDescriptor,
composition->showHostedSurfaceImage,
"Native layout preview pending",
"Waiting for queued native preview output to publish into the layout card.")
: previewConsumption;
if (composition != nullptr &&
composition->frameResult != nullptr &&
composedPreviewConsumption.appendRuntimeDrawDataToShell) {
AppendDrawData(composedDrawData, composition->frameResult->drawData);
}
NativePanelFrameSummary summary = {};
summary.layout = panelLayout;
summary.lineA = m_nativeLayoutReloadSucceeded
summary.lineA =
m_layoutLabPanel != nullptr && m_layoutLabPanel->GetLastReloadSucceeded()
? Application::ComposeNativeHostedPreviewStatusLine(
previewConsumption,
frame.stats.statusMessage)
composedPreviewConsumption,
composition != nullptr && composition->frameResult != nullptr
? composition->frameResult->stats.statusMessage
: std::string_view("XCUI layout frame unavailable"))
: "Layout lab reload failed; showing last retained runtime state.";
summary.lineB =
std::to_string(frame.stats.rowCount) + " rows | " +
std::to_string(frame.stats.columnCount) + " cols | " +
std::to_string(frame.stats.commandCount) + " cmds";
std::to_string(
composition != nullptr && composition->frameResult != nullptr
? composition->frameResult->stats.rowCount
: 0u) + " rows | " +
std::to_string(
composition != nullptr && composition->frameResult != nullptr
? composition->frameResult->stats.columnCount
: 0u) + " cols | " +
std::to_string(
composition != nullptr && composition->frameResult != nullptr
? composition->frameResult->stats.commandCount
: 0u) + " cmds";
summary.overlay = extractCanvasOverlay(m_nativeLayoutCanvasHost);
panelSummaries.push_back(std::move(summary));
m_nativeLayoutCanvasHost.ClearCanvasSession();