mirror of
https://github.com/vczh-libraries/Release.git
synced 2026-03-23 15:52:56 +08:00
1998 lines
69 KiB
C++
1998 lines
69 KiB
C++
/***********************************************************************
|
|
THIS FILE IS AUTOMATICALLY GENERATED. DO NOT MODIFY
|
|
DEVELOPER: Zihan Chen(vczh)
|
|
***********************************************************************/
|
|
#include "GacUI.UnitTest.h"
|
|
|
|
/***********************************************************************
|
|
.\GUIUNITTESTPROTOCOL_RENDERING _DOCUMENT.CPP
|
|
***********************************************************************/
|
|
|
|
namespace vl::presentation::unittest
|
|
{
|
|
using namespace collections;
|
|
using namespace remoteprotocol;
|
|
|
|
/***********************************************************************
|
|
Helper Functions for Document Paragraph
|
|
***********************************************************************/
|
|
|
|
vint GetFontSizeForPosition(
|
|
const DocumentParagraphState& state,
|
|
vint pos,
|
|
Nullable<Pair<vint, DocumentInlineObjectRunProperty>>& inlineProp)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::GetFontSizeForPosition(...)#"
|
|
inlineProp.Reset();
|
|
|
|
for (auto [range, prop] : state.mergedRuns)
|
|
{
|
|
if (pos >= range.caretBegin && pos < range.caretEnd)
|
|
{
|
|
if (auto textProp = prop.TryGet<DocumentTextRunProperty>())
|
|
{
|
|
return textProp->fontProperties.size;
|
|
}
|
|
if (auto objProp = prop.TryGet<DocumentInlineObjectRunProperty>())
|
|
{
|
|
inlineProp = { range.caretEnd - range.caretBegin,*objProp };
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
CHECK_FAIL(ERROR_MESSAGE_PREFIX L"Every character is expected to have a font.");
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
double GetCharacterWidth(wchar_t c, vint fontSize)
|
|
{
|
|
return (c < 128 ? 0.6 : 1.0) * fontSize;
|
|
}
|
|
|
|
void CalculateParagraphLayout(DocumentParagraphState& state)
|
|
{
|
|
state.characterLayouts.Clear();
|
|
state.lines.Clear();
|
|
state.cachedSize = Size(0, 16);
|
|
state.cachedInlineObjectBounds.Clear();
|
|
|
|
const WString& text = state.text;
|
|
|
|
if (text.Length() == 0)
|
|
{
|
|
// Empty paragraph has default size
|
|
DocumentParagraphLineInfo line;
|
|
line.startPos = 0;
|
|
line.endPos = 0;
|
|
line.y = 0;
|
|
line.height = 16; // Default: 12 (font) + 4
|
|
line.baseline = 12;
|
|
line.width = 0;
|
|
state.lines.Add(line);
|
|
return;
|
|
}
|
|
|
|
// First pass: calculate per-character metrics
|
|
struct TempCharInfo
|
|
{
|
|
double x;
|
|
double width;
|
|
vint height;
|
|
Nullable<DocumentInlineObjectRunProperty> inlineObjectProp;
|
|
};
|
|
List<TempCharInfo> tempChars;
|
|
List<Pair<vint, vint>> lineRanges; // [start, end) for each line
|
|
|
|
double currentX = 0;
|
|
vint currentLineStart = 0;
|
|
|
|
for (vint i = 0; i < text.Length(); i++)
|
|
{
|
|
wchar_t c = text[i];
|
|
TempCharInfo info = { currentX, 0, 0, {} };
|
|
|
|
// Handle \r - zero width, no line break
|
|
if (c == L'\r')
|
|
{
|
|
tempChars.Add(info);
|
|
continue;
|
|
}
|
|
|
|
// Get character properties
|
|
Nullable<Pair<vint, DocumentInlineObjectRunProperty>> inlinePair;
|
|
vint fontSize = GetFontSizeForPosition(state, i, inlinePair);
|
|
|
|
if (inlinePair)
|
|
{
|
|
auto& prop = inlinePair.Value().value;
|
|
info.width = (double)prop.size.x;
|
|
info.height = prop.size.y;
|
|
info.inlineObjectProp = inlinePair.Value().value;
|
|
}
|
|
else
|
|
{
|
|
if (fontSize <= 0) fontSize = 12;
|
|
info.width = GetCharacterWidth(c, fontSize);
|
|
info.height = fontSize;
|
|
}
|
|
|
|
// Handle \n - always break line
|
|
if (c == L'\n')
|
|
{
|
|
info.width = 0;
|
|
tempChars.Add(info);
|
|
lineRanges.Add({ currentLineStart, i + 1 });
|
|
currentLineStart = i + 1;
|
|
currentX = 0;
|
|
continue;
|
|
}
|
|
|
|
// Check word wrap
|
|
if (state.wrapLine && state.maxWidth > 0 && currentX > 0)
|
|
{
|
|
if (currentX + info.width > state.maxWidth)
|
|
{
|
|
lineRanges.Add({ currentLineStart, i });
|
|
currentLineStart = i;
|
|
currentX = 0;
|
|
}
|
|
}
|
|
|
|
info.x = currentX;
|
|
tempChars.Add(info);
|
|
currentX += info.width;
|
|
|
|
if(inlinePair)
|
|
{
|
|
i += inlinePair.Value().key - 1;
|
|
}
|
|
}
|
|
|
|
// Add final line
|
|
if (currentLineStart <= text.Length())
|
|
{
|
|
lineRanges.Add({ currentLineStart, text.Length() });
|
|
}
|
|
|
|
// Handle empty case
|
|
if (lineRanges.Count() == 0)
|
|
{
|
|
lineRanges.Add({ 0, 0 });
|
|
}
|
|
|
|
// Second pass: calculate line heights using baseline alignment
|
|
vint currentY = 0;
|
|
for (auto [lineStart, lineEnd] : lineRanges)
|
|
{
|
|
vint maxAboveBaseline = 0;
|
|
vint maxBelowBaseline = 0;
|
|
|
|
for (vint i = lineStart; i < lineEnd && i < tempChars.Count(); i++)
|
|
{
|
|
auto& info = tempChars[i];
|
|
if (info.inlineObjectProp)
|
|
{
|
|
auto&& prop = info.inlineObjectProp.Value();
|
|
vint baseline = prop.baseline;
|
|
if (baseline == -1)
|
|
baseline = info.height;
|
|
vint above = baseline;
|
|
vint below = info.height - baseline;
|
|
if (above < 0) above = 0;
|
|
if (below < 0) below = 0;
|
|
if (maxAboveBaseline < above)
|
|
maxAboveBaseline = above;
|
|
if (maxBelowBaseline < below)
|
|
maxBelowBaseline = below;
|
|
}
|
|
else
|
|
{
|
|
if (maxAboveBaseline < info.height)
|
|
maxAboveBaseline = info.height;
|
|
}
|
|
}
|
|
|
|
DocumentParagraphLineInfo line;
|
|
line.startPos = lineStart;
|
|
line.endPos = lineEnd;
|
|
line.y = currentY;
|
|
line.height = maxAboveBaseline + maxBelowBaseline + 4;
|
|
line.baseline = maxAboveBaseline;
|
|
|
|
// Calculate line width
|
|
double lineWidth = 0;
|
|
for (vint i = lineStart; i < lineEnd && i < tempChars.Count(); i++)
|
|
{
|
|
double endX = tempChars[i].x + tempChars[i].width;
|
|
if (endX > lineWidth) lineWidth = endX;
|
|
}
|
|
line.width = (vint)lineWidth;
|
|
|
|
// Fill inline object bounds
|
|
|
|
for (vint i = lineStart; i < lineEnd && i < tempChars.Count(); i++)
|
|
{
|
|
auto& info = tempChars[i];
|
|
if (info.inlineObjectProp)
|
|
{
|
|
auto&& prop = info.inlineObjectProp.Value();
|
|
if (prop.callbackId != -1)
|
|
{
|
|
vint baseline = prop.baseline;
|
|
if (baseline == -1)
|
|
baseline = info.height;
|
|
vint y = line.y + 2 + line.baseline - baseline;
|
|
state.cachedInlineObjectBounds.Add(prop.callbackId, Rect(Point((vint)info.x, y), prop.size));
|
|
}
|
|
}
|
|
}
|
|
|
|
state.lines.Add(line);
|
|
currentY += line.height;
|
|
}
|
|
|
|
// Third pass: create final character layouts with line indices
|
|
vint lineIdx = 0;
|
|
for (vint i = 0; i < tempChars.Count(); i++)
|
|
{
|
|
while (lineIdx < state.lines.Count() - 1 && i >= state.lines[lineIdx].endPos)
|
|
{
|
|
lineIdx++;
|
|
}
|
|
DocumentParagraphCharLayout cl;
|
|
cl.x = tempChars[i].x;
|
|
cl.width = tempChars[i].width;
|
|
cl.lineIndex = lineIdx;
|
|
cl.height = tempChars[i].height;
|
|
state.characterLayouts.Add(cl);
|
|
}
|
|
|
|
// Calculate total size
|
|
vint maxWidth = 0;
|
|
for (auto&& line : state.lines)
|
|
{
|
|
if (line.width > maxWidth) maxWidth = line.width;
|
|
}
|
|
state.cachedSize = Size(maxWidth, currentY > 0 ? currentY : 16);
|
|
}
|
|
|
|
/***********************************************************************
|
|
IGuiRemoteProtocolMessages (Elements - Document)
|
|
***********************************************************************/
|
|
|
|
void UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_DocumentParagraph(vint id, const ElementDesc_DocumentParagraph& arguments)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_DocumentParagraph(vint, const ElementDesc_DocumentParagraph&)#"
|
|
vint index = paragraphStates.Keys().IndexOf(arguments.id);
|
|
CHECK_ERROR(index != -1, ERROR_MESSAGE_PREFIX L"Paragraph not created.");
|
|
auto state = paragraphStates.Values()[index];
|
|
|
|
// Apply text if provided (distinguish null vs empty string)
|
|
if (arguments.text)
|
|
{
|
|
state->text = arguments.text.Value();
|
|
// Text changed - clear run maps since positions may be invalid
|
|
state->textRuns.Clear();
|
|
state->inlineObjectRuns.Clear();
|
|
}
|
|
|
|
// Always update these
|
|
state->wrapLine = arguments.wrapLine;
|
|
state->maxWidth = arguments.maxWidth;
|
|
state->alignment = arguments.alignment;
|
|
|
|
// Process removed inline objects first
|
|
if (arguments.removedInlineObjects)
|
|
{
|
|
for (auto callbackId : *arguments.removedInlineObjects.Obj())
|
|
{
|
|
// Find the range for this inline object and reset it
|
|
for (auto [range, prop] : state->inlineObjectRuns)
|
|
{
|
|
if (prop.callbackId == callbackId)
|
|
{
|
|
elements::ResetInlineObjectRun(state->inlineObjectRuns, range);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply runsDiff using helper functions
|
|
if (arguments.runsDiff)
|
|
{
|
|
for (auto run : *arguments.runsDiff.Obj())
|
|
{
|
|
elements::CaretRange range{ run.caretBegin, run.caretEnd };
|
|
|
|
if (auto textProp = run.props.TryGet<DocumentTextRunProperty>())
|
|
{
|
|
elements::DocumentTextRunPropertyOverrides overrides;
|
|
overrides.textColor = textProp->textColor;
|
|
overrides.backgroundColor = textProp->backgroundColor;
|
|
overrides.fontFamily = textProp->fontProperties.fontFamily;
|
|
overrides.size = textProp->fontProperties.size;
|
|
// Convert bool flags back to TextStyle
|
|
elements::IGuiGraphicsParagraph::TextStyle style = (elements::IGuiGraphicsParagraph::TextStyle)0;
|
|
if (textProp->fontProperties.bold) style = (elements::IGuiGraphicsParagraph::TextStyle)((vint)style | (vint)elements::IGuiGraphicsParagraph::TextStyle::Bold);
|
|
if (textProp->fontProperties.italic) style = (elements::IGuiGraphicsParagraph::TextStyle)((vint)style | (vint)elements::IGuiGraphicsParagraph::TextStyle::Italic);
|
|
if (textProp->fontProperties.underline) style = (elements::IGuiGraphicsParagraph::TextStyle)((vint)style | (vint)elements::IGuiGraphicsParagraph::TextStyle::Underline);
|
|
if (textProp->fontProperties.strikeline) style = (elements::IGuiGraphicsParagraph::TextStyle)((vint)style | (vint)elements::IGuiGraphicsParagraph::TextStyle::Strikeline);
|
|
overrides.textStyle = style;
|
|
elements::AddTextRun(state->textRuns, range, overrides);
|
|
}
|
|
else if (auto inlineProp = run.props.TryGet<DocumentInlineObjectRunProperty>())
|
|
{
|
|
elements::AddInlineObjectRun(state->inlineObjectRuns, range, *inlineProp);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Merge runs to create final result
|
|
state->mergedRuns.Clear();
|
|
elements::MergeRuns(state->textRuns, state->inlineObjectRuns, state->mergedRuns);
|
|
|
|
// Recalculate layout
|
|
CalculateParagraphLayout(*state.Obj());
|
|
|
|
{
|
|
index = loggedTrace.createdElements->Keys().IndexOf(arguments.id);
|
|
CHECK_ERROR(index != -1, ERROR_MESSAGE_PREFIX L"Renderer with the specified id has not been created.");
|
|
|
|
auto rendererType = loggedTrace.createdElements->Values()[index];
|
|
CHECK_ERROR(rendererType == RendererType::DocumentParagraph, ERROR_MESSAGE_PREFIX L"Renderer with the specified id is not of the expected type.");
|
|
|
|
index = lastElementDescs.Keys().IndexOf(arguments.id);
|
|
ElementDesc_DocumentParagraphFull element;
|
|
if (index != -1)
|
|
{
|
|
auto paragraphRef = lastElementDescs.Values()[index].TryGet<ElementDesc_DocumentParagraphFull>();
|
|
CHECK_ERROR(paragraphRef, ERROR_MESSAGE_PREFIX L"Renderer with the specified id is not of the expected type.");
|
|
element = *paragraphRef;
|
|
}
|
|
element.paragraph = arguments;
|
|
element.paragraph.text = state->text;
|
|
element.paragraph.createdInlineObjects = {};
|
|
element.paragraph.removedInlineObjects = {};
|
|
element.paragraph.runsDiff = Ptr(new List<remoteprotocol::DocumentRun>);
|
|
for (auto [range, props] : state->mergedRuns)
|
|
{
|
|
remoteprotocol::DocumentRun run;
|
|
run.caretBegin = range.caretBegin;
|
|
run.caretEnd = range.caretEnd;
|
|
run.props = props;
|
|
element.paragraph.runsDiff->Add(run);
|
|
}
|
|
lastElementDescs.Set(arguments.id, element);
|
|
}
|
|
|
|
// Send response with calculated size and inline object bounds
|
|
UpdateElement_DocumentParagraphResponse response;
|
|
response.documentSize = state->cachedSize;
|
|
if (state->cachedInlineObjectBounds.Count() > 0)
|
|
{
|
|
response.inlineObjectBounds = Ptr(new Dictionary<vint, Rect>);
|
|
CopyFrom(*response.inlineObjectBounds.Obj(), state->cachedInlineObjectBounds);
|
|
}
|
|
|
|
GetEvents()->RespondRendererUpdateElement_DocumentParagraph(id, response);
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
void UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_GetCaretBounds(vint id, const GetCaretBoundsRequest& arguments)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_GetCaretBounds(vint, const GetCaretBoundsRequest&)#"
|
|
vint index = paragraphStates.Keys().IndexOf(arguments.id);
|
|
CHECK_ERROR(index != -1, ERROR_MESSAGE_PREFIX L"No active paragraph.");
|
|
auto state = paragraphStates.Values()[index];
|
|
|
|
vint caret = arguments.caret;
|
|
|
|
// Handle empty text
|
|
if (state->text.Length() == 0 || state->lines.Count() == 0)
|
|
{
|
|
auto& line = state->lines[0];
|
|
GetEvents()->RespondDocumentParagraph_GetCaretBounds(id, Rect(Point(0, line.y), Size(0, line.height)));
|
|
return;
|
|
}
|
|
|
|
// Clamp caret to valid range
|
|
if (caret < 0) caret = 0;
|
|
if (caret > state->text.Length()) caret = state->text.Length();
|
|
|
|
// Find which line the caret is on
|
|
vint lineIdx = 0;
|
|
for (vint i = 0; i < state->lines.Count(); i++)
|
|
{
|
|
if (caret >= state->lines[i].startPos && caret <= state->lines[i].endPos)
|
|
{
|
|
lineIdx = i;
|
|
break;
|
|
}
|
|
if (i == state->lines.Count() - 1)
|
|
{
|
|
lineIdx = i;
|
|
}
|
|
}
|
|
|
|
auto& line = state->lines[lineIdx];
|
|
|
|
// Calculate x position
|
|
vint x = 0;
|
|
if (caret > 0 && caret <= state->characterLayouts.Count())
|
|
{
|
|
// Caret is at the end of the previous character
|
|
auto& prevChar = state->characterLayouts[caret - 1];
|
|
x = (vint)(prevChar.x + prevChar.width);
|
|
}
|
|
else if (caret < state->characterLayouts.Count())
|
|
{
|
|
// Caret is at the start of this character
|
|
x = (vint)state->characterLayouts[caret].x;
|
|
}
|
|
|
|
// Apply alignment offset
|
|
vint alignmentOffset = 0;
|
|
if (state->alignment == ElementHorizontalAlignment::Center)
|
|
{
|
|
alignmentOffset = (state->cachedSize.x - line.width) / 2;
|
|
}
|
|
else if (state->alignment == ElementHorizontalAlignment::Right)
|
|
{
|
|
alignmentOffset = state->cachedSize.x - line.width;
|
|
}
|
|
|
|
Rect bounds(Point(x + alignmentOffset, line.y), Size(0, line.height));
|
|
GetEvents()->RespondDocumentParagraph_GetCaretBounds(id, bounds);
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
void UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_GetCaret(vint id, const GetCaretRequest& arguments)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_GetCaret(vint, const GetCaretRequest&)#"
|
|
vint index = paragraphStates.Keys().IndexOf(arguments.id);
|
|
CHECK_ERROR(index != -1, ERROR_MESSAGE_PREFIX L"No active paragraph.");
|
|
auto state = paragraphStates.Values()[index];
|
|
|
|
vint caret = arguments.caret;
|
|
auto relPos = arguments.relativePosition;
|
|
GetCaretResponse response;
|
|
response.preferFrontSide = true;
|
|
|
|
vint textLen = state->text.Length();
|
|
|
|
// Clamp caret
|
|
if (caret < 0) caret = 0;
|
|
if (caret > textLen) caret = textLen;
|
|
|
|
// Find current line
|
|
vint lineIdx = 0;
|
|
for (vint i = 0; i < state->lines.Count(); i++)
|
|
{
|
|
if (caret >= state->lines[i].startPos && caret <= state->lines[i].endPos)
|
|
{
|
|
lineIdx = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
using CRP = elements::IGuiGraphicsParagraph::CaretRelativePosition;
|
|
|
|
switch (relPos)
|
|
{
|
|
case CRP::CaretFirst:
|
|
response.newCaret = 0;
|
|
break;
|
|
case CRP::CaretLast:
|
|
response.newCaret = textLen;
|
|
break;
|
|
case CRP::CaretLineFirst:
|
|
response.newCaret = state->lines[lineIdx].startPos;
|
|
break;
|
|
case CRP::CaretLineLast:
|
|
response.newCaret = state->lines[lineIdx].endPos;
|
|
if (response.newCaret > state->lines[lineIdx].startPos && response.newCaret > 0)
|
|
{
|
|
// Don't include CR/LF at end of line
|
|
while (response.newCaret > state->lines[lineIdx].startPos && response.newCaret > 0)
|
|
{
|
|
auto ch = state->text[response.newCaret - 1];
|
|
if (ch == L'\r' || ch == L'\n')
|
|
{
|
|
response.newCaret--;
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case CRP::CaretMoveLeft:
|
|
response.newCaret = caret > 0 ? caret - 1 : 0;
|
|
break;
|
|
case CRP::CaretMoveRight:
|
|
response.newCaret = caret < textLen ? caret + 1 : textLen;
|
|
break;
|
|
case CRP::CaretMoveUp:
|
|
if (lineIdx > 0)
|
|
{
|
|
// Calculate x offset in current line
|
|
vint xOffset = 0;
|
|
if (caret > 0 && caret <= state->characterLayouts.Count())
|
|
{
|
|
auto& prevChar = state->characterLayouts[caret - 1];
|
|
xOffset = (vint)(prevChar.x + prevChar.width);
|
|
}
|
|
// Find corresponding position in previous line
|
|
auto& prevLine = state->lines[lineIdx - 1];
|
|
response.newCaret = prevLine.startPos;
|
|
for (vint i = prevLine.startPos; i < prevLine.endPos && i < state->characterLayouts.Count(); i++)
|
|
{
|
|
auto& ch = state->characterLayouts[i];
|
|
if (ch.x + ch.width / 2 > xOffset)
|
|
break;
|
|
response.newCaret = i + 1;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
response.newCaret = caret;
|
|
}
|
|
break;
|
|
case CRP::CaretMoveDown:
|
|
if (lineIdx < state->lines.Count() - 1)
|
|
{
|
|
// Calculate x offset in current line
|
|
vint xOffset = 0;
|
|
if (caret > 0 && caret <= state->characterLayouts.Count())
|
|
{
|
|
auto& prevChar = state->characterLayouts[caret - 1];
|
|
xOffset = (vint)(prevChar.x + prevChar.width);
|
|
}
|
|
// Find corresponding position in next line
|
|
auto& nextLine = state->lines[lineIdx + 1];
|
|
response.newCaret = nextLine.startPos;
|
|
for (vint i = nextLine.startPos; i < nextLine.endPos && i < state->characterLayouts.Count(); i++)
|
|
{
|
|
auto& ch = state->characterLayouts[i];
|
|
if (ch.x + ch.width / 2 > xOffset)
|
|
break;
|
|
response.newCaret = i + 1;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
response.newCaret = caret;
|
|
}
|
|
break;
|
|
default:
|
|
response.newCaret = caret;
|
|
break;
|
|
}
|
|
|
|
GetEvents()->RespondDocumentParagraph_GetCaret(id, response);
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
void UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_GetNearestCaretFromTextPos(vint id, const GetCaretBoundsRequest& arguments)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_GetNearestCaretFromTextPos(vint, const GetCaretBoundsRequest&)#"
|
|
vint index = paragraphStates.Keys().IndexOf(arguments.id);
|
|
CHECK_ERROR(index != -1, ERROR_MESSAGE_PREFIX L"No active paragraph.");
|
|
auto state = paragraphStates.Values()[index];
|
|
|
|
vint textPos = arguments.caret;
|
|
vint textLen = state->text.Length();
|
|
|
|
// Clamp to valid range
|
|
if (textPos < 0) textPos = 0;
|
|
if (textPos > textLen) textPos = textLen;
|
|
|
|
// For simple implementation, text position equals caret position
|
|
GetEvents()->RespondDocumentParagraph_GetNearestCaretFromTextPos(id, textPos);
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
void UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_GetInlineObjectFromPoint(vint id, const GetInlineObjectFromPointRequest& arguments)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_GetInlineObjectFromPoint(vint, const GetInlineObjectFromPointRequest&)#"
|
|
vint index = paragraphStates.Keys().IndexOf(arguments.id);
|
|
CHECK_ERROR(index != -1, ERROR_MESSAGE_PREFIX L"No active paragraph.");
|
|
auto state = paragraphStates.Values()[index];
|
|
|
|
Point pt = arguments.point;
|
|
Nullable<remoteprotocol::DocumentRun> result;
|
|
|
|
// Find the line containing the Y coordinate
|
|
vint lineIdx = -1;
|
|
for (vint i = 0; i < state->lines.Count(); i++)
|
|
{
|
|
if (pt.y >= state->lines[i].y && pt.y < state->lines[i].y + state->lines[i].height)
|
|
{
|
|
lineIdx = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (lineIdx >= 0)
|
|
{
|
|
auto& line = state->lines[lineIdx];
|
|
|
|
// Apply alignment offset
|
|
vint alignmentOffset = 0;
|
|
if (state->alignment == ElementHorizontalAlignment::Center)
|
|
{
|
|
alignmentOffset = (state->cachedSize.x - line.width) / 2;
|
|
}
|
|
else if (state->alignment == ElementHorizontalAlignment::Right)
|
|
{
|
|
alignmentOffset = state->cachedSize.x - line.width;
|
|
}
|
|
vint relativeX = pt.x - alignmentOffset;
|
|
|
|
// Check each character in the line
|
|
for (vint i = line.startPos; i < line.endPos && i < state->characterLayouts.Count(); i++)
|
|
{
|
|
auto& ch = state->characterLayouts[i];
|
|
if (ch.isInlineObject && relativeX >= ch.x && relativeX < ch.x + ch.width)
|
|
{
|
|
// Found an inline object - look up its properties
|
|
for (auto [range, prop] : state->mergedRuns)
|
|
{
|
|
if (i >= range.caretBegin && i < range.caretEnd)
|
|
{
|
|
if (auto inlineProp = prop.TryGet<DocumentInlineObjectRunProperty>())
|
|
{
|
|
remoteprotocol::DocumentRun run;
|
|
run.caretBegin = range.caretBegin;
|
|
run.caretEnd = range.caretEnd;
|
|
run.props = *inlineProp;
|
|
result = run;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
GetEvents()->RespondDocumentParagraph_GetInlineObjectFromPoint(id, result);
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
void UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_IsValidCaret(vint id, const IsValidCaretRequest& arguments)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_IsValidCaret(vint, const IsValidCaretRequest&)#"
|
|
vint index = paragraphStates.Keys().IndexOf(arguments.id);
|
|
if (index == -1)
|
|
{
|
|
GetEvents()->RespondDocumentParagraph_IsValidCaret(id, false);
|
|
return;
|
|
}
|
|
auto state = paragraphStates.Values()[index];
|
|
|
|
vint caret = arguments.caret;
|
|
|
|
// Check range: valid positions are 0 to text.Length() inclusive
|
|
if (caret < 0 || caret > state->text.Length())
|
|
{
|
|
GetEvents()->RespondDocumentParagraph_IsValidCaret(id, false);
|
|
return;
|
|
}
|
|
|
|
// Check if position is inside an inline object (not at the beginning)
|
|
// Inline objects occupy a range [caretBegin, caretEnd)
|
|
// Position at caretBegin is valid (cursor can be placed before the object)
|
|
// Positions inside (caretBegin < pos < caretEnd) are invalid
|
|
for (auto&& [range, _] : state->inlineObjectRuns)
|
|
{
|
|
if (caret > range.caretBegin && caret < range.caretEnd)
|
|
{
|
|
GetEvents()->RespondDocumentParagraph_IsValidCaret(id, false);
|
|
return;
|
|
}
|
|
}
|
|
|
|
GetEvents()->RespondDocumentParagraph_IsValidCaret(id, true);
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
void UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_OpenCaret(const OpenCaretRequest& arguments)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_OpenCaret(const OpenCaretRequest&)#"
|
|
vint index = loggedTrace.createdElements->Keys().IndexOf(arguments.id);
|
|
CHECK_ERROR(index != -1, ERROR_MESSAGE_PREFIX L"Renderer with the specified id has not been created.");
|
|
|
|
auto rendererType = loggedTrace.createdElements->Values()[index];
|
|
CHECK_ERROR(rendererType == RendererType::DocumentParagraph, ERROR_MESSAGE_PREFIX L"Renderer with the specified id is not of the expected type.");
|
|
|
|
index = lastElementDescs.Keys().IndexOf(arguments.id);
|
|
ElementDesc_DocumentParagraphFull element;
|
|
if (index != -1)
|
|
{
|
|
auto paragraphRef = lastElementDescs.Values()[index].TryGet<ElementDesc_DocumentParagraphFull>();
|
|
CHECK_ERROR(paragraphRef, ERROR_MESSAGE_PREFIX L"Renderer with the specified id is not of the expected type.");
|
|
element = *paragraphRef;
|
|
}
|
|
element.caret = arguments;
|
|
lastElementDescs.Set(arguments.id, element);
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
void UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_CloseCaret(const vint& arguments)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_DocumentParagraph_CloseCaret(const vint&)#"
|
|
vint index = loggedTrace.createdElements->Keys().IndexOf(arguments);
|
|
CHECK_ERROR(index != -1, ERROR_MESSAGE_PREFIX L"Renderer with the specified id has not been created.");
|
|
|
|
auto rendererType = loggedTrace.createdElements->Values()[index];
|
|
CHECK_ERROR(rendererType == RendererType::DocumentParagraph, ERROR_MESSAGE_PREFIX L"Renderer with the specified id is not of the expected type.");
|
|
|
|
index = lastElementDescs.Keys().IndexOf(arguments);
|
|
ElementDesc_DocumentParagraphFull element;
|
|
if (index != -1)
|
|
{
|
|
auto paragraphRef = lastElementDescs.Values()[index].TryGet<ElementDesc_DocumentParagraphFull>();
|
|
CHECK_ERROR(paragraphRef, ERROR_MESSAGE_PREFIX L"Renderer with the specified id is not of the expected type.");
|
|
element = *paragraphRef;
|
|
}
|
|
element.caret.Reset();
|
|
lastElementDescs.Set(arguments, element);
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
}
|
|
|
|
|
|
/***********************************************************************
|
|
.\GUIUNITTESTPROTOCOL_RENDERING.CPP
|
|
***********************************************************************/
|
|
|
|
namespace vl::presentation::unittest
|
|
{
|
|
using namespace collections;
|
|
using namespace remoteprotocol;
|
|
|
|
/***********************************************************************
|
|
IGuiRemoteProtocolMessages (Rendering)
|
|
***********************************************************************/
|
|
|
|
Ptr<UnitTestLoggedFrame> UnitTestRemoteProtocol_Rendering::GetLastRenderingFrame()
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::GetLastRenderingCommands()#"
|
|
CHECK_ERROR(lastRenderingCommandsOpening, ERROR_MESSAGE_PREFIX L"The latest frame of commands is not accepting new commands.");
|
|
return loggedFrames[loggedFrames.Count() - 1];
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
Ptr<UnitTestLoggedFrame> UnitTestRemoteProtocol_Rendering::TryGetLastRenderingFrameAndReset()
|
|
{
|
|
if (loggedFrames.Count() == 0) return nullptr;
|
|
if (!lastRenderingCommandsOpening) return nullptr;
|
|
lastRenderingCommandsOpening = false;
|
|
return loggedFrames[loggedFrames.Count() - 1];
|
|
}
|
|
|
|
void UnitTestRemoteProtocol_Rendering::Impl_RendererBeginRendering(const ElementBeginRendering& arguments)
|
|
{
|
|
receivedDomDiffMessage = false;
|
|
receivedElementMessage = false;
|
|
lastRenderingCommandsOpening = true;
|
|
auto frame = Ptr(new UnitTestLoggedFrame);
|
|
frame->frameId = arguments.frameId;
|
|
loggedFrames.Add(frame);
|
|
}
|
|
|
|
void UnitTestRemoteProtocol_Rendering::Impl_RendererEndRendering(vint id)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererEndRendering(vint)#"
|
|
CHECK_ERROR(receivedElementMessage || receivedDomDiffMessage, ERROR_MESSAGE_PREFIX L"Either dom-diff or element message should be sent before this message.");
|
|
|
|
auto lastFrame = GetLastRenderingFrame();
|
|
if (receivedElementMessage)
|
|
{
|
|
lastFrame->renderingDom = renderingDomBuilder.RequestRendererEndRendering();
|
|
}
|
|
if (receivedDomDiffMessage)
|
|
{
|
|
// receivedDom will be updated in RequestRendererRenderDomDiff
|
|
// store a copy to log
|
|
lastFrame->renderingDom = CopyDom(receivedDom);
|
|
}
|
|
this->GetEvents()->RespondRendererEndRendering(id, measuringForNextRendering);
|
|
measuringForNextRendering = {};
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
/***********************************************************************
|
|
IGuiRemoteProtocolMessages (Rendering - Element)
|
|
***********************************************************************/
|
|
|
|
void UnitTestRemoteProtocol_Rendering::Impl_RendererBeginBoundary(const ElementBoundary& arguments)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererBeginBoundary(const ElementBoundary&)#"
|
|
CHECK_ERROR(!receivedDomDiffMessage, ERROR_MESSAGE_PREFIX L"This message could not be used with dom-diff messages in the same rendering cycle.");
|
|
if (!receivedElementMessage)
|
|
{
|
|
renderingDomBuilder.RequestRendererBeginRendering();
|
|
receivedElementMessage = true;
|
|
}
|
|
|
|
renderingDomBuilder.RequestRendererBeginBoundary(arguments);
|
|
|
|
glr::json::JsonFormatting formatting;
|
|
formatting.spaceAfterColon = true;
|
|
formatting.spaceAfterComma = true;
|
|
formatting.crlf = false;
|
|
formatting.compact = true;
|
|
|
|
GetLastRenderingFrame()->renderingCommandsLog.Add(L"RequestRendererBeginBoundary: " + stream::GenerateToStream([&](stream::TextWriter& writer)
|
|
{
|
|
auto jsonLog = ConvertCustomTypeToJson(arguments);
|
|
writer.WriteString(glr::json::JsonToString(jsonLog, formatting));
|
|
}));
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
void UnitTestRemoteProtocol_Rendering::Impl_RendererEndBoundary()
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererEndBoundary()#"
|
|
CHECK_ERROR(!receivedDomDiffMessage, ERROR_MESSAGE_PREFIX L"This message could not be used with dom-diff messages in the same rendering cycle.");
|
|
if (!receivedElementMessage)
|
|
{
|
|
renderingDomBuilder.RequestRendererBeginRendering();
|
|
receivedElementMessage = true;
|
|
}
|
|
|
|
renderingDomBuilder.RequestRendererEndBoundary();
|
|
GetLastRenderingFrame()->renderingCommandsLog.Add(L"RequestRendererEndBoundary");
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
void UnitTestRemoteProtocol_Rendering::EnsureRenderedElement(vint id, Rect bounds)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::EnsureRenderedElement(id&)#"
|
|
|
|
vint index = loggedTrace.createdElements->Keys().IndexOf(id);
|
|
CHECK_ERROR(index != -1, ERROR_MESSAGE_PREFIX L"Renderer with the specified id has not been created.");
|
|
auto rendererType = loggedTrace.createdElements->Values()[index];
|
|
if (rendererType == RendererType::FocusRectangle || rendererType == RendererType::Raw)
|
|
{
|
|
// FocusRectangle or Raw does not have a ElementDesc
|
|
return;
|
|
}
|
|
|
|
index = lastElementDescs.Keys().IndexOf(id);
|
|
CHECK_ERROR(index != -1, ERROR_MESSAGE_PREFIX L"Renderer with the specified id has not been updated after created.");
|
|
lastElementDescs.Values()[index].Apply(Overloading(
|
|
[](RendererType)
|
|
{
|
|
CHECK_FAIL(ERROR_MESSAGE_PREFIX L"Renderer with the specified id has not been updated after created.");
|
|
},
|
|
[&](const ElementDesc_SolidLabel& solidLabel)
|
|
{
|
|
CalculateSolidLabelSizeIfNecessary(bounds.Width(), bounds.Height(), solidLabel);
|
|
},
|
|
[&](const auto& element)
|
|
{
|
|
}));
|
|
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
void UnitTestRemoteProtocol_Rendering::Impl_RendererRenderElement(const ElementRendering& arguments)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererRenderElement(const ElementRendering&)#"
|
|
CHECK_ERROR(!receivedDomDiffMessage, ERROR_MESSAGE_PREFIX L"This message could not be used with dom-diff messages in the same rendering cycle.");
|
|
if (!receivedElementMessage)
|
|
{
|
|
renderingDomBuilder.RequestRendererBeginRendering();
|
|
receivedElementMessage = true;
|
|
}
|
|
|
|
{
|
|
renderingDomBuilder.RequestRendererRenderElement(arguments);
|
|
|
|
glr::json::JsonFormatting formatting;
|
|
formatting.spaceAfterColon = true;
|
|
formatting.spaceAfterComma = true;
|
|
formatting.crlf = false;
|
|
formatting.compact = true;
|
|
GetLastRenderingFrame()->renderingCommandsLog.Add(L"RequestRendererRenderElement: " + stream::GenerateToStream([&](stream::TextWriter& writer)
|
|
{
|
|
auto jsonLog = ConvertCustomTypeToJson(arguments);
|
|
writer.WriteString(glr::json::JsonToString(jsonLog, formatting));
|
|
}));
|
|
}
|
|
|
|
EnsureRenderedElement(arguments.id, arguments.bounds);
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
/***********************************************************************
|
|
IGuiRemoteProtocolMessages (Rendering - Dom)
|
|
***********************************************************************/
|
|
|
|
void UnitTestRemoteProtocol_Rendering::CalculateSolidLabelSizesIfNecessary(Ptr<RenderingDom> dom)
|
|
{
|
|
if (dom->content.element)
|
|
{
|
|
EnsureRenderedElement(dom->content.element.Value(), dom->content.bounds);
|
|
}
|
|
if (dom->children)
|
|
{
|
|
for (auto child : *dom->children.Obj())
|
|
{
|
|
CalculateSolidLabelSizesIfNecessary(child);
|
|
}
|
|
}
|
|
}
|
|
|
|
void UnitTestRemoteProtocol_Rendering::Impl_RendererRenderDom(const Ptr<RenderingDom>& arguments)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererRenderElement(const RenderingDom&)#"
|
|
CHECK_ERROR(!receivedElementMessage, ERROR_MESSAGE_PREFIX L"This message could not be used with element messages in the same rendering cycle.");
|
|
if (!receivedDomDiffMessage)
|
|
{
|
|
receivedDomDiffMessage = true;
|
|
}
|
|
|
|
receivedDom = arguments;
|
|
BuildDomIndex(receivedDom, receivedDomIndex);
|
|
CalculateSolidLabelSizesIfNecessary(receivedDom);
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
void UnitTestRemoteProtocol_Rendering::Impl_RendererRenderDomDiff(const RenderingDom_DiffsInOrder& arguments)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererRenderElement(const RenderingDom_DiffsInOrder&)#"
|
|
CHECK_ERROR(!receivedElementMessage, ERROR_MESSAGE_PREFIX L"This message could not be used with element messages in the same rendering cycle.");
|
|
if (!receivedDomDiffMessage)
|
|
{
|
|
receivedDomDiffMessage = true;
|
|
}
|
|
|
|
UpdateDomInplace(receivedDom, receivedDomIndex, arguments);
|
|
GetLastRenderingFrame()->renderingDiffs = arguments;
|
|
CalculateSolidLabelSizesIfNecessary(receivedDom);
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
/***********************************************************************
|
|
IGuiRemoteProtocolMessages (Elements)
|
|
***********************************************************************/
|
|
|
|
void UnitTestRemoteProtocol_Rendering::Impl_RendererCreated(const Ptr<List<RendererCreation>>& arguments)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererCreated(const Ptr<List<RendererCreation>>&)#"
|
|
if (arguments)
|
|
{
|
|
for (auto creation : *arguments.Obj())
|
|
{
|
|
CHECK_ERROR(!loggedTrace.createdElements->Keys().Contains(creation.id), ERROR_MESSAGE_PREFIX L"Renderer with the specified id has been created or used.");
|
|
loggedTrace.createdElements->Add(creation.id, creation.type);
|
|
|
|
// Create paragraph state for DocumentParagraph elements
|
|
if (creation.type == RendererType::DocumentParagraph)
|
|
{
|
|
auto state = Ptr(new DocumentParagraphState());
|
|
paragraphStates.Add(creation.id, state);
|
|
}
|
|
}
|
|
}
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
void UnitTestRemoteProtocol_Rendering::Impl_RendererDestroyed(const Ptr<List<vint>>& arguments)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererDestroyed(const Ptr<List<vint>>&)#"
|
|
if (arguments)
|
|
{
|
|
for (auto id : *arguments.Obj())
|
|
{
|
|
CHECK_ERROR(loggedTrace.createdElements->Keys().Contains(id), ERROR_MESSAGE_PREFIX L"Renderer with the specified id has not been created.");
|
|
CHECK_ERROR(!removedElementIds.Contains(id), ERROR_MESSAGE_PREFIX L"Renderer with the specified id has been destroyed.");
|
|
removedElementIds.Add(id);
|
|
lastElementDescs.Remove(id);
|
|
|
|
// Remove paragraph state if this was a DocumentParagraph
|
|
paragraphStates.Remove(id);
|
|
}
|
|
}
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
#define REQUEST_RENDERER_UPDATE_ELEMENT2(ARGUMENTS, RENDERER_TYPE)\
|
|
RequestRendererUpdateElement<RENDERER_TYPE>(\
|
|
ARGUMENTS,\
|
|
ERROR_MESSAGE_PREFIX L"Renderer with the specified id has not been created.",\
|
|
ERROR_MESSAGE_PREFIX L"Renderer with the specified id is not of the expected type."\
|
|
)
|
|
|
|
#define REQUEST_RENDERER_UPDATE_ELEMENT(RENDERER_TYPE) REQUEST_RENDERER_UPDATE_ELEMENT2(arguments, RENDERER_TYPE)
|
|
|
|
void UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_SolidBorder(const ElementDesc_SolidBorder& arguments)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_SolidBorder(const ElementDesc_SolidBorder&)#"
|
|
REQUEST_RENDERER_UPDATE_ELEMENT(RendererType::SolidBorder);
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
void UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_SinkBorder(const ElementDesc_SinkBorder& arguments)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_SinkBorder(const ElementDesc_SinkBorder&)#"
|
|
REQUEST_RENDERER_UPDATE_ELEMENT(RendererType::SinkBorder);
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
void UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_SinkSplitter(const ElementDesc_SinkSplitter& arguments)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_SinkSplitter(const ElementDesc_SinkSplitter&)#"
|
|
REQUEST_RENDERER_UPDATE_ELEMENT(RendererType::SinkSplitter);
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
void UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_SolidBackground(const ElementDesc_SolidBackground& arguments)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_SolidBackground(const ElementDesc_SolidBackground&)#"
|
|
REQUEST_RENDERER_UPDATE_ELEMENT(RendererType::SolidBackground);
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
void UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_GradientBackground(const ElementDesc_GradientBackground& arguments)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_GradientBackground(const ElementDesc_GradientBackground&)#"
|
|
REQUEST_RENDERER_UPDATE_ELEMENT(RendererType::GradientBackground);
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
void UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_InnerShadow(const ElementDesc_InnerShadow& arguments)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_InnerShadow(const ElementDesc_InnerShadow&)#"
|
|
REQUEST_RENDERER_UPDATE_ELEMENT(RendererType::InnerShadow);
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
void UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_Polygon(const ElementDesc_Polygon& arguments)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_Polygon(const ElementDesc_Polygon&)#"
|
|
REQUEST_RENDERER_UPDATE_ELEMENT(RendererType::Polygon);
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
/***********************************************************************
|
|
IGuiRemoteProtocolMessages (Elements - SolidLabel)
|
|
***********************************************************************/
|
|
|
|
void UnitTestRemoteProtocol_Rendering::CalculateSolidLabelSizeIfNecessary(vint width, vint height, const ElementDesc_SolidLabel& arguments)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::CalculateSolidLabelSizeIfNecessary(vint, vint, const ElementDesc_SolidLabel&)#"
|
|
|
|
if (arguments.measuringRequest)
|
|
{
|
|
switch (arguments.measuringRequest.Value())
|
|
{
|
|
case ElementSolidLabelMeasuringRequest::FontHeight:
|
|
CHECK_ERROR(arguments.font, ERROR_MESSAGE_PREFIX L"Font is missing for calculating font height.");
|
|
if (!measuringForNextRendering.fontHeights)
|
|
{
|
|
measuringForNextRendering.fontHeights = Ptr(new List<ElementMeasuring_FontHeight>);
|
|
}
|
|
{
|
|
ElementMeasuring_FontHeight measuring;
|
|
measuring.fontFamily = arguments.font.Value().fontFamily;
|
|
measuring.fontSize = arguments.font.Value().size;
|
|
measuring.height = measuring.fontSize + 4;
|
|
measuringForNextRendering.fontHeights->Add(measuring);
|
|
}
|
|
break;
|
|
case ElementSolidLabelMeasuringRequest::TotalSize:
|
|
{
|
|
// font and text has already been verified exist in RequestRendererUpdateElement_SolidLabel
|
|
vint size = arguments.font.Value().size;
|
|
auto text = arguments.text.Value();
|
|
vint textWidth = 0;
|
|
vint textHeight = 0;
|
|
|
|
List<WString> lines;
|
|
{
|
|
List<Ptr<regex::RegexMatch>> matches;
|
|
regexCrLf.Split(text, true, matches);
|
|
|
|
if (matches.Count() == 0)
|
|
{
|
|
// when there is no text, measure a space
|
|
lines.Add(WString::Unmanaged(L" "));
|
|
}
|
|
else if (arguments.multiline)
|
|
{
|
|
// add all lines, and if any line is empty, measure a space
|
|
for (auto match : matches)
|
|
{
|
|
auto line = match->Result().Value();
|
|
lines.Add(line.Length() ? line : WString::Unmanaged(L" "));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
lines.Add(stream::GenerateToStream([&](stream::TextWriter& writer)
|
|
{
|
|
for (auto [match, index] : indexed(matches))
|
|
{
|
|
if (index > 0) writer.WriteChar(L' ');
|
|
auto line = match->Result().Value();
|
|
writer.WriteString(line);
|
|
}
|
|
}));
|
|
if(lines[0].Length() == 0)
|
|
{
|
|
// when there is no text, measure a space
|
|
lines[0] = WString::Unmanaged(L" ");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (arguments.wrapLine)
|
|
{
|
|
// width of the text is 0
|
|
// insert a line break when there is no space horizontally
|
|
vint totalLines = 0;
|
|
for (auto&& line : lines)
|
|
{
|
|
if (line.Length() == 0)
|
|
{
|
|
totalLines++;
|
|
continue;
|
|
}
|
|
|
|
double accumulatedWidth = 0;
|
|
for (vint i = 0; i < line.Length(); i++)
|
|
{
|
|
auto c = line[i];
|
|
auto w = (c < 128 ? 0.6 : 1) * size;
|
|
if (accumulatedWidth + w > width)
|
|
{
|
|
if (accumulatedWidth == 0)
|
|
{
|
|
totalLines++;
|
|
}
|
|
else
|
|
{
|
|
totalLines++;
|
|
accumulatedWidth = w;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
accumulatedWidth += w;
|
|
}
|
|
}
|
|
if (accumulatedWidth > 0)
|
|
{
|
|
totalLines++;
|
|
}
|
|
}
|
|
textHeight = 4 + size * totalLines;
|
|
}
|
|
else
|
|
{
|
|
// width of the text is width of the longest line
|
|
textWidth = (vint)(size * From(lines)
|
|
.Select([](const WString& line)
|
|
{
|
|
double sum = 0;
|
|
for (vint i = 0; i < line.Length(); i++)
|
|
{
|
|
auto c = line[i];
|
|
sum += (c < 128 ? 0.6 : 1);
|
|
}
|
|
return sum;
|
|
})
|
|
.Max());
|
|
textHeight = 4 + size * lines.Count();
|
|
}
|
|
|
|
if (!measuringForNextRendering.minSizes)
|
|
{
|
|
measuringForNextRendering.minSizes = Ptr(new List<ElementMeasuring_ElementMinSize>);
|
|
}
|
|
{
|
|
ElementMeasuring_ElementMinSize measuring;
|
|
measuring.id = arguments.id;
|
|
measuring.minSize = { textWidth,textHeight };
|
|
measuringForNextRendering.minSizes->Add(measuring);
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
CHECK_FAIL(L"Unknown value of ElementSolidLabelMeasuringRequest.");
|
|
}
|
|
}
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
void UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_SolidLabel(const ElementDesc_SolidLabel& arguments)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_SolidLabel(const ElementDesc_SolidLabel&)#"
|
|
auto element = arguments;
|
|
if (!element.font || !element.text)
|
|
{
|
|
vint index = loggedTrace.createdElements->Keys().IndexOf(element.id);
|
|
CHECK_ERROR(index != -1, ERROR_MESSAGE_PREFIX L"Renderer with the specified id has not been created.");
|
|
|
|
auto rendererType = loggedTrace.createdElements->Values()[index];
|
|
CHECK_ERROR(rendererType == RendererType::SolidLabel, ERROR_MESSAGE_PREFIX L"Renderer with the specified id is not of the expected type.");
|
|
|
|
index = lastElementDescs.Keys().IndexOf(arguments.id);
|
|
if (index != -1)
|
|
{
|
|
auto solidLabel = lastElementDescs.Values()[index].TryGet<ElementDesc_SolidLabel>();
|
|
CHECK_ERROR(solidLabel, ERROR_MESSAGE_PREFIX L"Renderer with the specified id is not of the expected type.");
|
|
if (!element.font) element.font = solidLabel->font;
|
|
if (!element.text) element.text = solidLabel->text;
|
|
}
|
|
else
|
|
{
|
|
if (!element.font) element.font = FontProperties();
|
|
if (!element.text) element.text = WString::Empty;
|
|
}
|
|
}
|
|
REQUEST_RENDERER_UPDATE_ELEMENT2(element, RendererType::SolidLabel);
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
/***********************************************************************
|
|
IGuiRemoteProtocolMessages (Elements - Image)
|
|
***********************************************************************/
|
|
|
|
WString UnitTestRemoteProtocol_Rendering::GetBinaryKeyFromBinary(stream::IStream& binary)
|
|
{
|
|
stream::MemoryStream base64WStringStream;
|
|
{
|
|
stream::UtfGeneralEncoder<wchar_t, char8_t> utf8ToWCharEncoder;
|
|
stream::EncoderStream utf8ToWCharStream(base64WStringStream, utf8ToWCharEncoder);
|
|
stream::Utf8Base64Encoder binaryToBase64Utf8Encoder;
|
|
stream::EncoderStream binaryToBase64Utf8Stream(utf8ToWCharStream, binaryToBase64Utf8Encoder);
|
|
binary.SeekFromBegin(0);
|
|
stream::CopyStream(binary, binaryToBase64Utf8Stream);
|
|
}
|
|
{
|
|
base64WStringStream.SeekFromBegin(0);
|
|
stream::StreamReader reader(base64WStringStream);
|
|
return reader.ReadToEnd();
|
|
}
|
|
}
|
|
|
|
WString UnitTestRemoteProtocol_Rendering::GetBinaryKeyFromImage(Ptr<INativeImage> image)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::GetBinaryKeyFromImage(Ptr<INativeImage>)#"
|
|
auto remoteImage = image.Cast<GuiRemoteGraphicsImage>();
|
|
CHECK_ERROR(remoteImage, ERROR_MESSAGE_PREFIX L"The image object must be GuiRemoteGraphicsImage.");
|
|
return GetBinaryKeyFromBinary(remoteImage->GetBinaryData());
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
ImageMetadata UnitTestRemoteProtocol_Rendering::MakeImageMetadata(const ImageCreation& arguments)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::MakeImageMetadata(const remoteprotocol::ImageCreation)#"
|
|
if (!cachedImageMetadatas)
|
|
{
|
|
cachedImageMetadatas = Ptr(new Base64ToImageMetadataMap);
|
|
for (auto resource : GetResourceManager()->GetLoadedResources())
|
|
{
|
|
if (auto xmlImageData = resource->GetValueByPath(WString::Unmanaged(L"UnitTestConfig/ImageData")).Cast<glr::xml::XmlDocument>())
|
|
{
|
|
for (auto elementImage : glr::xml::XmlGetElements(xmlImageData->rootElement, WString::Unmanaged(L"Image")))
|
|
{
|
|
WString path, format, frames = WString::Unmanaged(L"1"), width, height;
|
|
|
|
auto attPath = glr::xml::XmlGetAttribute(elementImage.Obj(), WString::Unmanaged(L"Path"));
|
|
auto attFormat = glr::xml::XmlGetAttribute(elementImage.Obj(), WString::Unmanaged(L"Format"));
|
|
auto attFrames = glr::xml::XmlGetAttribute(elementImage.Obj(), WString::Unmanaged(L"Frames"));
|
|
auto attWidth = glr::xml::XmlGetAttribute(elementImage.Obj(), WString::Unmanaged(L"Width"));
|
|
auto attHeight = glr::xml::XmlGetAttribute(elementImage.Obj(), WString::Unmanaged(L"Height"));
|
|
|
|
CHECK_ERROR(attPath, ERROR_MESSAGE_PREFIX L"Missing Path attribute in Image element in an UnitTestConfig/ImageData.");
|
|
CHECK_ERROR(attFormat, ERROR_MESSAGE_PREFIX L"Missing Format attribute in Image element in an UnitTestConfig/ImageData.");
|
|
CHECK_ERROR(attWidth, ERROR_MESSAGE_PREFIX L"Missing Width attribute in Image element in an UnitTestConfig/ImageData.");
|
|
CHECK_ERROR(attHeight, ERROR_MESSAGE_PREFIX L"Missing Height attribute in Image element in an UnitTestConfig/ImageData.");
|
|
|
|
path = attPath->value.value;
|
|
format = attFormat->value.value;
|
|
width = attWidth->value.value;
|
|
height = attHeight->value.value;
|
|
if (attFrames) frames = attFrames->value.value;
|
|
|
|
vint valueFrames = wtoi(frames);
|
|
vint valueWidth = wtoi(width);
|
|
vint valueHeight = wtoi(height);
|
|
|
|
CHECK_ERROR(itow(valueFrames) == frames, ERROR_MESSAGE_PREFIX L"Frames attribute must be an integer in Image element in an UnitTestConfig/ImageData.");
|
|
CHECK_ERROR(itow(valueWidth) == width, ERROR_MESSAGE_PREFIX L"Width attribute must be an integer in Image element in an UnitTestConfig/ImageData.");
|
|
CHECK_ERROR(itow(valueHeight) == height, ERROR_MESSAGE_PREFIX L"Height attribute must be an integer in Image element in an UnitTestConfig/ImageData.");
|
|
|
|
auto imageData = resource->GetImageByPath(path);
|
|
WString binaryKey = GetBinaryKeyFromImage(imageData->GetImage());
|
|
|
|
if (!cachedImageMetadatas->Keys().Contains(binaryKey))
|
|
{
|
|
ImageMetadata imageMetadata;
|
|
imageMetadata.id = -1;
|
|
imageMetadata.frames = Ptr(new List<ImageFrameMetadata>);
|
|
{
|
|
auto node = Ptr(new glr::json::JsonString);
|
|
node->content.value = format;
|
|
ConvertJsonToCustomType(node, imageMetadata.format);
|
|
}
|
|
for (vint frame = 0; frame < valueFrames; frame++)
|
|
{
|
|
imageMetadata.frames->Add({ {valueWidth,valueHeight} });
|
|
}
|
|
|
|
cachedImageMetadatas->Add(binaryKey, imageMetadata);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
auto binaryKey = GetBinaryKeyFromBinary(*arguments.imageData.Obj());
|
|
vint binaryIndex = cachedImageMetadatas->Keys().IndexOf(binaryKey);
|
|
CHECK_ERROR(binaryIndex != -1, ERROR_MESSAGE_PREFIX L"The image is not registered in any UnitTestConfig/ImageData.");
|
|
auto metadata = cachedImageMetadatas->Values()[binaryIndex];
|
|
metadata.id = arguments.id;
|
|
|
|
loggedTrace.imageCreations->Add(arguments);
|
|
loggedTrace.imageMetadatas->Add(metadata);
|
|
return metadata;
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
void UnitTestRemoteProtocol_Rendering::Impl_ImageCreated(vint id, const ImageCreation& arguments)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_ImageCreated(vint, const vint&)#"
|
|
CHECK_ERROR(!loggedTrace.imageMetadatas->Keys().Contains(arguments.id), ERROR_MESSAGE_PREFIX L"Image with the specified id has been created or used.");
|
|
this->GetEvents()->RespondImageCreated(id, MakeImageMetadata(arguments));
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
void UnitTestRemoteProtocol_Rendering::Impl_ImageDestroyed(const vint& arguments)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_ImageDestroyed(const vint&)#"
|
|
CHECK_ERROR(loggedTrace.imageMetadatas->Keys().Contains(arguments), ERROR_MESSAGE_PREFIX L"Image with the specified id has not been created.");
|
|
CHECK_ERROR(!removedImageIds.Contains(arguments), ERROR_MESSAGE_PREFIX L"Image with the specified id has been destroyed.");
|
|
removedImageIds.Add(arguments);
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
void UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_ImageFrame(const ElementDesc_ImageFrame& arguments)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::presentation::unittest::UnitTestRemoteProtocol_Rendering::Impl_RendererUpdateElement_ImageFrame(const ElementDesc_ImageFrame&)#"
|
|
if (arguments.imageCreation)
|
|
{
|
|
auto&& imageCreation = arguments.imageCreation.Value();
|
|
if (!imageCreation.imageDataOmitted)
|
|
{
|
|
CHECK_ERROR(arguments.imageId && arguments.imageId.Value() != !imageCreation.id, ERROR_MESSAGE_PREFIX L"It should satisfy that (arguments.imageId.Value()id == imageCreation.id).");
|
|
CHECK_ERROR(!loggedTrace.imageMetadatas->Keys().Contains(imageCreation.id), ERROR_MESSAGE_PREFIX L"Image with the specified id has been created.");
|
|
CHECK_ERROR(imageCreation.imageData, ERROR_MESSAGE_PREFIX L"When imageDataOmitted == false, imageData should not be null.");
|
|
if (!measuringForNextRendering.createdImages)
|
|
{
|
|
measuringForNextRendering.createdImages = Ptr(new List<ImageMetadata>);
|
|
}
|
|
measuringForNextRendering.createdImages->Add(MakeImageMetadata(imageCreation));
|
|
}
|
|
else
|
|
{
|
|
CHECK_ERROR(!imageCreation.imageData, ERROR_MESSAGE_PREFIX L"When imageDataOmitted == true, imageData should be null.");
|
|
}
|
|
}
|
|
else if (arguments.imageId)
|
|
{
|
|
CHECK_ERROR(loggedTrace.imageMetadatas->Keys().Contains(arguments.imageId.Value()), ERROR_MESSAGE_PREFIX L"Image with the specified id has not been created.");
|
|
}
|
|
|
|
auto element = arguments;
|
|
element.imageCreation.Reset();
|
|
REQUEST_RENDERER_UPDATE_ELEMENT2(element, RendererType::ImageFrame);
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
#undef REQUEST_RENDERER_UPDATE_ELEMENT
|
|
#undef REQUEST_RENDERER_UPDATE_ELEMENT2
|
|
}
|
|
|
|
|
|
/***********************************************************************
|
|
.\GUIUNITTESTPROTOCOL_SHARED.CPP
|
|
***********************************************************************/
|
|
|
|
namespace vl::presentation::unittest
|
|
{
|
|
using namespace vl::collections;
|
|
|
|
/***********************************************************************
|
|
UnitTestScreenConfig
|
|
***********************************************************************/
|
|
|
|
void UnitTestScreenConfig::FastInitialize(vint width, vint height, vint taskBarHeight)
|
|
{
|
|
executablePath = WString::Unmanaged(L"/GacUI/Remote/Protocol/UnitTest.exe");
|
|
customFramePadding = { 8,8,8,8 };
|
|
|
|
fontConfig.defaultFont.fontFamily = WString::Unmanaged(L"GacUI Default Font");
|
|
fontConfig.defaultFont.size = 12;
|
|
fontConfig.supportedFonts = Ptr(new List<WString>());
|
|
fontConfig.supportedFonts->Add(fontConfig.defaultFont.fontFamily);
|
|
|
|
screenConfig.bounds = { 0,0,width,height };
|
|
screenConfig.clientBounds = { 0,0,width,(height - taskBarHeight) };
|
|
screenConfig.scalingX = 1;
|
|
screenConfig.scalingY = 1;
|
|
}
|
|
}
|
|
|
|
/***********************************************************************
|
|
.\GUIUNITTESTUTILITIES.CPP
|
|
***********************************************************************/
|
|
|
|
namespace vl::presentation::controls
|
|
{
|
|
extern bool GACUI_UNITTEST_ONLY_SKIP_THREAD_LOCAL_STORAGE_DISPOSE_STORAGES;
|
|
extern bool GACUI_UNITTEST_ONLY_SKIP_TYPE_AND_PLUGIN_LOAD_UNLOAD;
|
|
}
|
|
|
|
namespace vl::presentation::unittest
|
|
{
|
|
const UnitTestFrameworkConfig* globalUnitTestFrameworkConfig = nullptr;
|
|
|
|
const UnitTestFrameworkConfig& GetUnitTestFrameworkConfig()
|
|
{
|
|
CHECK_ERROR(globalUnitTestFrameworkConfig, L"vl::presentation::unittest::GetUnitTestFrameworkConfig()#GacUIUnitTest_Initialize has not been called.");
|
|
return *globalUnitTestFrameworkConfig;
|
|
}
|
|
}
|
|
|
|
namespace vl::presentation::GuiHostedController_UnitTestHelper
|
|
{
|
|
extern bool ExceptionOccuredUnderUnitTestReleaseMode();
|
|
}
|
|
|
|
using namespace vl;
|
|
using namespace vl::collections;
|
|
using namespace vl::filesystem;
|
|
using namespace vl::reflection::description;
|
|
using namespace vl::glr::json;
|
|
using namespace vl::presentation;
|
|
using namespace vl::presentation::remoteprotocol;
|
|
using namespace vl::presentation::controls;
|
|
using namespace vl::presentation::unittest;
|
|
using namespace vl::presentation::GuiHostedController_UnitTestHelper;
|
|
|
|
class UnitTestContextImpl : public Object, public virtual IUnitTestContext
|
|
{
|
|
UnitTestRemoteProtocol* protocol = nullptr;
|
|
|
|
public:
|
|
UnitTestContextImpl(UnitTestRemoteProtocol* _protocol)
|
|
: protocol(_protocol)
|
|
{
|
|
}
|
|
|
|
UnitTestRemoteProtocol* GetProtocol()
|
|
{
|
|
return protocol;
|
|
}
|
|
};
|
|
|
|
UnitTestMainFunc guiMainProxy;
|
|
UnitTestContextImpl* guiMainUnitTestContext = nullptr;
|
|
|
|
void GacUIUnitTest_Initialize(const UnitTestFrameworkConfig* config)
|
|
{
|
|
CHECK_ERROR(config, L"GacUIUnitTest_Initialize()#Argument config should not be null.");
|
|
globalUnitTestFrameworkConfig = config;
|
|
|
|
GACUI_UNITTEST_ONLY_SKIP_THREAD_LOCAL_STORAGE_DISPOSE_STORAGES = true;
|
|
GACUI_UNITTEST_ONLY_SKIP_TYPE_AND_PLUGIN_LOAD_UNLOAD = true;
|
|
|
|
GetGlobalTypeManager()->Load();
|
|
GetPluginManager()->Load(true, false);
|
|
}
|
|
|
|
void GacUIUnitTest_Finalize()
|
|
{
|
|
ResetGlobalTypeManager();
|
|
GetPluginManager()->Unload(true, false);
|
|
DestroyPluginManager();
|
|
ThreadLocalStorage::DisposeStorages();
|
|
|
|
GACUI_UNITTEST_ONLY_SKIP_THREAD_LOCAL_STORAGE_DISPOSE_STORAGES = false;
|
|
GACUI_UNITTEST_ONLY_SKIP_TYPE_AND_PLUGIN_LOAD_UNLOAD = false;
|
|
globalUnitTestFrameworkConfig = nullptr;
|
|
}
|
|
|
|
void GacUIUnitTest_SetGuiMainProxy(const UnitTestMainFunc& proxy)
|
|
{
|
|
guiMainProxy = proxy;
|
|
}
|
|
|
|
void GacUIUnitTest_LinkGuiMainProxy(const UnitTestLinkFunc& proxy)
|
|
{
|
|
auto previousMainProxy = guiMainProxy;
|
|
GacUIUnitTest_SetGuiMainProxy([=](UnitTestRemoteProtocol* protocol, IUnitTestContext* context)
|
|
{
|
|
proxy(protocol, context, previousMainProxy);
|
|
});
|
|
}
|
|
|
|
File GacUIUnitTest_PrepareSnapshotFile(const WString& appName, const WString& extension)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"GacUIUnitTest_PrepareSnapshotFile(const WString&, const WString&)#"
|
|
Folder snapshotFolder = GetUnitTestFrameworkConfig().snapshotFolder;
|
|
CHECK_ERROR(snapshotFolder.Exists(), ERROR_MESSAGE_PREFIX L"UnitTestFrameworkConfig::snapshotFolder does not point to an existing folder.");
|
|
|
|
File snapshotFile = snapshotFolder.GetFilePath() / (appName + extension);
|
|
{
|
|
auto pathPrefix = snapshotFolder.GetFilePath().GetFullPath() + WString::FromChar(FilePath::Delimiter);
|
|
auto snapshotPath = snapshotFile.GetFilePath().GetFullPath();
|
|
CHECK_ERROR(
|
|
snapshotPath.Length() > pathPrefix.Length() && snapshotPath.Left(pathPrefix.Length()) == pathPrefix,
|
|
ERROR_MESSAGE_PREFIX L"Argument appName should specify a file that is inside UnitTestFrameworkConfig::snapshotFolder"
|
|
);
|
|
Folder snapshotFileFolder = snapshotFile.GetFilePath().GetFolder();
|
|
if (!snapshotFileFolder.Exists())
|
|
{
|
|
CHECK_ERROR(snapshotFileFolder.Create(true), ERROR_MESSAGE_PREFIX L"Failed to create the folder to contain the snapshot file specified by argument appName.");
|
|
}
|
|
}
|
|
|
|
return snapshotFile;
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
void GacUIUnitTest_WriteSnapshotFileIfChanged(File& snapshotFile, const WString& textLog)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"GacUIUnitTest_WriteSnapshotFileIfChanged(File&, const WString&)#"
|
|
bool skipWriting = false;
|
|
if (snapshotFile.Exists())
|
|
{
|
|
auto previousLog = snapshotFile.ReadAllTextByBom();
|
|
if (previousLog == textLog)
|
|
{
|
|
skipWriting = true;
|
|
}
|
|
}
|
|
if (!skipWriting)
|
|
{
|
|
bool succeeded = snapshotFile.WriteAllText(textLog, true, stream::BomEncoder::Utf8);
|
|
CHECK_ERROR(succeeded, ERROR_MESSAGE_PREFIX L"Failed to write the snapshot file.");
|
|
}
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
void GacUIUnitTest_LogUI(const WString& appName, UnitTestRemoteProtocol& unitTestProtocol)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"GacUIUnitTest_LogUI(const WString&, UnitTestRemoteProtocol&)#"
|
|
File snapshotFile = GacUIUnitTest_PrepareSnapshotFile(appName, WString::Unmanaged(L".json"));
|
|
|
|
JsonFormatting formatting;
|
|
formatting.spaceAfterColon = true;
|
|
formatting.spaceAfterComma = true;
|
|
formatting.crlf = true;
|
|
formatting.compact = true;
|
|
|
|
auto jsonLog = remoteprotocol::ConvertCustomTypeToJson(unitTestProtocol.GetLoggedTrace());
|
|
auto textLog = JsonToString(jsonLog, formatting);
|
|
{
|
|
remoteprotocol::UnitTest_RenderingTrace deserialized;
|
|
remoteprotocol::ConvertJsonToCustomType(jsonLog, deserialized);
|
|
auto jsonLog2 = remoteprotocol::ConvertCustomTypeToJson(deserialized);
|
|
auto textLog2 = JsonToString(jsonLog2, formatting);
|
|
CHECK_ERROR(textLog == textLog2, ERROR_MESSAGE_PREFIX L"Serialization and deserialization doesn't match.");
|
|
}
|
|
|
|
GacUIUnitTest_WriteSnapshotFileIfChanged(snapshotFile, textLog);
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
void GacUIUnitTest_LogCommands(const WString& appName, UnitTestRemoteProtocol& unitTestProtocol)
|
|
{
|
|
File snapshotFile = GacUIUnitTest_PrepareSnapshotFile(appName, WString::Unmanaged(L"[commands].txt"));
|
|
|
|
JsonFormatting formatting;
|
|
formatting.spaceAfterColon = true;
|
|
formatting.spaceAfterComma = true;
|
|
formatting.crlf = false;
|
|
formatting.compact = true;
|
|
|
|
auto textLog = stream::GenerateToStream([&unitTestProtocol, &formatting](stream::TextWriter& writer)
|
|
{
|
|
auto&& loggedFrames = unitTestProtocol.GetLoggedFrames();
|
|
for (auto loggedFrame : loggedFrames)
|
|
{
|
|
writer.WriteLine(L"========================================");
|
|
writer.WriteLine(itow(loggedFrame->frameId));
|
|
writer.WriteLine(L"========================================");
|
|
for (auto&& commandLog : loggedFrame->renderingCommandsLog)
|
|
{
|
|
writer.WriteLine(commandLog);
|
|
}
|
|
};
|
|
});
|
|
|
|
GacUIUnitTest_WriteSnapshotFileIfChanged(snapshotFile, textLog);
|
|
}
|
|
|
|
void GacUIUnitTest_LogDiffs(const WString& appName, UnitTestRemoteProtocol& unitTestProtocol)
|
|
{
|
|
File snapshotFile = GacUIUnitTest_PrepareSnapshotFile(appName, WString::Unmanaged(L"[diffs].txt"));
|
|
|
|
JsonFormatting formatting;
|
|
formatting.spaceAfterColon = true;
|
|
formatting.spaceAfterComma = true;
|
|
formatting.crlf = false;
|
|
formatting.compact = true;
|
|
|
|
auto textLog = stream::GenerateToStream([&unitTestProtocol, &formatting](stream::TextWriter& writer)
|
|
{
|
|
Ptr<RenderingDom> dom;
|
|
DomIndex domIndex;
|
|
auto&& loggedFrames = unitTestProtocol.GetLoggedFrames();
|
|
for (auto loggedFrame : loggedFrames)
|
|
{
|
|
writer.WriteLine(L"========================================");
|
|
writer.WriteLine(itow(loggedFrame->frameId));
|
|
writer.WriteLine(L"========================================");
|
|
|
|
if (!dom)
|
|
{
|
|
dom = loggedFrame->renderingDom;
|
|
BuildDomIndex(dom, domIndex);
|
|
|
|
List<Pair<vint, Ptr<RenderingDom>>> lines;
|
|
lines.Add({ 0,dom });
|
|
for (vint i = 0; i < lines.Count(); i++)
|
|
{
|
|
for (vint j = 0; j < lines[i].key; j++)
|
|
{
|
|
writer.WriteString(L" ");
|
|
}
|
|
|
|
auto line = lines[i].value;
|
|
writer.WriteString(itow(line->id));
|
|
writer.WriteString(L": ");
|
|
|
|
auto jsonLog = remoteprotocol::ConvertCustomTypeToJson(line->content);
|
|
writer.WriteLine(JsonToString(jsonLog, formatting));
|
|
|
|
if (line->children)
|
|
{
|
|
for (auto child : *line->children.Obj())
|
|
{
|
|
lines.Add({ lines[i].key + 1,child });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
DomIndex nextDomIndex;
|
|
BuildDomIndex(loggedFrame->renderingDom, nextDomIndex);
|
|
|
|
Ptr<List<RenderingDom_Diff>> diffList;
|
|
if (loggedFrame->renderingDiffs)
|
|
{
|
|
diffList = loggedFrame->renderingDiffs.Value().diffsInOrder;
|
|
}
|
|
else
|
|
{
|
|
RenderingDom_DiffsInOrder diffs;
|
|
DiffDom(dom, domIndex, loggedFrame->renderingDom, nextDomIndex, diffs);
|
|
diffList = diffs.diffsInOrder;
|
|
|
|
auto copiedDom = CopyDom(dom);
|
|
DomIndex copiedDomIndex;
|
|
BuildDomIndex(copiedDom, copiedDomIndex);
|
|
UpdateDomInplace(copiedDom, copiedDomIndex, diffs);
|
|
|
|
auto expectedJson = JsonToString(remoteprotocol::ConvertCustomTypeToJson(loggedFrame->renderingDom));
|
|
auto actualJson = JsonToString(remoteprotocol::ConvertCustomTypeToJson(copiedDom));
|
|
TEST_ASSERT(actualJson == expectedJson);
|
|
}
|
|
|
|
if (diffList)
|
|
{
|
|
for (auto&& diff : *diffList.Obj())
|
|
{
|
|
auto jsonLog = remoteprotocol::ConvertCustomTypeToJson(diff);
|
|
writer.WriteLine(JsonToString(jsonLog, formatting));
|
|
}
|
|
}
|
|
|
|
dom = loggedFrame->renderingDom;
|
|
domIndex = std::move(nextDomIndex);
|
|
}
|
|
};
|
|
});
|
|
|
|
GacUIUnitTest_WriteSnapshotFileIfChanged(snapshotFile, textLog);
|
|
}
|
|
|
|
void GacUIUnitTest_Start(const WString& appName, Nullable<UnitTestScreenConfig> config)
|
|
{
|
|
UnitTestScreenConfig globalConfig;
|
|
if (config)
|
|
{
|
|
globalConfig = config.Value();
|
|
}
|
|
else
|
|
{
|
|
globalConfig.FastInitialize(1024, 768);
|
|
}
|
|
|
|
// Renderer
|
|
UnitTestRemoteProtocol unitTestProtocol(appName, globalConfig);
|
|
auto jsonParser = Ptr(new glr::json::Parser);
|
|
|
|
// Data Processing in Renderer
|
|
channeling::GuiRemoteJsonChannelFromProtocol channelReceiver(unitTestProtocol.GetProtocol());
|
|
channeling::GuiRemoteJsonChannelStringDeserializer channelJsonDeserializer(&channelReceiver, jsonParser);
|
|
channeling::GuiRemoteUtfStringChannelDeserializer<wchar_t, char8_t> channelUtf8Deserializer(&channelJsonDeserializer);
|
|
|
|
// Boundary between Binaries
|
|
|
|
// Data Processing in Core
|
|
channeling::GuiRemoteUtfStringChannelSerializer<wchar_t, char8_t> channelUtf8Serializer(&channelUtf8Deserializer);
|
|
channeling::GuiRemoteJsonChannelStringSerializer channelJsonSerializer(&channelUtf8Serializer, jsonParser);
|
|
|
|
// Boundary between threads
|
|
|
|
channeling::GuiRemoteProtocolFromJsonChannel channelSender(&channelJsonSerializer);
|
|
|
|
// Core
|
|
repeatfiltering::GuiRemoteProtocolFilterVerifier verifierProtocol(
|
|
globalConfig.useChannel == UnitTestRemoteChannel::None
|
|
? unitTestProtocol.GetProtocol()
|
|
: &channelSender
|
|
);
|
|
repeatfiltering::GuiRemoteProtocolFilter filteredProtocol(&verifierProtocol);
|
|
GuiRemoteProtocolDomDiffConverter diffConverterProtocol(&filteredProtocol);
|
|
|
|
UnitTestContextImpl unitTestContext(&unitTestProtocol);
|
|
guiMainUnitTestContext = &unitTestContext;
|
|
SetupRemoteNativeController(
|
|
globalConfig.useDomDiff
|
|
? static_cast<IGuiRemoteProtocol*>(&diffConverterProtocol)
|
|
: &filteredProtocol
|
|
);
|
|
GacUIUnitTest_SetGuiMainProxy({});
|
|
TEST_ASSERT(!ExceptionOccuredUnderUnitTestReleaseMode());
|
|
|
|
GacUIUnitTest_LogUI(appName, unitTestProtocol);
|
|
if (!globalConfig.useDomDiff)
|
|
{
|
|
GacUIUnitTest_LogCommands(appName, unitTestProtocol);
|
|
}
|
|
GacUIUnitTest_LogDiffs(appName, unitTestProtocol);
|
|
}
|
|
|
|
template<typename T>
|
|
void RunInNewThread(T&& threadProc)
|
|
{
|
|
Thread::CreateAndStart([threadProc]()
|
|
{
|
|
try
|
|
{
|
|
threadProc();
|
|
}
|
|
catch (const Exception& e)
|
|
{
|
|
(void)e;
|
|
throw;
|
|
}
|
|
catch (const Error& e)
|
|
{
|
|
(void)e;
|
|
throw;
|
|
}
|
|
});
|
|
}
|
|
|
|
void GacUIUnitTest_StartAsync(const WString& appName, Nullable<UnitTestScreenConfig> config)
|
|
{
|
|
TEST_ASSERT(config && config.Value().useChannel == UnitTestRemoteChannel::Async);
|
|
|
|
// Renderer
|
|
UnitTestRemoteProtocol unitTestProtocol(appName, config.Value());
|
|
auto jsonParser = Ptr(new glr::json::Parser);
|
|
|
|
// Data Processing in Renderer
|
|
channeling::GuiRemoteJsonChannelFromProtocol channelReceiver(unitTestProtocol.GetProtocol());
|
|
channeling::GuiRemoteJsonChannelStringDeserializer channelJsonDeserializer(&channelReceiver, jsonParser);
|
|
channeling::GuiRemoteUtfStringChannelDeserializer<wchar_t, char8_t> channelUtf8Deserializer(&channelJsonDeserializer);
|
|
|
|
// Boundary between Binaries
|
|
|
|
// Data Processing in Core
|
|
channeling::GuiRemoteUtfStringChannelSerializer<wchar_t, char8_t> channelUtf8Serializer(&channelUtf8Deserializer);
|
|
channeling::GuiRemoteJsonChannelStringSerializer channelJsonSerializer(&channelUtf8Serializer, jsonParser);
|
|
|
|
// Boundary between threads
|
|
|
|
channeling::GuiRemoteProtocolAsyncJsonChannelSerializer asyncChannelSender;
|
|
asyncChannelSender.Start(
|
|
&channelJsonSerializer,
|
|
[&unitTestProtocol, config](channeling::GuiRemoteProtocolAsyncJsonChannelSerializer* channel)
|
|
{
|
|
channeling::GuiRemoteProtocolFromJsonChannel channelSender(channel);
|
|
|
|
// Core
|
|
repeatfiltering::GuiRemoteProtocolFilterVerifier verifierProtocol(&channelSender);
|
|
repeatfiltering::GuiRemoteProtocolFilter filteredProtocol(&verifierProtocol);
|
|
GuiRemoteProtocolDomDiffConverter diffConverterProtocol(&filteredProtocol);
|
|
|
|
UnitTestContextImpl unitTestContext(&unitTestProtocol);
|
|
guiMainUnitTestContext = &unitTestContext;
|
|
SetupRemoteNativeController(
|
|
config.Value().useDomDiff
|
|
? static_cast<IGuiRemoteProtocol*>(&diffConverterProtocol)
|
|
: &filteredProtocol
|
|
);
|
|
GacUIUnitTest_SetGuiMainProxy({});
|
|
},
|
|
[](
|
|
channeling::GuiRemoteProtocolAsyncJsonChannelSerializer::TChannelThreadProc channelThreadProc,
|
|
channeling::GuiRemoteProtocolAsyncJsonChannelSerializer::TUIThreadProc uiThreadProc
|
|
)
|
|
{
|
|
RunInNewThread(channelThreadProc);
|
|
RunInNewThread(uiThreadProc);
|
|
});
|
|
|
|
asyncChannelSender.WaitForStopped();
|
|
TEST_ASSERT(!ExceptionOccuredUnderUnitTestReleaseMode());
|
|
|
|
GacUIUnitTest_LogUI(appName, unitTestProtocol);
|
|
if (!config.Value().useDomDiff)
|
|
{
|
|
GacUIUnitTest_LogCommands(appName, unitTestProtocol);
|
|
}
|
|
GacUIUnitTest_LogDiffs(appName, unitTestProtocol);
|
|
}
|
|
|
|
void GacUIUnitTest_Start_WithResourceAsText(const WString& appName, Nullable<UnitTestScreenConfig> config, const WString& resourceText)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"GacUIUnitTest_Start_WithResourceAsText(const WString&, Nullable<UnitTestScreenConfig>, const WString&)#"
|
|
auto previousMainProxy = guiMainProxy;
|
|
GacUIUnitTest_LinkGuiMainProxy([=](UnitTestRemoteProtocol* protocol, IUnitTestContext* context, const UnitTestMainFunc& previousMainProxy)
|
|
{
|
|
auto resource = GacUIUnitTest_CompileAndLoad(resourceText);
|
|
{
|
|
auto workflow = resource->GetStringByPath(L"UnitTest/Workflow");
|
|
File snapshotFile = GacUIUnitTest_PrepareSnapshotFile(
|
|
appName,
|
|
#ifdef VCZH_64
|
|
WString::Unmanaged(L"[x64].txt")
|
|
#else
|
|
WString::Unmanaged(L"[x86].txt")
|
|
#endif
|
|
);
|
|
|
|
bool skipWriting = false;
|
|
if (snapshotFile.Exists())
|
|
{
|
|
auto previousLog = snapshotFile.ReadAllTextByBom();
|
|
if (previousLog == workflow)
|
|
{
|
|
skipWriting = true;
|
|
}
|
|
}
|
|
if (!skipWriting)
|
|
{
|
|
bool succeeded = snapshotFile.WriteAllText(workflow, true, stream::BomEncoder::Utf8);
|
|
CHECK_ERROR(succeeded, ERROR_MESSAGE_PREFIX L"Failed to write the snapshot file.");
|
|
}
|
|
}
|
|
previousMainProxy(protocol, context);
|
|
});
|
|
|
|
if (config && config.Value().useChannel == UnitTestRemoteChannel::Async)
|
|
{
|
|
GacUIUnitTest_StartAsync(appName, config);
|
|
}
|
|
else
|
|
{
|
|
GacUIUnitTest_Start(appName, config);
|
|
}
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
void GacUIUnitTest_PrintErrors(GuiResourceError::List& errors)
|
|
{
|
|
for (auto&& error : errors)
|
|
{
|
|
TEST_PRINT(L"Error in resource: " + error.location.resourcePath);
|
|
TEST_PRINT(L" ROW: " + itow(error.position.row) + L", COLUMN: " + itow(error.position.column));
|
|
TEST_PRINT(L" REASON: " + error.message);
|
|
}
|
|
}
|
|
|
|
Ptr<GuiResource> GacUIUnitTest_CompileAndLoad(const WString& xmlResource)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"GacUIUnitTest_CompileAndLoad(const WString&)#"
|
|
Ptr<GuiResource> resource;
|
|
GuiResourceError::List errors;
|
|
{
|
|
auto resourcePath = (GetUnitTestFrameworkConfig().resourceFolder / L"Resource.xml").GetFullPath();
|
|
auto parser = GetParserManager()->GetParser<glr::xml::XmlDocument>(L"XML");
|
|
auto xml = parser->Parse({ WString::Empty,resourcePath }, xmlResource, errors);
|
|
if(!xml || errors.Count()> 0)
|
|
{
|
|
GacUIUnitTest_PrintErrors(errors);
|
|
CHECK_FAIL(ERROR_MESSAGE_PREFIX L"Failed to parse XML resource.");
|
|
}
|
|
|
|
resource = GuiResource::LoadFromXml(xml, resourcePath, GetFolderPath(resourcePath), errors);
|
|
if (!resource || errors.Count() > 0)
|
|
{
|
|
GacUIUnitTest_PrintErrors(errors);
|
|
CHECK_FAIL(ERROR_MESSAGE_PREFIX L"Failed to load XML resource.");
|
|
}
|
|
}
|
|
|
|
auto precompiledFolder = resource->Precompile(
|
|
#ifdef VCZH_64
|
|
GuiResourceCpuArchitecture::x64,
|
|
#else
|
|
GuiResourceCpuArchitecture::x86,
|
|
#endif
|
|
nullptr,
|
|
errors
|
|
);
|
|
if (!precompiledFolder || errors.Count() > 0)
|
|
{
|
|
GacUIUnitTest_PrintErrors(errors);
|
|
CHECK_FAIL(ERROR_MESSAGE_PREFIX L"Failed to precompile XML resource.");
|
|
}
|
|
|
|
auto compiledWorkflow = precompiledFolder->GetValueByPath(WString::Unmanaged(L"Workflow/InstanceClass")).Cast<GuiInstanceCompiledWorkflow>();
|
|
CHECK_ERROR(compiledWorkflow, ERROR_MESSAGE_PREFIX L"Failed to compile generated Workflow script.");
|
|
CHECK_ERROR(compiledWorkflow->assembly, ERROR_MESSAGE_PREFIX L"Failed to load Workflow assembly.");
|
|
|
|
{
|
|
WString text;
|
|
auto& codes = compiledWorkflow->assembly->insAfterCodegen->moduleCodes;
|
|
for (auto [code, codeIndex] : indexed(codes))
|
|
{
|
|
text += L"================================(" + itow(codeIndex + 1) + L"/" + itow(codes.Count()) + L")================================\r\n";
|
|
text += code + L"\r\n";
|
|
}
|
|
resource->CreateValueByPath(
|
|
WString::Unmanaged(L"UnitTest/Workflow"),
|
|
WString::Unmanaged(L"Text"),
|
|
Ptr(new GuiTextData(text))
|
|
);
|
|
}
|
|
|
|
GetResourceManager()->SetResource(resource, errors, GuiResourceUsage::InstanceClass);
|
|
CHECK_ERROR(errors.Count() == 0, ERROR_MESSAGE_PREFIX L"Failed to load compiled XML resource.");
|
|
|
|
return resource;
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
void GuiMain()
|
|
{
|
|
if (guiMainUnitTestContext)
|
|
{
|
|
guiMainProxy(guiMainUnitTestContext->GetProtocol(), guiMainUnitTestContext);
|
|
}
|
|
else
|
|
{
|
|
guiMainProxy(nullptr, nullptr);
|
|
}
|
|
guiMainUnitTestContext = nullptr;
|
|
}
|