Class ShapedText
TextRuler.shape(String, Font) and held by the widget, because shaping is the
expensive half of drawing text and the answer is needed twice — once to lay out, once to
paint — and those two have to agree.
Holding it is the point, and it is the move I18nString makes for a translation applied
to the text's geometry: a value recomputed at the point of use is recomputed once per frame
forever. A widget keeps one field and refreshes it against its inputs:
private ShapedText shaped;
...
TextRuler ruler = textRuler();
ShapedText.Direction base = ShapedText.Direction.of(text, neutralBase);
if (shaped == null || !shaped.matches(text, font, base, ruler)) {
shaped = ruler.shape(text, font, base);
}
canvas.drawText(shaped, x, baseline, ink);
neutralBase there is the direction of the widget that owns the text: what a string
with no strong character of its own falls back to. A widget with no direction axis under it
passes ShapedText.Direction.LTR and has the two-argument TextRuler.shape(String, Font)
written out.
Why a value and not a measurement. With a shaper in the pipeline the width of the first
n characters of a string is not the width of those characters measured alone: inside
their line they join, ligate, kern and reorder differently than in isolation. Prefix measurement
is therefore not a slower way to place a caret, it is a wrong one. Everything a widget would have
asked a prefix for is asked of the whole line instead — where an index
draws, which index is under this x, which
boxes cover this range, where to cut it — and asking is a lookup or a
binary search against work already done.
Two axes, and they must never be substituted for one another. caretAt(int),
caretX(limn.graphics.ShapedText.Position), hitTest(float), indexAt(float), selection(int, int),
caretLeft(limn.graphics.ShapedText.Position) and caretRight(limn.graphics.ShapedText.Position) are visual: they speak in x positions on the
screen. advanceTo(int), indexForAdvance(float) and fitEnd(int, float) are logical:
they speak in advance consumed by a range of the string, which is what a wrap budget or an
ellipsis budget is made of and which is order-independent, so it still means something on a
reordered line. Where isSimple() holds the two coincide; where it does not, using one
for the other is the bug that makes a selection band cover the wrong half of a line.
Coordinates. One line, no \n: the widget splits paragraphs and shapes each
line. x is in logical points and grows rightwards from the origin the run is
drawn at, whatever the base direction, so the run always occupies
[0, metrics().width()] and right-to-left text is not drawn at negative x — it fills
the same box from the other end, and right-aligning it is a matter of choosing the origin.
y is an offset from the baseline, positive down, matching Canvas. Vertical
extent is the caller's: metrics() gives the band, and the caret, the selection box and
the underline are all drawn against the same ascent and descent, so there is only one answer to
keep in agreement.
Indices are char offsets into text() in logical order, the
same index space the edit model, the clipboard and the IME speak. Every index accepted here is
clamped into [0, text().length()] and snapped to a cluster boundary rather than rejected:
a hit test on a moving pointer and a caret restored from a stale model both have to produce a
position, not an exception. Caret stops are the cluster boundaries the shaper reported; where one
disagrees with an extended grapheme cluster — a Devanagari conjunct is the case that bites
— the shaper's cluster wins, because a caret cannot be placed inside a glyph.
What makes a held value stale is exactly the three things matches(java.lang.String, limn.graphics.Font, limn.graphics.ShapedText.Direction, limn.graphics.TextRuler) tests, and
nothing else: the text, the Font (a value, so a control-size step or a theme change
produces a different one and is caught by the same comparison), and the ruler's
epoch, which covers every input the caller cannot see —
which file a family resolves to, which faces are registered and still resident, the shaping
language. The monitor content scale is not on that list. Positions here are unquantized
logical points measured in font units, so a window dragged to a 2× display re-rasterizes
glyph bitmaps and re-shapes nothing. That is a promise this type makes to the backend, not an
accident: a shaper asked for hinted positions would break it, and every value in the process
would have to be re-shaped whenever a window crossed a monitor boundary.
Two allocation classes, deliberately visible in the shape of the API. Runs are few
— one for the overwhelming majority of strings — so runs() is a list of
records built once when the value is built. Glyphs are many and are read once per glyph per
frame, so they live in parallel primitive arrays behind glyphId(int) and its neighbours;
no per-glyph object exists at any point, and no array is handed out to be mutated.
selection(int, int) allocates because it is called at most once per paint;
selection(int, int, float[]) is the same answer into a buffer the caller keeps, for the
drag that repaints at frame rate.
No accessor on this type returns anything but an int, a float, a
String, a Font, a TextMetrics or a record of those. A face is an
int that the ruler which produced the value assigns and only that ruler interprets. That
is structural rather than a rule someone has to remember: it is what lets this type live in a
module that cannot import a graphics library, and it is what makes holding one across frames safe
— keeping a value alive cannot keep a font file mapped.
Immutable, and therefore free to travel. Producing one is UI-thread only, for the
reasons TextRuler.measure(java.lang.String, limn.graphics.Font) gives; reading one is not, so layout precomputed off the UI
thread can hand back the value rather than the string. Equality is identity: two shapings of the
same string are the same answer but not the same object, and the question a widget actually has
is matches(java.lang.String, limn.graphics.Font, limn.graphics.ShapedText.Direction, limn.graphics.TextRuler), which is different and cheaper.
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionstatic enumWhich side of a char index a caret sits on, and so which of the two points that index can occupy is meant.static final classAssembles aShapedTextrun by run.static final recordThe two places a caret may sit for one char index, for a caller that stores no side.static enumWhich way a paragraph reads.static final recordA caret position: where text is inserted, plus which side of that index the caret is on.static final recordOne shaped stretch: a single face, a single embedding level, a single shaping call, and the unit the backend resolves a face at and batches a draw at.static final recordOne horizontal box of a selection, in logical points from the left edge of the line. -
Field Summary
Fields -
Method Summary
Modifier and TypeMethodDescriptionfloatadvanceTo(int charIndex) Advance consumed by the characters beforecharIndex: the sum of the advances of every cluster logically preceding it.The paragraph direction this line resolved to: what a caller aligns against, and which of a split caret's two positions is the strong one.static ShapedText.Builderbuilder(String text, Font font, ShapedText.Direction baseDirection, int glyphCapacity) Starts building a shaped line: what aTextRulerimplementation calls, and the only other way one is made.caretAt(int charIndex) Both places a caret may sit forcharIndex, for a caller that stores an index and no side.intHow many caret stops this line has: one per cluster boundary, so always at least one, and exactly1for an empty line.intcaretIndex(int ordinal) Thecharoffset of one caret stop, in logical order: ascending,0at ordinal0andtext().length()atcaretCount() - 1.caretLeft(ShapedText.Position from) The caret stop one step to the left on the line — the Left arrow key, whatever direction the text under it runs.intcaretOrdinal(int charIndex) The ordinal of the caret stop at or beforecharIndex: the inverse ofcaretIndex(int), and the third and last piece of the stop table.The caret stop one step to the right on the line: the Right arrow key, and the mirror ofcaretLeft(limn.graphics.ShapedText.Position)in every respect, including why it speaksShapedText.Positionand not indices.floatcaretX(ShapedText.Position position) The one x a caller with a stored side means:caretAt(p.charIndex()).x(p.affinity()).longepoch()The ruler epoch this was shaped under, or0for a value that depends on no ruler state.intfitEnd(int from, float available) Where to cut: the end of the longest run of clusters starting atfrom, in logical order, whose advances fit withinavailable.font()The family, size and style it was shaped for.floatglyphAdvance(int glyphIndex) How far the pen moves after this glyph, in logical points.intglyphCluster(int glyphIndex) Thecharoffset intotext()that this glyph came from: the start of its cluster, so a ligature's several characters all report the offset of the first, and several marks on one base all report the offset of the base.intHow many glyphs this line draws.intglyphId(int glyphIndex) floatglyphX(int glyphIndex) The glyph's origin along the baseline, in logical points from the left edge of the line: the pen position plus whatever the shaper offset it by.floatglyphY(int glyphIndex) The glyph's offset from the baseline, positive down, in logical points: zero for everything except a mark the shaper attached.hitTest(float x) The caret position a click atxasks for, side included.intindexAt(float x) The char index a click atxasks for:hitTest(x).charIndex(), for a caller that places a caret and keeps no side.intindexForAdvance(float advance) The largest caret stop whoseadvanceTo(int)does not exceedadvance: the inverse of the budget.booleanisSimple()Whether this line is one left-to-right run in one face with no reordering: character order and screen order agree, so caret x is monotone in the char index and every geometry query is a binary search over the glyphs rather than a walk over the runs.booleanmatches(String text, Font font, ShapedText.Direction base, TextRuler ruler) Whether this value is still the right answer fortextandfontin a paragraph that readsbase, underruler: the whole invalidation test in one call, so no caller has to remember that there are four parts to it.metrics()Extents of the line, in logical points, on the same terms and with the same unquantized meaning asTextRuler.measure(java.lang.String, limn.graphics.Font).runs()The runs, in visual order, left to right: what the backend iterates to paint, the unit at which a face is resolved and a draw is batched, and one entry for almost every string the toolkit draws.selection(int start, int end) The boxes covering the characters in[start, end)— N of them, never one.intselection(int start, int end, float[] out) selection(int, int)into a buffer the caller owns: writesx0, x1pairs from index0and returns how many boxes were written.text()The line this was shaped from: the index space of every query here.static ShapedTextuniform(String text, Font font, float advance, TextMetrics lineMetrics, long epoch) A line whose every cluster has the same advance and no glyph: the drawn form of a masked field, and of anything else that paints marks of its own instead of text.
-
Field Details
-
NO_GLYPH
public static final int NO_GLYPHThe glyph id of a cluster this face draws by other means: the backend paints it fromtext()over the cluster's characters instead of from the glyph atlas.This one sentinel keeps two unrelated things working with one branch in the paint loop. A colour-emoji cluster is a bitmap strike with its own advance, not an outline, and always was. And a ruler with no shaper — a test fake, or a backend whose native did not load — reports every cluster this way, so its positions are exact and its painting falls back to the per-code-point path that produced them. A whole-value "is this shaped" flag cannot express the mixed case at all.
- See Also:
-
-
Method Details
-
text
The line this was shaped from: the index space of every query here. -
font
The family, size and style it was shaped for. -
metrics
Extents of the line, in logical points, on the same terms and with the same unquantized meaning asTextRuler.measure(java.lang.String, limn.graphics.Font).width()is the sum of the glyph advances, so it isadvanceTo(text().length())by construction and the two cannot be made to disagree however the runs were assembled. -
baseDirection
The paragraph direction this line resolved to: what a caller aligns against, and which of a split caret's two positions is the strong one. Never absent — by the time a value exists the question has been answered, which is whyShapedText.Directionhas no third constant. -
isSimple
public boolean isSimple()Whether this line is one left-to-right run in one face with no reordering: character order and screen order agree, so caret x is monotone in the char index and every geometry query is a binary search over the glyphs rather than a walk over the runs.Derived, never asserted.
ShapedText.Builder.build()computes this from the runs and glyphs it was actually fed, so itemization cannot change and leave the flag behind — which is the failure that would make every fast path in the toolkit silently wrong for the one string that needed the slow one. Ligatures and kerning do not break it: they change how wide characters are, not what order they are in. A second face, a right-to-left run, or a reordered matra does.Callers use it to decide whether an operation that is only defined on monotone text is legal at all. It is never needed to make
caretAt(int),hitTest(float)orselection(int, int)correct; those handle both. -
epoch
public long epoch()The ruler epoch this was shaped under, or0for a value that depends on no ruler state.Compare through
matches(java.lang.String, limn.graphics.Font, limn.graphics.ShapedText.Direction, limn.graphics.TextRuler)rather than reading this: the comparison that matters is against a ruler's current epoch, and a caller holding a bare number is a caller that can compare it against the wrong one. It is exposed because a renderer that is also the producing ruler can use it to decide, in one integer comparison, whether the face ids in the glyph payload are still worth trusting. -
matches
Whether this value is still the right answer fortextandfontin a paragraph that readsbase, underruler: the whole invalidation test in one call, so no caller has to remember that there are four parts to it.textis compared by identity first, so a widget holding theStringanI18nStringmemoized never pays a character scan. Thefontcomparison isFont's own, which is why a control-size step or a theme change needs nothing extra here.baseis compared because a paragraph direction is an input to shaping and not just to placement. It decides which bidi level a boundary neutral takes, which decides which run that neutral extends, which decides which face measures it — so a line of mixed content is genuinely a fraction of a point wider in one direction than the other. Without this part a widget whose subtree changed direction would keep drawing a value shaped for yesterday's direction and be told it was current: invisible in a screenshot, wrong in every geometry query asked of it.The ruler is the reason this is a method and not three field comparisons at the call site: a
FontnamingFont.DEFAULT_FAMILYis equal to itself across aFonts.setDefaultFamily(java.lang.String)call, and a face this was shaped against can have been evicted and closed since, so a widget checking only text, font and direction would keep drawing glyph ids that name a face that is gone.A value carrying epoch
0is current under every ruler, which is right for a fake and for geometry that no ruler produced. That exemption is the ruler's alone: a fake's direction still has to agree, because the direction is a property of the value and not of the machinery that produced it.TextRuler.NONEdeliberately declines the exemption — it stamps a reserved epoch of its own, so a line shaped while a widget was detached goes stale under the first real ruler instead of staying a zero-width answer forever.- Parameters:
text- the string the caller is about to drawfont- the font it will be drawn inbase- the paragraph direction it would be shaped for; pass what would be passed toTextRuler.shape(String, Font, Direction)ruler- the ruler that would re-shape it; its epoch is read, never stored
-
caretCount
public int caretCount()How many caret stops this line has: one per cluster boundary, so always at least one, and exactly1for an empty line.The stop table is what every geometry query here searches, and enumerating it is how a test pins bidi caret behaviour over known cases — logical order in, expected visual positions out — instead of probing x values and hoping to hit one. It is also how a widget that draws a mark per cluster gets its count without a second rule for finding cluster boundaries, which is the drift that puts the caret between two dots.
-
caretIndex
public int caretIndex(int ordinal) Thecharoffset of one caret stop, in logical order: ascending,0at ordinal0andtext().length()atcaretCount() - 1. Visual order is a different question, andcaretLeft(limn.graphics.ShapedText.Position)/caretRight(limn.graphics.ShapedText.Position)are how it is asked.- Parameters:
ordinal- in[0, caretCount()), clamped
-
caretOrdinal
public int caretOrdinal(int charIndex) The ordinal of the caret stop at or beforecharIndex: the inverse ofcaretIndex(int), and the third and last piece of the stop table.With it the snapping rule every index-taking method here obeys is expressible in this type's own vocabulary — an index
iis treated ascaretIndex(caretOrdinal(i))— and the two things a caller cannot otherwise get become one call each: the stop after an index iscaretIndex(caretOrdinal(i) + 1), which is what a line breaker takes when a word is too wide for its line and not even one cluster fits, and the stop before it iscaretIndex(caretOrdinal(i) - 1). Both are logical neighbours;caretLeft(limn.graphics.ShapedText.Position)andcaretRight(limn.graphics.ShapedText.Position)are the visual ones, and they are not the same question.- Parameters:
charIndex- a char index intotext(), clamped into range
-
caretAt
Both places a caret may sit forcharIndex, for a caller that stores an index and no side.An index on a direction boundary has two visual positions, because the character before it and the character after it are drawn nowhere near each other, and which one the next typed character lands at depends on the direction of what is typed. A caret drawn at only one of them tells the user something false about their own cursor. Off a boundary the two are the same point; see
ShapedText.Caret.- Parameters:
charIndex- a char index intotext(), clamped and snapped to a caret stop
-
caretX
The one x a caller with a stored side means:caretAt(p.charIndex()).x(p.affinity()). This is whathitTest(float)hands back, so a click round-trips to the pixel it landed on.- Parameters:
position- where the caret is, index and side
-
hitTest
The caret position a click atxasks for, side included.Resolved through the cluster under the point, never by searching caret x values: the cluster whose visual box contains
xis found, and the caret goes to that cluster's leading edge withShapedText.Affinity.DOWNSTREAMwhenxfell in its leading half and to its trailing edge withShapedText.Affinity.UPSTREAMotherwise. Halves are visual, so the leading half of a right-to-left cluster is its right half. This is what makes a click either side of a direction boundary land on the two different insertion points that share that point on the line, which a search over caret x values structurally cannot do because both of them sit at the same x.Cluster boxes are half-open on the right, so an
xlanding exactly on the seam between two clusters belongs to the one on its right. That is arbitrary and it is fixed here anyway: a click on a seam has to resolve the same way every time, and a rule chosen at the call site is a rule two call sites will choose differently.xoutside[0, metrics().width()]clamps to the nearest cluster, which is what a drag past the end of the line wants. It is not what a click in the empty space to the right of a line wants, which is the logical end of the line: a caller that offers that space has to compare againstmetrics().width()first, because on a line that ends in the direction opposite the paragraph's the nearest cluster to the right edge is not the last character.- Parameters:
x- logical points from the run origin, growing rightwards
-
indexAt
public int indexAt(float x) The char index a click atxasks for:hitTest(x).charIndex(), for a caller that places a caret and keeps no side. Correct as far as it goes, and it goes exactly as far as one keystroke: a caret stored without its side is a caret that jumps the next time it moves across a direction boundary.- Parameters:
x- logical points from the run origin, growing rightwards
-
selection
The boxes covering the characters in[start, end)— N of them, never one.A range that is contiguous in the string stops being contiguous on the line the moment it crosses a direction boundary: selecting across the seam of Latin and Hebrew highlights the Latin part and the Hebrew part with untouched text between them, because the characters between them in the string are drawn outside the visual range. A single rectangle cannot express that, and rounding it up to one would highlight text that is not selected, so this returns every box and a caller that wants one rectangle is asking a question with no true answer. The same call draws an IME preedit underline and the highlight under the block being converted, which is this question asked of a sub-range.
Boxes come back in ascending
x, never overlap, and are merged where they touch — so a whole-line selection is one box however many runs the line has, and a translucent fill never double-blends along a seam. An empty or inverted range returns an empty list: a caret is not a zero-width selection, and a caller that wants a mark for one is askingcaretAt(int). No box is ever zero-width, so a cluster that consumes no advance contributes none rather than a band the fill cannot show. There is no vertical extent here on purpose; the band is the widget's own ink box, which it already computes for the caret and the underline. The returned list is unmodifiable.- Parameters:
start- first char index of the range, clamped and snapped to a caret stopend- char index one past the range, clamped and snapped to a caret stop
-
selection
public int selection(int start, int end, float[] out) selection(int, int)into a buffer the caller owns: writesx0, x1pairs from index0and returns how many boxes were written.outmust hold2 * runs().size()floats, which is the exact upper bound — a logical range maps to a contiguous visual stretch within any one run, so a selection cannot produce more boxes than the line has runs, and merging only reduces the count. A selection drag repaints at frame rate, and this is the form that does not put a list and N records on the floor each time.It throws on a short buffer rather than writing what fits, because writing fewer boxes than the selection has paints a band over some of the user's own text and leaves the rest unhighlighted — which reads as a rendering glitch rather than as the sizing bug it is.
- Parameters:
start- first char index of the range, clamped and snapped to a caret stopend- char index one past the range, clamped and snapped to a caret stopout- destination, at least2 * runs().size()floats long- Returns:
- how many boxes were written;
2 *this many floats were touched - Throws:
IllegalArgumentException- ifoutis shorter than2 * runs().size()
-
caretLeft
The caret stop one step to the left on the line — the Left arrow key, whatever direction the text under it runs. Returnsfromunchanged when there is no stop further left, which is how a multi-line caller knows to move to the previous line.The rule is stated in clusters, not in indices, because that is the only form of it that is well defined: take the cluster whose visual box abuts
caretX(from)on the left, and return the position on that cluster's far (left) edge — its leading edge withShapedText.Affinity.DOWNSTREAMif it reads left to right, its trailing edge withShapedText.Affinity.UPSTREAMif it reads right to left.It takes and returns a
ShapedText.Positionrather than an index because visual movement is not a function of the index alone. An index on a direction boundary occupies two points on the line, and two presses in a row have to leave from the point the first press arrived at; an index does not say which of the two that was, so an index-taking form has to guess, and whichever side it guesses, the caret walks left out of one run and then jumps to the far end of the line on the next press. That is non-determinism across two keystrokes, not imprecision.Visual movement only. Logical movement — the next grapheme cluster in the string, which is what a delete, an undo or a text-range API works in — stays the editing model's job and does not come from here.
- Parameters:
from- where the caret is now, index and side
-
caretRight
The caret stop one step to the right on the line: the Right arrow key, and the mirror ofcaretLeft(limn.graphics.ShapedText.Position)in every respect, including why it speaksShapedText.Positionand not indices. Returnsfromunchanged at the right end of the line.- Parameters:
from- where the caret is now, index and side
-
advanceTo
public float advanceTo(int charIndex) Advance consumed by the characters beforecharIndex: the sum of the advances of every cluster logically preceding it. Monotone non-decreasing,0at0andmetrics().width()attext().length(), whatever the direction — a reordered line still has a total, and so does every prefix of it, because a sum does not care what order it was added in.This is a budget, not a promise about a substring. It says how much of the line's width the first
charIndexcharacters account for within this shaping. It does not say how wide that prefix would be if it were shaped on its own, and under a shaper those two differ: joining forms change at the cut, a ligature that spanned it disappears, and the kerning at the seam is gone. A caller that cuts a line at an index has to re-shape both pieces before painting them, and may then find the cut piece a hair wider or narrower than the budget promised.- Parameters:
charIndex- a char index intotext(), clamped and snapped to a caret stop
-
indexForAdvance
public int indexForAdvance(float advance) The largest caret stop whoseadvanceTo(int)does not exceedadvance: the inverse of the budget.- Parameters:
advance- a width in logical points; below zero yields0, and at or abovemetrics().width()yieldstext().length()
-
fitEnd
public int fitEnd(int from, float available) Where to cut: the end of the longest run of clusters starting atfrom, in logical order, whose advances fit withinavailable. ExactlyindexForAdvance(advanceTo(from) + available), named because thefromanchoring is the part a caller gets wrong.An ellipsis passes
from = 0. A greedy line breaker shapes the paragraph once and then walks it with this, one line per call, askingjava.text.BreakIteratorfor the last break opportunity at or before each answer; without the anchor it would have to re-shape the remainder to ask again, which is quadratic in the length of the paragraph. Used alone it cuts mid-word, and in a script without spaces it cuts where no break is allowed, so the break iterator is not optional.The result is a caret stop in
[from, text().length()], and equalsfromwhen not even one cluster fits — which a caller breaking a word too wide for its line has to handle by taking one cluster anyway. EverythingadvanceTo(int)says about re-shaping what is cut applies here.- Parameters:
from- char index to start from, clamped and snapped to a caret stopavailable- width budget in logical points; a non-positive budget returnsfrom
-
runs
The runs, in visual order, left to right: what the backend iterates to paint, the unit at which a face is resolved and a draw is batched, and one entry for almost every string the toolkit draws.Immutable. The runs' glyph ranges tile
[0, glyphCount())in order, and their character ranges tile the text — but those character ranges are not in ascending order once anything reorders, which is the whole reason this list exists in visual order and the builder is fed in logical order. -
glyphCount
public int glyphCount()How many glyphs this line draws. Unrelated totext().length()once anything ligates, and zero is legal rather than an error: an empty string and a line of control characters both carry geometry and no glyphs. -
glyphId
public int glyphId(int glyphIndex) The glyph index within the face of the run that contains it, orNO_GLYPH. An id from one face means nothing in another, which is why a run carries its face and a glyph does not, and why both are meaningful only to theTextRulerthat produced them.- Parameters:
glyphIndex- in[0, glyphCount())
-
glyphX
public float glyphX(int glyphIndex) The glyph's origin along the baseline, in logical points from the left edge of the line: the pen position plus whatever the shaper offset it by. Already in visual order and already reordered, so the paint loop is a walk with no arithmetic of its own and no pen to carry.- Parameters:
glyphIndex- in[0, glyphCount())
-
glyphY
public float glyphY(int glyphIndex) The glyph's offset from the baseline, positive down, in logical points: zero for everything except a mark the shaper attached. A shaper that reports mark attachment positive-up has to negate on the way in.- Parameters:
glyphIndex- in[0, glyphCount())
-
glyphAdvance
public float glyphAdvance(int glyphIndex) How far the pen moves after this glyph, in logical points. Zero for an attached mark, which is what keeps a cluster's box the width of its base instead of the width of the base plus its accents.- Parameters:
glyphIndex- in[0, glyphCount())
-
glyphCluster
public int glyphCluster(int glyphIndex) Thecharoffset intotext()that this glyph came from: the start of its cluster, so a ligature's several characters all report the offset of the first, and several marks on one base all report the offset of the base.This mapping is the contract the caret rests on. A shaper reports clusters as offsets into the buffer it was handed, and every run boundary moves that origin; an off-by-one here is a caret that lands one character away from every click, in every field, forever. It is why
ShapedText.Builder.glyph(int, int, float, float, float)demands whole-string offsets and rejects anything else at the call that supplied it.- Parameters:
glyphIndex- in[0, glyphCount())
-
uniform
public static ShapedText uniform(String text, Font font, float advance, TextMetrics lineMetrics, long epoch) A line whose every cluster has the same advance and no glyph: the drawn form of a masked field, and of anything else that paints marks of its own instead of text.It exists so that a password field is not shaped. Its dots are not glyphs, its content must never reach a shaper or the memo a shaper keeps, and every geometric question it asks has a closed form in one multiplication. What it buys is that the caret, the selection band, the hit test and the painted marks all come from one piece of arithmetic instead of two that must be kept in agreement: the number of marks to paint is
caretCount() - 1and the i-th one is centred at(i + 0.5f) * advance, so no caller has to divide a width by an advance to recover a count.Clusters here are extended grapheme clusters, so one mark stands for one user-perceived character. The result is simple and left-to-right: a mask has no direction, because it has no characters left to have one.
- Parameters:
text- the content being measured, which is never drawn and never shapedfont- the font whoselineMetricsthese areadvance- the width of one mark in logical points; must be positive and finitelineMetrics- ascent, descent and line height; its width is ignored, because the width here is the mark count times the advance and the two must not be able to disagreeepoch- the ruler epochlineMetricswas measured under, so thatmatches(java.lang.String, limn.graphics.Font, limn.graphics.ShapedText.Direction, limn.graphics.TextRuler)still notices a default-family change that leavesfontequal to itself;0for geometry that depends on no ruler state- Throws:
IllegalArgumentException- ifadvanceis not positive and finiteNullPointerException- iftext,fontorlineMetricsis null
-
builder
public static ShapedText.Builder builder(String text, Font font, ShapedText.Direction baseDirection, int glyphCapacity) Starts building a shaped line: what aTextRulerimplementation calls, and the only other way one is made.glyphCapacitysizes the arrays exactly once. A shaper knows its glyph count before it copies anything out, so passing the real number means the value is built with no growth and no copy; passing a wrong one is a wasted allocation and never a wrong answer.- Parameters:
text- the string being shaped; may be emptyfont- the font it is being shaped forbaseDirection- the resolved paragraph direction; seeShapedText.Directionfor why there is no third value to pass hereglyphCapacity- the exact glyph count when it is known, otherwise an estimate- Throws:
NullPointerException- if any reference argument is null
-