[go: nahoru, domu]

blob: 4f0d6ba528d5118b59834fccf0de6c7fbd5d7de3 [file] [log] [blame]
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.ui.painting
import androidx.ui.clamp
import androidx.ui.engine.geometry.Offset
import androidx.ui.engine.geometry.Rect
import androidx.ui.engine.geometry.Size
import androidx.ui.engine.text.Paragraph
import androidx.ui.engine.text.ParagraphBuilder
import androidx.ui.engine.text.ParagraphConstraints
import androidx.ui.engine.text.ParagraphStyle
import androidx.ui.engine.text.TextAlign
import androidx.ui.engine.text.TextBaseline
import androidx.ui.engine.text.TextBox
import androidx.ui.engine.text.TextDirection
import androidx.ui.engine.text.TextPosition
import androidx.ui.engine.window.Locale
import androidx.ui.services.text_editing.TextRange
import androidx.ui.services.text_editing.TextSelection
import kotlin.math.ceil
/**
* Unfortunately, using full precision floating point here causes bad layouts because floating
* point math isn't associative. If we add and subtract padding, for example, we'll get
* different values when we estimate sizes and when we actually compute layout because the
* operations will end up associated differently. To work around this problem for now, we round
* fractional pixel values up to the nearest whole pixel value. The right long-term fix is to do
* layout using fixed precision arithmetic.
*/
fun applyFloatingPointHack(layoutValue: Float): Float {
return ceil(layoutValue)
}
/**
* An object that paints a [TextSpan] tree into a [Canvas].
*
* To use a [TextPainter], follow these steps:
*
* 1. Create a [TextSpan] tree and pass it to the [TextPainter] constructor.
*
* 2. Call [layout] to prepare the paragraph.
*
* 3. Call [paint] as often as desired to paint the paragraph.
*
* If the width of the area into which the text is being painted changes, return to step 2. If the
* text to be painted changes, return to step 1.
*
* * `text`: The (potentially styled) text to paint. After this is set, you must call [layout]
* before the next call to [paint]. This and [textDirection] must be non-null before you call
* [layout].
*
* * `textAlign`: How the text should be aligned horizontally. After this is set, you must call
* [layout] before the next call to [paint]. The [textAlign] property must not be null.
*
* * `textDirection`: The default directionality of the text. This controls how the
* [TextAlign.start], [TextAlign.end], and [TextAlign.justify] values of [textAlign] are resolved.
* After this is set, you must call [layout] before the next call to [paint]. This and [text] must
* be non-null before you call [layout].
*
* * `textScaleFactor`: The number of font pixels for each logical pixel.
* After this is set, you must call [layout] before the next call to [paint].
*
* * `maxLines`: An optional maximum number of lines for the text to span, wrapping if necessary.
* If the text exceeds the given number of lines, it is truncated such that subsequent lines are
* dropped.
* After this is set, you must call [layout] before the next call to [paint].
*
* * `ellipsis`: The string used to ellipsize overflowing text. Setting this to a non-empty
* string will cause this string to be substituted for the remaining text if the text can not fit
* within the specified maximum width.
* Specifically, the ellipsis is applied to the last line before the line truncated by [maxLines],
* if [maxLines] is non-null and that line overflows the width constraint, or to the first line
* that is wider than the width constraint, if [maxLines] is null. The width constraint is the
* `maxWidth` passed to [layout].
* After this is set, you must call [layout] before the next call to [paint].
* The higher layers of the system, such as the [Text] widget, represent overflow effects using
* the [TextOverflow] enum. The [TextOverflow.ellipsis] value corresponds to setting this property
* to U+2026 HORIZONTAL ELLIPSIS (…).
*
* `locale`: The locale used to select region-specific glyphs.
*/
class TextPainter(
text: TextSpan? = null,
textAlign: TextAlign = TextAlign.START,
textDirection: TextDirection? = null,
textScaleFactor: Float = 1.0f,
maxLines: Int? = null,
// TODO(Migration/qqd): We won't be able to use customized ellipsis, but lets leave it here for
// now. Maybe remove it later.
ellipsis: String? = null,
locale: Locale? = null
) {
init {
assert(maxLines == null || maxLines > 0)
}
var paragraph: Paragraph? = null
var needsLayout = true
var layoutTemplate: Paragraph? = null
var text: TextSpan? = text
set(value) {
if (field == value) return
if (field?.style != value?.style) layoutTemplate = null
field = value
paragraph = null
needsLayout = true
}
var textAlign: TextAlign = textAlign
set(value) {
if (field == value) return
field = value
paragraph = null
needsLayout = true
}
var textDirection: TextDirection? = textDirection
set(value) {
if (field == value) return
field = value
paragraph = null
layoutTemplate = null // Shouldn't really matter, but for strict correctness...
needsLayout = true
}
var textScaleFactor: Float = textScaleFactor
set(value) {
if (field == value) return
field = value
paragraph = null
layoutTemplate = null
needsLayout = true
}
var maxLines: Int? = maxLines
set(value) {
assert(value == null || value > 0)
if (field == value) return
field = value
paragraph = null
needsLayout = true
}
var ellipsis: String? = ellipsis
set(value) {
assert(value == null || value.isNotEmpty())
if (field == value) return
field = value
paragraph = null
needsLayout = true
}
var locale: Locale? = locale
set(value) {
if (field == value) return
field = value
paragraph = null
needsLayout = true
}
fun createParagraphStyle(defaultTextDirection: TextDirection? = null): ParagraphStyle {
// The defaultTextDirection argument is used for preferredLineHeight in case
// textDirection hasn't yet been set.
assert(textDirection != null || defaultTextDirection != null) {
"TextPainter.textDirection must be set to a non-null value before using the " +
"TextPainter."
}
return text?.style?.getParagraphStyle(
textAlign = textAlign,
textDirection = textDirection ?: defaultTextDirection,
textScaleFactor = textScaleFactor,
maxLines = maxLines,
ellipsis = ellipsis,
locale = locale
) ?: ParagraphStyle(
textAlign = textAlign,
textDirection = textDirection ?: defaultTextDirection,
maxLines = maxLines,
ellipsis = ellipsis,
locale = locale
)
}
/**
* The height of a space in [text] in logical pixels.
*
* Not every line of text in [text] will have this height, but this height is "typical" for
* text in [text] and useful for sizing other objects relative a typical line of text.
*
* Obtaining this value does not require calling [layout].
*
* The style of the [text] property is used to determine the font settings that contribute to
* the [preferredLineHeight]. If [text] is null or if it specifies no styles, the default
* [TextStyle] values are used (a 10 pixel sans-serif font).
*/
val preferredLineHeight: Float
get() {
if (layoutTemplate == null) {
val builder = ParagraphBuilder(
// TODO(Migration/qqd): The textDirection below used to be RTL.
createParagraphStyle(TextDirection.LTR)
) // direction doesn't matter, text is just a space
if (text?.style != null) {
builder.pushStyle(text?.style!!.getTextStyle(textScaleFactor = textScaleFactor))
}
builder.addText(" ")
layoutTemplate = builder.build()
layoutTemplate?.layout(ParagraphConstraints(width = Float.POSITIVE_INFINITY))
}
return layoutTemplate!!.height
}
private fun assertNeedsLayout(name: String) {
assert(!needsLayout) {
"TextPainter.$name should only be called after layout has been called."
}
}
/**
* The width at which decreasing the width of the text would prevent it from painting itself
* completely within its bounds.
*
* Valid only after [layout] has been called.
*/
val minIntrinsicWidth: Float
get() {
assertNeedsLayout("minIntrinsicWidth")
return applyFloatingPointHack(paragraph!!.minIntrinsicWidth)
}
/**
* The width at which increasing the width of the text no longer decreases the height.
*
* Valid only after [layout] has been called.
*/
val maxIntrinsicWidth: Float
get() {
assertNeedsLayout("maxIntrinsicWidth")
return applyFloatingPointHack(paragraph!!.maxIntrinsicWidth)
}
/**
* The horizontal space required to paint this text.
*
* Valid only after [layout] has been called.
*/
val width: Float
get() {
assertNeedsLayout("width")
return applyFloatingPointHack(paragraph!!.width)
}
/**
* The vertical space required to paint this text.
*
* Valid only after [layout] has been called.
*/
val height: Float
get() {
assertNeedsLayout("height")
return applyFloatingPointHack(paragraph!!.height)
}
/**
* The amount of space required to paint this text.
*
* Valid only after [layout] has been called.
*/
val size: Size
get() {
assertNeedsLayout("size")
return Size(width, height)
}
/**
* Returns the distance from the top of the text to the first baseline of the given type.
*
* Valid only after [layout] has been called.
*/
fun computeDistanceToActualBaseline(baseline: TextBaseline): Float {
assertNeedsLayout("computeDistanceToActualBaseline")
return when (baseline) {
TextBaseline.alphabetic -> paragraph!!.alphabeticBaseline
TextBaseline.ideographic -> paragraph!!.ideographicBaseline
}
}
/**
* Whether any text was truncated or ellipsized.
*
* If [maxLines] is not null, this is true if there were more lines to be drawn than the given
* [maxLines], and thus at least one line was omitted in the output; otherwise it is false.
*
* If [maxLines] is null, this is true if [ellipsis] is not the empty string and there was a
* line that overflowed the `maxWidth` argument passed to [layout]; otherwise it is false.
*
* Valid only after [layout] has been called.
*/
val didExceedMaxLines: Boolean
get() {
assertNeedsLayout("didExceedMaxLines")
return paragraph!!.didExceedMaxLines
}
private var lastMinWidth: Float = 0.0f
private var lastMaxWidth: Float = 0.0f
/**
* Computes the visual position of the glyphs for painting the text.
*
* The text will layout with a width that's as close to its max intrinsic width as possible
* while still being greater than or equal to `minWidth` and less than or equal to `maxWidth`.
*
* The [text] and [textDirection] properties must be non-null before this is called.
*/
fun layout(minWidth: Float = 0.0f, maxWidth: Float = Float.POSITIVE_INFINITY) {
assert(text != null) {
"TextPainter.text must be set to a non-null value before using the TextPainter."
}
assert(textDirection != null) {
"TextPainter.textDirection must be set to a non-null value before using the" +
" TextPainter."
}
if (!needsLayout && minWidth == lastMinWidth && maxWidth == lastMaxWidth) return
needsLayout = false
if (paragraph == null) {
val builder = ParagraphBuilder(createParagraphStyle())
text!!.build(builder, textScaleFactor = textScaleFactor)
paragraph = builder.build()
}
lastMinWidth = minWidth
lastMaxWidth = maxWidth
paragraph!!.layout(ParagraphConstraints(width = maxWidth))
if (minWidth != maxWidth) {
val newWidth = maxIntrinsicWidth.clamp(minWidth, maxWidth)
if (newWidth != width) {
paragraph!!.layout(ParagraphConstraints(width = newWidth))
}
}
}
/**
* Paints the text onto the given canvas at the given offset.
*
* Valid only after [layout] has been called.
*
* If you cannot see the text being painted, check that your text color does not conflict with
* the background on which you are drawing. The default text color is white (to contrast with
* the default black background color), so if you are writing an application with a white
* background, the text will not be visible by default.
*
* To set the text style, specify a [TextStyle] when creating the [TextSpan] that you pass to
* the [TextPainter] constructor or to the [text] property.
*/
fun paint(canvas: Canvas, offset: Offset) {
assert(!needsLayout) {
"TextPainter.paint called when text geometry was not yet calculated.\n" +
"Please call layout() before paint() to position the text before painting it."
}
canvas.drawParagraph(paragraph!!, offset)
}
// TODO(Migration/qqd): Implement offset related functions.
// bool _isUtf16Surrogate(int value) {
// return value & 0xF800 == 0xD800;
// }
/** Returns the closest offset after `offset` at which the inout cursor can be positioned. */
// TODO(Migration/qqd): Implement getOffsetAfter.
fun getOffsetAfter(offset: Int): Int {
TODO()
// final int nextCodeUnit = _text.codeUnitAt(offset);
// if (nextCodeUnit == null)
// return null;
// // TODO(goderbauer): doesn't handle extended grapheme clusters with more than one Unicode scalar value (https://github.com/flutter/flutter/issues/13404).
// return _isUtf16Surrogate(nextCodeUnit) ? offset + 2 : offset + 1;
}
/** Returns the closest offset before `offset` at which the inout cursor can be positioned. */
// TODO(Migration/qqd): Implement getOffsetBefore.
fun getOffsetBefore(offset: Int): Int {
TODO()
// final int prevCodeUnit = _text.codeUnitAt(offset - 1);
// if (prevCodeUnit == null)
// return null;
// // TODO(goderbauer): doesn't handle extended grapheme clusters with more than one Unicode scalar value (https://github.com/flutter/flutter/issues/13404).
// return _isUtf16Surrogate(prevCodeUnit) ? offset - 2 : offset - 1;
}
// TODO(Migration/qqd): Implement _getOffsetFromUpstream.
fun _getOffsetFromUpstream(offset: Int, caretPrototype: Rect): Offset {
TODO()
// final int prevCodeUnit = _text.codeUnitAt(offset - 1);
// if (prevCodeUnit == null)
// return null;
// final int prevRuneOffset = _isUtf16Surrogate(prevCodeUnit) ? offset - 2 : offset - 1;
// final List<TextBox> boxes = _paragraph.getBoxesForRange(prevRuneOffset, offset);
// if (boxes.isEmpty)
// return null;
// final TextBox box = boxes[0];
// final double caretEnd = box.end;
// final double dx = box.direction == TextDirection.rtl ? caretEnd : caretEnd - caretPrototype.width;
// return new Offset(dx, box.top);
}
// TODO(Migration/qqd): Implement _getOffsetFromDownstream.
fun _getOffsetFromDownstream(offset: Int, caretPrototype: Rect): Offset {
TODO()
// final int nextCodeUnit = _text.codeUnitAt(offset);
// if (nextCodeUnit == null)
// return null;
// final int nextRuneOffset = _isUtf16Surrogate(nextCodeUnit) ? offset + 2 : offset + 1;
// final List<TextBox> boxes = _paragraph.getBoxesForRange(offset, nextRuneOffset);
// if (boxes.isEmpty)
// return null;
// final TextBox box = boxes[0];
// final double caretStart = box.start;
// final double dx = box.direction == TextDirection.rtl ? caretStart - caretPrototype.width : caretStart;
// return new Offset(dx, box.top);
}
// TODO(Migration/qqd): Implement _emptyOffset.
var _emptyOffset: Offset = Offset(0.0f, 0.0f)
get() {
TODO()
// assert(!_needsLayout); // implies textDirection is non-null
// assert(textAlign != null);
// switch (textAlign) {
// case TextAlign.left:
// return Offset.zero;
// case TextAlign.right:
// return new Offset(width, 0.0);
// case TextAlign.center:
// return new Offset(width / 2.0, 0.0);
// case TextAlign.justify:
// case TextAlign.start:
// assert(textDirection != null);
// switch (textDirection) {
// case TextDirection.rtl:
// return new Offset(width, 0.0);
// case TextDirection.ltr:
// return Offset.zero;
// }
// return null;
// case TextAlign.end:
// assert(textDirection != null);
// switch (textDirection) {
// case TextDirection.rtl:
// return Offset.zero;
// case TextDirection.ltr:
// return new Offset(width, 0.0);
// }
// return null;
// }
// return null;
}
/**
* Returns the offset at which to paint the caret.
*
* Valid only after [layout] has been called.
*/
// TODO(Migration/qqd): Implement getOffsetForCaret.
fun getOffsetForCaret(position: TextPosition, caretPrototype: Rect): Offset {
TODO()
// assert(!_needsLayout);
// final int offset = position.offset;
// assert(position.affinity != null);
// switch (position.affinity) {
// case TextAffinity.upstream:
// return _getOffsetFromUpstream(offset, caretPrototype)
// ?? _getOffsetFromDownstream(offset, caretPrototype)
// ?? _emptyOffset;
// case TextAffinity.downstream:
// return _getOffsetFromDownstream(offset, caretPrototype)
// ?? _getOffsetFromUpstream(offset, caretPrototype)
// ?? _emptyOffset;
// }
// return null;
}
/**
* Returns a list of rects that bound the given selection.
*
* A given selection might have more than one rect if this text painter contains bidirectional
* text because logically contiguous text might not be visually contiguous.
*/
// TODO(Migration/qqd): Implement getBoxesForSelection.
fun getBoxesForSelection(selection: TextSelection): List<TextBox> {
TODO()
// assert(!needsLayout)
// return paragraph!!.getBoxesForRange(selection.start, selection.end)
}
/** Returns the position within the text for the given pixel offset. */
// TODO(Migration/qqd): Implement getPositionForOffset.
fun getPositionForOffset(offset: Offset): TextPosition {
TODO()
// assert(!needsLayout)
// return paragraph!!.getPositionForOffset(offset)
}
/**
* Returns the text range of the word at the given offset. Characters not part of a word, such
* as spaces, symbols, and punctuation, have word breaks on both sides. In such cases, this
* method will return a text range that contains the given text position.
*
* Word boundaries are defined more precisely in Unicode Standard Annex #29
* <http://www.unicode.org/reports/tr29/#Word_Boundaries>.
*/
// TODO(Migration/qqd): Implement getWordBoundary.
fun getWordBoundary(position: TextPosition): TextRange {
TODO()
// assert(!needsLayout)
// val indices: List<Int> = paragraph!!.getWordBoundary(position.offset)
// return TextRange(start = indices[0], end = indices[1])
}
}