| |
| |
| |
|
|
| const { RTL_REGEX, NEUTRAL_REGEX } = require('./constants') |
|
|
| |
| |
| |
| |
| function getLineDirection (segments, startIndex) { |
| for (let i = startIndex; i < segments.length; i++) { |
| const seg = segments[i] |
| if (seg.kind === 'space' || seg.kind === 'break') continue |
| if (seg.text.match(RTL_REGEX)) return 'rtl' |
| if (!seg.text.match(NEUTRAL_REGEX)) return 'ltr' |
| } |
| return 'ltr' |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| function layoutText (prepared, maxWidth, maxHeight) { |
| const { segments, fontSize, lineHeight, emojiSize } = prepared |
| |
| |
| |
| const ascent = Number.isFinite(prepared.ascent) ? prepared.ascent : fontSize |
| const descent = Number.isFinite(prepared.descent) ? prepared.descent : fontSize |
| if (segments.length === 0) { |
| return { lines: [], width: 0, height: 0, lineCount: 0, truncated: false } |
| } |
|
|
| |
| if (maxWidth > 10000) maxWidth = 10000 |
| if (maxHeight > 10000) maxHeight = 10000 |
|
|
| const lines = [] |
| let currentLine = { segments: [], width: 0, direction: 'ltr' } |
| let lineY = ascent |
| let maxLineWidth = 0 |
| let truncated = false |
|
|
| |
| |
| let pendingBreak = null |
|
|
| currentLine.direction = getLineDirection(segments, 0) |
|
|
| for (let i = 0; i < segments.length; i++) { |
| const seg = segments[i] |
|
|
| |
| if (seg.kind === 'break') { |
| |
| const hasMoreContent = i + 1 < segments.length |
| if (hasMoreContent && maxHeight > 0 && lineY + lineHeight > maxHeight) { |
| truncated = true |
| applyTruncation(currentLine, prepared, maxWidth) |
| break |
| } |
|
|
| |
| if (currentLine.width > maxLineWidth) maxLineWidth = currentLine.width |
| currentLine.y = lineY |
| lines.push(currentLine) |
| lineY += lineHeight |
| pendingBreak = null |
|
|
| |
| currentLine = { segments: [], width: 0, direction: getLineDirection(segments, i + 1) } |
| continue |
| } |
|
|
| const segWidth = seg.kind === 'emoji' ? emojiSize : seg.width |
|
|
| |
| if (seg.kind === 'space' && currentLine.width + segWidth > maxWidth) { |
| |
| currentLine.segments.push({ index: i, width: segWidth }) |
| continue |
| } |
|
|
| |
| if (currentLine.segments.length === 0 && segWidth > maxWidth && seg.kind === 'text' && prepared.computeGraphemeWidths) { |
| var gData = prepared.computeGraphemeWidths(seg) |
| if (gData && gData.widths.length > 1) { |
| var cursor = 0 |
| while (cursor < gData.texts.length) { |
| var fitCount = 0 |
| var fitWidth = 0 |
| for (var g = cursor; g < gData.widths.length; g++) { |
| if (fitWidth + gData.widths[g] > maxWidth && fitCount > 0) break |
| fitWidth += gData.widths[g] |
| fitCount++ |
| } |
| if (fitCount === 0) { fitCount = 1; fitWidth = gData.widths[cursor] } |
|
|
| var isLast = cursor + fitCount >= gData.texts.length |
| currentLine.segments.push({ |
| index: i, |
| width: fitWidth, |
| graphemeStart: cursor > 0 ? cursor : undefined, |
| graphemeEnd: isLast ? undefined : cursor + fitCount |
| }) |
| currentLine.width += fitWidth |
|
|
| if (!isLast) { |
| if (currentLine.width > maxLineWidth) maxLineWidth = currentLine.width |
| currentLine.y = lineY |
| lines.push(currentLine) |
| lineY += lineHeight |
| currentLine = { segments: [], width: 0, direction: currentLine.direction } |
| } |
| cursor += fitCount |
| } |
| continue |
| } |
| } |
|
|
| |
| if (currentLine.width + segWidth > maxWidth && currentLine.segments.length > 0) { |
| |
|
|
| |
| if (maxHeight > 0 && lineY + lineHeight > maxHeight) { |
| truncated = true |
| applyTruncation(currentLine, prepared, maxWidth) |
| break |
| } |
|
|
| if (pendingBreak) { |
| |
| const { segIndex, lineWidth, lineSegCount } = pendingBreak |
|
|
| |
| currentLine.segments.length = lineSegCount |
| currentLine.width = lineWidth |
|
|
| |
| if (currentLine.width > maxLineWidth) maxLineWidth = currentLine.width |
| currentLine.y = lineY |
| lines.push(currentLine) |
| lineY += lineHeight |
| pendingBreak = null |
|
|
| |
| const newDirection = getLineDirection(segments, segIndex + 1) |
| currentLine = { segments: [], width: 0, direction: newDirection } |
| i = segIndex |
| continue |
| } |
|
|
| |
| if (prepared.computeGraphemeWidths) { |
| const remainingWidth = currentLine.width === 0 ? maxWidth : maxWidth - currentLine.width |
| const gData = prepared.computeGraphemeWidths(seg) |
| if (gData && gData.widths.length > 1) { |
| let fitCount = 0 |
| let fitWidth = 0 |
|
|
| for (let g = 0; g < gData.widths.length; g++) { |
| if (fitWidth + gData.widths[g] > remainingWidth && fitCount > 0) break |
| fitWidth += gData.widths[g] |
| fitCount++ |
| } |
|
|
| if (fitCount > 0 && fitCount < gData.texts.length) { |
| |
| currentLine.segments.push({ index: i, width: fitWidth, graphemeEnd: fitCount }) |
| currentLine.width += fitWidth |
|
|
| |
| if (currentLine.width > maxLineWidth) maxLineWidth = currentLine.width |
| currentLine.y = lineY |
| lines.push(currentLine) |
| lineY += lineHeight |
|
|
| |
| var remCursor = fitCount |
| var remDirection = getLineDirection(segments, i) |
| currentLine = { segments: [], width: 0, direction: remDirection } |
|
|
| while (remCursor < gData.texts.length) { |
| var rFit = 0 |
| var rWidth = 0 |
| for (var rg = remCursor; rg < gData.widths.length; rg++) { |
| if (rWidth + gData.widths[rg] > maxWidth && rFit > 0) break |
| rWidth += gData.widths[rg] |
| rFit++ |
| } |
| if (rFit === 0) { rFit = 1; rWidth = gData.widths[remCursor] } |
|
|
| var remIsLast = remCursor + rFit >= gData.texts.length |
| currentLine.segments.push({ |
| index: i, |
| width: rWidth, |
| graphemeStart: remCursor > 0 ? remCursor : undefined, |
| graphemeEnd: remIsLast ? undefined : remCursor + rFit |
| }) |
| currentLine.width += rWidth |
|
|
| if (!remIsLast) { |
| if (currentLine.width > maxLineWidth) maxLineWidth = currentLine.width |
| currentLine.y = lineY |
| lines.push(currentLine) |
| lineY += lineHeight |
| currentLine = { segments: [], width: 0, direction: remDirection } |
| } |
| remCursor += rFit |
| } |
| continue |
| } |
| } |
| } |
|
|
| |
| if (currentLine.width > maxLineWidth) maxLineWidth = currentLine.width |
| currentLine.y = lineY |
| lines.push(currentLine) |
| lineY += lineHeight |
|
|
| |
| const newDirection = getLineDirection(segments, i) |
| currentLine = { segments: [], width: 0, direction: newDirection } |
|
|
| if (seg.kind !== 'space') { |
| currentLine.segments.push({ index: i, width: segWidth }) |
| currentLine.width += segWidth |
| } |
|
|
| pendingBreak = null |
| continue |
| } |
|
|
| |
| if (seg.kind === 'space') { |
| pendingBreak = { |
| segIndex: i, |
| lineWidth: currentLine.width, |
| lineSegCount: currentLine.segments.length |
| } |
| } |
|
|
| |
| currentLine.segments.push({ index: i, width: segWidth }) |
| currentLine.width += segWidth |
| } |
|
|
| |
| if (currentLine.segments.length > 0) { |
| if (currentLine.width > maxLineWidth) maxLineWidth = currentLine.width |
| currentLine.y = lineY |
| lines.push(currentLine) |
| } |
|
|
| |
| for (const line of lines) { |
| let x = 0 |
| |
| let contentWidth = 0 |
| for (const seg of line.segments) { |
| seg.x = x |
| x += seg.width |
| const srcSeg = segments[seg.index] |
| if (srcSeg && srcSeg.kind !== 'space') { |
| contentWidth = x |
| } |
| } |
| line.contentWidth = contentWidth || line.width |
| } |
|
|
| const totalHeight = lines.length > 0 ? lines[lines.length - 1].y + descent : 0 |
|
|
| return { |
| lines, |
| width: maxLineWidth, |
| height: totalHeight, |
| lineCount: lines.length, |
| truncated |
| } |
| } |
|
|
| |
| |
| |
| function applyTruncation (line, prepared, maxWidth) { |
| |
| if (line.segments.length === 0) { |
| line.width = 0 |
| line.truncated = true |
| return |
| } |
|
|
| const ctx = require('./text-prepare').getMeasureCtx() |
| const ellipsis = '\u2026' |
| |
| const lastLayoutSeg = line.segments[line.segments.length - 1] |
| if (lastLayoutSeg) { |
| const lastSeg = prepared.segments[lastLayoutSeg.index] |
| if (lastSeg) ctx.font = lastSeg.font |
| } |
| const ellipsisWidth = ctx.measureText(ellipsis).width |
| const targetWidth = maxWidth - ellipsisWidth |
|
|
| |
| let totalWidth = 0 |
| let trimIndex = line.segments.length |
| for (let s = 0; s < line.segments.length; s++) { |
| if (totalWidth + line.segments[s].width > targetWidth) { |
| trimIndex = s |
| break |
| } |
| totalWidth += line.segments[s].width |
| } |
|
|
| line.segments.length = Math.max(1, trimIndex) |
| if (totalWidth === 0) { |
| totalWidth = line.segments[0].width |
| } |
| line.width = totalWidth |
| line.truncated = true |
| } |
|
|
| |
| |
| |
| |
| function shrinkWrap (prepared, maxWidth, maxHeight) { |
| const initial = layoutText(prepared, maxWidth, maxHeight) |
|
|
| |
| if (initial.lineCount <= 1 || initial.truncated) return initial |
|
|
| |
| let maxSegWidth = 0 |
| for (const seg of prepared.segments) { |
| if (seg.width > maxSegWidth) maxSegWidth = seg.width |
| } |
|
|
| |
| let lo = Math.max(maxSegWidth, initial.width / initial.lineCount * 0.7) |
| let hi = initial.width |
| const targetLineCount = initial.lineCount |
|
|
| |
| while (hi - lo > 2) { |
| const mid = (lo + hi) / 2 |
| const trial = layoutText(prepared, mid, maxHeight) |
|
|
| if (trial.lineCount <= targetLineCount && !trial.truncated) { |
| hi = mid |
| } else { |
| lo = mid |
| } |
| } |
|
|
| return layoutText(prepared, Math.ceil(hi), maxHeight) |
| } |
|
|
| module.exports = { layoutText, shrinkWrap, getLineDirection } |
|
|