Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 43 additions & 4 deletions src/text.cc
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,14 @@ Napi::Value LayoutCreate(const Napi::CallbackInfo& info) {
: align == "right" ? DWRITE_TEXT_ALIGNMENT_TRAILING
: DWRITE_TEXT_ALIGNMENT_LEADING);
}
// `justify: true`: every line but a paragraph's last set to fill the
// width at its word separators, after it is broken — DirectWrite's own
// justification, from the shaping the layout already did, where a caller
// that spaced the words itself laid the paragraph out three times over.
// The line before a newline ends a paragraph and is left as it is.
if (Given(options, "justify") && options.Get("justify").ToBoolean().Value() && !unbounded) {
format->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_JUSTIFIED);
}
format->SetReadingDirection(rtl ? DWRITE_READING_DIRECTION_RIGHT_TO_LEFT
: DWRITE_READING_DIRECTION_LEFT_TO_RIGHT);
if (Given(options, "lineHeight")) {
Expand Down Expand Up @@ -697,15 +705,22 @@ Napi::Value LayoutMetrics(const Napi::CallbackInfo& info) {
DWRITE_TEXT_METRICS metrics = {};
entry->layout->GetMetrics(&metrics);

float minWidth = 0;
entry->layout->DetermineMinWidth(&minWidth);

Napi::Object out = Napi::Object::New(env);
// Whole device pixels, rounded up, at the same boundary the CoreText engine
// rounds: a fractional measure is what makes a yoga layout blow up.
out.Set("width", Napi::Number::New(env, std::ceil(metrics.width)));
out.Set("height", Napi::Number::New(env, std::ceil(metrics.height)));
out.Set("minWidth", Napi::Number::New(env, std::ceil(minWidth)));
// `{ minWidth: false }` leaves the min-content width to `layoutMinWidth`:
// DetermineMinWidth runs the line breaker over the whole text again, and a
// caller that lays a paragraph out to draw it never asks.
const bool wantsMin = !(info.Length() > 1 && info[1].IsObject() &&
info[1].As<Napi::Object>().Has("minWidth") &&
!info[1].As<Napi::Object>().Get("minWidth").ToBoolean().Value());
if (wantsMin) {
float minWidth = 0;
entry->layout->DetermineMinWidth(&minWidth);
out.Set("minWidth", Napi::Number::New(env, std::ceil(minWidth)));
}
out.Set("lineCount", Napi::Number::New(env, metrics.lineCount));

std::vector<DWRITE_LINE_METRICS> lines(metrics.lineCount);
Expand Down Expand Up @@ -1049,6 +1064,28 @@ Napi::Value LayoutCaret(const Napi::CallbackInfo& info) {
return out;
}

// layoutMinWidth(handle) -> the min-content width, in whole pixels rounded up:
// what `layoutMetrics` answers as `minWidth` unless asked not to.
Napi::Value LayoutMinWidth(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
TextLayout* entry = LayoutFor(info[0].As<Napi::Number>().Int32Value());
if (!entry) return env.Null();
float minWidth = 0;
entry->layout->DetermineMinWidth(&minWidth);
return Napi::Number::New(env, std::ceil(minWidth));
}

// textFeatures() -> what this bridge's layouts take that an older one's do
// not, for a caller to ask rather than to know a version: `justify`, and
// `minWidth: false` on `layoutMetrics` with `layoutMinWidth` beside it.
Napi::Value TextFeatures(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
Napi::Object out = Napi::Object::New(env);
out.Set("justify", Napi::Boolean::New(env, true));
out.Set("lazyMinWidth", Napi::Boolean::New(env, true));
return out;
}

Napi::Value LayoutRelease(const Napi::CallbackInfo& info) {
TextLayout* entry = LayoutFor(info[0].As<Napi::Number>().Int32Value());
if (!entry) return info.Env().Undefined();
Expand Down Expand Up @@ -1588,6 +1625,8 @@ void InitTextExports(Napi::Env env, Napi::Object exports) {
set("layoutCreate", LayoutCreate);
set("layoutMetrics", LayoutMetrics);
set("layoutRelease", LayoutRelease);
set("layoutMinWidth", LayoutMinWidth);
set("textFeatures", TextFeatures);
set("layoutIndexAt", LayoutIndexAt);
set("layoutCaret", LayoutCaret);
// These two names are BackendContext2D's, not ours: it calls them on the
Expand Down
39 changes: 39 additions & 0 deletions test/text.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,45 @@ assert.ok(
'a span with features laid out nothing',
);

// --- what a layout takes ----------------------------------------------------

const features = win32.textFeatures();
assert.strictEqual(features.justify, true);
assert.strictEqual(features.lazyMinWidth, true);

// Justified: every line but the paragraph's last fills the width, the last
// is left as it is.
const prose =
'The quick brown fox jumps over the lazy dog, again and again, until the ' +
'paragraph runs over several lines of a narrow column.';
const linesOf = (justify) =>
win32.layoutMetrics(
win32.layoutCreate(prose, { family: 'Segoe UI', size: 14, maxWidth: 160, justify }),
).lines;
const ragged = linesOf(false);
const justified = linesOf(true);
assert.ok(justified.length > 2, 'the column did not wrap');
assert.strictEqual(justified.length, ragged.length, 'justifying moved a break');
// (a line's width here takes in the space it wrapped at)
for (const line of justified.slice(0, -1)) {
assert.ok(line.width >= 159, `a justified line ${line.width} wide, short of 160`);
}
assert.ok(
ragged.slice(0, -1).some((line) => line.width < 159),
'no ragged line fell short, so the test shows nothing',
);
assert.ok(
Math.abs(justified.at(-1).width - ragged.at(-1).width) <= 0.5,
'the last line was justified too',
);

// The min-content width on request: left out of the metrics when asked,
// and the same number from `layoutMinWidth`.
const lazy = win32.layoutCreate(prose, { family: 'Segoe UI', size: 14, maxWidth: 160 });
assert.strictEqual(win32.layoutMetrics(lazy, { minWidth: false }).minWidth, undefined);
assert.strictEqual(win32.layoutMinWidth(lazy), win32.layoutMetrics(lazy).minWidth);
assert.ok(win32.layoutMinWidth(lazy) > 0);

// --- drawing through the verb table ----------------------------------------

const surface = win32.createSurface(200, 100, 1);
Expand Down
Loading