Skip to content

Commit

Permalink
Fix support for text vertical alignment
Browse files Browse the repository at this point in the history
Vertically position text based on vertical alignment set in Figma as well as text height. To calculate this, the text size from Figma is now saved into a text_size field in ViewStyle. This value is used in conjunction with the compose text height calculated to get a y offset for drawing text.

Support for rendering text across multiple lines still needs some work.
  • Loading branch information
rylin8 committed Sep 7, 2023
1 parent a0fc515 commit 4ddacd7
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 28 deletions.
5 changes: 5 additions & 0 deletions crates/figma_import/src/toolkit_style.rs
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,7 @@ pub struct ViewStyle {
pub text_align_vertical: TextAlignVertical,
pub text_overflow: TextOverflow,
pub text_shadow: Option<TextShadow>,
pub text_size: Size<f32>,
pub line_height: LineHeight,
pub line_count: Option<usize>, // None means no limit on # lines.
pub font_features: Vec<FontFeature>,
Expand Down Expand Up @@ -576,6 +577,7 @@ impl Default for ViewStyle {
text_align_vertical: TextAlignVertical::Top,
text_overflow: TextOverflow::Clip,
text_shadow: None,
text_size: Size::default(),
line_height: LineHeight::Percent(1.0),
line_count: None,
font_features: Vec::new(),
Expand Down Expand Up @@ -734,6 +736,9 @@ impl ViewStyle {
if self.text_shadow != other.text_shadow {
delta.text_shadow = other.text_shadow;
}
if self.text_size != other.text_size {
delta.text_size = other.text_size;
}
if self.line_height != other.line_height {
delta.line_height = other.line_height;
}
Expand Down
5 changes: 5 additions & 0 deletions crates/figma_import/src/transform_flexbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,11 @@ fn compute_layout(node: &Node, parent: Option<&Node>) -> ViewStyle {
style.horizontal_sizing = vector.layout_sizing_horizontal.into();
style.vertical_sizing = vector.layout_sizing_vertical.into();

if let Some(Vector { x: Some(x), y: Some(y) }) = &node.size {
style.text_size.width = *x;
style.text_size.height = *y;
}

// The text style also contains some layout information. We previously exposed
// auto-width text in our plugin.
match text_style.text_auto_resize {
Expand Down
2 changes: 2 additions & 0 deletions crates/jni/src/jni.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,10 +203,12 @@ fn jni_add_node<'local>(
}
}

/*
warn!(
"### jni_add_node {} layoutid {} parent {} index {}",
view.name, layout_id, parent_layout_id, child_index
);
*/
let layout_response =
add_view(layout_id, parent_layout_id, child_index, view, variant_view);
return layout_response_to_bytearray(env, &layout_response);
Expand Down
108 changes: 81 additions & 27 deletions designcompose/src/main/java/com/android/designcompose/DesignText.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.android.designcompose

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
Expand All @@ -33,7 +34,6 @@ import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.graphics.isIdentity
import androidx.compose.ui.graphics.toComposeRect
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFontLoader
import androidx.compose.ui.text.AnnotatedString
Expand All @@ -51,6 +51,7 @@ import com.android.designcompose.serdegen.Layout
import com.android.designcompose.serdegen.LineHeight
import com.android.designcompose.serdegen.StyledTextRun
import com.android.designcompose.serdegen.TextAlign
import com.android.designcompose.serdegen.TextAlignVertical
import com.android.designcompose.serdegen.View
import com.android.designcompose.serdegen.ViewStyle
import java.util.Optional
Expand Down Expand Up @@ -220,7 +221,8 @@ internal fun DesignText(
TextOverflow.Clip
else TextOverflow.Ellipsis

val textLayoutData = TextLayoutData(annotatedText, textStyle, LocalFontLoader.current)
val textLayoutData =
TextLayoutData(annotatedText, textStyle, LocalFontLoader.current, style.text_size)
val maxLines = if (style.line_count.isPresent) style.line_count.get().toInt() else Int.MAX_VALUE
val textMeasureData =
TextMeasureData(textLayoutData, density, maxLines, style.min_width.pointsAsDp().value)
Expand Down Expand Up @@ -280,17 +282,21 @@ internal fun DesignText(
setLayout(newLayout)
}

val verticalOffset = remember { mutableStateOf(0) }
val renderHeight = remember { mutableStateOf(0) }
LaunchedEffect(style, textLayoutData, density) {
// Only set the size if autoWidthHeight is false, because otherwise the measureFunc is used
if (!isAutoHeightFillWidth(style)) {
val textBounds = measureTextBounds(style, textLayoutData, density)
verticalOffset.value = textBounds.verticalOffset
renderHeight.value = textBounds.renderHeight
println(
"### Text Layout $nodeName: width ${style.width.pointsAsDp()} height ${style.height} textBounds ${textBounds.width} ${textBounds.height}"
"### Text Layout $nodeName: textBounds ${textBounds.width} ${textBounds.layoutHeight} vertOffset ${textBounds.verticalOffset} renderHeight ${textBounds.renderHeight}"
)
LayoutManager.setNodeSize(
layoutId,
textBounds.width.roundToInt(),
textBounds.height.roundToInt()
textBounds.width,
textBounds.layoutHeight,
)
}
}
Expand All @@ -299,17 +305,28 @@ internal fun DesignText(
// "### DesignText $nodeName left ${layout?.left} width ${layout?.width} height
// ${layout?.height}"
// )

var adjustedLayout =
Layout(
layout?.order ?: 0,
layout?.width ?: 0F,
renderHeight.value.toFloat(),
layout?.left ?: 0F,
(layout?.top ?: 0F) + verticalOffset.value.toFloat(),
)

// The modifier needs .wrapContentSize() so that the text can render outside of the parent frame
// if the parent frame does not clip contents.
val layoutModifier =
Modifier.layoutToModifier(layout)
Modifier.layoutToModifier(adjustedLayout)
// .offset(y = verticalOffset.value.dp)
.wrapContentSize(align = Alignment.TopStart, unbounded = true)
.textTransform(style) // TODO needed?
.clipToBounds() // TODO needed?

// The text modifier needs to have its width and height, but not offset, set so that it renders
// multiple lines if needed
val textModifier = modifier.layoutSizeToModifier(layout)
val textModifier = modifier.layoutSizeToModifier(adjustedLayout)
Box(modifier = layoutModifier) {
val replacementComponent = customizations.getComponent(nodeName)
if (replacementComponent != null) {
Expand Down Expand Up @@ -372,45 +389,82 @@ fun measureTextBoundsFunc(
return Pair(rectBounds.width().toFloat(), outHeight)
}

private class TextBounds(
// Width of the measured text
val width: Int,
// Height of the text for layout purposes. This is usually the height of the text in Figma,
// which is used so that text is laid out in a way to match Figma.
val layoutHeight: Int,
// Height of the text for rendering purposes. Since text in Compose is a bit different than in
// Figma, this is usually taller than layoutHeight and is used to render text so that it does
// not get cut off at the bottom.
val renderHeight: Int,
// Vertical offset to render text, calculated from the text vertical alignment
val verticalOffset: Int,
)

private fun measureTextBounds(
style: ViewStyle,
textLayoutData: TextLayoutData,
density: Density
): Rect {
): TextBounds {
var textWidth: Int
var textHeight: Int
var selfHeight =
if (style.height is Dimension.Points) (style.height as Dimension.Points).value.roundToInt()
else 0
var verticalAlignmentOffset: Int
when (val width = style.width) {
is Dimension.Points -> {
// Fixed width
val fixedWidth = width.value
return when (val height = style.height) {
textWidth = width.value.roundToInt()
when (style.height) {
is Dimension.Points -> {
// Fixed height
val fixedHeight = height.value
android.graphics
.Rect(0, 0, fixedWidth.roundToInt(), fixedHeight.roundToInt())
.toComposeRect()
// Fixed height. Get actual height so we can calculate vertical alignment
val (rectBounds, _) = textLayoutData.boundsForWidth(Int.MAX_VALUE, 1, density)
textHeight = rectBounds.height()
verticalAlignmentOffset = rectBounds.top
}
else -> {
// Auto height
val maxLines =
if (style.line_count.isPresent) style.line_count.get().toInt()
else Int.MAX_VALUE
val (rectBounds, lines) =
textLayoutData.boundsForWidth(fixedWidth.toInt(), maxLines, density)
val fixedRectBounds =
android.graphics.Rect(
rectBounds.left,
rectBounds.top,
fixedWidth.roundToInt(),
rectBounds.bottom
)
fixedRectBounds.toComposeRect()
val (rectBounds, _) =
textLayoutData.boundsForWidth(textWidth, maxLines, density)
textHeight = rectBounds.height()
selfHeight = textLayoutData.textBoxSize.height.roundToInt()
verticalAlignmentOffset = rectBounds.top
}
}
}
else -> {
// Auto width, meaning everything is in one line
val (rectBounds, lines) = textLayoutData.boundsForWidth(Int.MAX_VALUE, 1, density)
return rectBounds.toComposeRect()
// TODO auto width can also span multiple lines; support this
// val maxLines = if (style.line_height is LineHeight.Pixels)
// (textLayoutData.textBoxSize.height / (style.line_height as
// LineHeight.Pixels).value).roundToInt().coerceAtLeast(1)
// else 1

val (rectBounds, _) = textLayoutData.boundsForWidth(Int.MAX_VALUE, 1, density)
textWidth = rectBounds.width()
textHeight = rectBounds.height()
selfHeight = textLayoutData.textBoxSize.height.roundToInt()
verticalAlignmentOffset = rectBounds.top
}
}

if (selfHeight == 0) selfHeight = textHeight
if (selfHeight > textHeight) {
when (style.text_align_vertical) {
is TextAlignVertical.Center -> verticalAlignmentOffset = (selfHeight - textHeight) / 2
is TextAlignVertical.Bottom -> verticalAlignmentOffset = (selfHeight - textHeight)
}
}

println(
"### MeasureText selfHeight $selfHeight textHeight $textHeight vertOffset $verticalAlignmentOffset text ${textLayoutData.annotatedString}"
)

return TextBounds(textWidth, selfHeight, textHeight, verticalAlignmentOffset)
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import com.android.designcompose.serdegen.GridSpan
import com.android.designcompose.serdegen.ItemSpacing
import com.android.designcompose.serdegen.Layout
import com.android.designcompose.serdegen.LayoutChangedResponse
import com.android.designcompose.serdegen.Size
import com.android.designcompose.serdegen.View
import com.novi.bincode.BincodeDeserializer
import com.novi.bincode.BincodeSerializer
Expand All @@ -45,7 +46,8 @@ import kotlin.math.ceil
internal data class TextLayoutData(
val annotatedString: AnnotatedString,
val textStyle: androidx.compose.ui.text.TextStyle,
val resourceLoader: Font.ResourceLoader
val resourceLoader: Font.ResourceLoader,
val textBoxSize: Size,
)

internal data class TextMeasureData(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,12 @@ internal fun mergeStyles(base: ViewStyle, override: ViewStyle): ViewStyle {
} else {
base.text_shadow
}
style.text_size =
if (override.text_size.width != 0.0f || override.text_size.height != 0.0f) {
override.text_size
} else {
base.text_size
}
style.line_height =
if (!override.line_height.equals(LineHeight.Percent(1.0f))) {
override.line_height
Expand Down

0 comments on commit 4ddacd7

Please sign in to comment.