Enhance XCUI demo text editing and host bridge
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user