Andrei Rudenko | 2d7eb40 | 2020-06-11 11:11:52 +0200 | [diff] [blame] | 1 | /* |
| 2 | * Copyright 2020 The Android Open Source Project |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | package androidx.ui.desktop |
| 17 | |
Louis Pullen-Freilich | 4dc4dac | 2020-07-22 14:39:14 +0100 | [diff] [blame] | 18 | import androidx.compose.ui.graphics.Color |
| 19 | import androidx.compose.ui.graphics.toArgb |
Louis Pullen-Freilich | ab19475 | 2020-07-21 22:21:22 +0100 | [diff] [blame] | 20 | import androidx.compose.ui.text.AnnotatedString |
| 21 | import androidx.compose.ui.text.ParagraphIntrinsics |
| 22 | import androidx.compose.ui.text.Placeholder |
| 23 | import androidx.compose.ui.text.SpanStyle |
| 24 | import androidx.compose.ui.text.SpanStyleRange |
| 25 | import androidx.compose.ui.text.TextStyle |
| 26 | import androidx.compose.ui.text.font.Font |
Louis Pullen-Freilich | a7eeb10 | 2020-07-22 17:54:24 +0100 | [diff] [blame^] | 27 | import androidx.compose.ui.unit.Density |
| 28 | import androidx.compose.ui.unit.TextUnit |
Andrei Rudenko | 2d7eb40 | 2020-06-11 11:11:52 +0200 | [diff] [blame] | 29 | import kotlin.math.ceil |
Igor Demin | 4a0e9f0 | 2020-06-26 17:58:39 +0300 | [diff] [blame] | 30 | import org.jetbrains.skija.paragraph.Paragraph |
| 31 | import org.jetbrains.skija.paragraph.ParagraphBuilder |
| 32 | import org.jetbrains.skija.paragraph.ParagraphStyle |
| 33 | import org.jetbrains.skija.paragraph.TextStyle as SkTextStyle |
Andrei Rudenko | 2d7eb40 | 2020-06-11 11:11:52 +0200 | [diff] [blame] | 34 | |
| 35 | internal class DesktopParagraphIntrinsics( |
| 36 | val text: String, |
| 37 | style: TextStyle, |
| 38 | spanStyles: List<SpanStyleRange>, |
Nikolay Igotti | 05d96a4 | 2020-06-17 14:25:19 +0300 | [diff] [blame] | 39 | @Suppress("UNUSED_PARAMETER") placeholders: List<AnnotatedString.Range<Placeholder>>, |
| 40 | @Suppress("UNUSED_PARAMETER") density: Density, |
Andrei Rudenko | 2d7eb40 | 2020-06-11 11:11:52 +0200 | [diff] [blame] | 41 | resourceLoader: Font.ResourceLoader |
| 42 | ) : ParagraphIntrinsics { |
| 43 | |
| 44 | val fontLoader = resourceLoader as FontLoader |
| 45 | val para: Paragraph |
| 46 | init { |
| 47 | para = buildParagraph(text, style, spanStyles) |
| 48 | |
| 49 | para.layout(Float.POSITIVE_INFINITY) |
| 50 | } |
| 51 | |
| 52 | override val minIntrinsicWidth = ceil(para.getMinIntrinsicWidth()) |
| 53 | override val maxIntrinsicWidth = ceil(para.getMaxIntrinsicWidth()) |
| 54 | |
| 55 | /** |
| 56 | * SkParagraph styles model doesn't match Compose's one. |
| 57 | * SkParagraph has only a stack-based push/pop styles interface that works great with Span |
| 58 | * trees. |
| 59 | * But in Compose we have a list of SpanStyles attached to arbitrary ranges, possibly |
| 60 | * overlapped, where a position in the list denotes style's priority |
| 61 | * We map Compose styles to SkParagraph styles by projecting every range start/end to single |
| 62 | * positions line and maintaining a list of active styles while building a paragraph. This list |
| 63 | * of active styles is being compiled into single SkParagraph's style for every chunk of text |
| 64 | */ |
| 65 | private fun buildParagraph( |
| 66 | text: String, |
| 67 | textStyle: TextStyle, |
| 68 | spanStyles: List<SpanStyleRange> |
| 69 | ): Paragraph { |
| 70 | val cuts = spansToCuts(spanStyles) |
| 71 | |
| 72 | var pos = 0 |
| 73 | val ps = textStyleToParagraphStyle(textStyle) |
| 74 | val pb = ParagraphBuilder(ps, fontLoader.fonts) |
| 75 | // TODO: for some reasons paragraph style doesn't apply to text. maybe it's Skia bug, |
| 76 | // we need to investigate |
| 77 | val currentStyles = mutableListOf(Pair(0, textStyle.toSpanStyle())) |
| 78 | pb.pushStyle(textStylesToSkStyle(currentStyles)!!) |
| 79 | |
| 80 | for (cut in cuts) { |
| 81 | pb.addText(text.subSequence(pos, cut.position).toString()) |
| 82 | pb.popStyle() |
| 83 | |
| 84 | when (cut.instruction) { |
| 85 | StyleInstruction.ADD -> currentStyles.add(Pair(cut.priority, cut.style)) |
| 86 | StyleInstruction.REMOVE -> currentStyles.remove(Pair(cut.priority, cut.style)) |
| 87 | } |
| 88 | |
| 89 | textStylesToSkStyle(currentStyles)?.let { ts -> |
| 90 | pb.pushStyle(ts) |
| 91 | } |
| 92 | pos = cut.position |
| 93 | } |
| 94 | |
| 95 | pb.addText(text.subSequence(pos, text.length).toString()) |
| 96 | |
| 97 | return pb.build() |
| 98 | } |
| 99 | |
| 100 | private enum class StyleInstruction { |
| 101 | ADD, |
| 102 | REMOVE |
| 103 | } |
| 104 | |
| 105 | private data class Cut( |
| 106 | val position: Int, |
| 107 | val priority: Int, |
| 108 | val style: SpanStyle, |
| 109 | val instruction: StyleInstruction |
| 110 | ) |
| 111 | |
| 112 | private fun spansToCuts(spans: List<SpanStyleRange>): List<Cut> { |
| 113 | val positions = mutableMapOf<Int, Cut>() |
| 114 | for ((i, span) in spans.withIndex()) { |
| 115 | positions[span.start] = Cut(span.start, i, span.item, StyleInstruction.ADD) |
| 116 | positions[span.end] = Cut(span.end, i, span.item, StyleInstruction.REMOVE) |
| 117 | } |
| 118 | |
| 119 | val cuts = ArrayList<Cut>(positions.size) |
| 120 | |
| 121 | for (v in positions.toSortedMap().values) { |
| 122 | cuts.add(v) |
| 123 | } |
| 124 | return cuts |
| 125 | } |
| 126 | |
| 127 | private fun textStyleToParagraphStyle(style: TextStyle): ParagraphStyle { |
| 128 | val pStyle = ParagraphStyle() |
| 129 | val textStyle = SkTextStyle() |
| 130 | applyStyles(style.toSpanStyle(), textStyle) |
| 131 | pStyle.setTextStyle(textStyle) |
| 132 | return pStyle |
| 133 | } |
| 134 | |
| 135 | private fun applyStyles(from: SpanStyle, to: SkTextStyle) { |
| 136 | if (from.color != Color.Unset) { |
| 137 | to.setColor(from.color.toArgb()) |
| 138 | } |
| 139 | from.fontFamily?.let { |
| 140 | val fontFamilies = fontLoader.ensureRegistered(it) |
| 141 | to.setFontFamilies(fontFamilies.toTypedArray()) |
| 142 | } |
| 143 | // TODO: support [TextUnit.Em] |
| 144 | if (from.fontSize != TextUnit.Inherit) { |
| 145 | to.setFontSize(from.fontSize.value) |
| 146 | } |
| 147 | } |
| 148 | |
| 149 | private fun textStylesToSkStyle(styles: List<Pair<Int, SpanStyle>>): SkTextStyle? { |
| 150 | if (styles.isEmpty()) { |
| 151 | return null |
| 152 | } |
| 153 | val skStyle = SkTextStyle() |
| 154 | for (s in styles.sortedBy { (priority, _) -> priority }.map { (_, v) -> v }) { |
| 155 | applyStyles(s, skStyle) |
| 156 | } |
| 157 | return skStyle |
| 158 | } |
| 159 | } |