mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-10 10:25:03 +00:00
Speed up cold diffs and wrapped text measurement (#4676)
* perf(app): optimize wrapped measurements and checkpoint native layout experiment Compute full grapheme geometry only for final wrap fragments. Preserve shaped-text search behavior while bounding additive wrap probes. Keep the Android background layout experiment isolated from the normal diff renderer; native renderer integration and iOS bindings remain unfinished. * perf(server): reduce cold diff generation work Keep active diff reads owned across invalidation and wait for the initial watcher inventory before cold generation. Batch patch and blob reads while preserving per-file size isolation, bounded output, and daemon responsiveness. Avoid constructing fallback tokens when full-file highlighting already covers both sides. * test(server): exercise oversized highlighted diff rejection The single-line fixture now bypasses highlighting under the long-line guard, so it no longer reaches the structured payload limit. Use short dense lines to keep testing oversized rejection and continued socket usability. * fix(server): queue fresh diffs for forced reads A forced request following a repository mutation must not reuse an older active build. Invalidate that result and coalesce waiting callers onto one subsequent build. Give the cache a named object input and completed-value type. * test(server): share deferred diff loader setup * test(server): name completed diff cache setup * refactor(server): use aliases for new diff imports * fix(server): keep shipped diff imports resolvable by Node The TypeScript build preserves path aliases without a runtime resolver. Restore relative imports for shipped code and declarations, and correct the server import guideline that prompted the regression.
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
/android/.cxx/
|
||||
/android/build/
|
||||
@@ -0,0 +1,35 @@
|
||||
# Native diff layout experiment
|
||||
|
||||
Android-only Expo module for evaluating background wrapped-text layout. The normal
|
||||
diff surface does not call this module; it still uses the JavaScript model and
|
||||
renderer. This is a checkpoint of the layout experiment, not a replacement renderer.
|
||||
|
||||
The C++ core owns cell text and wrapped grapheme geometry. Kotlin/JNI supplies the
|
||||
Android font manager and dispatches preparation through Expo's background queue.
|
||||
It links the Skia libraries shipped by the installed React Native Skia package.
|
||||
An iOS binding, renderer integration, and shipping library ownership are unfinished.
|
||||
|
||||
## Diagnostic interface
|
||||
|
||||
Access the module through `expo.modules.PaseoDiffPrototype` in a development build:
|
||||
|
||||
- `create()` returns a numeric handle.
|
||||
- `prepare(handle, texts, widths, family, fontSize)` asynchronously builds geometry
|
||||
and returns counts, copy/layout timings, and a partial geometry allocation estimate.
|
||||
Text and width arrays must have matching lengths. Input is copied; it is not zero-copy.
|
||||
- `read(handle, startCell, count)` synchronously returns packed numerical geometry
|
||||
for at most 256 cells. This exists for parity checks, not whole-model transfer.
|
||||
- `release(handle)` cancels pending work and releases ownership. Release handles
|
||||
after success or failure; module destruction also cancels and releases all handles.
|
||||
|
||||
Each packed cell begins with its index and fragment count. Each fragment contains
|
||||
UTF-16 start/end offsets, width, and grapheme count, followed by each grapheme's
|
||||
UTF-16 start/end offsets and width. The caller retains source text.
|
||||
|
||||
The experiment accepts normalized cell inputs. It does not parse wire payloads,
|
||||
construct file/header/review models, apply highlighting, or draw pixels. A renderer
|
||||
integration must preserve native ownership and request bounded visible data rather
|
||||
than reconstructing the entire JavaScript graph.
|
||||
|
||||
Grapheme property ranges are generated at build time from the installed JavaScript
|
||||
dependency so segmentation uses the same data as the reference implementation.
|
||||
@@ -0,0 +1,15 @@
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
project(paseo_diff_prototype)
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
execute_process(COMMAND node "${CMAKE_CURRENT_SOURCE_DIR}/../cpp/generate-graphemes.mjs" "${CMAKE_CURRENT_BINARY_DIR}/graphemes.h" COMMAND_ERROR_IS_FATAL ANY)
|
||||
|
||||
add_library(paseo_diff_prototype SHARED ../cpp/layout.cpp ../cpp/jni.cpp)
|
||||
target_include_directories(paseo_diff_prototype PRIVATE "${SKIA_PACKAGE}/cpp/skia" "${CMAKE_CURRENT_BINARY_DIR}")
|
||||
target_compile_definitions(paseo_diff_prototype PRIVATE SK_BUILD_FOR_ANDROID)
|
||||
target_compile_options(paseo_diff_prototype PRIVATE -O2 -Wall -Wextra)
|
||||
target_link_options(paseo_diff_prototype PRIVATE "-Wl,-z,max-page-size=16384")
|
||||
foreach(lib skparagraph skshaper skunicode_core skunicode_icu skia)
|
||||
add_library(${lib} STATIC IMPORTED)
|
||||
set_target_properties(${lib} PROPERTIES IMPORTED_LOCATION "${SKIA_PACKAGE}/libs/android/${ANDROID_ABI}/lib${lib}.a")
|
||||
endforeach()
|
||||
target_link_libraries(paseo_diff_prototype PRIVATE skparagraph skshaper skunicode_core skunicode_icu skia android log EGL GLESv2)
|
||||
@@ -0,0 +1,27 @@
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
group = 'sh.paseo.diffprototype'
|
||||
version = '0.1.0'
|
||||
|
||||
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
||||
apply from: expoModulesCorePlugin
|
||||
applyKotlinExpoModulesCorePlugin()
|
||||
useCoreDependencies()
|
||||
useDefaultAndroidSdkVersions()
|
||||
|
||||
def skiaPackage = new File(["node", "--print", "require.resolve('@shopify/react-native-skia/package.json')"].execute(null, projectDir).text.trim()).parentFile
|
||||
|
||||
android {
|
||||
namespace "sh.paseo.diffprototype"
|
||||
defaultConfig {
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments "-DSKIA_PACKAGE=${skiaPackage.absolutePath}"
|
||||
cppFlags "-std=c++20 -O2 -fexceptions -frtti"
|
||||
}
|
||||
}
|
||||
}
|
||||
externalNativeBuild {
|
||||
cmake { path "CMakeLists.txt" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package sh.paseo.diffprototype
|
||||
|
||||
import expo.modules.kotlin.modules.Module
|
||||
import expo.modules.kotlin.modules.ModuleDefinition
|
||||
|
||||
// Experimental binding only. The layout engine has no JVM or platform dependency.
|
||||
class PaseoDiffPrototypeModule : Module() {
|
||||
companion object {
|
||||
init { System.loadLibrary("paseo_diff_prototype") }
|
||||
}
|
||||
|
||||
private external fun createDocument(): Double
|
||||
private external fun prepareDocument(id: Double, texts: Array<String>, widths: DoubleArray, family: String, size: Double): DoubleArray
|
||||
private external fun readDocument(id: Double, start: Int, count: Int): DoubleArray
|
||||
private external fun releaseDocument(id: Double)
|
||||
private external fun releaseAllDocuments()
|
||||
|
||||
override fun definition() = ModuleDefinition {
|
||||
Name("PaseoDiffPrototype")
|
||||
|
||||
Function("create") { createDocument() }
|
||||
// Expo dispatches this non-suspending AsyncFunction on its background queue.
|
||||
AsyncFunction("prepare") { id: Double, texts: List<String>, widths: List<Double>, family: String, size: Double ->
|
||||
val stats = prepareDocument(id, texts.toTypedArray(), widths.toDoubleArray(), family, size)
|
||||
mapOf(
|
||||
"cells" to stats[0], "fragments" to stats[1], "graphemes" to stats[2],
|
||||
"copyMs" to stats[3], "layoutMs" to stats[4], "geometryBytes" to stats[5]
|
||||
)
|
||||
}
|
||||
Function("read") { id: Double, start: Int, count: Int -> readDocument(id, start, count) }
|
||||
Function("release") { id: Double -> releaseDocument(id) }
|
||||
OnDestroy { releaseAllDocuments() }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { isExtendingChar } from "@marijn/find-cluster-break";
|
||||
|
||||
// Use the installed JS engine's Unicode property data, not a different ICU version.
|
||||
const ranges = [];
|
||||
let start = null;
|
||||
for (let code = 0; code <= 0x110000; code++) {
|
||||
const extending = code < 0x110000 && isExtendingChar(code);
|
||||
if (extending && start === null) start = code;
|
||||
if (!extending && start !== null) {
|
||||
ranges.push(`{${start},${code}}`);
|
||||
start = null;
|
||||
}
|
||||
}
|
||||
writeFileSync(
|
||||
process.argv[2],
|
||||
`#pragma once\n#include <array>\n#include <utility>\ninline constexpr std::array<std::pair<int,int>,${ranges.length}> extendingRanges = {{${ranges.join(",")}}};\n`,
|
||||
);
|
||||
@@ -0,0 +1,136 @@
|
||||
#include "layout.h"
|
||||
#include <jni.h>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
#include <unordered_map>
|
||||
#include "include/ports/SkFontMgr_android.h"
|
||||
#include "modules/skunicode/include/SkUnicode.h"
|
||||
|
||||
namespace {
|
||||
using Clock = std::chrono::steady_clock;
|
||||
using paseo::diff::Document;
|
||||
struct Job {
|
||||
std::atomic<bool> cancelled{false};
|
||||
std::atomic<bool> started{false};
|
||||
std::shared_ptr<const Document> document;
|
||||
};
|
||||
std::mutex mutex;
|
||||
std::unordered_map<uint64_t, std::shared_ptr<Job>> jobs;
|
||||
uint64_t nextId = 0;
|
||||
|
||||
double elapsed(Clock::time_point start) {
|
||||
return std::chrono::duration<double, std::milli>(Clock::now() - start).count();
|
||||
}
|
||||
std::shared_ptr<Job> findJob(double id) {
|
||||
std::lock_guard lock(mutex);
|
||||
const auto found = jobs.find(static_cast<uint64_t>(id));
|
||||
if (found == jobs.end()) throw std::runtime_error("Unknown or released layout");
|
||||
return found->second;
|
||||
}
|
||||
std::u16string copyString(JNIEnv* env, jstring source) {
|
||||
if (!source) throw std::runtime_error("Missing text");
|
||||
const jsize length = env->GetStringLength(source);
|
||||
const jchar* chars = env->GetStringChars(source, nullptr);
|
||||
if (!chars) throw std::runtime_error("Could not copy text");
|
||||
std::u16string result(reinterpret_cast<const char16_t*>(chars), length);
|
||||
env->ReleaseStringChars(source, chars);
|
||||
return result;
|
||||
}
|
||||
jdoubleArray numbers(JNIEnv* env, const std::vector<double>& values) {
|
||||
auto result = env->NewDoubleArray(values.size());
|
||||
if (result) env->SetDoubleArrayRegion(result, 0, values.size(), values.data());
|
||||
return result;
|
||||
}
|
||||
void fail(JNIEnv* env, const std::exception& error) {
|
||||
env->ThrowNew(env->FindClass("java/lang/IllegalStateException"), error.what());
|
||||
}
|
||||
} // namespace
|
||||
|
||||
#define METHOD(name) Java_sh_paseo_diffprototype_PaseoDiffPrototypeModule_##name
|
||||
|
||||
extern "C" JNIEXPORT jdouble JNICALL METHOD(createDocument)(JNIEnv*, jobject) {
|
||||
std::lock_guard lock(mutex);
|
||||
const auto id = ++nextId;
|
||||
jobs.emplace(id, std::make_shared<Job>());
|
||||
return id;
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jdoubleArray JNICALL METHOD(prepareDocument)(
|
||||
JNIEnv* env, jobject, jdouble id, jobjectArray texts, jdoubleArray widths,
|
||||
jstring family, jdouble size) {
|
||||
try {
|
||||
auto job = findJob(id);
|
||||
if (job->started.exchange(true)) throw std::runtime_error("Layout already started");
|
||||
const auto copyStart = Clock::now();
|
||||
const jsize count = env->GetArrayLength(texts);
|
||||
if (count > 100000 || env->GetArrayLength(widths) != count || !std::isfinite(size) || size <= 0 || size > 100)
|
||||
throw std::runtime_error("Invalid layout input");
|
||||
std::vector<double> available(count);
|
||||
env->GetDoubleArrayRegion(widths, 0, count, available.data());
|
||||
auto document = std::make_shared<Document>();
|
||||
document->cells.reserve(count);
|
||||
size_t units = 0;
|
||||
for (jsize i = 0; i < count; i++) {
|
||||
if (job->cancelled.load()) throw std::runtime_error("Layout cancelled");
|
||||
if (!std::isfinite(available[i]) || available[i] <= 0) throw std::runtime_error("Invalid width");
|
||||
auto text = static_cast<jstring>(env->GetObjectArrayElement(texts, i));
|
||||
auto copied = copyString(env, text);
|
||||
env->DeleteLocalRef(text);
|
||||
units += copied.size();
|
||||
if (units > 16000000) throw std::runtime_error("Prototype input limit exceeded");
|
||||
document->cells.push_back({std::move(copied), available[i], {}});
|
||||
}
|
||||
const auto fontFamily = SkUnicode::convertUtf16ToUtf8(copyString(env, family));
|
||||
const double copyMs = elapsed(copyStart);
|
||||
const auto layoutStart = Clock::now();
|
||||
paseo::diff::prepare(*document, SkFontMgr_New_Android(nullptr), fontFamily.c_str(), size, job->cancelled);
|
||||
const double layoutMs = elapsed(layoutStart);
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
if (job->cancelled.load()) throw std::runtime_error("Layout cancelled");
|
||||
job->document = document;
|
||||
}
|
||||
return numbers(env, {static_cast<double>(document->cells.size()),
|
||||
static_cast<double>(document->fragmentCount), static_cast<double>(document->graphemeCount),
|
||||
copyMs, layoutMs, static_cast<double>(document->geometryBytes)});
|
||||
} catch (const std::exception& error) {
|
||||
fail(env, error);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jdoubleArray JNICALL METHOD(readDocument)(
|
||||
JNIEnv* env, jobject, jdouble id, jint start, jint count) {
|
||||
try {
|
||||
const auto job = findJob(id);
|
||||
std::shared_ptr<const Document> document;
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
document = job->document;
|
||||
}
|
||||
if (!document) throw std::runtime_error("Layout is not ready");
|
||||
if (start < 0 || count < 0 || count > 256 || static_cast<size_t>(start) > document->cells.size())
|
||||
throw std::runtime_error("Invalid viewport range");
|
||||
return numbers(env, paseo::diff::read(*document, start, count));
|
||||
} catch (const std::exception& error) {
|
||||
fail(env, error);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL METHOD(releaseDocument)(JNIEnv*, jobject, jdouble id) {
|
||||
std::lock_guard lock(mutex);
|
||||
const auto found = jobs.find(static_cast<uint64_t>(id));
|
||||
if (found == jobs.end()) return;
|
||||
found->second->cancelled.store(true);
|
||||
jobs.erase(found);
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL METHOD(releaseAllDocuments)(JNIEnv*, jobject) {
|
||||
std::lock_guard lock(mutex);
|
||||
for (auto& [id, job] : jobs) job->cancelled.store(true);
|
||||
jobs.clear();
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
#include "layout.h"
|
||||
#include "graphemes.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <numeric>
|
||||
#include <stdexcept>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "include/core/SkFont.h"
|
||||
#include "modules/skparagraph/include/FontCollection.h"
|
||||
#include "modules/skparagraph/include/Paragraph.h"
|
||||
#include "modules/skparagraph/include/ParagraphBuilder.h"
|
||||
#include "modules/skunicode/include/SkUnicode.h"
|
||||
|
||||
namespace paseo::diff {
|
||||
namespace {
|
||||
namespace para = skia::textlayout;
|
||||
|
||||
int codePoint(const std::u16string& text, size_t pos) {
|
||||
int code = text[pos];
|
||||
if (code >= 0xd800 && code < 0xdc00 && pos + 1 < text.size()) {
|
||||
int low = text[pos + 1];
|
||||
if (low >= 0xdc00 && low < 0xe000) return ((code - 0xd800) << 10) + low - 0xdc00 + 0x10000;
|
||||
}
|
||||
return code;
|
||||
}
|
||||
bool extending(int code) {
|
||||
const auto found = std::upper_bound(extendingRanges.begin(), extendingRanges.end(), code,
|
||||
[](int value, const auto& range) { return value < range.first; });
|
||||
return found != extendingRanges.begin() && code < std::prev(found)->second;
|
||||
}
|
||||
bool regional(int code) { return code >= 0x1f1e6 && code <= 0x1f1ff; }
|
||||
|
||||
// Same forward-cluster rules as @marijn/find-cluster-break, including ZWJ/RI behavior.
|
||||
std::vector<Grapheme> segment(const std::u16string& text) {
|
||||
std::vector<Grapheme> result;
|
||||
size_t pos = 0;
|
||||
size_t column = 0;
|
||||
while (pos < text.size()) {
|
||||
const auto start = pos;
|
||||
int previous = codePoint(text, pos);
|
||||
pos += previous < 0x10000 ? 1 : 2;
|
||||
while (pos < text.size()) {
|
||||
const int next = codePoint(text, pos);
|
||||
if (previous == 0x200d || next == 0x200d || extending(next)) {
|
||||
pos += next < 0x10000 ? 1 : 2;
|
||||
previous = next;
|
||||
} else if (regional(next)) {
|
||||
size_t count = 0;
|
||||
for (int index = static_cast<int>(pos) - 2; index >= 0 && regional(codePoint(text, index)); index -= 2) count++;
|
||||
if (count % 2 == 0) break;
|
||||
pos += 2;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
auto display = text.substr(start, pos - start);
|
||||
const bool tab = display == u"\t";
|
||||
const size_t spaces = tab ? 4 - column % 4 : 0;
|
||||
if (tab) display.assign(spaces, u' ');
|
||||
result.push_back({static_cast<int>(start), static_cast<int>(pos), std::move(display)});
|
||||
column += tab ? spaces : 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool needsShaping(const std::u16string& text) {
|
||||
for (const auto c : text) if ((c < 0x20 || c > 0x7e) && c != u'\t') return true;
|
||||
for (const auto* pair : {u"==", u"!=", u"=>", u"<=", u">=", u"->", u"::", u"++", u"--"}) {
|
||||
if (text.find(pair) != std::u16string::npos) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
bool cleanBoundary(const Grapheme& grapheme) {
|
||||
for (const auto c : grapheme.text) {
|
||||
if (c < 0x20 || c > 0x7e || std::u16string_view(u"=!<>+-:&|*/~^?.").find(c) != std::u16string_view::npos) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
size_t chunkEnd(const std::vector<Grapheme>& graphemes, size_t start, size_t end) {
|
||||
const size_t limit = start + 64;
|
||||
if (limit >= end) return end;
|
||||
const size_t floor = std::max(start + 1, limit - 16);
|
||||
for (size_t seam = limit; seam > floor; seam--) {
|
||||
if (cleanBoundary(graphemes[seam - 1]) && cleanBoundary(graphemes[seam])) return seam;
|
||||
}
|
||||
return limit;
|
||||
}
|
||||
|
||||
class Measurer {
|
||||
public:
|
||||
Measurer(sk_sp<SkFontMgr> manager, const std::string& family, float size,
|
||||
const std::atomic<bool>& cancelled) : cancelled_(cancelled) {
|
||||
families_ = {SkString(family), SkString("Menlo"), SkString("SF Mono"), SkString("monospace"), SkString("System")};
|
||||
std::unordered_set<std::string> available;
|
||||
for (int i = 0; i < manager->countFamilies(); i++) {
|
||||
SkString name;
|
||||
manager->getFamilyName(i, &name);
|
||||
available.insert(name.c_str());
|
||||
}
|
||||
std::string primary = "System";
|
||||
for (const auto& name : families_) {
|
||||
if (available.contains(name.c_str())) { primary = name.c_str(); break; }
|
||||
}
|
||||
font_ = SkFont(manager->matchFamilyStyle(primary.c_str(), SkFontStyle()), size);
|
||||
fonts_ = sk_make_sp<para::FontCollection>();
|
||||
fonts_->setDefaultFontManager(manager);
|
||||
fonts_->enableFontFallback();
|
||||
para::StrutStyle strut;
|
||||
strut.setStrutEnabled(true);
|
||||
strut.setForceStrutHeight(true);
|
||||
strut.setFontFamilies(families_);
|
||||
strut.setFontSize(size);
|
||||
strut.setHeight(std::round(size * 1.5) / size);
|
||||
strut.setHeightOverride(true);
|
||||
strut.setHalfLeading(true);
|
||||
style_.setMaxLines(1);
|
||||
style_.setStrutStyle(strut);
|
||||
textStyle_.setFontFamilies(families_);
|
||||
textStyle_.setFontSize(size);
|
||||
textStyle_.setColor(SK_ColorBLACK);
|
||||
}
|
||||
|
||||
double measure(const std::vector<Grapheme>& graphemes, size_t start, size_t end,
|
||||
std::vector<double>* advances = nullptr) {
|
||||
double base = 0;
|
||||
for (size_t chunkStart = start; chunkStart < end;) {
|
||||
if (cancelled_.load()) throw std::runtime_error("Layout cancelled");
|
||||
const size_t chunkLimit = chunkEnd(graphemes, chunkStart, end);
|
||||
std::u16string text;
|
||||
for (size_t i = chunkStart; i < chunkLimit; i++) text += graphemes[i].text;
|
||||
double width = 0;
|
||||
if (!needsShaping(text) && hasEveryGlyph(text)) {
|
||||
for (size_t i = chunkStart; i < chunkLimit; i++) {
|
||||
width += additiveWidth(graphemes[i].text);
|
||||
if (advances) advances->push_back(base + width);
|
||||
}
|
||||
} else {
|
||||
auto paragraph = shape(text);
|
||||
if (advances) {
|
||||
size_t offset = 0;
|
||||
for (size_t i = chunkStart; i < chunkLimit; i++) {
|
||||
offset += graphemes[i].text.size();
|
||||
width = paragraphWidth(*paragraph, offset);
|
||||
advances->push_back(base + width);
|
||||
}
|
||||
} else {
|
||||
width = paragraphWidth(*paragraph, text.size());
|
||||
}
|
||||
}
|
||||
base += width;
|
||||
chunkStart = chunkLimit;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
private:
|
||||
bool hasEveryGlyph(const std::u16string& text) {
|
||||
for (const auto c : text) {
|
||||
auto [it, inserted] = glyphs_.try_emplace(c, false);
|
||||
if (inserted) it->second = font_.unicharToGlyph(c) != 0;
|
||||
if (!it->second) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
double additiveWidth(const std::u16string& text) {
|
||||
auto [it, inserted] = widths_.try_emplace(text, 0);
|
||||
if (inserted) {
|
||||
std::vector<SkGlyphID> glyphs(font_.countText(text.data(), text.size() * 2, SkTextEncoding::kUTF16));
|
||||
font_.textToGlyphs(text.data(), text.size() * 2, SkTextEncoding::kUTF16, SkSpan(glyphs));
|
||||
std::vector<SkScalar> widths(glyphs.size());
|
||||
font_.getWidthsBounds(SkSpan(glyphs), SkSpan(widths), {}, nullptr);
|
||||
// Match RNSkia 2.2.12 getTextWidth's integer accumulator exactly.
|
||||
it->second = std::accumulate(widths.begin(), widths.end(), 0);
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
std::unique_ptr<para::Paragraph> shape(const std::u16string& text) {
|
||||
auto builder = para::ParagraphBuilder::make(style_, fonts_);
|
||||
builder->pushStyle(textStyle_);
|
||||
const auto utf8 = SkUnicode::convertUtf16ToUtf8(text);
|
||||
builder->addText(utf8.c_str(), utf8.size());
|
||||
builder->pop();
|
||||
auto paragraph = builder->Build();
|
||||
paragraph->layout(100000);
|
||||
return paragraph;
|
||||
}
|
||||
static double paragraphWidth(para::Paragraph& paragraph, size_t end) {
|
||||
double width = 0;
|
||||
for (const auto& box : paragraph.getRectsForRange(0, end, para::RectHeightStyle::kTight, para::RectWidthStyle::kTight)) {
|
||||
// JS adds the two float-valued properties in double precision.
|
||||
width = std::max(width, static_cast<double>(box.rect.x()) + box.rect.width());
|
||||
}
|
||||
return width;
|
||||
}
|
||||
SkFont font_;
|
||||
std::vector<SkString> families_;
|
||||
sk_sp<para::FontCollection> fonts_;
|
||||
para::ParagraphStyle style_;
|
||||
para::TextStyle textStyle_;
|
||||
std::unordered_map<char16_t, bool> glyphs_;
|
||||
std::unordered_map<std::u16string, double> widths_;
|
||||
const std::atomic<bool>& cancelled_;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
void prepare(Document& document, sk_sp<SkFontMgr> fonts, const std::string& family, float size,
|
||||
const std::atomic<bool>& cancelled) {
|
||||
Measurer measure(std::move(fonts), family, size, cancelled);
|
||||
for (auto& cell : document.cells) {
|
||||
if (cancelled.load()) throw std::runtime_error("Layout cancelled");
|
||||
auto graphemes = segment(cell.text);
|
||||
const bool bounded = !needsShaping(cell.text);
|
||||
size_t start = 0;
|
||||
do {
|
||||
size_t fitting = graphemes.size();
|
||||
if (!graphemes.empty()) {
|
||||
size_t low = start + 1;
|
||||
size_t high = bounded ? std::min(graphemes.size(), start + 64) : graphemes.size();
|
||||
fitting = low;
|
||||
if (bounded) {
|
||||
while (measure.measure(graphemes, start, high) <= cell.availableWidth) {
|
||||
fitting = high;
|
||||
low = high + 1;
|
||||
if (high == graphemes.size()) break;
|
||||
high = std::min(graphemes.size(), start + (high - start) * 2);
|
||||
}
|
||||
}
|
||||
while (low <= high) {
|
||||
const size_t middle = (low + high) / 2;
|
||||
const double width = measure.measure(graphemes, start, middle);
|
||||
if (width <= cell.availableWidth || middle == start + 1) {
|
||||
fitting = middle;
|
||||
low = middle + 1;
|
||||
} else {
|
||||
high = middle - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::vector<double> advances;
|
||||
advances.reserve(fitting - start);
|
||||
const double width = measure.measure(graphemes, start, fitting, &advances);
|
||||
Fragment fragment{start < graphemes.size() ? graphemes[start].start : 0,
|
||||
fitting > start ? graphemes[fitting - 1].end : 0, width, {}};
|
||||
fragment.graphemes.reserve(fitting - start);
|
||||
for (size_t i = start; i < fitting; i++) {
|
||||
auto grapheme = graphemes[i];
|
||||
grapheme.width = advances[i - start] - (i > start ? advances[i - start - 1] : 0);
|
||||
fragment.graphemes.push_back(std::move(grapheme));
|
||||
}
|
||||
document.fragmentCount++;
|
||||
document.graphemeCount += fragment.graphemes.size();
|
||||
document.geometryBytes += sizeof(Fragment) + fragment.graphemes.capacity() * sizeof(Grapheme);
|
||||
cell.fragments.push_back(std::move(fragment));
|
||||
start = fitting;
|
||||
} while (start < graphemes.size());
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<double> read(const Document& document, size_t start, size_t count) {
|
||||
std::vector<double> output;
|
||||
const size_t end = start + std::min(count, document.cells.size() - start);
|
||||
for (size_t i = start; i < end; i++) {
|
||||
const auto& cell = document.cells[i];
|
||||
output.insert(output.end(), {static_cast<double>(i), static_cast<double>(cell.fragments.size())});
|
||||
for (const auto& fragment : cell.fragments) {
|
||||
output.insert(output.end(), {static_cast<double>(fragment.start), static_cast<double>(fragment.end), fragment.width, static_cast<double>(fragment.graphemes.size())});
|
||||
for (const auto& grapheme : fragment.graphemes) {
|
||||
output.insert(output.end(), {static_cast<double>(grapheme.start), static_cast<double>(grapheme.end), grapheme.width});
|
||||
}
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
} // namespace paseo::diff
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "include/core/SkFontMgr.h"
|
||||
|
||||
namespace paseo::diff {
|
||||
struct Grapheme {
|
||||
int start;
|
||||
int end;
|
||||
std::u16string text;
|
||||
double width = 0;
|
||||
};
|
||||
struct Fragment {
|
||||
int start;
|
||||
int end;
|
||||
double width;
|
||||
std::vector<Grapheme> graphemes;
|
||||
};
|
||||
struct Cell {
|
||||
std::u16string text;
|
||||
double availableWidth;
|
||||
std::vector<Fragment> fragments;
|
||||
};
|
||||
struct Document {
|
||||
std::vector<Cell> cells;
|
||||
size_t fragmentCount = 0;
|
||||
size_t graphemeCount = 0;
|
||||
size_t geometryBytes = 0;
|
||||
};
|
||||
|
||||
void prepare(Document& document, sk_sp<SkFontMgr> fonts, const std::string& family, float fontSize,
|
||||
const std::atomic<bool>& cancelled);
|
||||
std::vector<double> read(const Document& document, size_t start, size_t count);
|
||||
} // namespace paseo::diff
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"platforms": ["android"],
|
||||
"android": {
|
||||
"modules": ["sh.paseo.diffprototype.PaseoDiffPrototypeModule"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "paseo-diff-prototype",
|
||||
"version": "0.1.0",
|
||||
"private": true
|
||||
}
|
||||
Reference in New Issue
Block a user