Interface TextRuler
- Functional Interface:
- This is a functional interface and can therefore be used as the assignment target for a lambda expression or method reference.
Canvas exists). Metrics
are logical points, identical to Canvas.measureText(java.lang.String, limn.graphics.Font). Backends
install their ruler in TextRulers at startup; tests inject
deterministic fakes.-
Field Summary
FieldsModifier and TypeFieldDescriptionstatic final TextRulerA ruler that measures everything as zero: what a detached widget gets, and whatTextRulersserves before a backend installs a real one. -
Method Summary
Modifier and TypeMethodDescriptiondefault longepoch()A counter that moves whenever this ruler would shape the same string differently: the one input to shaping that a caller holding aShapedTextcannot see for itself.Measurestextinfont, in logical points, on the UI thread.default floatThe width oftextinfontfor a caller measuring far more strings than it will draw: cheap, allowed to be approximate, and required to leave no trace in whatever the ruler remembers.default ShapedTextShapes one line oftextinfont, resolving the paragraph direction from the text itself: the form nearly every caller wants, because nearly every caller is drawing a string it did not write.default ShapedTextshape(String text, Font font, ShapedText.Direction base) Shapes one line oftextinfontfor a paragraph that readsbase: the glyphs, their positions, and the geometry every caret, hit test, selection and line break is then asked for.
-
Field Details
-
NONE
A ruler that measures everything as zero: what a detached widget gets, and whatTextRulersserves before a backend installs a real one.Its
epoch()is a reserved value no other ruler ever answers, and deliberately not the0a lambda inherits from the default. Epoch0means "depends on no ruler state" and is current under every ruler — right for a test fake, whose answers are the answers. This ruler's answers are placeholders: a line shaped against it must go stale the moment a real ruler can be asked, or a widget whose first shaping happened while detached would hold a zero-width line that every real ruler afterwards certified as current (ADR 032 §9.6). Under this ruler itself the stamp still matches, so a detached widget shapes once, not once per layout pass.
-
-
Method Details
-
measure
Measurestextinfont, in logical points, on the UI thread. An implementation may assume that confinement and keep unsynchronized caches; layout, which is the main caller, runs there anyway.There is deliberately no asynchronous form, and one consequence has to be stated rather than discovered. An implementation is allowed to resolve a face on first use, so the first measurement in a family it has not yet loaded may read and parse a font file (tens of megabytes for a CJK face) on this thread, inside whatever frame asked. Since measurement cannot leave the UI thread, that cost cannot be moved off it either; the only lever a caller has is when a new family is first measured, which is why
Fonts.setDefaultFamily(java.lang.String)belongs on a settings action and not in an animation.An implementation that shapes should answer here what
shape(java.lang.String, limn.graphics.Font)answers, and the backend's does — itsmeasureisshape(…).metrics(). A ruler that computes the two independently is allowed to, and will drift: measuring per code point resolves a face per character while shaping resolves one per run, so the two disagree about a space between two Hebrew words, per seam, accumulating down the line. That is not a hairline, it is a caret painted outside the clip. This is a "should" rather than a contract because theshapedefault below cannot honour it: it measures one grapheme cluster at a time and so loses the kern at every cluster seam. A caller that must not be caught by the gap — sizing a scroll extent frommeasureand painting fromshape— has to reconcile the two itself, asTextAreadoes.- Parameters:
text- the string to measure; empty is legal and measures as zero widthfont- the face and size to measure in
-
scanWidth
The width oftextinfontfor a caller measuring far more strings than it will draw: cheap, allowed to be approximate, and required to leave no trace in whatever the ruler remembers.Why this is not
measure(java.lang.String, limn.graphics.Font). A shaping ruler answersmeasureby shaping and memoizing, which is right for the strings a frame is about to paint: the layout pass warms the memo and the paint pass finds them in it. It is exactly wrong for a scan over text nobody is drawing — a scroll extent that has to look at every line of a document. That scan walks its key set cyclically, so past the memo's depth it misses every entry every time and re-shapes the whole document; and because the memo is process-wide, it also evicts the strings that are on the screen, so the damage lands on widgets that did nothing. A bounded cache in front of an unbounded scan is not a cache, and the fix cannot live in the cache. So the scan asks a different question, and this is it.What is given up, stated so a caller cannot be surprised by it. This may disagree with
shape(java.lang.String, limn.graphics.Font)'s width, in either direction and by more than a rounding: an implementation is free to sum per-character advances, which misses a ligature (narrower when shaped) and misses a run's face resolution (wider when shaped, per word seam, accumulating). A caller that sizes something a caret must stay inside cannot treat this as an upper bound and has to reconcile it with widths it has actually shaped, asTextAreadoes. A caller that would rather be exact than cheap wantsmeasure(text, font).width()and should call it.Same thread and the same first-use costs as
measure(java.lang.String, limn.graphics.Font). The default ismeasure(text, font).width(), which is always correct and is the right answer for a ruler with nothing cheaper to offer — including every fake, which is why this is a default and this interface is still one a lambda can satisfy.- Parameters:
text- the string to measure; empty is legal and scans as zero widthfont- the face and size to measure in
-
shape
Shapes one line oftextinfont, resolving the paragraph direction from the text itself: the form nearly every caller wants, because nearly every caller is drawing a string it did not write.The direction comes from the first strong character, defaulting to
ShapedText.Direction.LTRfor a string that has none; the rule isShapedText.Direction.of(java.lang.String, limn.graphics.ShapedText.Direction). Declining to state a direction is this overload, not a third value of an enum — which is what keeps "not yet decided" out of every field that could hold a direction.A widget is the caller that can do better, and should decline to decline: it knows which way it reads, which is the one thing an all-neutral string — a count, a year, a clock face — cannot say for itself. The widget layer's
Widget.shapeTextpasses that answer as the neutral base; a widget shaping through this overload instead gets a left-to-right paragraph for exactly those strings, silently.Same thread, same first-use costs and the same reasons as
measure(java.lang.String, limn.graphics.Font), plus one more: shaping is the expensive half of drawing text. Call it when the text or the font changes and hold the result;ShapedText.matches(java.lang.String, limn.graphics.Font, limn.graphics.ShapedText.Direction, limn.graphics.TextRuler)is the check, andShapedTexthas the idiom. An implementation is expected to memoize as well, because the callers that cannot hold a value — a chart rebuilding its axis labels every frame — would otherwise re-shape at frame rate; what it hands back is immutable and may outlive the cache entry.- Parameters:
text- the line to shape; empty is legal and shapes to a zero-width linefont- the face and size to shape in
-
shape
Shapes one line oftextinfontfor a paragraph that readsbase: the glyphs, their positions, and the geometry every caret, hit test, selection and line break is then asked for. For the callers that know something the string does not say — a field whose content is a phone number in an Arabic form reads right to left however many Latin digits it starts with, and the first-strong rule cannot know that.The default implementation is the degraded path, and it is a default rather than an abstract method on purpose: a ruler with no shaper — a test fake, or a backend whose native did not load — inherits a correct value built from
measure(java.lang.String, limn.graphics.Font)alone, and this interface stays a@FunctionalInterfacethat a lambda can still satisfy. It measures one grapheme cluster at a time, reorders byjava.text.Bidi, and reports every glyph asShapedText.NO_GLYPHso that painting falls back to the per-code-point path that produced those positions. What is lost is what a shaper does: no contextual forms, no ligatures, no mark attachment, and marks occupying their own advance instead of attaching. A missing native narrows what the toolkit can draw and never stops it.Two consequences of measuring per cluster rather than per prefix, both deliberate. Reordering is done even here, so bidi caret and selection geometry — the part that looks right in a screenshot while being wrong — is testable against known cases with a fake ruler, no native and no font file. And kerning across a cluster seam is lost, so this default's
metrics().width()can differ slightly frommeasure(java.lang.String, limn.graphics.Font)of the same string: a ruler that kerns should override this rather than inherit it, which the backend does on both its shaping path and its degraded one.- Parameters:
text- the line to shape; empty is legalfont- the face and size to shape inbase- the paragraph direction to impose
-
epoch
default long epoch()A counter that moves whenever this ruler would shape the same string differently: the one input to shaping that a caller holding aShapedTextcannot see for itself.It must move when the resolution of a family to a face changes — a family registered, the font catalog replaced as system enumeration finishes, the default family switched — when the set of resident faces changes, because a ruler that evicts and closes faces leaves held values holding ids for faces that are gone, and when the shaping language changes, because the same characters take different forms in different languages. It must not move for a content-scale change:
ShapedTextpositions are unquantized and scale-independent, and bumping here would re-shape every string in the process every time a window crossed a monitor boundary.It lives on the ruler rather than on
Fontsbecause the ruler is the only thing that knows all of it: face residency is the shaping seam's business, and a facade that never loaded a face cannot report one being evicted.Epochs must be unique across rulers, not merely monotone within one. Draw them from one process-wide counter, because
ShapedText.matches(java.lang.String, limn.graphics.Font, limn.graphics.ShapedText.Direction, limn.graphics.TextRuler)compares a held value's stamp against whatever ruler it is handed: two rulers numbering independently would eventually both answer the same number, and a value shaped by one would then report itself current under the other. Installing a backend replaces the ruler outright, so this is the only thing standing between a held value and a ruler that never produced it.Compared through
ShapedText.matches(java.lang.String, limn.graphics.Font, limn.graphics.ShapedText.Direction, limn.graphics.TextRuler). A counter rather than a listener because the re-shape rides the relayout these changes already cause, so nothing has to subscribe and nothing can leak — the same reason anI18nStringmemoizes against an epoch instead of watching one.- Returns:
- the current epoch; a ruler whose answers never change may return a constant, and the
default returns
0, whichShapedText.matches(java.lang.String, limn.graphics.Font, limn.graphics.ShapedText.Direction, limn.graphics.TextRuler)treats as always current
-