mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-25 13:01:05 +01:00
656 lines
24 KiB
C++
656 lines
24 KiB
C++
#include "runtime/roblox_text_display_state.h"
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <algorithm>
|
|
#include <cstdint>
|
|
#include <limits>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "runtime/roblox_text_surface_overlay.h"
|
|
|
|
namespace mocktail {
|
|
namespace runtime {
|
|
namespace {
|
|
|
|
RobloxTextDisplayUpdate Show(uint64_t generation, const std::string& text,
|
|
int32_t cursor_utf16 = 0,
|
|
int32_t text_input_type = 1) {
|
|
RobloxTextDisplayUpdate update;
|
|
update.event = RobloxTextDisplayEvent::kShow;
|
|
update.generation = generation;
|
|
update.area_x = 120;
|
|
update.area_y = 80;
|
|
update.area_width = 360;
|
|
update.area_height = 44;
|
|
update.text_input_type = text_input_type;
|
|
update.utf8 = text.data();
|
|
update.utf8_size = text.size();
|
|
update.cursor_utf16 = cursor_utf16;
|
|
update.selection_begin_utf16 = cursor_utf16;
|
|
update.selection_end_utf16 = cursor_utf16;
|
|
return update;
|
|
}
|
|
|
|
struct AlphaBounds {
|
|
int minimum_x = std::numeric_limits<int>::max();
|
|
int maximum_x = -1;
|
|
|
|
bool valid() const { return maximum_x >= minimum_x; }
|
|
};
|
|
|
|
AlphaBounds FindAlphaBounds(const MocktailTextOverlayFrameInfo& frame,
|
|
const std::vector<std::uint8_t>& rgba) {
|
|
AlphaBounds bounds;
|
|
for (std::uint32_t y = 0; y < frame.height; ++y) {
|
|
for (std::uint32_t x = 0; x < frame.width; ++x) {
|
|
const std::size_t alpha =
|
|
(static_cast<std::size_t>(y) * frame.width + x) * 4 + 3;
|
|
if (alpha < rgba.size() && rgba[alpha] != 0) {
|
|
bounds.minimum_x = std::min(bounds.minimum_x, static_cast<int>(x));
|
|
bounds.maximum_x = std::max(bounds.maximum_x, static_cast<int>(x));
|
|
}
|
|
}
|
|
}
|
|
return bounds;
|
|
}
|
|
|
|
TEST(RobloxTextDisplayStateTest, PreservesValidNativeTextBoxGeometry) {
|
|
const std::string text = "hello";
|
|
const RobloxTextDisplayUpdate update = Show(1, text, 5);
|
|
|
|
const RobloxTextOverlayGeometry geometry =
|
|
ResolveRobloxTextOverlayGeometry(update, {1280, 720});
|
|
|
|
EXPECT_EQ(geometry.x, 120);
|
|
EXPECT_EQ(geometry.y, 80);
|
|
EXPECT_EQ(geometry.width, 360);
|
|
EXPECT_EQ(geometry.height, 44);
|
|
EXPECT_FALSE(geometry.used_fallback);
|
|
}
|
|
|
|
TEST(RobloxTextDisplayStateTest, RejectsDetachedFallbackForMissingGeometry) {
|
|
const std::string text = "hello";
|
|
RobloxTextDisplayUpdate update = Show(1, text, 5);
|
|
update.area_x = 0;
|
|
update.area_y = 0;
|
|
update.area_width = 0;
|
|
update.area_height = 0;
|
|
|
|
const RobloxTextOverlayGeometry geometry =
|
|
ResolveRobloxTextOverlayGeometry(update, {1000, 700});
|
|
|
|
EXPECT_FALSE(geometry.valid());
|
|
EXPECT_FALSE(geometry.used_fallback);
|
|
}
|
|
|
|
TEST(RobloxTextDisplayStateTest, RendersCompactChatFields) {
|
|
RobloxTextSurfaceOverlay overlay;
|
|
ASSERT_TRUE(overlay.Initialize({1630, 985, 1.145833F}).ok());
|
|
const std::string text = "chat test";
|
|
auto show = Show(1, text, 9);
|
|
// Actual NativeTextBoxInfo from MIC UP; 17px is a valid chat line.
|
|
show.area_x = 38;
|
|
show.area_y = 328;
|
|
show.area_width = 387;
|
|
show.area_height = 17;
|
|
show.font_size = 14.0F;
|
|
overlay.sink().update(overlay.sink().context, show);
|
|
EXPECT_TRUE(mocktail_text_overlay_may_present());
|
|
MocktailTextOverlayFrameInfo frame{};
|
|
ASSERT_TRUE(overlay.QueryFrame(&frame));
|
|
ASSERT_NE(frame.visible, 0U);
|
|
EXPECT_EQ(frame.width, 387U);
|
|
EXPECT_EQ(frame.height, 17U);
|
|
std::vector<std::uint8_t> rgba(frame.rgba_bytes);
|
|
ASSERT_TRUE(overlay.CopyFrame(frame.revision, rgba.data(), rgba.size()));
|
|
const auto bounds = FindAlphaBounds(frame, rgba);
|
|
ASSERT_TRUE(bounds.valid());
|
|
EXPECT_GT(bounds.maximum_x - bounds.minimum_x, 10);
|
|
EXPECT_TRUE(overlay.Shutdown().ok());
|
|
}
|
|
|
|
TEST(RobloxTextDisplayStateTest,
|
|
PreservesNativeExtentForCompositorSourceClipping) {
|
|
const std::string text = "edge";
|
|
RobloxTextDisplayUpdate update = Show(1, text, 4);
|
|
update.area_x = 760;
|
|
update.area_y = 570;
|
|
update.area_width = 120;
|
|
update.area_height = 44;
|
|
|
|
const RobloxTextOverlayGeometry geometry =
|
|
ResolveRobloxTextOverlayGeometry(update, {800, 600});
|
|
|
|
EXPECT_EQ(geometry.x, 760);
|
|
EXPECT_EQ(geometry.y, 570);
|
|
EXPECT_EQ(geometry.width, 120);
|
|
EXPECT_EQ(geometry.height, 44);
|
|
}
|
|
|
|
TEST(RobloxTextDisplayStateTest,
|
|
MasksEveryPasswordTypeWithoutRetainingPlaintext) {
|
|
for (const int32_t type : {5, 9, 10}) {
|
|
const std::string plaintext = u8"a😀z";
|
|
RobloxTextOverlayPresentation presentation;
|
|
|
|
ASSERT_TRUE(BuildRobloxTextOverlayPresentation(Show(1, plaintext, 3, type),
|
|
{1280, 720}, &presentation)
|
|
.ok());
|
|
EXPECT_EQ(presentation.display_utf8, u8"•••");
|
|
EXPECT_EQ(presentation.caret_utf8_byte, std::string(u8"••").size());
|
|
EXPECT_EQ(presentation.display_utf8.find("a"), std::string::npos);
|
|
EXPECT_EQ(presentation.display_utf8.find("z"), std::string::npos);
|
|
}
|
|
}
|
|
|
|
TEST(RobloxTextDisplayStateTest, MapsUtf16CursorToUnicodeByteBoundary) {
|
|
const std::string text = u8"A😀Б";
|
|
RobloxTextOverlayPresentation presentation;
|
|
|
|
ASSERT_TRUE(BuildRobloxTextOverlayPresentation(Show(7, text, 3), {1280, 720},
|
|
&presentation)
|
|
.ok());
|
|
|
|
EXPECT_EQ(presentation.display_utf8, text);
|
|
EXPECT_EQ(presentation.caret_utf8_byte, std::string(u8"A😀").size());
|
|
}
|
|
|
|
TEST(RobloxTextDisplayStateTest, PreservesRobloxFontAndWrappingProperties) {
|
|
const std::string text = "styled";
|
|
RobloxTextDisplayUpdate update = Show(7, text, 6);
|
|
update.font_size = 21.0F;
|
|
update.font = 13;
|
|
update.multiline = true;
|
|
update.text_wrapped = true;
|
|
RobloxTextOverlayPresentation presentation;
|
|
|
|
ASSERT_TRUE(BuildRobloxTextOverlayPresentation(update, {1280, 720},
|
|
&presentation)
|
|
.ok());
|
|
|
|
EXPECT_FLOAT_EQ(presentation.font_size, 21.0F);
|
|
EXPECT_EQ(presentation.font, 13);
|
|
EXPECT_TRUE(presentation.multiline);
|
|
EXPECT_TRUE(presentation.text_wrapped);
|
|
}
|
|
|
|
TEST(RobloxTextDisplayStateTest, MapsUnicodeSelectionToVisibleByteRange) {
|
|
const std::string text = u8"A😀Б";
|
|
RobloxTextDisplayUpdate update = Show(7, text, 3);
|
|
update.selection_begin_utf16 = 1;
|
|
update.selection_end_utf16 = 3;
|
|
RobloxTextOverlayPresentation presentation;
|
|
|
|
ASSERT_TRUE(
|
|
BuildRobloxTextOverlayPresentation(update, {1280, 720}, &presentation)
|
|
.ok());
|
|
|
|
EXPECT_EQ(presentation.selection_begin_utf8_byte, std::string("A").size());
|
|
EXPECT_EQ(presentation.selection_end_utf8_byte,
|
|
std::string(u8"A😀").size());
|
|
}
|
|
|
|
TEST(RobloxTextDisplayStateTest, MapsPasswordSelectionWithoutPlaintext) {
|
|
const std::string plaintext = u8"a😀z";
|
|
RobloxTextDisplayUpdate update = Show(1, plaintext, 3, 5);
|
|
update.selection_begin_utf16 = 1;
|
|
update.selection_end_utf16 = 3;
|
|
RobloxTextOverlayPresentation presentation;
|
|
|
|
ASSERT_TRUE(BuildRobloxTextOverlayPresentation(update, {1280, 720},
|
|
&presentation)
|
|
.ok());
|
|
|
|
EXPECT_EQ(presentation.display_utf8, u8"•••");
|
|
EXPECT_EQ(presentation.selection_begin_utf8_byte,
|
|
std::string(u8"•").size());
|
|
EXPECT_EQ(presentation.selection_end_utf8_byte,
|
|
std::string(u8"••").size());
|
|
}
|
|
|
|
TEST(RobloxTextDisplayStateTest, IgnoresStaleUpdatesAndHideEvents) {
|
|
RobloxTextDisplayState state;
|
|
const std::string current = "current";
|
|
bool changed = false;
|
|
ASSERT_TRUE(state.Apply(Show(4, current, 7), {1280, 720}, &changed).ok());
|
|
ASSERT_TRUE(changed);
|
|
|
|
const std::string stale = "stale";
|
|
RobloxTextDisplayUpdate stale_update = Show(3, stale, 5);
|
|
stale_update.event = RobloxTextDisplayEvent::kUpdate;
|
|
ASSERT_TRUE(state.Apply(stale_update, {1280, 720}, &changed).ok());
|
|
EXPECT_FALSE(changed);
|
|
EXPECT_EQ(state.presentation().display_utf8, current);
|
|
|
|
RobloxTextDisplayUpdate stale_hide;
|
|
stale_hide.event = RobloxTextDisplayEvent::kHide;
|
|
stale_hide.generation = 3;
|
|
ASSERT_TRUE(state.Apply(stale_hide, {1280, 720}, &changed).ok());
|
|
EXPECT_FALSE(changed);
|
|
EXPECT_TRUE(state.presentation().visible);
|
|
|
|
RobloxTextDisplayUpdate current_hide = stale_hide;
|
|
current_hide.generation = 4;
|
|
ASSERT_TRUE(state.Apply(current_hide, {1280, 720}, &changed).ok());
|
|
EXPECT_TRUE(changed);
|
|
EXPECT_FALSE(state.presentation().visible);
|
|
EXPECT_TRUE(state.presentation().display_utf8.empty());
|
|
}
|
|
|
|
TEST(RobloxTextDisplayStateTest,
|
|
PublishesLibraryRasterForTheExistingVulkanSurface) {
|
|
RobloxTextSurfaceOverlay overlay;
|
|
EXPECT_FALSE(mocktail_text_overlay_may_present());
|
|
ASSERT_TRUE(overlay.Initialize({800, 600}).ok());
|
|
EXPECT_FALSE(mocktail_text_overlay_may_present());
|
|
|
|
const std::string text = u8"Hello, мир";
|
|
RobloxTextDisplayUpdate show = Show(7, text, 10);
|
|
show.area_height = 48;
|
|
RobloxTextDisplaySink sink = overlay.sink();
|
|
ASSERT_NE(sink.update, nullptr);
|
|
sink.update(sink.context, show);
|
|
EXPECT_TRUE(mocktail_text_overlay_may_present());
|
|
|
|
RobloxTextDisplayUpdate stale_hide;
|
|
stale_hide.event = RobloxTextDisplayEvent::kHide;
|
|
stale_hide.generation = 6;
|
|
sink.update(sink.context, stale_hide);
|
|
EXPECT_TRUE(mocktail_text_overlay_may_present());
|
|
|
|
MocktailTextOverlayFrameInfo frame;
|
|
ASSERT_TRUE(overlay.QueryFrame(&frame));
|
|
EXPECT_EQ(frame.abi_version, MocktailTextOverlayFrameInfo::kAbiVersion);
|
|
EXPECT_EQ(frame.visible, 1U);
|
|
EXPECT_EQ(frame.coordinate_width, 800U);
|
|
EXPECT_EQ(frame.coordinate_height, 600U);
|
|
EXPECT_EQ(frame.x, 120);
|
|
EXPECT_EQ(frame.y, 80);
|
|
EXPECT_EQ(frame.width, 360U);
|
|
EXPECT_EQ(frame.height, 48U);
|
|
ASSERT_EQ(frame.rgba_bytes,
|
|
static_cast<std::uint64_t>(frame.width) * frame.height * 4U);
|
|
|
|
std::vector<std::uint8_t> rgba(frame.rgba_bytes);
|
|
ASSERT_TRUE(overlay.CopyFrame(frame.revision, rgba.data(), rgba.size()));
|
|
EXPECT_EQ(rgba[3], 0U);
|
|
bool has_visible_pixel = false;
|
|
for (std::size_t index = 3; index < rgba.size(); index += 4) {
|
|
has_visible_pixel = has_visible_pixel || rgba[index] != 0;
|
|
}
|
|
EXPECT_TRUE(has_visible_pixel);
|
|
|
|
ASSERT_TRUE(overlay.UpdateViewport({1000, 700}).ok());
|
|
ASSERT_TRUE(overlay.QueryFrame(&frame));
|
|
EXPECT_EQ(frame.coordinate_width, 1000U);
|
|
EXPECT_EQ(frame.coordinate_height, 700U);
|
|
|
|
RobloxTextDisplayUpdate hide;
|
|
hide.event = RobloxTextDisplayEvent::kHide;
|
|
hide.generation = 7;
|
|
sink.update(sink.context, hide);
|
|
EXPECT_FALSE(mocktail_text_overlay_may_present());
|
|
MocktailTextOverlayFrameInfo hidden;
|
|
ASSERT_TRUE(overlay.QueryFrame(&hidden));
|
|
EXPECT_EQ(hidden.visible, 0U);
|
|
EXPECT_GT(hidden.revision, frame.revision);
|
|
EXPECT_FALSE(overlay.CopyFrame(frame.revision, rgba.data(), rgba.size()));
|
|
EXPECT_TRUE(overlay.Shutdown().ok());
|
|
EXPECT_FALSE(mocktail_text_overlay_may_present());
|
|
}
|
|
|
|
TEST(RobloxTextDisplayStateTest,
|
|
InvalidGeometryNeverPublishesDrawableOverlayHint) {
|
|
RobloxTextSurfaceOverlay overlay;
|
|
ASSERT_TRUE(overlay.Initialize({800, 600}).ok());
|
|
RobloxTextDisplaySink sink = overlay.sink();
|
|
|
|
const std::string text = "hidden geometry";
|
|
RobloxTextDisplayUpdate show = Show(9, text, 4);
|
|
show.area_width = 0;
|
|
show.area_height = 0;
|
|
sink.update(sink.context, show);
|
|
|
|
EXPECT_FALSE(mocktail_text_overlay_may_present());
|
|
MocktailTextOverlayFrameInfo frame;
|
|
ASSERT_TRUE(overlay.QueryFrame(&frame));
|
|
EXPECT_EQ(frame.visible, 0U);
|
|
EXPECT_TRUE(overlay.Shutdown().ok());
|
|
}
|
|
|
|
TEST(RobloxTextDisplayStateTest,
|
|
PublishesNativeTextGeometryInAndroidDensityCoordinates) {
|
|
RobloxTextSurfaceOverlay overlay;
|
|
ASSERT_TRUE(overlay.Initialize({1624, 811, 1.145833F}).ok());
|
|
RobloxTextDisplaySink sink = overlay.sink();
|
|
const std::string text = "density mapped";
|
|
RobloxTextDisplayUpdate show = Show(10, text, 0);
|
|
show.area_x = 943;
|
|
show.area_y = 527;
|
|
show.area_width = 409;
|
|
show.area_height = 76;
|
|
sink.update(sink.context, show);
|
|
|
|
MocktailTextOverlayFrameInfo frame;
|
|
ASSERT_TRUE(overlay.QueryFrame(&frame));
|
|
EXPECT_EQ(frame.coordinate_width, 1417U);
|
|
EXPECT_EQ(frame.coordinate_height, 708U);
|
|
EXPECT_EQ(frame.x, 943);
|
|
EXPECT_EQ(frame.y, 527);
|
|
EXPECT_EQ(frame.width, 409U);
|
|
EXPECT_EQ(frame.height, 76U);
|
|
EXPECT_TRUE(overlay.Shutdown().ok());
|
|
}
|
|
|
|
TEST(RobloxTextDisplayStateTest,
|
|
InactiveOverlayCannotClearAnotherOverlayPublication) {
|
|
RobloxTextSurfaceOverlay active;
|
|
ASSERT_TRUE(active.Initialize({800, 600}).ok());
|
|
RobloxTextDisplaySink sink = active.sink();
|
|
const std::string text = "active";
|
|
sink.update(sink.context, Show(11, text, 6));
|
|
ASSERT_TRUE(mocktail_text_overlay_may_present());
|
|
|
|
RobloxTextSurfaceOverlay rejected;
|
|
EXPECT_FALSE(rejected.Initialize({800, 600}).ok());
|
|
EXPECT_TRUE(rejected.Shutdown().ok());
|
|
EXPECT_TRUE(mocktail_text_overlay_may_present());
|
|
|
|
EXPECT_TRUE(active.Shutdown().ok());
|
|
EXPECT_FALSE(mocktail_text_overlay_may_present());
|
|
}
|
|
|
|
TEST(RobloxTextDisplayStateTest, MatchesApkHorizontalGravityEnum) {
|
|
RobloxTextSurfaceOverlay overlay;
|
|
ASSERT_TRUE(overlay.Initialize({800, 600}).ok());
|
|
RobloxTextDisplaySink sink = overlay.sink();
|
|
const std::string text = "M";
|
|
|
|
const auto render = [&](uint64_t generation, int32_t alignment) {
|
|
RobloxTextDisplayUpdate show = Show(generation, text, 0);
|
|
show.area_width = 320;
|
|
show.area_height = 64;
|
|
show.font_size = 28.0F;
|
|
show.x_alignment = alignment;
|
|
show.text_color = static_cast<int32_t>(0xFFFFFFFFU);
|
|
sink.update(sink.context, show);
|
|
|
|
MocktailTextOverlayFrameInfo frame;
|
|
EXPECT_TRUE(overlay.QueryFrame(&frame));
|
|
std::vector<std::uint8_t> rgba(frame.rgba_bytes);
|
|
EXPECT_TRUE(overlay.CopyFrame(frame.revision, rgba.data(), rgba.size()));
|
|
return FindAlphaBounds(frame, rgba);
|
|
};
|
|
|
|
const AlphaBounds left = render(1, 0);
|
|
const AlphaBounds right = render(2, 1);
|
|
const AlphaBounds center = render(3, 2);
|
|
ASSERT_TRUE(left.valid());
|
|
ASSERT_TRUE(center.valid());
|
|
ASSERT_TRUE(right.valid());
|
|
EXPECT_LT(left.minimum_x, center.minimum_x);
|
|
EXPECT_LT(center.minimum_x, right.minimum_x);
|
|
EXPECT_GT(center.minimum_x - left.minimum_x, 40);
|
|
EXPECT_GT(right.minimum_x - center.minimum_x, 40);
|
|
EXPECT_TRUE(overlay.Shutdown().ok());
|
|
}
|
|
|
|
TEST(RobloxTextDisplayStateTest, UsesNativeTextOriginRegardlessOfFieldPosition) {
|
|
RobloxTextSurfaceOverlay overlay;
|
|
ASSERT_TRUE(overlay.Initialize({2560, 1440, 1.145833F}).ok());
|
|
RobloxTextDisplaySink sink = overlay.sink();
|
|
const std::string text = "search text";
|
|
|
|
const auto render = [&](uint64_t generation, int32_t y, int32_t width,
|
|
int32_t height) {
|
|
RobloxTextDisplayUpdate show = Show(generation, text, 0);
|
|
show.area_y = y;
|
|
show.area_width = width;
|
|
show.area_height = height;
|
|
show.font_size = 18.0F;
|
|
show.x_alignment = 0;
|
|
show.text_input_type = 1;
|
|
show.text_color = static_cast<int32_t>(0xFFFFFFFFU);
|
|
sink.update(sink.context, show);
|
|
|
|
MocktailTextOverlayFrameInfo frame;
|
|
EXPECT_TRUE(overlay.QueryFrame(&frame));
|
|
EXPECT_EQ(frame.x, show.area_x);
|
|
EXPECT_EQ(frame.y, show.area_y);
|
|
EXPECT_EQ(frame.width, static_cast<uint32_t>(width));
|
|
std::vector<std::uint8_t> rgba(frame.rgba_bytes);
|
|
EXPECT_TRUE(overlay.CopyFrame(frame.revision, rgba.data(), rgba.size()));
|
|
return FindAlphaBounds(frame, rgba);
|
|
};
|
|
|
|
// Moving or resizing a field must not invent an icon margin inside the
|
|
// native text rectangle. The caret at byte zero marks its text origin.
|
|
const AlphaBounds search = render(1, 34, 1047, 36);
|
|
const AlphaBounds ordinary = render(2, 200, 1047, 36);
|
|
const AlphaBounds narrow = render(3, 34, 320, 36);
|
|
ASSERT_TRUE(search.valid());
|
|
ASSERT_TRUE(ordinary.valid());
|
|
ASSERT_TRUE(narrow.valid());
|
|
EXPECT_EQ(search.minimum_x, 0);
|
|
EXPECT_EQ(ordinary.minimum_x, 0);
|
|
EXPECT_EQ(narrow.minimum_x, 0);
|
|
EXPECT_EQ(search.maximum_x, ordinary.maximum_x);
|
|
EXPECT_EQ(search.maximum_x, narrow.maximum_x);
|
|
EXPECT_TRUE(overlay.Shutdown().ok());
|
|
}
|
|
|
|
TEST(RobloxTextDisplayStateTest, LongSearchTextScrollsWithinNativeWidth) {
|
|
RobloxTextSurfaceOverlay overlay;
|
|
ASSERT_TRUE(overlay.Initialize({2560, 1440, 1.145833F}).ok());
|
|
RobloxTextDisplaySink sink = overlay.sink();
|
|
const std::string text(512, 'M');
|
|
RobloxTextDisplayUpdate show = Show(1, text, 0);
|
|
show.area_y = 34;
|
|
show.area_width = 1047;
|
|
show.area_height = 36;
|
|
show.font_size = 18.0F;
|
|
|
|
const auto render = [&](int32_t cursor) {
|
|
show.cursor_utf16 = cursor;
|
|
show.selection_begin_utf16 = cursor;
|
|
show.selection_end_utf16 = cursor;
|
|
sink.update(sink.context, show);
|
|
MocktailTextOverlayFrameInfo frame;
|
|
EXPECT_TRUE(overlay.QueryFrame(&frame));
|
|
EXPECT_EQ(frame.x, show.area_x);
|
|
EXPECT_EQ(frame.width, static_cast<uint32_t>(show.area_width));
|
|
std::vector<std::uint8_t> rgba(frame.rgba_bytes);
|
|
EXPECT_TRUE(overlay.CopyFrame(frame.revision, rgba.data(), rgba.size()));
|
|
return FindAlphaBounds(frame, rgba);
|
|
};
|
|
|
|
const AlphaBounds beginning = render(0);
|
|
const AlphaBounds end = render(static_cast<int32_t>(text.size()));
|
|
const AlphaBounds returned_to_beginning = render(0);
|
|
ASSERT_TRUE(beginning.valid());
|
|
ASSERT_TRUE(end.valid());
|
|
// Glyph bearings may leave a small gap; there must be no field inset.
|
|
EXPECT_LE(beginning.minimum_x, 3);
|
|
// Only the caret's own clearance may remain at the right edge.
|
|
EXPECT_GE(end.maximum_x, show.area_width - 3);
|
|
EXPECT_LT(end.maximum_x, show.area_width);
|
|
EXPECT_EQ(returned_to_beginning.minimum_x, beginning.minimum_x);
|
|
EXPECT_EQ(returned_to_beginning.maximum_x, beginning.maximum_x);
|
|
EXPECT_TRUE(overlay.Shutdown().ok());
|
|
}
|
|
|
|
TEST(RobloxTextDisplayStateTest, KeepsBackgroundTransparentAndArgbCaretAlpha) {
|
|
RobloxTextSurfaceOverlay overlay;
|
|
ASSERT_TRUE(overlay.Initialize({800, 600}).ok());
|
|
const std::string text = "M";
|
|
RobloxTextDisplayUpdate show = Show(1, text, 1);
|
|
show.area_height = 64;
|
|
show.font_size = 28.0F;
|
|
show.text_color = static_cast<int32_t>(0x80FF0000U);
|
|
RobloxTextDisplaySink sink = overlay.sink();
|
|
sink.update(sink.context, show);
|
|
|
|
MocktailTextOverlayFrameInfo frame;
|
|
ASSERT_TRUE(overlay.QueryFrame(&frame));
|
|
std::vector<std::uint8_t> rgba(frame.rgba_bytes);
|
|
ASSERT_TRUE(overlay.CopyFrame(frame.revision, rgba.data(), rgba.size()));
|
|
ASSERT_GE(rgba.size(), 4U);
|
|
EXPECT_EQ(rgba[3], 0U);
|
|
bool found_argb_pixel = false;
|
|
std::uint8_t maximum_alpha = 0;
|
|
for (std::size_t offset = 0; offset + 3 < rgba.size(); offset += 4) {
|
|
maximum_alpha = std::max(maximum_alpha, rgba[offset + 3]);
|
|
found_argb_pixel = found_argb_pixel ||
|
|
(rgba[offset] == 255U && rgba[offset + 1] == 0U &&
|
|
rgba[offset + 2] == 0U && rgba[offset + 3] == 128U);
|
|
}
|
|
EXPECT_LE(maximum_alpha, 128U);
|
|
EXPECT_TRUE(found_argb_pixel);
|
|
EXPECT_TRUE(overlay.Shutdown().ok());
|
|
}
|
|
|
|
TEST(RobloxTextDisplayStateTest, RasterizesSelectionHighlightBehindText) {
|
|
RobloxTextSurfaceOverlay overlay;
|
|
ASSERT_TRUE(overlay.Initialize({800, 600}).ok());
|
|
const std::string text = "selected text";
|
|
RobloxTextDisplayUpdate show = Show(1, text, 13);
|
|
show.area_height = 64;
|
|
show.font_size = 28.0F;
|
|
show.selection_begin_utf16 = 0;
|
|
show.selection_end_utf16 = 8;
|
|
RobloxTextDisplaySink sink = overlay.sink();
|
|
sink.update(sink.context, show);
|
|
|
|
MocktailTextOverlayFrameInfo frame;
|
|
ASSERT_TRUE(overlay.QueryFrame(&frame));
|
|
std::vector<std::uint8_t> rgba(frame.rgba_bytes);
|
|
ASSERT_TRUE(overlay.CopyFrame(frame.revision, rgba.data(), rgba.size()));
|
|
bool found_highlight = false;
|
|
for (std::size_t offset = 0; offset + 3 < rgba.size(); offset += 4) {
|
|
found_highlight =
|
|
found_highlight ||
|
|
(rgba[offset] == 65U && rgba[offset + 1] == 132U &&
|
|
rgba[offset + 2] == 228U && rgba[offset + 3] == 112U);
|
|
}
|
|
EXPECT_TRUE(found_highlight);
|
|
EXPECT_TRUE(overlay.Shutdown().ok());
|
|
}
|
|
|
|
TEST(RobloxTextDisplayStateTest, WrapsMultilineTextInsideNativeTextBox) {
|
|
RobloxTextSurfaceOverlay overlay;
|
|
ASSERT_TRUE(overlay.Initialize({800, 600}).ok());
|
|
const std::string text =
|
|
"one two three four five six seven eight nine ten eleven twelve";
|
|
RobloxTextDisplayUpdate show = Show(1, text, 0);
|
|
show.area_width = 150;
|
|
show.area_height = 140;
|
|
show.font_size = 20.0F;
|
|
show.y_alignment = 0;
|
|
show.text_wrapped = true;
|
|
RobloxTextDisplaySink sink = overlay.sink();
|
|
sink.update(sink.context, show);
|
|
|
|
MocktailTextOverlayFrameInfo frame;
|
|
ASSERT_TRUE(overlay.QueryFrame(&frame));
|
|
std::vector<std::uint8_t> rgba(frame.rgba_bytes);
|
|
ASSERT_TRUE(overlay.CopyFrame(frame.revision, rgba.data(), rgba.size()));
|
|
bool has_lower_line_pixel = false;
|
|
for (std::uint32_t y = 40; y < frame.height; ++y) {
|
|
for (std::uint32_t x = 0; x < frame.width; ++x) {
|
|
const std::size_t alpha =
|
|
(static_cast<std::size_t>(y) * frame.width + x) * 4 + 3;
|
|
has_lower_line_pixel =
|
|
has_lower_line_pixel || (alpha < rgba.size() && rgba[alpha] != 0);
|
|
}
|
|
}
|
|
EXPECT_TRUE(has_lower_line_pixel);
|
|
EXPECT_TRUE(overlay.Shutdown().ok());
|
|
}
|
|
|
|
TEST(RobloxTextDisplayStateTest, AdvancesCaretForTrailingSpace) {
|
|
RobloxTextSurfaceOverlay overlay;
|
|
ASSERT_TRUE(overlay.Initialize({800, 600}).ok());
|
|
RobloxTextDisplaySink sink = overlay.sink();
|
|
const auto rightmost_pixel = [&](const std::string& text,
|
|
RobloxTextDisplayEvent event) {
|
|
RobloxTextDisplayUpdate update =
|
|
Show(1, text, static_cast<int32_t>(text.size()));
|
|
update.event = event;
|
|
update.font_size = 18.0F;
|
|
sink.update(sink.context, update);
|
|
MocktailTextOverlayFrameInfo frame;
|
|
EXPECT_TRUE(overlay.QueryFrame(&frame));
|
|
std::vector<std::uint8_t> rgba(frame.rgba_bytes);
|
|
EXPECT_TRUE(overlay.CopyFrame(frame.revision, rgba.data(), rgba.size()));
|
|
return FindAlphaBounds(frame, rgba).maximum_x;
|
|
};
|
|
|
|
const int typed = rightmost_pixel("hi", RobloxTextDisplayEvent::kShow);
|
|
const int spaced = rightmost_pixel("hi ", RobloxTextDisplayEvent::kUpdate);
|
|
|
|
EXPECT_GT(spaced, typed);
|
|
EXPECT_TRUE(overlay.Shutdown().ok());
|
|
}
|
|
|
|
|
|
TEST(RobloxTextDisplayStateTest, KeepsCaretBeforeTrailingSpaceDistinct) {
|
|
RobloxTextSurfaceOverlay overlay;
|
|
ASSERT_TRUE(overlay.Initialize({800, 600}).ok());
|
|
RobloxTextDisplaySink sink = overlay.sink();
|
|
const std::string text = "hi ";
|
|
const auto rightmost_pixel = [&](int32_t cursor_utf16,
|
|
RobloxTextDisplayEvent event) {
|
|
RobloxTextDisplayUpdate update = Show(1, text, cursor_utf16);
|
|
update.event = event;
|
|
update.font_size = 18.0F;
|
|
sink.update(sink.context, update);
|
|
MocktailTextOverlayFrameInfo frame;
|
|
EXPECT_TRUE(overlay.QueryFrame(&frame));
|
|
std::vector<std::uint8_t> rgba(frame.rgba_bytes);
|
|
EXPECT_TRUE(overlay.CopyFrame(frame.revision, rgba.data(), rgba.size()));
|
|
const auto bounds = FindAlphaBounds(frame, rgba);
|
|
EXPECT_TRUE(bounds.valid());
|
|
return bounds.valid() ? bounds.maximum_x : -1;
|
|
};
|
|
|
|
const int before_space = rightmost_pixel(2, RobloxTextDisplayEvent::kShow);
|
|
const int after_space = rightmost_pixel(3, RobloxTextDisplayEvent::kUpdate);
|
|
|
|
EXPECT_GT(after_space, before_space);
|
|
EXPECT_TRUE(overlay.Shutdown().ok());
|
|
}
|
|
|
|
TEST(RobloxTextDisplayStateTest, ReopensFontsAfterPointSizeChange) {
|
|
RobloxTextSurfaceOverlay overlay;
|
|
ASSERT_TRUE(overlay.Initialize({800, 600}).ok());
|
|
RobloxTextDisplaySink sink = overlay.sink();
|
|
const std::string text = "size";
|
|
const auto text_width = [&](float font_size, RobloxTextDisplayEvent event) {
|
|
RobloxTextDisplayUpdate update = Show(1, text, 4);
|
|
update.event = event;
|
|
update.area_height = 64;
|
|
update.font_size = font_size;
|
|
sink.update(sink.context, update);
|
|
MocktailTextOverlayFrameInfo frame;
|
|
EXPECT_TRUE(overlay.QueryFrame(&frame));
|
|
std::vector<std::uint8_t> rgba(frame.rgba_bytes);
|
|
EXPECT_TRUE(overlay.CopyFrame(frame.revision, rgba.data(), rgba.size()));
|
|
const auto bounds = FindAlphaBounds(frame, rgba);
|
|
EXPECT_TRUE(bounds.valid());
|
|
return bounds.valid() ? bounds.maximum_x - bounds.minimum_x : 0;
|
|
};
|
|
|
|
const int small = text_width(14.0F, RobloxTextDisplayEvent::kShow);
|
|
const int large = text_width(28.0F, RobloxTextDisplayEvent::kUpdate);
|
|
|
|
EXPECT_GT(large, small);
|
|
EXPECT_TRUE(overlay.Shutdown().ok());
|
|
}
|
|
|
|
|
|
} // namespace
|
|
} // namespace runtime
|
|
} // namespace mocktail
|