-
Notifications
You must be signed in to change notification settings - Fork 19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Move layout code into Rust #314
Conversation
I only have superficial feedback on this so far, but it has been helpful in my starting to understand how we will use the Rust code here in our pure rust client code to manage layout. In the layout crate the primary thing I'd like to see would be a pure Rust example that shows a kind of example integration. That is, how to use in a pure Rust environment. I imagine we'd have to provide functions that stand in where existing Currently we have a (very basic, and static) layout implementation which is struggling with how to manage variants, so despite the difficulty here this code is very much needed. This is much appreciated Rob. |
Honestly I haven't fully thought through how to integrate this with Rust and would like to have a conversation about that soon. However I have been testing the layout code purely in Rust in a number of scenarios and plan to add a bunch of unit tests. This should be a good starting point on how to use it. From there I'll try to add another level of abstraction for the Rust code to use, similar to the JNI interface used by Android. |
8fbcd8a
to
9a5a81c
Compare
@rylin8 I think the |
Currently the layout crate isn't used. I made it initially and put all the layout code in it, then decided to use the live update crate because at the time, layout was tied to live update. Now I need to move it back to the other crate, after which I'll add the code back into this PR. |
f432d95
to
af70d2c
Compare
This is too big for me to really be able to review, but for the JNI stuff, it'll be best to only have one library / one interface, rather than two. I would split the crates such that one crate (currently live_update) contains all of the Android/JNI related code, and the other crate (layout?) contains all of the shared code. |
Yeah, that makes sense. I think if the library is being used for native targets then developers will come in looking from a native perspective -- given that, I think it would also be good to rename |
6cce407
to
8c9afa0
Compare
Done! |
543d96c
to
0de2c58
Compare
I tried this out a bit on an emulator with 1.0 density.
So overall most tests are working. I think making the root stretch/resize to parent by default will either reveal some other layout issues or prove that it's behaving as expected. Text metrics are pretty challenging to get right, and the current implementation is quite complicated because at the time it was written, Compose always included some additional vertical padding on text. Compose changed the text measurement logic in 1.2.0 (hence this confusing "Added in 1.2.0, Deprecated in 1.2.0" note on PlatformTextStyle#includeFontPadding), so we might be able to remove our measurement logic which only existed to remove the extra padding since we only target Compose > 1.2.0. |
@rylin8 on the most recent change for vertical alignment, how does |
|
4ddacd7
to
fa0e614
Compare
Got it, yes, that makes sense. Android has really beautiful text layouts, but they tend to be a tiny bit wider than what harfbuzz generates (at least, when we used harfbuzz directly for text layout it seemed to perfectly match what Figma generates itself). I think it could be a glyph offset rounding thing, but I don't know Android's text implementation well enough. |
let height_points = bounds.height().ceil(); | ||
style.width = match frame.layout_sizing_horizontal { | ||
LayoutSizing::Fixed => Dimension::Points(width_points), | ||
LayoutSizing::Fill => Dimension::Points(width_points), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it work if you just set min_width / min_height? (You probably tried that...).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope that doesn't work, that makes the size grow too large also. I'm thinking I may have to make autolayout frames with children that have a FILL sizing mode to somehow ignore the children when calculating layout size.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, ok, this is another dumb idea, but what are the default settings for flex and align-items? I'm wondering if there's some default style value that's set inappropriately.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
flex grow/shrink both default to 0, and align-items defaults to Stretch. However align-items should get set to something else if it is a frame, based on the value of counter_axis_align_items. I can dig it into it more and try and reproduce with a flexbox example somewhere else.
184f6ba
to
64caa2b
Compare
0ad55e7
to
217519d
Compare
ecd6165
to
6f357d0
Compare
f34ebe5
to
8ba18a1
Compare
Closes #174, #292 Layout code has been mostly removed from Android. A new layout crate has been created in Rust that handles layout. The general way in which this crate works is: * add_view() adds a view to the view tree * remove_view() removes a view from the view tree * set_node_size() forces a fixed size for a view. This is generally called by text nodes, whose size is calculated in Android * Whenever layout is computed, a list of nodes that have changed is sent to Android In Android, views get assigned a layoutID by a layout manager and subscribe themselves to layout changes for their layoutID. Subscribing calls through to Rust via JNI and adds their view to the view tree using add_view, which is computed by taffy. Whenever taffy does a layout computation, it checks to see which layoutIDs have changed and sends a message back to Android with a list of these IDs. The layout manager then informs all subscribers whose layout changed, and those subscribers recompose. When a view is no longer rendered, it unsubscribes from the LayoutManager, which calls remove_view via JNI. Taffy uses a style structure similar to our toolkit's ViewStyle. To support their structure, a few fields in ViewStyle have been changed or added, and the parsing code in transform_flexbox that takes Figma's node structure and converts it to a ViewStyle has also been modified. ViewStyle gets converted into Taffy's Style structure with the into() trait. Text measurement is still done in Android but has been revamped a bit. New and modified text properties from Figma are now parsed, including trunction and max lines, so there is no need for the text layout plugin. Text is then measured in one of the following ways: 1. Auto width, fixed height: Width should expand as much as needed to fit the text. This always results in a single line of text. 2. Auto height, fixed width: Height should expand as much as needed to fit the text with the text wrapping. If max lines are set, text should stop after that number of lines, ending either with truncation or ellipsis. 3. Fixed size: Width and height are fixed. Text either truncates or elides depending on truncation property. 4. Auto height, fill container width: This special case requires a measure function to get passed into taffy. For this scenario, when the text view adds itself to layout, it also adds a measure function. This measure function is called by taffy possibly several times as taffy computes layout, giving the measure function the text's current and available size. Additionally, since Compose text is a bit different than Figma text, text will look slightly different and be positioned slightly differently in Android than in Figma. To account for this, text has both a layout size/position and a render size/position. The render height and y offset can be slightly different in order to render closely with Figma but still allow for correct layout. Here is a list of additional changes worth noting that were made to support layout in special circumstances: * Component instances: Component instances and the corresponding component set have the same name, so their layout calculation would sometimes overwrite the other one, resulting in the wrong layout for the instance being used. Fix this by ignoring component sets when doing layout. * Component variants: When an instance of a variant changes through an interaction or @DesignVariant customization, in order to avoid its offset from its parent was resetting to 0, provide the layout system with both the new variant and the original variant views so that it can use the offset from original variant. Variant resolution has been moved from DesignFrame to DesignView in order to be consistent with variants changed through an interaction. * Radial gradient crash: Due to layouts from the layout manager possibly being null, don't create a radial gradient if the width is 0 in order to avoid a crash. * To support rotated nodes, add a bounding box property to ViewStyle. Save a node's bounding box, which accounts for scale and rotation transformations on the node, separately from it's size. Use this bounding box as the basis for layout calculations, and continue to use the size property for rendering. * ViewStyle's margin field determines its offset from its parent instead of ViewStyle's left/top fields in order to align with Taffy's style. Several areas of code have been updated to accomodate this change including code support for dials, progress bars, progress markers, and masking. * Since adding a view to the layout manager requires knowing the view's parent ID and its child index into the parent, the customization for replacing children has been modified from a @composable () -> Unit customization to a class called ReplacementContent. Customizing the children of a node now requires a little more work in filling out data for this class such as the number of children, and for each child, the composable for the child. * Several of the battleship tiles were rendering incorrectly after rebasing with main and pulling in the change to support scaled vectors. Support for scaled vectors has temporarily been disabled, as it fixes the rendering of Battleship. Some more investigation is needed to see if we need to do anything to support scaled vectors or if it already works as is, since they seem to look correct now even when stretching the parent to force a vector scale. * In order to reduce recomputing layout over and over again when adding views, don't compute layout until the root view and all its children have finished composition using a boolean in layout manager. This boolean gets reset when we load a new root view. * To support the list preview widget, when encountering a frame that is set up as a widget, the code diverges from the usual means of calculating layout via the Rust layout manager. Instead, children of the list widget are laid out directly with a lazy grid, row, or column, depending on the parameters set in the list widget. Currently there is an issue with the "Hug Contents" feature of horizontal and vertical layouts within the list widget. This should be fixed in a follow up change. * When parsing frames, set size to a fixed size when sizing mode set to FILL. This may not be correct, but without this change, FILL is treated the same as HUG in that the width/height is set to AUTO in ViewStyle, causing FILL containers to grow larger than they should. Need to reassess to see how FILL containers should be sized in taffy. * To support the visibility customization, DesignFrame and DesignText return a boolean indicating whether they were rendered or not. This is important to properly calculate the child index of each visible view for the Rust layout manager -- otherwise it could crash by adding a view with an index higher than the current size of the children list. * Filter out old extended layout properties that may have been written to Figma nodes. These were causing the new layout to think that the list widget was being used when it wasn't. * Added a new layout test activity that tests some custom layout code to see how it works. * To support layout and rendering at different pixel densities, the layout algorithm is done at the raw pixel level (matching Figma). When getting layout values from the layout manager to use in Android, layout values are multiplied by the current pixel density. There are a few scenarios where support for densities other than 1 needed additional changes: - Code that referenced the pixel values specified in ViewStyle, such as margin, width or height, now multiply those values by the pixel density using the pointsAsDp(density) function. This has been fixed in a number of places including dials and gauges and masks, which previously had bugs. - Since text size is calculated in Android and then sent back to the Rust layout system, the size is converted back into raw pixels by dividing by the pixel density first. When calculating text size given existing size data, convert the size data from raw pixels first by multiplying with the pixel density. * To optimize layout, attempt to only compute layout when we are done adding all of a node's children. Add a beginLayout() and endLayout() function to the LayoutManager that keeps track of layout IDs in progress. When a DesignFrame subscribes to layout, call beginLayout() which adds its layout ID to a set of IDs. After the frame's children composables have been called, call endLayout() which removes the layout ID from the set. When the set is empty, the frame and all its children have completed, so compute layout. This reduces layout computation in scenarios such as a LazyGridView with lots of children. This now computes layout for every child of the LazyGridView instead of every ancestor of every child. * Added a unit testing file that should be added to for additional layout features, and for any additional features or bugs we fix. REMAINING ISSUES: - Text alignment with multiple lines is off - Text with constraints not supported - Rotated autolayout frames are not laid out properly - Can't specify Modifiers that change the size or position, such as Modifier.fillMaxSize(), Modifier.offset() - Problems with scale transformations and vector scale - List widget horizontal/vertical layout “Hug Contents” doesn't work - List widget horizontal/vertical layout "space between" doesn't work - Autolayout containers set to FILL don't fill -- they are set to a fixed size. - Don't support scrollable containers that are not in a list widget - Performance issues
Use the base variant view mechanism to support component replacement node placement. When doing a component replacement, we now pass the parent layout info as context to the replacement node. The parent layout info now has the base view, or original view, which gets sent to the layout manager when subscribing for layout changes. The layout manager uses the original view for positioning the component replacement. Added unit tests for component replacement. Since each unit test adds views to the layout manager, additional functions to clear all views from the layout manager have been added. When running unit tests for layout, tests must be single threaded, so run with: cargo test -- --nocapture --test-threads=1
8ba18a1
to
b80a96f
Compare
Apply the size modifier to the Row/Column.
…widgets Don't call frameRender() for list widgets since that clips children and any decorative elements to the widget should be applied to the parent.
799b7ae
to
a6f5874
Compare
Let Row() and Column() determine their own size when hug contents is specified. When the size is determined, let the layout manager know so that it can resize the parent and other nodes if the parent is set to autolayout.
@rylin8 Are we ready to close this PR since we're working off the new feature branch? |
Merged into feature/rustlayout |
No description provided.