Package limn.graphics

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.

@FunctionalInterface public interface TextRuler
Measures text without a frame in flight: what widget layout uses (the layout pass runs before painting, when no 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

    Fields
    Modifier and Type
    Field
    Description
    static final TextRuler
    A ruler that measures everything as zero: what a detached widget gets, and what TextRulers serves before a backend installs a real one.
  • Method Summary

    Modifier and Type
    Method
    Description
    default long
    A counter that moves whenever this ruler would shape the same string differently: the one input to shaping that a caller holding a ShapedText cannot see for itself.
    measure(String text, Font font)
    Measures text in font, in logical points, on the UI thread.
    default float
    scanWidth(String text, Font font)
    The width of text in font for 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 ShapedText
    shape(String text, Font font)
    Shapes one line of text in font, 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 ShapedText
    shape(String text, Font font, ShapedText.Direction base)
    Shapes one line of text in font for a paragraph that reads base: the glyphs, their positions, and the geometry every caret, hit test, selection and line break is then asked for.
  • Field Details

    • NONE

      static final TextRuler NONE
      A ruler that measures everything as zero: what a detached widget gets, and what TextRulers serves before a backend installs a real one.

      Its epoch() is a reserved value no other ruler ever answers, and deliberately not the 0 a lambda inherits from the default. Epoch 0 means "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

      TextMetrics measure(String text, Font font)
      Measures text in font, 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 — its measure is shape(…).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 the shape default 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 from measure and painting from shape — has to reconcile the two itself, as TextArea does.

      Parameters:
      text - the string to measure; empty is legal and measures as zero width
      font - the face and size to measure in
    • scanWidth

      default float scanWidth(String text, Font font)
      The width of text in font for 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 answers measure by 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, as TextArea does. A caller that would rather be exact than cheap wants measure(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 is measure(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 width
      font - the face and size to measure in
    • shape

      default ShapedText shape(String text, Font font)
      Shapes one line of text in font, 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.LTR for a string that has none; the rule is ShapedText.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.shapeText passes 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, and ShapedText has 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 line
      font - the face and size to shape in
    • shape

      default ShapedText shape(String text, Font font, ShapedText.Direction base)
      Shapes one line of text in font for a paragraph that reads base: 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 @FunctionalInterface that a lambda can still satisfy. It measures one grapheme cluster at a time, reorders by java.text.Bidi, and reports every glyph as ShapedText.NO_GLYPH so 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 from measure(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 legal
      font - the face and size to shape in
      base - 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 a ShapedText cannot 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: ShapedText positions 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 Fonts because 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 an I18nString memoizes 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, which ShapedText.matches(java.lang.String, limn.graphics.Font, limn.graphics.ShapedText.Direction, limn.graphics.TextRuler) treats as always current