From 9a5a81ce2b83611747e942c866bddef7f6ba8ab8 Mon Sep 17 00:00:00 2001 From: Robert Lin Date: Fri, 25 Aug 2023 21:38:01 -0700 Subject: [PATCH] Fix text layouts For auto width text fields, set their viewstyle's width and height to auto. The width needs to be auto so that it can expand. The height needs to be auto because Compose's text rendering is slightly bigger than Figma's so if it isn't auto it would get cut off. For auto height with a fill container width text field, set the viewstyle's width and height to auto, and also set the flex_grow field to 1. This is what differentiates this type of text layout with just auto width. Fix an issue where text would get clipped by its parent frame, even if the parent frame did not have clip contents set. To fix this, the Box() parent of DesignText() needs to have wrapContentSize(align = Alignment.TopStart, unbounded = true) set in its Modifier. --- crates/figma_import/src/bin/fetch.rs | 20 ++- crates/figma_import/src/figma_schema.rs | 6 + crates/figma_import/src/transform_flexbox.rs | 14 +- .../com/android/designcompose/DesignFrame.kt | 74 +++++++-- .../com/android/designcompose/DesignText.kt | 95 +++-------- .../com/android/designcompose/DesignView.kt | 84 ++-------- .../java/com/android/designcompose/Layout.kt | 71 ++++++++- .../java/com/android/designcompose/Utils.kt | 8 +- .../testapp/validation/MainActivity.kt | 148 ++++++++++++++---- 9 files changed, 305 insertions(+), 215 deletions(-) diff --git a/crates/figma_import/src/bin/fetch.rs b/crates/figma_import/src/bin/fetch.rs index 587258f95..0aabc48c1 100644 --- a/crates/figma_import/src/bin/fetch.rs +++ b/crates/figma_import/src/bin/fetch.rs @@ -17,6 +17,7 @@ use clap::Parser; use figma_import::{ add_view, add_view_measure, print_layout, remove_view, set_node_size, toolkit_schema::View, Document, NodeQuery, ProxyConfig, SerializedDesignDoc, ViewData, + toolkit_layout_style::Dimension, }; use std::io; use std::io::prelude::*; @@ -100,7 +101,19 @@ fn test_layout(view: &View, id: &mut i32, parent_layout_id: i32, child_index: i3 let my_id: i32 = id.clone(); *id = *id + 1; if let ViewData::Text { content: _ } = &view.data { - add_view_measure(my_id, parent_layout_id, child_index, view.clone(), measure_func); + let mut use_measure_func = false; + if let Dimension::Auto = view.style.width { + if let Dimension::Auto = view.style.height { + if view.style.flex_grow > 0.0 { + use_measure_func = true; + } + } + } + if use_measure_func { + add_view_measure(my_id, parent_layout_id, child_index, view.clone(), measure_func); + } else { + add_view(my_id, parent_layout_id, child_index, view.clone(), None); + } //taffy::node::MeasureFunc::Raw(measure_func)); } else if let ViewData::Container { shape: _, children } = &view.data { add_view(my_id, parent_layout_id, child_index, view.clone(), None); @@ -155,8 +168,9 @@ fn fetch_impl(args: Args) -> Result<(), ConvertError> { } } - println!("### SetNodeSize 400, 65"); - set_node_size(1, 400, 65); + println!("### SetNodeSize 250, 24"); + set_node_size(2, 250, 24); + set_node_size(5, 250, 24); print_layout(0); // TODO make a unit test for this below based off of #stage-replacements diff --git a/crates/figma_import/src/figma_schema.rs b/crates/figma_import/src/figma_schema.rs index 6ae360c2d..7f2a105bf 100644 --- a/crates/figma_import/src/figma_schema.rs +++ b/crates/figma_import/src/figma_schema.rs @@ -924,6 +924,12 @@ impl Node { _ => false, } } + pub fn is_text(&self) -> bool { + match &self.data { + NodeData::Text {..} => true, + _ => false, + } + } pub fn constraints(&self) -> Option<&LayoutConstraint> { if let Some(frame) = self.frame() { Some(&frame.constraints) diff --git a/crates/figma_import/src/transform_flexbox.rs b/crates/figma_import/src/transform_flexbox.rs index 1cf32b1e6..592b55fb2 100644 --- a/crates/figma_import/src/transform_flexbox.rs +++ b/crates/figma_import/src/transform_flexbox.rs @@ -31,7 +31,7 @@ use crate::figma_schema::{TextAutoResize, TextTruncation}; use crate::vector_schema; use crate::{ component_context::ComponentContext, - extended_layout_schema::{ExtendedAutoLayout, ExtendedTextLayout, LayoutType, SizePolicy}, + extended_layout_schema::{ExtendedAutoLayout, LayoutType, SizePolicy}, //ExtendedTextLayout figma_schema::{ BlendMode, Component, ComponentSet, EffectType, HorizontalLayoutConstraintValue, LayoutAlign, LayoutAlignItems, LayoutMode, LayoutSizing, LayoutSizingMode, LineHeightUnit, @@ -308,7 +308,7 @@ fn compute_layout(node: &Node, parent: Option<&Node>) -> ViewStyle { style.left = Dimension::Percent(0.0); style.right = Dimension::Auto; style.margin.start = Dimension::Points(left); - if !hug_width { + if !hug_width && !node.is_text() { style.width = Dimension::Points(width); } } @@ -316,7 +316,7 @@ fn compute_layout(node: &Node, parent: Option<&Node>) -> ViewStyle { style.left = Dimension::Auto; style.right = Dimension::Percent(0.0); style.margin.end = Dimension::Points(right); - if !hug_width { + if !hug_width && !node.is_text() { style.width = Dimension::Points(width); } } @@ -338,7 +338,7 @@ fn compute_layout(node: &Node, parent: Option<&Node>) -> ViewStyle { style.right = Dimension::Auto; style.margin.start = Dimension::Points(left - parent_bounds.width().ceil() / 2.0); - if !hug_width { + if !hug_width && !node.is_text() { style.width = Dimension::Points(width); } } @@ -354,7 +354,7 @@ fn compute_layout(node: &Node, parent: Option<&Node>) -> ViewStyle { style.top = Dimension::Percent(0.0); style.bottom = Dimension::Auto; style.margin.top = Dimension::Points(top); - if !hug_height { + if !hug_height && !node.is_text() { style.height = Dimension::Points(height); } } @@ -362,7 +362,7 @@ fn compute_layout(node: &Node, parent: Option<&Node>) -> ViewStyle { style.top = Dimension::Auto; style.bottom = Dimension::Percent(0.0); style.margin.bottom = Dimension::Points(bottom); - if !hug_height { + if !hug_height && !node.is_text() { style.height = Dimension::Points(height); } } @@ -378,7 +378,7 @@ fn compute_layout(node: &Node, parent: Option<&Node>) -> ViewStyle { style.bottom = Dimension::Auto; style.margin.top = Dimension::Points(top - parent_bounds.height().ceil() / 2.0); - if !hug_height { + if !hug_height && !node.is_text() { style.height = Dimension::Points(height); } } diff --git a/designcompose/src/main/java/com/android/designcompose/DesignFrame.kt b/designcompose/src/main/java/com/android/designcompose/DesignFrame.kt index f5e526114..a8840bc7d 100644 --- a/designcompose/src/main/java/com/android/designcompose/DesignFrame.kt +++ b/designcompose/src/main/java/com/android/designcompose/DesignFrame.kt @@ -28,6 +28,8 @@ import androidx.compose.foundation.lazy.grid.LazyGridScope import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier @@ -38,6 +40,7 @@ import androidx.compose.ui.geometry.toRect import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Paint import androidx.compose.ui.graphics.withSaveLayer +import androidx.compose.ui.layout.layoutId import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.Density @@ -47,7 +50,10 @@ import com.android.designcompose.serdegen.ComponentInfo import com.android.designcompose.serdegen.GridLayoutType import com.android.designcompose.serdegen.GridSpan import com.android.designcompose.serdegen.ItemSpacing +import com.android.designcompose.serdegen.Layout import com.android.designcompose.serdegen.NodeQuery +import com.android.designcompose.serdegen.View +import com.android.designcompose.serdegen.ViewData import com.android.designcompose.serdegen.ViewShape import com.android.designcompose.serdegen.ViewStyle import java.util.Optional @@ -55,19 +61,21 @@ import java.util.Optional @Composable internal fun DesignFrame( modifier: Modifier = Modifier, - layoutModifier: Modifier, + view: View, + baseVariantView: View?, style: ViewStyle, - shape: ViewShape, - name: String, variantParentName: String, layoutInfo: SimplifiedLayoutInfo, document: DocContent, customizations: CustomizationContext, + parentLayout: ParentLayoutInfo?, + layoutId: Int, componentInfo: Optional, parentComponents: List, maskInfo: MaskInfo?, - content: @Composable (parentLayoutInfo: ParentLayoutInfo) -> Unit, + content: @Composable () -> Unit, ) { + val name = view.name if (!customizations.getVisible(name)) return // Check for an image customization with context. If it exists, call the custom image function @@ -97,10 +105,49 @@ internal fun DesignFrame( val meterValue = customizations.getMeterFunction(name)?.let { it() } meterValue?.let { customizations.setMeterValue(name, it) } + val shape = (view.data as ViewData.Container).shape + + // Get the layout for this view that describes its size and position. + val (layout, setLayout) = remember { mutableStateOf(null) } + // Keep track of the layout state, which changes whenever this view's layout changes + val (layoutState, setLayoutState) = remember { mutableStateOf(0) } + // Subscribe for layout changes whenever the view changes. The view can change if it is a + // component instance that changes to another variant. It can also change due to a live update. + // Subscribing when already subscribed simply updates the view in layout system. + // TODO: The new variant renders for a frame with the old layout, causing a twitch when + // variants change. Fix this by somehow waiting for the new layout before rendering. + LaunchedEffect(view) { + val parentLayoutId = parentLayout?.parentLayoutId ?: -1 + val childIndex = parentLayout?.childIndex ?: -1 + + println("### subscribe FRAME ${view.name} layoutId $layoutId parent $parentLayoutId index $childIndex") + LayoutManager.subscribe( + layoutId, + setLayoutState, + parentLayoutId, + childIndex, + view, + baseVariantView + ) + } + // Unsubscribe to layout changes when the composable is no longer in view. + DisposableEffect(Unit) { + onDispose { + println("### unsubscribe ${view.name} $layoutId") + LayoutManager.unsubscribe(layoutId) + } + } + LaunchedEffect(layoutState) { + val newLayout = LayoutManager.getLayout(layoutId) + //println("### setLayout $layoutState ${v.name} layoutId $layoutId => width ${newLayout?.width} height ${newLayout?.height} x ${newLayout?.left} y ${newLayout?.top}") + setLayout(newLayout) + } + // Check to see if this node is part of a component set with variants and if any @DesignVariant // annotations set variant properties that match. If so, variantNodeName will be set to the // name of the node with all the variants set to the @DesignVariant parameters val variantNodeName = customizations.getMatchingVariant(componentInfo) + val layoutModifier = Modifier.layoutToModifier(layout).layoutId(layoutId) var m = Modifier.layoutStyle(name, style).then(layoutModifier) // Only render the frame if we don't have a custom variant node that we are about to // render instead @@ -124,9 +171,7 @@ internal fun DesignFrame( override val appearanceModifier = m @Composable override fun Content() { - content( - ParentLayoutInfo(0, 0) - ) // TODO fix. Maybe move this replacement into DesignView + content() } override val textStyle: TextStyle? = null } @@ -197,7 +242,7 @@ internal fun DesignFrame( horizontalArrangement = layoutInfo.arrangement, verticalAlignment = layoutInfo.alignment ) { - content(ParentLayoutInfo(0, 0)) // TODO + content() } } } @@ -241,7 +286,7 @@ internal fun DesignFrame( verticalArrangement = layoutInfo.arrangement, horizontalAlignment = layoutInfo.alignment ) { - content(ParentLayoutInfo(0, 0)) // TODO + content() } } } @@ -546,15 +591,10 @@ internal fun DesignFrame( } } is LayoutInfoAbsolute -> { - // TODO call JNI function to get layout here?? - Box(modifier = layoutInfo.selfModifier.then(m)) { - content(ParentLayoutInfo(0, 0)) // TODO fix?? - } - /* - DesignLayout(modifier = layoutInfo.selfModifier.then(m), style = style, layout = layout, name = name) { - content(absoluteParentLayoutInfo) + println("### DesignFrame $name left ${layout?.left} width ${layout?.width}") + Box(layoutInfo.selfModifier.then(m)) { + content() } - */ } } } diff --git a/designcompose/src/main/java/com/android/designcompose/DesignText.kt b/designcompose/src/main/java/com/android/designcompose/DesignText.kt index 5c4d5b2d0..5f896a123 100644 --- a/designcompose/src/main/java/com/android/designcompose/DesignText.kt +++ b/designcompose/src/main/java/com/android/designcompose/DesignText.kt @@ -17,13 +17,16 @@ package com.android.designcompose import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.text.BasicText import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect @@ -31,6 +34,7 @@ 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.layout.layoutId import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalFontLoader import androidx.compose.ui.text.AnnotatedString @@ -259,7 +263,7 @@ internal fun DesignText( println( "### subscribe TEXT $nodeName layoutId $layoutId parent $parentLayoutId index $childIndex" ) - val useMeasure = autoWidthHeight(style) + val useMeasure = isAutoHeightFillWidth(style) if (useMeasure) LayoutManager.subscribeWithMeasure( layoutId, @@ -300,16 +304,27 @@ internal fun DesignText( "### Text Layout $nodeName: width ${style.width.pointsAsDp()} height ${style.height} textBounds ${textBounds.width} ${textBounds.height}" ) // Only set the size if autoWidthHeight is false, because otherwise the measureFunc is used - if (!autoWidthHeight(style)) + if (!isAutoHeightFillWidth(style)) LayoutManager.setNodeSize( layoutId, textBounds.width.roundToInt(), textBounds.height.roundToInt() ) - // (textLayoutData) } - val layoutModifier = Modifier.layoutToModifier(layout) + println("### DesignText $nodeName left ${layout?.left} width ${layout?.width} height ${layout?.height}") + // 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) + .layoutId(layoutId) + .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) Box(modifier = layoutModifier) { val replacementComponent = customizations.getComponent(nodeName) if (replacementComponent != null) { @@ -324,7 +339,7 @@ internal fun DesignText( } else { BasicText( annotatedText, - modifier = modifier, + modifier = textModifier, style = textStyle, overflow = overflow, ) @@ -332,13 +347,6 @@ internal fun DesignText( } } -internal fun Dimension.points(density: Float): Int { - return when (this) { - is Dimension.Points -> (value * density).roundToInt() - else -> 0 - } -} - // Measure text height given a width. Called from Rust as a measure function for text that has auto // height and variable width. Layout computes the width, then calls this function to get the // corresponding text height. @@ -385,7 +393,8 @@ private fun measureTextBounds( else Int.MAX_VALUE val (rectBounds, lines) = textLayoutData.boundsForWidth(fixedWidth.toInt(), maxLines, density) - rectBounds.toComposeRect() + val fixedRectBounds = android.graphics.Rect(rectBounds.left, rectBounds.top, fixedWidth.roundToInt(), rectBounds.bottom) + fixedRectBounds.toComposeRect() } } } @@ -396,63 +405,3 @@ private fun measureTextBounds( } } } - -@Composable -private fun measureTextBoundsOld(style: ViewStyle, textLayoutData: TextLayoutData): Rect { - val density = LocalDensity.current.density - var selfWidth = style.min_width.points(density) - var selfHeight = style.min_height.points(density) - // var selfWidth = constraints.minWidth - // var selfHeight = constraints.minHeight - var hasWidth = false - var hasHeight = false - var minWidth = selfWidth - var minHeight = selfHeight - // var minWidth = constraints.minWidth - // var minHeight = constraints.minHeight - - /* - // If we're absolutely positioned then we can extract our bounds directly from our style. - when (style.position_type) { - is PositionType.Absolute -> { - val absBounds = absoluteLayout(style, constraints, density) - selfWidth = absBounds.width() - selfHeight = absBounds.height() - if (style.width !is Dimension.Undefined && selfWidth > 0) hasWidth = true - if (style.height !is Dimension.Undefined && selfHeight > 0) hasHeight = true - } - is PositionType.Relative -> { - val relBounds = relativeLayout(style, constraints, density) - if (style.min_width !is Dimension.Undefined && relBounds.width() > minWidth) { - minWidth = relBounds.width() - } - if (style.width !is Dimension.Undefined) { - selfWidth = relBounds.width() - hasWidth = true - } - if (style.min_height !is Dimension.Undefined && relBounds.height() > minHeight) { - minHeight = relBounds.height() - } - if (style.height !is Dimension.Undefined) { - selfHeight = relBounds.height() - hasHeight = true - } - } - } - - // If we don't have a width in style, then we can measure within the width given to us - // in our constraints. - val measureWidth = - if (hasWidth) { - selfWidth - } else { - constraints.maxWidth - } - */ - // Measure our text and figure out the offset to match baselines, and the intrinsic - // height of the layout. - val maxLines = if (style.line_count.isPresent) style.line_count.get().toInt() else Int.MAX_VALUE - - val (rectBounds, lines) = textLayoutData.boundsForWidth(20, 1, LocalDensity.current) - return rectBounds.toComposeRect() -} diff --git a/designcompose/src/main/java/com/android/designcompose/DesignView.kt b/designcompose/src/main/java/com/android/designcompose/DesignView.kt index 2b55c231c..5d6e201fb 100644 --- a/designcompose/src/main/java/com/android/designcompose/DesignView.kt +++ b/designcompose/src/main/java/com/android/designcompose/DesignView.kt @@ -566,62 +566,6 @@ internal fun DesignView( view.style } - // Get a unique ID identifying this composable. We use this to register and unregister this - // view for layout. - val layoutId = remember { LayoutManager.getNextLayoutId() } - // Get the layout for this view that describes its size and position. - val (layout, setLayout) = remember { mutableStateOf(null) } - // Keep track of the layout state, which changes whenever this view's layout changes - val (layoutState, setLayoutState) = remember { mutableStateOf(0) } - // Subscribe for layout changes whenever the view changes. The view can change if it is a - // component instance that changes to another variant. It can also change due to a live update. - // Subscribing when already subscribed simply updates the view in layout system. - // TODO: The new variant renders for a frame with the old layout, causing a twitch when - // variants change. Fix this by somehow waiting for the new layout before rendering. - LaunchedEffect(view) { - val parentLayoutId = parentLayout?.parentLayoutId ?: -1 - val childIndex = parentLayout?.childIndex ?: -1 - val baseVariantView = if (hasVariantReplacement) v else null - println( - "### subscribe ${v.name} layoutId $layoutId parent $parentLayoutId index $childIndex" - ) - - // For now, only subscribe for non-text views. DesignText() will subscribe text. - // TODO move this into DesignFrame() to be consistent, and because DesignFrame() does some - // additional customizations that should be handled before subscribing. - if (!view.isTextView()) - LayoutManager.subscribe( - layoutId, - setLayoutState, - parentLayoutId, - childIndex, - view, - baseVariantView - ) - - /* - val useMeasure = needsMeasureFunc(view.data, view.style) - if (useMeasure) - LayoutManager.subscribeWithMeasure(layoutId, setLayoutState, parentLayoutId, childIndex, view) - else - LayoutManager.subscribe(layoutId, setLayoutState, parentLayoutId, childIndex, view, baseVariantView) - - */ - } - // Unsubscribe to layout changes when the composable is no longer in view. - DisposableEffect(Unit) { - onDispose { - println("### unsubscribe ${v.name} $layoutId") - LayoutManager.unsubscribe(layoutId) - } - } - LaunchedEffect(layoutState) { - val newLayout = LayoutManager.getLayout(layoutId) - println( - "### setLayout $layoutState ${v.name} layoutId $layoutId => width ${newLayout?.width} height ${newLayout?.height} x ${newLayout?.left} y ${newLayout?.top}" - ) - setLayout(newLayout) - } val viewLayoutInfo: SimplifiedLayoutInfo = LayoutInfoAbsolute(modifier, Modifier) // Add various scroll modifiers depending on the overflow flag. @@ -716,19 +660,21 @@ internal fun DesignView( parentSize = remember { mutableStateOf(Size(0F, 0F)) } } - println( - "### DesignFrame $layoutState ${v.name} layoutId $layoutId => width ${layout?.width} height ${layout?.height} x ${layout?.left} y ${layout?.top}" - ) + // Get a unique ID identifying this composable. We use this to register and unregister + // this view for layout and as a parent ID for children + val layoutId = remember { LayoutManager.getNextLayoutId() } + DesignFrame( m, - Modifier.layoutToModifier(layout), + view, + if (hasVariantReplacement) v else null, style, - (view.data as ViewData.Container).shape, - view.name, variantParentName, viewLayoutInfo, document, customizations, + parentLayout, + layoutId, // Check to see whether an interaction has changed the current variant. If it did, // then // we ignore any variant properties set from @DesignVariant annotations by passing @@ -736,17 +682,9 @@ internal fun DesignView( if (hasVariantReplacement) Optional.empty() else view.component_info, parentComponents, MaskInfo(parentSize, maskViewType), - ) { parentLayoutInfoForChildren -> + ) { val customContent2 = customizations.getContent2(view.name) if (customContent2 != null) { - /* - val children = ArrayList() - for (i in 0 until customContent2.count) { - val nodeName = customContent2.nodeNames(i) - children.add(nodeName) - } - LayoutManager.setNodeChildren(view.id, children) - */ for (i in 0 until customContent2.count) { customContent2.content(i)(ContentReplacementContext(layoutId)) } @@ -803,9 +741,10 @@ internal fun DesignView( interactionState, interactionScope, parentComps, - parentLayoutInfoForChildren, + ParentLayoutInfo(layoutId, childIndex), MaskInfo(parentSize, maskViewType), ) + ++childIndex } maskViewType = MaskViewType.MaskNode } @@ -819,7 +758,6 @@ internal fun DesignView( interactionState, interactionScope, parentComps, - // parentLayoutInfoForChildren, ParentLayoutInfo(layoutId, childIndex), MaskInfo(parentSize, maskViewType), ) diff --git a/designcompose/src/main/java/com/android/designcompose/Layout.kt b/designcompose/src/main/java/com/android/designcompose/Layout.kt index 45c1de3ba..0d4d03972 100644 --- a/designcompose/src/main/java/com/android/designcompose/Layout.kt +++ b/designcompose/src/main/java/com/android/designcompose/Layout.kt @@ -18,6 +18,7 @@ package com.android.designcompose import android.graphics.Bitmap import android.graphics.Paint +import android.graphics.Point import android.graphics.Rect import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.height @@ -33,6 +34,7 @@ import androidx.compose.ui.layout.Measurable import androidx.compose.ui.layout.MeasurePolicy import androidx.compose.ui.layout.ParentDataModifier import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.layout.layoutId import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.Paragraph import androidx.compose.ui.text.font.Font @@ -52,6 +54,7 @@ import com.android.designcompose.serdegen.ViewStyle import com.novi.bincode.BincodeDeserializer import com.novi.bincode.BincodeSerializer import kotlin.math.ceil +import kotlin.math.roundToInt /// TextLayoutData is used so that a parent can perform a height-for-width calculation on internal data class TextLayoutData( @@ -135,9 +138,7 @@ internal object LayoutManager { val deserializer = BincodeDeserializer(responseBytes) val response: LayoutChangedResponse = LayoutChangedResponse.deserialize(deserializer) notifySubscribers(response.changed_layout_ids, response.layout_state) - println( - "### handleResponse ${response.layout_state}, changed: ${response.changed_layout_ids}" - ) + //println("### handleResponse ${response.layout_state}, changed: ${response.changed_layout_ids}") } else { println("### responseBytes NULL") } @@ -220,7 +221,7 @@ internal fun TextLayoutData.boundsForWidth( text = annotatedString.text, style = textStyle, spanStyles = annotatedString.spanStyles, - width = inWidth.toFloat(), // + 5.0f, // TODO why add 5.0?? + width = inWidth.toFloat() + 5.0f, // TODO why add 5.0?? density = density, resourceLoader = resourceLoader, maxLines = 1 @@ -230,7 +231,7 @@ internal fun TextLayoutData.boundsForWidth( text = annotatedString.text, style = textStyle, spanStyles = annotatedString.spanStyles, - width = inWidth.toFloat(), // + 5.0f, + width = inWidth.toFloat() + 5.0f, density = density, resourceLoader = resourceLoader, maxLines = maxLines @@ -510,10 +511,68 @@ internal fun itemSpacingAbs(itemSpacing: ItemSpacing): Int { } } -// Converts a layout from Rust into a width/height/offset Modfiier +// Converts a layout from Rust into a width/height/offset Modifier internal fun Modifier.layoutToModifier(layout: Layout?) = this.then( Modifier.width(layout?.width?.dp ?: 0.dp) .height(layout?.height?.dp ?: 0.dp) .offset(layout?.left?.dp ?: 0.dp, layout?.top?.dp ?: 0.dp) ) + +// Converts a layout from Rust into a width/height Modifier +internal fun Modifier.layoutSizeToModifier(layout: Layout?) = + this.then( + Modifier.width(layout?.width?.dp ?: 0.dp) + .height(layout?.height?.dp ?: 0.dp) + ) + + +@Composable +internal inline fun DesignFrameLayout( + modifier: Modifier = Modifier, + name: String = "unnamed", + layoutState: Int, + content: @Composable () -> Unit +) { + val measurePolicy = rememberMeasurePolicy(name, layoutState) + Layout(content = content, measurePolicy = measurePolicy, modifier = modifier) +} +@Composable +internal fun rememberMeasurePolicy(name: String, layoutState: Int) = + remember(name, layoutState) { measurePolicy(name) } + +internal fun measurePolicy(name: String) = + MeasurePolicy { measurables, constraints -> + //val layout = LayoutManager.getLayout(layoutId) + val placeableList = arrayListOf>() + var selfWidth = 0 + var selfHeight = 0 + println("### MeasurePolicy $name") + measurables.forEachIndexed { index, measurable -> + println("### $name measureable $index: ${measurable.layoutId}") + val childId = (measurable.layoutId as Int?) ?: -1 + val placeable = measurable.measure(constraints) + val childLayout = LayoutManager.getLayout(childId) + + println("### $name measureable $index: ${measurable.layoutId} width ${placeable.width}") + //val childMaxX = childLayout.left.roundToInt() + childLayout.width.roundToInt() + val childMaxWidth = childLayout?.left?.roundToInt() ?: 0 + placeable.width + val childMaxHeight = childLayout?.top?.roundToInt() ?: 0+ placeable.height + if (selfWidth < childMaxWidth) + selfWidth = childMaxWidth + if (selfHeight < childMaxHeight) + selfHeight = childMaxHeight + + placeableList.add(Pair(placeable, Point(childLayout?.left?.roundToInt() ?: 0, childLayout?.top?.roundToInt() ?: 0))) + } + + println("### MeasurePolicy $name selfWidth $selfWidth selfHeight $selfHeight") + + layout(selfWidth, selfHeight) { + placeableList.forEachIndexed { index, placeable -> + placeable.first.place(placeable.second.x, placeable.second.y) + println("### $name place $index ${placeable.second.x}, ${placeable.second.y}") + } + } + } + diff --git a/designcompose/src/main/java/com/android/designcompose/Utils.kt b/designcompose/src/main/java/com/android/designcompose/Utils.kt index 45884e82d..94d9e6b68 100644 --- a/designcompose/src/main/java/com/android/designcompose/Utils.kt +++ b/designcompose/src/main/java/com/android/designcompose/Utils.kt @@ -1140,15 +1140,15 @@ internal fun com.android.designcompose.serdegen.Path.log() { } } -internal fun autoWidthHeight(style: ViewStyle) = - style.width is Dimension.Auto && style.height is Dimension.Auto +internal fun isAutoHeightFillWidth(style: ViewStyle) = + style.width is Dimension.Auto && style.height is Dimension.Auto && style.flex_grow > 0F // Determine if view data needs a measure func. // Currently, we need a measure func for text nodes that are both auto height and auto width internal fun needsMeasureFunc(viewData: ViewData, style: ViewStyle): Boolean { return when (viewData) { - is ViewData.Text -> autoWidthHeight(style) - is ViewData.StyledText -> autoWidthHeight(style) + is ViewData.Text -> isAutoHeightFillWidth(style) + is ViewData.StyledText -> isAutoHeightFillWidth(style) else -> false } } diff --git a/integration-tests/validation/src/main/java/com/android/designcompose/testapp/validation/MainActivity.kt b/integration-tests/validation/src/main/java/com/android/designcompose/testapp/validation/MainActivity.kt index c2824587b..11cf15d57 100644 --- a/integration-tests/validation/src/main/java/com/android/designcompose/testapp/validation/MainActivity.kt +++ b/integration-tests/validation/src/main/java/com/android/designcompose/testapp/validation/MainActivity.kt @@ -17,6 +17,7 @@ package com.android.designcompose.testapp.validation import android.graphics.Bitmap +import android.graphics.Point import android.os.Bundle import android.util.Log import androidx.activity.ComponentActivity @@ -59,7 +60,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.MeasurePolicy +import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.layout.layoutId import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily @@ -92,6 +96,7 @@ import com.android.designcompose.annotation.DesignMetaKey import com.android.designcompose.annotation.DesignPreviewContent import com.android.designcompose.annotation.DesignVariant import com.android.designcompose.annotation.PreviewNode +import com.android.designcompose.serdegen.Layout import java.lang.Float.max import java.lang.Float.min import kotlin.math.roundToInt @@ -179,28 +184,99 @@ interface HelloWorld { @DesignComponent(node = "#RedSquare") fun RedSquare() } + +internal fun getLayout(id: Int): Layout { + return when (id) { + 0 -> Layout(0, 400F, 400F, 0F, 0F) + 1 -> Layout(0, 200F, 200F, 10F, 10F) + 2 -> Layout(0, 200F, 200F, 10F, 10F) + else -> Layout(0, 50F, 50F, (id.toFloat() - 3) * 90F - 10, 30F) + } +} + @Composable -fun HelloWorld() { - val (autoWidthText, setAutoWidthText) = remember { mutableStateOf("This is some text") } - val (autoHeightText, setAutoHeightText) = remember { mutableStateOf("This is some text") } - val (fixedSizeText, setFixedSizeText) = remember { mutableStateOf("This is some text") } - val (fillWidthAutoHeightText, setFillWidthAutoHeightText) = - remember { - mutableStateOf( - "This text needs to be quite long so that it spans multiple lines and makes the box to the right taller. This text width also stretches to fit the container." - ) +internal inline fun TestLayout( + modifier: Modifier = Modifier, + name: String = "unnamed", + id: Int, + content: @Composable () -> Unit +) { + val measurePolicy = rememberMeasurePolicy(name, id) + Layout(content = content, measurePolicy = measurePolicy, modifier = modifier) +} +@Composable +internal fun rememberMeasurePolicy(name: String, id: Int) = + remember(name, id) { measurePolicy(name, id) } + +internal fun measurePolicy(name: String, id: Int) = + MeasurePolicy { measurables, constraints -> + val layout = getLayout(id) + val placeableList = arrayListOf>() + var selfWidth = 0 + var selfHeight = 0 + println("### MeasurePolicy $name") + measurables.forEachIndexed { index, measurable -> + val childId = measurable.layoutId as Int + val placeable = measurable.measure(constraints) + val childLayout = getLayout(childId) + + println("### $name measureable $index: ${measurable.layoutId} width ${placeable.width}") + //val childMaxX = childLayout.left.roundToInt() + childLayout.width.roundToInt() + val childMaxWidth = childLayout.left.roundToInt() + placeable.width + val childMaxHeight = childLayout.top.roundToInt() + placeable.height + if (selfWidth < childMaxWidth) + selfWidth = childMaxWidth + if (selfHeight < childMaxHeight) + selfHeight = childMaxHeight + + placeableList.add(Pair(placeable, Point(childLayout.left.roundToInt(), childLayout.top.roundToInt()))) } + + println("### MeasurePolicy $name selfWidth $selfWidth selfHeight $selfHeight") + + layout(selfWidth, selfHeight) { + placeableList.forEachIndexed { index, placeable -> + placeable.first.place(placeable.second.x, placeable.second.y) + println("### $name place $index ${placeable.second.x}, ${placeable.second.y}") + } + } + } + + + + +@Composable +fun HelloWorld() { + val loremText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." + val (autoWidthLen, setAutoWidthLen) = remember { mutableStateOf(17) } + val (autoHeightLen, setAutoHeightLen) = remember { mutableStateOf(17) } + val (fixedSizeLen, setFixedSizeLen) = remember { mutableStateOf(17) } + val (fillWidthAutoHeightLen, setFillWidthAutoHeightLen) = remember { mutableStateOf(89) } val (buttonSquare, setButtonSquare) = remember { mutableStateOf(ButtonSquare.Off) } val (numChildren, setNumChildren) = remember { mutableStateOf(2) } + + /* + TestLayout( + modifier = Modifier.layoutId(0), + name = "Main", + id = 0, + ) { + Box(Modifier.size(200.dp, 200.dp).border(1.dp, Color.Black).layoutId(1)) + TestLayout(modifier = Modifier.layoutId(1), id = 1, name = "Box") { + Box(Modifier.size(50.dp, 50.dp).background(Color.Red).layoutId(3)) + Box(Modifier.size(50.dp, 50.dp).background(Color.Green).layoutId(4)) + Box(Modifier.size(50.dp, 50.dp).background(Color.Blue).border(2.dp, Color.Magenta).layoutId(5)) + } + } + */ + HelloWorldDoc.Main( - modifier = - Modifier.onGloballyPositioned { println("### HELLO onGloballyPositioned ${it.size}") }, name = "LongerText", - nameAutoWidth = autoWidthText, - nameAutoHeight = autoHeightText, - nameFixed = fixedSizeText, - nameFillWidthAutoHeight = fillWidthAutoHeightText, + nameAutoWidth = loremText.subSequence(0, autoWidthLen).toString(), + nameAutoHeight = loremText.subSequence(0, autoHeightLen).toString(), + nameFixed = loremText.subSequence(0, fixedSizeLen).toString(), + nameFillWidthAutoHeight = loremText.subSequence(0, fillWidthAutoHeightLen).toString(), buttonSquare = buttonSquare, horizontalContent2 = ReplacementContent( @@ -237,36 +313,46 @@ fun HelloWorld() { Row(verticalAlignment = Alignment.CenterVertically) { Text("AutoWidth", fontSize = 30.sp, color = Color.Black) Button("-", false) { - val endIndex = (autoWidthText.length - 2).coerceAtLeast(0) - setAutoWidthText(autoWidthText.subSequence(0, endIndex).toString()) + val len = (autoWidthLen - 1).coerceAtLeast(1) + setAutoWidthLen(len) + } + Button("+", false) { + val len = (autoWidthLen + 1).coerceAtMost(loremText.length) + setAutoWidthLen(len) } - Button("+", false) { setAutoWidthText(autoWidthText + "A") } } Row(verticalAlignment = Alignment.CenterVertically) { Text("AutoHeight", fontSize = 30.sp, color = Color.Black) Button("-", false) { - val endIndex = (autoHeightText.length - 2).coerceAtLeast(0) - setAutoHeightText(autoHeightText.subSequence(0, endIndex).toString()) + val len = (autoHeightLen - 1).coerceAtLeast(1) + setAutoHeightLen(len) + } + Button("+", false) { + val len = (autoHeightLen + 1).coerceAtMost(loremText.length) + setAutoHeightLen(len) } - Button("+", false) { setAutoHeightText(autoHeightText + "A") } } Row(verticalAlignment = Alignment.CenterVertically) { Text("Fixed", fontSize = 30.sp, color = Color.Black) Button("-", false) { - val endIndex = (fixedSizeText.length - 2).coerceAtLeast(0) - setFixedSizeText(fixedSizeText.subSequence(0, endIndex).toString()) + val len = (fixedSizeLen - 1).coerceAtLeast(1) + setFixedSizeLen(len) + } + Button("+", false) { + val len = (fixedSizeLen + 1).coerceAtMost(loremText.length) + setFixedSizeLen(len) } - Button("+", false) { setFixedSizeText(fixedSizeText + "A") } } Row(verticalAlignment = Alignment.CenterVertically) { Text("FillWidth AutoHeight", fontSize = 30.sp, color = Color.Black) Button("-", false) { - val endIndex = (fillWidthAutoHeightText.length - 2).coerceAtLeast(0) - setFillWidthAutoHeightText( - fillWidthAutoHeightText.subSequence(0, endIndex).toString() - ) + val len = (fillWidthAutoHeightLen - 1).coerceAtLeast(1) + setFillWidthAutoHeightLen(len) + } + Button("+", false) { + val len = (fillWidthAutoHeightLen + 1).coerceAtMost(loremText.length) + setFillWidthAutoHeightLen(len) } - Button("+", false) { setFillWidthAutoHeightText(fillWidthAutoHeightText + "A") } } Row(verticalAlignment = Alignment.CenterVertically) { Text("ButtonSquare", fontSize = 30.sp, color = Color.Black) @@ -279,8 +365,6 @@ fun HelloWorld() { ButtonSquare.Green -> ButtonSquare.Off } ) - val endIndex = (fixedSizeText.length - 2).coerceAtLeast(0) - setFixedSizeText(fixedSizeText.subSequence(0, endIndex).toString()) } } Row(verticalAlignment = Alignment.CenterVertically) {