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 String
    HORIZONTAL ELLIPSIS, U+2026: what ellipsize(limn.graphics.ShapedText, float) appends where it cuts.
    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 float
    How far the capitals of font stand above the baseline, in logical points: the top of a flat capital such as H in the face font resolves to, which is the face whose ascent and descent measure(java.lang.String, limn.graphics.Font) reports.
    default float
    centredBaseline(Font font, float boxHeight)
    Where one line of font puts its baseline, from the top of a box boxHeight tall, for its capitals to stand in the vertical middle of the box: half the box plus half the cap height.
    default ShapedText
    ellipsize(ShapedText line, float available)
    Cuts a shaped line to available points and ends it with an ellipsis, re-shaping the kept prefix beside it through this ruler.
    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

    • ELLIPSIS

      static final String ELLIPSIS
      HORIZONTAL ELLIPSIS, U+2026: what ellipsize(limn.graphics.ShapedText, float) appends where it cuts.
      See Also:
    • 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. 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. Measurement cannot leave the UI thread, so the lever is to have the faces resident before a family is first measured: Fonts.preload(java.lang.String) reads a family's regular and bold faces on a worker, and Fonts.setDefaultFamilyWhenResident(java.lang.String) switches the UI font only once they are in.

      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
    • capHeight

      default float capHeight(Font font)
      How far the capitals of font stand above the baseline, in logical points: the top of a flat capital such as H in the face font resolves to, which is the face whose ascent and descent measure(java.lang.String, limn.graphics.Font) reports.

      It is what centredBaseline(limn.graphics.Font, float) centres on. A box's text reads as centred when the band from the baseline to the cap line is centred, because that band is what the eye takes for the line: the room the ascent and the descent reserve above and below it is not the same in any face, so centring the whole line box puts the capitals a little high or a little low depending on the face, and the bundled one stood them about a seventieth of an em high.

      The default answers ascent - descent of an empty measurement, the one value for which centredBaseline(limn.graphics.Font, float) centres the line box exactly as a ruler with no outlines to read has to; a ruler that reads its faces answers the face's own cap height.

      Parameters:
      font - the face and size
      Returns:
      the cap height, in logical points
    • centredBaseline

      default float centredBaseline(Font font, float boxHeight)
      Where one line of font puts its baseline, from the top of a box boxHeight tall, for its capitals to stand in the vertical middle of the box: half the box plus half the cap height. Every single-line control in this toolkit places its text with this, and reports it as its baseline, so a field, a combo, a button and a date all read on the same line and on their box's centre.

      Lower-case letters, digits with round tops and the glyphs of another script drawn by a fallback face sit where their face puts them against that baseline, which is the point of a baseline: a row of controls in several scripts still reads on one line.

      Parameters:
      font - the face and size
      boxHeight - the height of the box the line is centred in
      Returns:
      the baseline's distance from the top of the box
    • 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
    • ellipsize

      default ShapedText ellipsize(ShapedText line, float available)
      Cuts a shaped line to available points and ends it with an ellipsis, re-shaping the kept prefix beside it through this ruler.

      ShapedText.fitEnd(int, float) says where to cut, against the budget of the uncut shaping. Re-shaping the kept prefix beside an ellipsis can join, ligate or kern differently and come out a hair wider than the budget promised, and a label that overflows its box by a hair is a label whose ellipsis is clipped. So the cut retreats one caret position at a time until the re-shaped line fits: zero or one iteration for Latin, and the loop exists for the scripts where a cut changes the forms on both sides of it. Label and Chart each carried this loop; it belongs to the ruler that does the shaping.

      Parameters:
      line - the uncut line, shaped by this ruler
      available - the width it must fit in, in logical points
      Returns:
      the line as it will be drawn; the lone ellipsis when nothing fits beside it
    • 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