Enhance XCUI demo text editing and host bridge

This commit is contained in:
2026-04-05 05:58:05 +08:00
parent a7662a1d43
commit d1cb0c874b
11 changed files with 479 additions and 24 deletions

View File

@@ -64,6 +64,9 @@ constexpr char kViewRelativePath[] = "new_editor/resources/xcui_demo_view.xcui";
constexpr char kThemeRelativePath[] = "new_editor/resources/xcui_demo_theme.xctheme";
constexpr char kToggleAccentCommandId[] = "demo.toggleAccent";
constexpr float kApproximateTextWidthFactor = 0.58f;
constexpr float kTextInputTextInset = 2.0f;
constexpr float kTextAreaLineNumberGap = 10.0f;
constexpr std::size_t kTextAreaTabWidth = 4u;
struct DemoNode {
UIElementId elementId = 0;
@@ -480,6 +483,19 @@ std::vector<std::string> SplitLines(const std::string& text) {
return lines;
}
std::size_t CountTextLines(const std::string& text) {
return SplitLines(text).size();
}
std::size_t CountDecimalDigits(std::size_t value) {
std::size_t digits = 1u;
while (value >= 10u) {
value /= 10u;
++digits;
}
return digits;
}
std::size_t CountUtf8CodepointsInRange(
const std::string& text,
std::size_t beginOffset,
@@ -564,6 +580,50 @@ std::size_t MoveCaretVertically(
(std::min)(column, nextLineLength));
}
bool ShouldShowTextAreaLineNumbers(const DemoNode& node) {
bool showLineNumbers = false;
return IsTextAreaNode(node) &&
TryParseBool(GetNodeAttribute(node, "show-line-numbers"), showLineNumbers) &&
showLineNumbers;
}
float ResolveTextAreaGutterWidth(
const DemoNode& node,
std::size_t visibleLineCount,
float fontSize) {
if (!ShouldShowTextAreaLineNumbers(node)) {
return 0.0f;
}
const std::size_t digits = CountDecimalDigits((std::max)(std::size_t(1u), visibleLineCount));
const float numberWidth = MeasureGlyphRunWidth(std::string(digits, '8'), fontSize * 0.9f);
return numberWidth + kTextAreaLineNumberGap;
}
std::size_t FindCaretOffsetForHorizontalPosition(
const std::string& text,
std::size_t lineStart,
std::size_t lineEnd,
float targetX,
float fontSize) {
lineStart = (std::min)(lineStart, text.size());
lineEnd = (std::min)(lineEnd, text.size());
targetX = (std::max)(0.0f, targetX);
std::size_t caret = lineStart;
while (caret < lineEnd) {
const std::size_t nextCaret = AdvanceUtf8Offset(text, caret);
const float currentWidth = MeasureGlyphRunWidth(text.substr(lineStart, caret - lineStart), fontSize);
const float nextWidth = MeasureGlyphRunWidth(text.substr(lineStart, nextCaret - lineStart), fontSize);
if (targetX < (currentWidth + nextWidth) * 0.5f) {
return caret;
}
caret = nextCaret;
}
return lineEnd;
}
Style::UIStyleValue ParseThemeTokenValue(
const std::string& typeName,
const std::string& rawValue,
@@ -756,6 +816,9 @@ void ConfigureDemoStyleSheet(Style::UIStyleSheet& styleSheet) {
styleSheet.GetOrCreateNamedStyle("CommandField").SetProperty(
Style::UIStylePropertyId::BorderColor,
Style::UIStyleValue::Token("color.accent.alt"));
styleSheet.GetOrCreateNamedStyle("CommandArea").SetProperty(
Style::UIStylePropertyId::BorderColor,
Style::UIStyleValue::Token("color.outline"));
styleSheet.GetOrCreateNamedStyle("Meta").SetProperty(
Style::UIStylePropertyId::ForegroundColor,
Style::UIStyleValue::Token("color.text.secondary"));
@@ -887,6 +950,22 @@ std::string BuildNodeDisplayText(
" / debug " +
std::string(diagnosticsEnabled ? "on" : "off");
}
if (node.elementKey == "promptMeta") {
const auto promptIt = state.textFieldValues.find("agentPrompt");
const std::string& promptValue =
promptIt != state.textFieldValues.end() ? promptIt->second : node.staticText;
return "Single-line input, Enter submits, " +
std::to_string(static_cast<unsigned long long>(CountUtf8Codepoints(promptValue))) +
" chars";
}
if (node.elementKey == "notesMeta") {
const auto notesIt = state.textFieldValues.find("sessionNotes");
const std::string& notesValue =
notesIt != state.textFieldValues.end() ? notesIt->second : node.staticText;
return "Multiline input, click caret, Tab indent, " +
std::to_string(static_cast<unsigned long long>(CountTextLines(notesValue))) +
" lines";
}
if (node.elementKey == "subtitle" && stats.accentEnabled) {
return "Markup -> Layout -> Style -> DrawData (accent alt active)";
}
@@ -1076,6 +1155,62 @@ DemoNode* TryGetNodeByElementId(RuntimeBuildContext& state, UIElementId elementI
return it != state.nodeIndexById.end() ? &state.nodes[it->second] : nullptr;
}
std::size_t FindCaretOffsetFromPoint(
RuntimeBuildContext& state,
const DemoNode& node,
const UIPoint& point) {
EnsureTextInputStateInitialized(state, node);
const std::string value = ResolveTextInputValue(state, node);
const Layout::UILayoutThickness padding =
ResolvePadding(node, state.activeTheme, state.styleSheet);
const UIRect contentRect = InsetRect(node.rect, padding);
const float fontSize = GetFloatProperty(
node,
state.activeTheme,
state.styleSheet,
Style::UIStylePropertyId::FontSize,
14.0f);
if (!IsTextAreaNode(node)) {
const float targetX = point.x - (contentRect.x + kTextInputTextInset);
return FindCaretOffsetForHorizontalPosition(value, 0u, value.size(), targetX, fontSize);
}
const float lineHeight = MeasureTextHeight(fontSize);
const std::size_t lineCount = (std::max)(std::size_t(1u), CountTextLines(value));
const float gutterWidth = ResolveTextAreaGutterWidth(node, lineCount, fontSize);
const float localY = point.y - (contentRect.y + kTextInputTextInset);
const float clampedY = (std::max)(0.0f, localY);
const std::size_t lineIndex = (std::min)(
static_cast<std::size_t>(clampedY / (std::max)(1.0f, lineHeight)),
lineCount - 1u);
std::size_t lineStart = 0u;
for (std::size_t currentLine = 0u; currentLine < lineIndex && lineStart < value.size(); ++currentLine) {
lineStart = FindLineEndOffset(value, lineStart);
if (lineStart < value.size()) {
++lineStart;
}
}
const std::size_t lineEnd = FindLineEndOffset(value, lineStart);
const float targetX = point.x - (contentRect.x + kTextInputTextInset + gutterWidth);
return FindCaretOffsetForHorizontalPosition(value, lineStart, lineEnd, targetX, fontSize);
}
void SetTextInputCaretFromPoint(
RuntimeBuildContext& state,
UIElementId elementId,
const UIPoint& point) {
DemoNode* node = TryGetNodeByElementId(state, elementId);
if (node == nullptr || !IsTextInputNode(*node)) {
return;
}
const std::string stateKey = ResolveTextInputStateKey(*node);
state.textFieldCarets[stateKey] = FindCaretOffsetFromPoint(state, *node, point);
}
bool HandleTextInputCharacterInput(
RuntimeBuildContext& state,
UIElementId elementId,
@@ -1110,7 +1245,8 @@ bool HandleTextInputCharacterInput(
bool HandleTextInputKeyDown(
RuntimeBuildContext& state,
UIElementId elementId,
std::int32_t keyCode) {
std::int32_t keyCode,
const UI::UIInputModifiers& inputModifiers) {
DemoNode* node = TryGetNodeByElementId(state, elementId);
if (node == nullptr || !IsTextInputNode(*node)) {
return false;
@@ -1122,8 +1258,7 @@ bool HandleTextInputKeyDown(
std::size_t& caret = state.textFieldCarets[stateKey];
caret = (std::min)(caret, value.size());
if (keyCode == static_cast<std::int32_t>(KeyCode::Backspace) ||
keyCode == static_cast<std::int32_t>(KeyCode::Delete)) {
if (keyCode == static_cast<std::int32_t>(KeyCode::Backspace)) {
if (caret > 0u) {
const std::size_t previousCaret = RetreatUtf8Offset(value, caret);
value.erase(previousCaret, caret - previousCaret);
@@ -1133,6 +1268,15 @@ bool HandleTextInputKeyDown(
return true;
}
if (keyCode == static_cast<std::int32_t>(KeyCode::Delete)) {
if (caret < value.size()) {
const std::size_t nextCaret = AdvanceUtf8Offset(value, caret);
value.erase(caret, nextCaret - caret);
state.lastCommandId = "demo.text.edit." + stateKey;
}
return true;
}
if (keyCode == static_cast<std::int32_t>(KeyCode::Left)) {
caret = RetreatUtf8Offset(value, caret);
return true;
@@ -1179,6 +1323,33 @@ bool HandleTextInputKeyDown(
return true;
}
if (keyCode == static_cast<std::int32_t>(KeyCode::Tab) &&
IsTextAreaNode(*node) &&
!inputModifiers.control &&
!inputModifiers.alt &&
!inputModifiers.super) {
if (inputModifiers.shift) {
const std::size_t lineStart = FindLineStartOffset(value, caret);
std::size_t removed = 0u;
while (removed < kTextAreaTabWidth &&
lineStart + removed < value.size() &&
value[lineStart + removed] == ' ') {
++removed;
}
if (removed > 0u) {
value.erase(lineStart, removed);
caret = caret >= lineStart + removed ? caret - removed : lineStart;
state.lastCommandId = "demo.text.edit." + stateKey;
}
} else {
value.insert(caret, std::string(kTextAreaTabWidth, ' '));
caret += kTextAreaTabWidth;
state.lastCommandId = "demo.text.edit." + stateKey;
}
return true;
}
return false;
}
@@ -1385,11 +1556,13 @@ UISize MeasureNode(RuntimeBuildContext& state, std::size_t index) {
lineCount,
static_cast<std::size_t>((std::max)(1.0f, requestedRows)));
}
const float gutterWidth = ResolveTextAreaGutterWidth(node, lineCount, fontSize);
node.desiredSize = UISize(
(std::max)(
minWidth,
widestLine +
gutterWidth +
padding.Horizontal() +
18.0f),
lineHeight * static_cast<float>(lineCount) +
@@ -1665,7 +1838,7 @@ void DrawTextFieldNode(RuntimeBuildContext& state, const DemoNode& node, UIDrawL
const float textY = contentRect.y + (contentRect.height - fontSize) * 0.5f - 2.0f;
drawList.AddText(
UIPoint(contentRect.x + 2.0f, textY),
UIPoint(contentRect.x + kTextInputTextInset, textY),
displayText,
ToUIColor(textColor),
fontSize);
@@ -1680,7 +1853,7 @@ void DrawTextFieldNode(RuntimeBuildContext& state, const DemoNode& node, UIDrawL
const std::size_t caret = ResolveTextInputCaret(state, node);
const float caretX =
contentRect.x +
2.0f +
kTextInputTextInset +
MeasureGlyphRunWidth(value.substr(0u, caret), fontSize);
const Color caretColor = ResolveColorToken(
state.activeTheme,
@@ -1709,6 +1882,7 @@ void DrawTextAreaNode(RuntimeBuildContext& state, const DemoNode& node, UIDrawLi
const std::string placeholder = GetNodeAttribute(node, "placeholder");
const bool showingPlaceholder = value.empty() && !placeholder.empty();
const std::vector<std::string> lines = SplitLines(showingPlaceholder ? placeholder : value);
const float gutterWidth = ResolveTextAreaGutterWidth(node, (std::max)(std::size_t(1u), lines.size()), fontSize);
const Color textColor = showingPlaceholder
? ResolveColorToken(state.activeTheme, "color.text.placeholder", Color(0.49f, 0.56f, 0.64f, 1.0f))
: GetColorProperty(
@@ -1717,12 +1891,33 @@ void DrawTextAreaNode(RuntimeBuildContext& state, const DemoNode& node, UIDrawLi
state.styleSheet,
Style::UIStylePropertyId::ForegroundColor,
Color(0.92f, 0.94f, 0.97f, 1.0f));
const Color lineNumberColor = ResolveColorToken(
state.activeTheme,
"color.text.secondary",
Color(0.74f, 0.78f, 0.86f, 1.0f));
if (gutterWidth > 0.0f) {
const float separatorX = contentRect.x + gutterWidth - kTextAreaLineNumberGap * 0.4f;
drawList.AddFilledRect(
UIRect(separatorX, contentRect.y, 1.0f, contentRect.height),
ToUIColor(ResolveColorToken(state.activeTheme, "color.outline", Color(0.23f, 0.29f, 0.35f, 1.0f))),
0.0f);
}
for (std::size_t lineIndex = 0u; lineIndex < lines.size(); ++lineIndex) {
if (gutterWidth > 0.0f) {
drawList.AddText(
UIPoint(
contentRect.x,
contentRect.y + kTextInputTextInset + static_cast<float>(lineIndex) * lineHeight),
std::to_string(static_cast<unsigned long long>(lineIndex + 1u)),
ToUIColor(lineNumberColor),
fontSize * 0.9f);
}
drawList.AddText(
UIPoint(
contentRect.x + 2.0f,
contentRect.y + 2.0f + static_cast<float>(lineIndex) * lineHeight),
contentRect.x + kTextInputTextInset + gutterWidth,
contentRect.y + kTextInputTextInset + static_cast<float>(lineIndex) * lineHeight),
lines[lineIndex].empty() ? std::string(" ") : lines[lineIndex],
ToUIColor(textColor),
fontSize);
@@ -1746,7 +1941,8 @@ void DrawTextAreaNode(RuntimeBuildContext& state, const DemoNode& node, UIDrawLi
const float caretX =
contentRect.x +
2.0f +
kTextInputTextInset +
gutterWidth +
MeasureGlyphRunWidth(value.substr(lineStart, caret - lineStart), fontSize);
const float caretY = contentRect.y + 3.0f + static_cast<float>(lineIndex) * lineHeight;
const Color caretColor = ResolveColorToken(
@@ -2088,6 +2284,7 @@ const XCUIDemoFrameResult& XCUIDemoRuntime::Update(const XCUIDemoInputState& inp
event.pointerButton == UI::UIPointerButton::Left) {
if (state.armedElementId != 0u &&
hoveredPath.Target() == state.armedElementId) {
SetTextInputCaretFromPoint(state, hoveredPath.Target(), event.position);
ActivateNode(state, hoveredPath.Target());
}
state.armedElementId = 0u;
@@ -2120,7 +2317,12 @@ const XCUIDemoFrameResult& XCUIDemoRuntime::Update(const XCUIDemoInputState& inp
}
if (event.type == UIInputEventType::KeyDown) {
if (HandleTextInputKeyDown(state, focusedElementId, event.keyCode)) {
const UI::UIInputModifiers inputModifiers = event.modifiers;
if (HandleTextInputKeyDown(
state,
focusedElementId,
event.keyCode,
inputModifiers)) {
return;
}
}