├── .gitignore ├── assets └── demo.png ├── import_map.json ├── src ├── utils.rs └── lib.rs ├── deno.json ├── mod.ts ├── Cargo.toml ├── examples └── canvas.ts ├── scripts └── build.ts ├── tests └── node_test.ts ├── README.md ├── LICENSE └── gelatin.d.ts /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | pkg 3 | Cargo.lock 4 | deno.lock -------------------------------------------------------------------------------- /assets/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/load1n9/gelatin/HEAD/assets/demo.png -------------------------------------------------------------------------------- /import_map.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "https://deno.land/x/gelatin/mod.ts": "./mod.ts" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | #[allow(dead_code)] 2 | pub fn set_panic_hook() { 3 | #[cfg(feature = "console_error_panic_hook")] 4 | console_error_panic_hook::set_once(); 5 | } -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "build": "deno run -A --unstable scripts/build.ts gelatin", 4 | "example_canvas": "deno run -A --unstable examples/canvas.ts" 5 | }, 6 | "importMap": "./import_map.json" 7 | } 8 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | // @deno-types="./gelatin.d.ts" 2 | 3 | import * as wasm from "./src/wasm.js"; 4 | 5 | export let loaded = false; 6 | 7 | export async function load(): Promise { 8 | if (!loaded) { 9 | await wasm.default(wasm.source); 10 | loaded = true; 11 | } 12 | } 13 | 14 | export function loadSync() { 15 | if (!loaded) { 16 | wasm.initSync(wasm.source); 17 | loaded = true; 18 | } 19 | } 20 | 21 | export * from "./src/wasm.js"; 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gelatin" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | [lib] 7 | crate-type = ["cdylib", "rlib"] 8 | 9 | [features] 10 | default = ["console_error_panic_hook"] 11 | 12 | [dependencies] 13 | wasm-bindgen = "0.2.84" 14 | js-sys = "0.3" 15 | taffy = { git = "https://github.com/DioxusLabs/taffy" } 16 | console_error_panic_hook = { version = "0.1.1", optional = true } 17 | 18 | [dev-dependencies] 19 | wasm-bindgen-test = "0.3.33" 20 | 21 | [profile.release] 22 | opt-level = "s" 23 | -------------------------------------------------------------------------------- /examples/canvas.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Allocator, 3 | JustifyContent, 4 | load, 5 | Node, 6 | Position, 7 | } from "https://deno.land/x/gelatin/mod.ts"; 8 | import { 9 | mainloop, 10 | WindowCanvas, 11 | } from "https://deno.land/x/dwm@0.3.0/ext/canvas.ts"; 12 | 13 | const win = new WindowCanvas({ 14 | title: "Stretch", 15 | width: 500, 16 | height: 500, 17 | resizable: true, 18 | floating: true, 19 | }); 20 | 21 | await load(); 22 | 23 | const allocator = new Allocator(); 24 | 25 | const container = new Node(allocator, { 26 | position: Position.Absolute, 27 | width: 500, 28 | height: 500, 29 | justifyContent: JustifyContent.FlexStart, 30 | }); 31 | 32 | // green 33 | container.addChild( 34 | new Node(allocator, { 35 | marginStart: 0, 36 | width: "35%", 37 | height: "100%", 38 | }), 39 | ); 40 | 41 | // purple 42 | container.addChild( 43 | new Node(allocator, { 44 | width: "25%", 45 | height: "50%", 46 | }), 47 | ); 48 | 49 | addEventListener("resize", (event) => { 50 | const styles = container.getStyle(); 51 | styles.width = event.width; 52 | styles.height = event.height; 53 | container.setStyle(styles); 54 | }); 55 | 56 | win.onDraw = (ctx) => { 57 | ctx.fillStyle = "black"; 58 | ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); 59 | const layout = container.computeLayout(); 60 | ctx.fillStyle = "#44911B"; 61 | ctx.fillRect( 62 | layout.child(0).x, 63 | layout.child(0).y, 64 | layout.child(0).width, 65 | layout.child(0).height, 66 | ); 67 | ctx.fillStyle = "#ff00ff"; 68 | ctx.fillRect( 69 | layout.child(1).x, 70 | layout.child(1).y, 71 | layout.child(1).width, 72 | layout.child(1).height, 73 | ); 74 | }; 75 | 76 | await mainloop(() => { 77 | win.draw(); 78 | }); 79 | -------------------------------------------------------------------------------- /scripts/build.ts: -------------------------------------------------------------------------------- 1 | import { encode } from "https://deno.land/std@0.145.0/encoding/base64.ts"; 2 | import { minify } from "https://jspm.dev/terser@5.12.1"; 3 | 4 | const name = Deno.args[0]; 5 | const target = "src/wasm.js"; 6 | 7 | export async function build() { 8 | if (!(await Deno.stat("Cargo.toml")).isFile) { 9 | console.log(`the build script should be executed in the "${name}" root`); 10 | Deno.exit(1); 11 | } 12 | const command = new Deno.Command("wasm-pack", { 13 | args: [ 14 | "build", 15 | "--target", 16 | "web", 17 | "--release", 18 | ], 19 | stdin: "piped", 20 | stdout: "piped", 21 | }); 22 | const child = command.spawn(); 23 | const _status = await child.status; 24 | 25 | const wasm = await Deno.readFile(`pkg/${name}_bg.wasm`); 26 | console.log(`read wasm (size: ${wasm.length} bytes)`); 27 | const encoded = encode(wasm); 28 | console.log( 29 | `encoded wasm using base64 (increase: ${ 30 | encoded.length - 31 | wasm.length 32 | } bytes, size: ${encoded.length} bytes)`, 33 | ); 34 | 35 | const init = await Deno.readTextFile(`pkg/${name}.js`); 36 | console.log(`read js (size: ${init.length} bytes)`); 37 | 38 | const source = 39 | `export const source = Deno[Deno.internal].core.ops.op_base64_decode(${encoded}); 40 | ${init}`; 41 | console.log(`inlined js and wasm (size: ${source.length} bytes)`); 42 | 43 | const output = await minify(source, { 44 | mangle: { module: true }, 45 | output: { 46 | preamble: `// deno-lint-ignore-file\n// deno-fmt-ignore-file`, 47 | }, 48 | // deno-lint-ignore no-explicit-any 49 | }, {}) as any; 50 | 51 | const reduction = new Blob([source]).size - 52 | new Blob([output.code]).size; 53 | console.log( 54 | `minified js (size reduction: ${reduction} bytes, size: ${output.code.length} bytes)`, 55 | ); 56 | 57 | console.log(`writing output to file (${target})`); 58 | await Deno.writeTextFile(target, output.code); 59 | 60 | const outputFile = await Deno.stat(target); 61 | console.log( 62 | `final size is: ${outputFile.size} bytes`, 63 | ); 64 | } 65 | 66 | if (import.meta.main) { 67 | await build(); 68 | Deno.exit(1); 69 | } 70 | -------------------------------------------------------------------------------- /tests/node_test.ts: -------------------------------------------------------------------------------- 1 | import { Allocator, JustifyContent, load, Node, Position } from "../mod.ts"; 2 | 3 | import { assertEquals } from "https://deno.land/std@0.171.0/testing/asserts.ts"; 4 | 5 | await load(); 6 | 7 | const allocator = new Allocator(); 8 | 9 | Deno.test("25% width", () => { 10 | const node = new Node(allocator, { 11 | position: Position.Absolute, 12 | width: 100, 13 | height: 100, 14 | justifyContent: JustifyContent.Center, 15 | }); 16 | node.addChild(new Node(allocator, { width: "25%", height: "auto" })); 17 | const layout = node.computeLayout(); 18 | assertEquals(layout.child(0).width, 25); 19 | }); 20 | 21 | Deno.test("100% width", () => { 22 | const node = new Node(allocator, { 23 | position: Position.Absolute, 24 | width: 100, 25 | height: 100, 26 | justifyContent: JustifyContent.Center, 27 | }); 28 | node.addChild(new Node(allocator, { width: "100%", height: "auto" })); 29 | const layout = node.computeLayout(); 30 | assertEquals(layout.child(0).width, 100); 31 | }); 32 | 33 | Deno.test("add child", () => { 34 | const node = new Node(allocator, {}); 35 | node.addChild(new Node(allocator, {})); 36 | assertEquals(node.childCount, 1); 37 | }); 38 | Deno.test("remove child", () => { 39 | const node = new Node(allocator, {}); 40 | const child = new Node(allocator, {}); 41 | node.addChild(child); 42 | node.removeChild(child); 43 | assertEquals(node.childCount, 0); 44 | }); 45 | 46 | Deno.test("remove child at index", () => { 47 | const node = new Node(allocator, {}); 48 | const child0 = new Node(allocator, {}); 49 | const child1 = new Node(allocator, {}); 50 | const child2 = new Node(allocator, {}); 51 | node.addChild(child0); 52 | node.addChild(child1); 53 | node.addChild(child2); 54 | node.removeChildAtIndex(1); 55 | assertEquals(node.childCount, 2); 56 | }); 57 | 58 | Deno.test("replace child at index", () => { 59 | const node = new Node(allocator, {}); 60 | const child0 = new Node(allocator, {}); 61 | const child1 = new Node(allocator, {}); 62 | const child2 = new Node(allocator, {}); 63 | node.addChild(child0); 64 | node.addChild(child1); 65 | node.replaceChildAtIndex(1, child2); 66 | assertEquals(node.childCount, 2); 67 | }); 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gelatin 2 | 3 | [![Tags](https://img.shields.io/github/release/load1n9/gelatin)](https://github.com/load1n9/gelatin/releases) 4 | [![Doc](https://doc.deno.land/badge.svg)](https://doc.deno.land/https/deno.land/x/gelatin/mod.ts) 5 | [![License](https://img.shields.io/github/license/load1n9/gelatin)](https://github.com/load1n9/gelatin/blob/main/LICENSE) 6 | [![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/load1n9) 7 | 8 | speedy wasm layout library for Deno using 9 | [taffy](https://github.com/DioxusLabs/taffy) 10 | 11 | ## Usage 12 | 13 | ```ts 14 | import { 15 | Allocator, 16 | Display, 17 | JustifyContent, 18 | load, 19 | Node, 20 | Position, 21 | } from "https://deno.land/x/gelatin/mod.ts"; 22 | import { 23 | mainloop, 24 | WindowCanvas, 25 | } from "https://deno.land/x/dwm@0.3.0/ext/canvas.ts"; 26 | 27 | const win = new WindowCanvas({ 28 | title: "Stretch", 29 | width: 500, 30 | height: 500, 31 | resizable: true, 32 | floating: true, 33 | }); 34 | 35 | await load(); 36 | 37 | const allocator = new Allocator(); 38 | 39 | const container = new Node(allocator, { 40 | position: Position.Absolute, 41 | width: 500, 42 | height: 500, 43 | justifyContent: JustifyContent.FlexStart, 44 | }); 45 | 46 | // green 47 | container.addChild( 48 | new Node(allocator, { 49 | marginStart: 0, 50 | width: "35%", 51 | height: "100%", 52 | }), 53 | ); 54 | 55 | // purple 56 | container.addChild( 57 | new Node(allocator, { 58 | width: "25%", 59 | height: "50%", 60 | }), 61 | ); 62 | 63 | addEventListener("resize", (event) => { 64 | const styles = container.getStyle(); 65 | styles.width = event.width; 66 | styles.height = event.height; 67 | container.setStyle(styles); 68 | }); 69 | 70 | win.onDraw = (ctx) => { 71 | ctx.fillStyle = "black"; 72 | ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); 73 | const layout = container.computeLayout(); 74 | ctx.fillStyle = "#44911B"; 75 | ctx.fillRect( 76 | layout.child(0).x, 77 | layout.child(0).y, 78 | layout.child(0).width, 79 | layout.child(0).height, 80 | ); 81 | ctx.fillStyle = "#ff00ff"; 82 | ctx.fillRect( 83 | layout.child(1).x, 84 | layout.child(1).y, 85 | layout.child(1).width, 86 | layout.child(1).height, 87 | ); 88 | }; 89 | 90 | await mainloop(() => { 91 | win.draw(); 92 | }); 93 | ``` 94 | 95 | ![demo](https://raw.githubusercontent.com/load1n9/gelatin/main/assets/demo.png) 96 | 97 | ## Maintainers 98 | 99 | - Dean Srebnik ([@load1n9](https://github.com/load1n9)) 100 | 101 | ## License 102 | 103 | [Apache-2.0](./LICENSE) licensed. 104 | 105 | Copyright 2023 © Dean Srebnik 106 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /gelatin.d.ts: -------------------------------------------------------------------------------- 1 | export enum AlignItems { 2 | FlexStart, 3 | FlexEnd, 4 | Start, 5 | End, 6 | Center, 7 | Baseline, 8 | Stretch, 9 | } 10 | 11 | export enum JustifyItems { 12 | FlexStart, 13 | FlexEnd, 14 | Start, 15 | End, 16 | Center, 17 | Baseline, 18 | Stretch, 19 | } 20 | 21 | export enum JustifySelf { 22 | FlexStart, 23 | FlexEnd, 24 | Start, 25 | End, 26 | Center, 27 | Baseline, 28 | Stretch, 29 | } 30 | 31 | export enum AlignSelf { 32 | FlexStart, 33 | FlexEnd, 34 | Start, 35 | End, 36 | Center, 37 | Baseline, 38 | Stretch, 39 | } 40 | 41 | export enum AlignContent { 42 | FlexStart, 43 | FlexEnd, 44 | Start, 45 | End, 46 | Center, 47 | Stretch, 48 | SpaceBetween, 49 | SpaceAround, 50 | } 51 | 52 | export enum Display { 53 | Flex, 54 | Grid, 55 | None, 56 | } 57 | 58 | export enum FlexDirection { 59 | Row, 60 | Column, 61 | RowReverse, 62 | ColumnReverse, 63 | } 64 | 65 | export enum GridAutoFlow { 66 | Row, 67 | Column, 68 | RowDense, 69 | ColumnDense, 70 | } 71 | 72 | export enum JustifyContent { 73 | FlexStart, 74 | FlexEnd, 75 | Start, 76 | End, 77 | Center, 78 | SpaceBetween, 79 | SpaceAround, 80 | SpaceEvenly, 81 | } 82 | 83 | export enum Position { 84 | Relative, 85 | Absolute, 86 | } 87 | 88 | export enum FlexWrap { 89 | NoWrap, 90 | Wrap, 91 | WrapReverse, 92 | } 93 | 94 | /** 95 | * Allocator class 96 | */ 97 | export class Allocator { 98 | /** 99 | * Free's the memory 100 | */ 101 | free(): void; 102 | constructor(); 103 | } 104 | 105 | /** 106 | * The flexbox layout information for a single Node 107 | */ 108 | export interface Styles { 109 | /** 110 | * What layout strategy should be used? 111 | */ 112 | display: Display; 113 | 114 | /** 115 | * The positioning strategy for this item. 116 | * 117 | * This controls both how the origin is determined for the [`Style::position`] field, 118 | * and whether or not the item will be controlled by flexbox's layout algorithm. 119 | */ 120 | position: Position; 121 | 122 | /** 123 | * How should the position of this element be tweaked relative to the layout defined? 124 | */ 125 | insetLeft: string | number; 126 | 127 | /** 128 | * How should the position of this element be tweaked relative to the layout defined? 129 | */ 130 | insetRight: string | number; 131 | 132 | /** 133 | * How should the position of this element be tweaked relative to the layout defined? 134 | */ 135 | insetTop: string | number; 136 | 137 | /** 138 | * How should the position of this element be tweaked relative to the layout defined? 139 | */ 140 | insetBottom: string | number; 141 | 142 | /** 143 | * Sets the initial width of the item 144 | */ 145 | width: string | number; 146 | 147 | /** 148 | * Sets the initial height of the item 149 | */ 150 | height: string | number; 151 | 152 | /** 153 | * Controls the minimum width of the item 154 | */ 155 | minWidth: string | number; 156 | 157 | /** 158 | * Controls the minimum height of the item 159 | */ 160 | minHeight: string | number; 161 | 162 | /** 163 | * Controls the maximum width of the item 164 | */ 165 | maxWidth: string | number; 166 | 167 | /** 168 | * Controls the maximum height of the item 169 | */ 170 | maxHeight: string | number; 171 | 172 | /** 173 | * The ratio is calculated as width divided by height. 174 | */ 175 | aspectRatio: number; 176 | 177 | /** 178 | * How large should the margin be on the left? 179 | */ 180 | marginLeft: string | number; 181 | 182 | /** 183 | * How large should the margin be on the right? 184 | */ 185 | marginRight: string | number; 186 | 187 | /** 188 | * How large should the margin be on the top? 189 | */ 190 | marginTop: string | number; 191 | 192 | /** 193 | * How large should the margin be on the bottom? 194 | */ 195 | marginBottom: string | number; 196 | 197 | /** 198 | * How large should the padding be on the left? 199 | */ 200 | paddingLeft: string | number; 201 | 202 | /** 203 | * How large should the padding be on the right? 204 | */ 205 | paddingRight: string | number; 206 | 207 | /** 208 | * How large should the padding be on the top? 209 | */ 210 | paddingTop: string | number; 211 | 212 | /** 213 | * How large should the padding be on the bottom? 214 | */ 215 | paddingBottom: string | number; 216 | 217 | /** 218 | * How large should the border be on the left? 219 | */ 220 | borderLeft: string | number; 221 | 222 | /** 223 | * How large should the border be on the right? 224 | */ 225 | borderRight: string | number; 226 | 227 | /** 228 | * How large should the border be on the top? 229 | */ 230 | borderTop: string | number; 231 | 232 | /** 233 | * How large should the border be on the bottom? 234 | */ 235 | borderBottom: string | number; 236 | 237 | /** 238 | * How this node's children aligned in the cross/block axis? 239 | */ 240 | alignItems: AlignItems; 241 | 242 | /** 243 | * How this node should be aligned in the cross/block axis 244 | */ 245 | alignSelf: AlignSelf; 246 | 247 | /** 248 | * How this node's children should be aligned in the inline axis 249 | */ 250 | justifyItems: JustifyItems; 251 | 252 | /** 253 | * How this node should be aligned in the inline axis 254 | */ 255 | justifySelf: JustifySelf; 256 | 257 | /** 258 | * How should content contained within this item be aligned in the cross/block axis 259 | */ 260 | alignContent: AlignContent; 261 | 262 | /** 263 | * How should contained within this item be aligned in the main/inline axis 264 | */ 265 | justifyContent: JustifyContent; 266 | 267 | /** 268 | * How large should the width of thegaps between items in a grid or flex container be? 269 | */ 270 | gapWidth: string | number; 271 | 272 | /** 273 | * How large should the height of the gaps between items in a grid or flex container be? 274 | */ 275 | gapHeight: string | number; 276 | 277 | /** 278 | * Which direction does the main axis flow in? 279 | */ 280 | flexDirection: FlexDirection; 281 | 282 | /** 283 | * Should elements wrap, or stay in a single line? 284 | */ 285 | flexWrap: FlexWrap; 286 | 287 | /** 288 | * Sets the initial main axis size of the item 289 | */ 290 | flexBasis: string | number; 291 | 292 | /** 293 | * The relative rate at which this item grows when it is expanding to fill space 294 | */ 295 | flexGrow: number; 296 | 297 | /** 298 | * The relative rate at which this item shrinks when it is contracting to fit into space 299 | */ 300 | flexShrink: number; 301 | 302 | /** 303 | * UNIMPLEMENTED: Defines the track sizing functions (widths) of the grid rows 304 | */ 305 | gridTemplateRows: undefined; 306 | 307 | /** 308 | * UNIMPLEMENTED: Defines the track sizing functions (heights) of the grid columns 309 | */ 310 | gridTemplateColumns: undefined; 311 | 312 | /** 313 | * UNIMPLEMENTED: Defines the size of implicitly created rows 314 | */ 315 | gridAutoRows: undefined; 316 | 317 | /** 318 | * UNIMPLEMENTED: Defined the size of implicitly created columns 319 | */ 320 | gridAutoColumns: undefined; 321 | 322 | /** 323 | * Controls how items get placed into the grid for auto-placed items 324 | */ 325 | gridAutoFlow: GridAutoFlow; 326 | 327 | /** 328 | * UNIMPLEMENTED: Defines which row in the grid the item should start and end at 329 | */ 330 | gridRow: undefined; 331 | 332 | /** 333 | * UNIMPLEMENTED: Defines which column in the grid the item should start and end at 334 | */ 335 | gridColumn: undefined; 336 | } 337 | 338 | /** 339 | * Layout class 340 | */ 341 | export class Layout { 342 | /** 343 | * Free's the layout of its children 344 | */ 345 | free(): void; 346 | 347 | /** 348 | * Gets a child at the given index 349 | */ 350 | child(at: number): Layout; 351 | 352 | /** 353 | * Number of children 354 | */ 355 | readonly childCount: number; 356 | 357 | /** 358 | * Height in pixels 359 | */ 360 | readonly height: number; 361 | 362 | /** 363 | * Width in pixels 364 | */ 365 | readonly width: number; 366 | 367 | /** 368 | * X coordinate 369 | */ 370 | readonly x: number; 371 | 372 | /** 373 | * Y coordinate 374 | */ 375 | readonly y: number; 376 | } 377 | 378 | /** 379 | * Node class 380 | */ 381 | export class Node { 382 | /** 383 | * Free's the node of its children 384 | */ 385 | free(): void; 386 | constructor(allocator: Allocator, style: Partial); 387 | 388 | /** 389 | * Sets the measure 390 | */ 391 | setMeasure(measure: unknown): void; 392 | 393 | /** 394 | * Add a child node to the node 395 | */ 396 | addChild(child: Node): void; 397 | 398 | /** 399 | * Removes the child from the node 400 | */ 401 | removeChild(child: Node): void; 402 | 403 | /** 404 | * Replaces the child at the current index with another child 405 | */ 406 | replaceChildAtIndex(index: number, child: Node): void; 407 | 408 | /** 409 | * Removes child at the given index 410 | */ 411 | removeChildAtIndex(index: number): void; 412 | 413 | /** 414 | * Gets the node's styles 415 | */ 416 | getStyle(): Partial; 417 | 418 | /** 419 | * Sets the styles of the current node 420 | */ 421 | setStyle(style: Partial): void; 422 | 423 | /** 424 | * Marks the node as dirty 425 | */ 426 | markDirty(): void; 427 | 428 | /** 429 | * Whether the node is dirty 430 | */ 431 | isDirty(): boolean; 432 | 433 | /** 434 | * Whether the node is childless 435 | */ 436 | isChildless(): boolean; 437 | 438 | /** 439 | * Computes the layout for the node and its children 440 | */ 441 | computeLayout( 442 | size?: { width: "max" | "min" | number; height: "max" | "min" | number }, 443 | ): Layout; 444 | 445 | /** 446 | * The amount of children that belong to this node 447 | */ 448 | readonly childCount: number; 449 | } 450 | 451 | export type InitInput = 452 | | RequestInfo 453 | | URL 454 | | Response 455 | | BufferSource 456 | | WebAssembly.Module; 457 | 458 | export interface InitOutput { 459 | readonly memory: WebAssembly.Memory; 460 | readonly __wbg_layout_free: (a: number) => void; 461 | readonly __wbg_get_layout_width: (a: number) => number; 462 | readonly __wbg_get_layout_height: (a: number) => number; 463 | readonly __wbg_get_layout_x: (a: number) => number; 464 | readonly __wbg_get_layout_y: (a: number) => number; 465 | readonly __wbg_get_layout_childCount: (a: number) => number; 466 | readonly layout_child: (a: number, b: number) => number; 467 | readonly __wbg_allocator_free: (a: number) => void; 468 | readonly allocator_new: () => number; 469 | readonly __wbg_node_free: (a: number) => void; 470 | readonly __wbg_get_node_childCount: (a: number) => number; 471 | readonly node_new: (a: number, b: number) => number; 472 | readonly node_setMeasure: (a: number, b: number) => void; 473 | readonly node_addChild: (a: number, b: number) => void; 474 | readonly node_removeChild: (a: number, b: number) => void; 475 | readonly node_replaceChildAtIndex: (a: number, b: number, c: number) => void; 476 | readonly node_removeChildAtIndex: (a: number, b: number) => void; 477 | readonly node_getStyle: (a: number) => number; 478 | readonly node_setStyle: (a: number, b: number) => void; 479 | readonly node_markDirty: (a: number) => void; 480 | readonly node_isDirty: (a: number) => number; 481 | readonly node_computeLayout: (a: number, b: number) => number; 482 | readonly __wbindgen_malloc: (a: number) => number; 483 | readonly __wbindgen_realloc: (a: number, b: number, c: number) => number; 484 | readonly __wbindgen_exn_store: (a: number) => void; 485 | } 486 | 487 | export type SyncInitInput = BufferSource | WebAssembly.Module; 488 | /** 489 | * Instantiates the given `module`, which can either be bytes or 490 | * a precompiled `WebAssembly.Module`. 491 | * 492 | * @param {SyncInitInput} module 493 | * 494 | * @returns {InitOutput} 495 | */ 496 | export function initSync(module: SyncInitInput): InitOutput; 497 | 498 | /** 499 | * If `module_or_path` is {RequestInfo} or {URL}, makes a request and 500 | * for everything else, calls `WebAssembly.instantiate` directly. 501 | * 502 | * @param {InitInput | Promise} module_or_path 503 | * 504 | * @returns {Promise} 505 | */ 506 | export default function init( 507 | module_or_path?: InitInput | Promise, 508 | ): Promise; 509 | 510 | export const source: Uint8Array; 511 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | 3 | mod utils; 4 | 5 | use std::cell::RefCell; 6 | use std::rc::Rc; 7 | 8 | use js_sys::Function; 9 | use js_sys::Reflect; 10 | use taffy::style_helpers::TaffyZero; 11 | use taffy::tree::LayoutTree; 12 | use wasm_bindgen::prelude::*; 13 | 14 | #[wasm_bindgen] 15 | #[repr(u8)] 16 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 17 | pub enum AlignItems { 18 | FlexStart, 19 | FlexEnd, 20 | Start, 21 | End, 22 | Center, 23 | Baseline, 24 | Stretch, 25 | } 26 | 27 | impl Into for AlignItems { 28 | fn into(self) -> taffy::style::AlignItems { 29 | match self { 30 | AlignItems::FlexStart => taffy::style::AlignItems::FlexStart, 31 | AlignItems::FlexEnd => taffy::style::AlignItems::FlexEnd, 32 | AlignItems::Start => taffy::style::AlignItems::Start, 33 | AlignItems::End => taffy::style::AlignItems::End, 34 | AlignItems::Center => taffy::style::AlignItems::Center, 35 | AlignItems::Baseline => taffy::style::AlignItems::Baseline, 36 | AlignItems::Stretch => taffy::style::AlignItems::Stretch, 37 | } 38 | } 39 | } 40 | 41 | impl From for AlignItems { 42 | fn from(n: i32) -> Self { 43 | match n { 44 | 0 => AlignItems::FlexStart, 45 | 1 => AlignItems::FlexEnd, 46 | 2 => AlignItems::Start, 47 | 3 => AlignItems::End, 48 | 4 => AlignItems::Center, 49 | 5 => AlignItems::Baseline, 50 | 6 => AlignItems::Stretch, 51 | _ => AlignItems::Stretch, 52 | } 53 | } 54 | } 55 | 56 | #[wasm_bindgen] 57 | #[repr(u8)] 58 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 59 | pub enum JustifyItems { 60 | FlexStart, 61 | FlexEnd, 62 | Start, 63 | End, 64 | Center, 65 | Baseline, 66 | Stretch, 67 | } 68 | 69 | impl Into for JustifyItems { 70 | fn into(self) -> taffy::style::JustifyItems { 71 | match self { 72 | JustifyItems::FlexStart => taffy::style::JustifyItems::FlexStart, 73 | JustifyItems::FlexEnd => taffy::style::JustifyItems::FlexEnd, 74 | JustifyItems::Start => taffy::style::JustifyItems::Start, 75 | JustifyItems::End => taffy::style::JustifyItems::End, 76 | JustifyItems::Center => taffy::style::JustifyItems::Center, 77 | JustifyItems::Baseline => taffy::style::JustifyItems::Baseline, 78 | JustifyItems::Stretch => taffy::style::JustifyItems::Stretch, 79 | } 80 | } 81 | } 82 | 83 | impl From for JustifyItems { 84 | fn from(n: i32) -> Self { 85 | match n { 86 | 0 => JustifyItems::FlexStart, 87 | 1 => JustifyItems::FlexEnd, 88 | 2 => JustifyItems::Start, 89 | 3 => JustifyItems::End, 90 | 4 => JustifyItems::Center, 91 | 5 => JustifyItems::Baseline, 92 | 6 => JustifyItems::Stretch, 93 | _ => JustifyItems::Stretch, 94 | } 95 | } 96 | } 97 | 98 | #[wasm_bindgen] 99 | #[repr(u8)] 100 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 101 | pub enum JustifySelf { 102 | FlexStart, 103 | FlexEnd, 104 | Start, 105 | End, 106 | Center, 107 | Baseline, 108 | Stretch, 109 | } 110 | 111 | impl Into for JustifySelf { 112 | fn into(self) -> taffy::style::JustifySelf { 113 | match self { 114 | JustifySelf::FlexStart => taffy::style::JustifySelf::FlexStart, 115 | JustifySelf::FlexEnd => taffy::style::JustifySelf::FlexEnd, 116 | JustifySelf::Start => taffy::style::JustifySelf::Start, 117 | JustifySelf::End => taffy::style::JustifySelf::End, 118 | JustifySelf::Center => taffy::style::JustifySelf::Center, 119 | JustifySelf::Baseline => taffy::style::JustifySelf::Baseline, 120 | JustifySelf::Stretch => taffy::style::JustifySelf::Stretch, 121 | } 122 | } 123 | } 124 | 125 | impl From for JustifySelf { 126 | fn from(n: i32) -> Self { 127 | match n { 128 | 0 => JustifySelf::FlexStart, 129 | 1 => JustifySelf::FlexEnd, 130 | 2 => JustifySelf::Start, 131 | 3 => JustifySelf::End, 132 | 4 => JustifySelf::Center, 133 | 5 => JustifySelf::Baseline, 134 | 6 => JustifySelf::Stretch, 135 | _ => JustifySelf::Stretch, 136 | } 137 | } 138 | } 139 | 140 | #[wasm_bindgen] 141 | #[repr(u8)] 142 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 143 | pub enum AlignSelf { 144 | FlexStart, 145 | FlexEnd, 146 | Start, 147 | End, 148 | Center, 149 | Baseline, 150 | Stretch, 151 | } 152 | 153 | impl Into for AlignSelf { 154 | fn into(self) -> taffy::style::AlignSelf { 155 | match self { 156 | AlignSelf::FlexStart => taffy::style::AlignSelf::FlexStart, 157 | AlignSelf::FlexEnd => taffy::style::AlignSelf::FlexEnd, 158 | AlignSelf::Start => taffy::style::AlignSelf::Start, 159 | AlignSelf::End => taffy::style::AlignSelf::End, 160 | AlignSelf::Center => taffy::style::AlignSelf::Center, 161 | AlignSelf::Baseline => taffy::style::AlignSelf::Baseline, 162 | AlignSelf::Stretch => taffy::style::AlignSelf::Stretch, 163 | } 164 | } 165 | } 166 | 167 | impl From for AlignSelf { 168 | fn from(n: i32) -> Self { 169 | match n { 170 | 0 => AlignSelf::FlexStart, 171 | 1 => AlignSelf::FlexEnd, 172 | 2 => AlignSelf::Start, 173 | 3 => AlignSelf::End, 174 | 4 => AlignSelf::Center, 175 | 5 => AlignSelf::Baseline, 176 | 6 => AlignSelf::Stretch, 177 | _ => AlignSelf::Start, 178 | } 179 | } 180 | } 181 | 182 | #[wasm_bindgen] 183 | #[repr(u8)] 184 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 185 | pub enum AlignContent { 186 | FlexStart, 187 | FlexEnd, 188 | Start, 189 | End, 190 | Center, 191 | Stretch, 192 | SpaceBetween, 193 | SpaceAround, 194 | } 195 | 196 | impl Into for AlignContent { 197 | fn into(self) -> taffy::style::AlignContent { 198 | match self { 199 | AlignContent::FlexStart => taffy::style::AlignContent::FlexStart, 200 | AlignContent::FlexEnd => taffy::style::AlignContent::FlexEnd, 201 | AlignContent::Start => taffy::style::AlignContent::FlexStart, 202 | AlignContent::End => taffy::style::AlignContent::FlexEnd, 203 | AlignContent::Center => taffy::style::AlignContent::Center, 204 | AlignContent::Stretch => taffy::style::AlignContent::Stretch, 205 | AlignContent::SpaceBetween => taffy::style::AlignContent::SpaceBetween, 206 | AlignContent::SpaceAround => taffy::style::AlignContent::SpaceAround, 207 | } 208 | } 209 | } 210 | 211 | impl From for AlignContent { 212 | fn from(n: i32) -> Self { 213 | match n { 214 | 0 => AlignContent::FlexStart, 215 | 1 => AlignContent::FlexEnd, 216 | 2 => AlignContent::Start, 217 | 3 => AlignContent::End, 218 | 4 => AlignContent::Center, 219 | 5 => AlignContent::Stretch, 220 | 6 => AlignContent::SpaceBetween, 221 | 7 => AlignContent::SpaceAround, 222 | _ => AlignContent::Stretch, 223 | } 224 | } 225 | } 226 | 227 | #[wasm_bindgen] 228 | #[repr(u8)] 229 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 230 | pub enum Display { 231 | Flex, 232 | Grid, 233 | None, 234 | } 235 | 236 | impl Into for Display { 237 | fn into(self) -> taffy::style::Display { 238 | match self { 239 | Display::Flex => taffy::style::Display::Flex, 240 | Display::Grid => taffy::style::Display::Grid, 241 | Display::None => taffy::style::Display::None, 242 | } 243 | } 244 | } 245 | 246 | impl From for Display { 247 | fn from(n: i32) -> Self { 248 | match n { 249 | 0 => Display::Flex, 250 | 1 => Display::Grid, 251 | 2 => Display::None, 252 | _ => Display::Flex, 253 | } 254 | } 255 | } 256 | 257 | #[wasm_bindgen] 258 | #[repr(u8)] 259 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 260 | pub enum FlexDirection { 261 | Row, 262 | Column, 263 | RowReverse, 264 | ColumnReverse, 265 | } 266 | 267 | impl Into for FlexDirection { 268 | fn into(self) -> taffy::style::FlexDirection { 269 | match self { 270 | FlexDirection::Row => taffy::style::FlexDirection::Row, 271 | FlexDirection::Column => taffy::style::FlexDirection::Column, 272 | FlexDirection::RowReverse => taffy::style::FlexDirection::RowReverse, 273 | FlexDirection::ColumnReverse => taffy::style::FlexDirection::ColumnReverse, 274 | } 275 | } 276 | } 277 | 278 | impl From for FlexDirection { 279 | fn from(n: i32) -> Self { 280 | match n { 281 | 0 => FlexDirection::Row, 282 | 1 => FlexDirection::Column, 283 | 2 => FlexDirection::RowReverse, 284 | 3 => FlexDirection::ColumnReverse, 285 | _ => FlexDirection::Row, 286 | } 287 | } 288 | } 289 | 290 | #[wasm_bindgen] 291 | #[repr(u8)] 292 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 293 | pub enum GridAutoFlow { 294 | Row, 295 | Column, 296 | RowDense, 297 | ColumnDense, 298 | } 299 | 300 | impl Into for GridAutoFlow { 301 | fn into(self) -> taffy::style::GridAutoFlow { 302 | match self { 303 | GridAutoFlow::Row => taffy::style::GridAutoFlow::Row, 304 | GridAutoFlow::Column => taffy::style::GridAutoFlow::Column, 305 | GridAutoFlow::RowDense => taffy::style::GridAutoFlow::RowDense, 306 | GridAutoFlow::ColumnDense => taffy::style::GridAutoFlow::ColumnDense, 307 | } 308 | } 309 | } 310 | 311 | impl From for GridAutoFlow { 312 | fn from(n: i32) -> Self { 313 | match n { 314 | 0 => GridAutoFlow::Row, 315 | 1 => GridAutoFlow::Column, 316 | 2 => GridAutoFlow::RowDense, 317 | 3 => GridAutoFlow::ColumnDense, 318 | _ => GridAutoFlow::Row, 319 | } 320 | } 321 | } 322 | 323 | #[wasm_bindgen] 324 | #[repr(u8)] 325 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 326 | pub enum JustifyContent { 327 | FlexStart, 328 | FlexEnd, 329 | Start, 330 | End, 331 | Center, 332 | SpaceBetween, 333 | SpaceAround, 334 | SpaceEvenly, 335 | } 336 | 337 | impl Into for JustifyContent { 338 | fn into(self) -> taffy::style::JustifyContent { 339 | match self { 340 | JustifyContent::FlexStart => taffy::style::JustifyContent::FlexStart, 341 | JustifyContent::FlexEnd => taffy::style::JustifyContent::FlexEnd, 342 | JustifyContent::Start => taffy::style::JustifyContent::Start, 343 | JustifyContent::End => taffy::style::JustifyContent::End, 344 | JustifyContent::Center => taffy::style::JustifyContent::Center, 345 | JustifyContent::SpaceBetween => taffy::style::JustifyContent::SpaceBetween, 346 | JustifyContent::SpaceAround => taffy::style::JustifyContent::SpaceAround, 347 | JustifyContent::SpaceEvenly => taffy::style::JustifyContent::SpaceEvenly, 348 | } 349 | } 350 | } 351 | 352 | impl From for JustifyContent { 353 | fn from(n: i32) -> Self { 354 | match n { 355 | 0 => JustifyContent::FlexStart, 356 | 1 => JustifyContent::FlexEnd, 357 | 2 => JustifyContent::Start, 358 | 3 => JustifyContent::End, 359 | 4 => JustifyContent::Center, 360 | 5 => JustifyContent::SpaceBetween, 361 | 6 => JustifyContent::SpaceAround, 362 | 7 => JustifyContent::SpaceEvenly, 363 | _ => JustifyContent::FlexStart, 364 | } 365 | } 366 | } 367 | 368 | #[wasm_bindgen] 369 | #[repr(u8)] 370 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 371 | pub enum Position { 372 | Relative, 373 | Absolute, 374 | } 375 | 376 | impl Into for Position { 377 | fn into(self) -> taffy::style::Position { 378 | match self { 379 | Position::Relative => taffy::style::Position::Relative, 380 | Position::Absolute => taffy::style::Position::Absolute, 381 | } 382 | } 383 | } 384 | 385 | impl From for Position { 386 | fn from(n: i32) -> Self { 387 | match n { 388 | 0 => Position::Relative, 389 | 1 => Position::Absolute, 390 | _ => Position::Relative, 391 | } 392 | } 393 | } 394 | 395 | #[wasm_bindgen] 396 | #[repr(u8)] 397 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 398 | pub enum FlexWrap { 399 | NoWrap, 400 | Wrap, 401 | WrapReverse, 402 | } 403 | 404 | impl Into for FlexWrap { 405 | fn into(self) -> taffy::style::FlexWrap { 406 | match self { 407 | FlexWrap::NoWrap => taffy::style::FlexWrap::NoWrap, 408 | FlexWrap::Wrap => taffy::style::FlexWrap::Wrap, 409 | FlexWrap::WrapReverse => taffy::style::FlexWrap::WrapReverse, 410 | } 411 | } 412 | } 413 | 414 | impl From for FlexWrap { 415 | fn from(n: i32) -> Self { 416 | match n { 417 | 0 => FlexWrap::NoWrap, 418 | 1 => FlexWrap::Wrap, 419 | 2 => FlexWrap::WrapReverse, 420 | _ => FlexWrap::NoWrap, 421 | } 422 | } 423 | } 424 | 425 | #[wasm_bindgen] 426 | #[derive(Clone, Debug)] 427 | pub struct Layout { 428 | #[wasm_bindgen(readonly)] 429 | pub width: f32, 430 | 431 | #[wasm_bindgen(readonly)] 432 | pub height: f32, 433 | 434 | #[wasm_bindgen(readonly)] 435 | pub x: f32, 436 | 437 | #[wasm_bindgen(readonly)] 438 | pub y: f32, 439 | 440 | #[wasm_bindgen(readonly)] 441 | pub childCount: usize, 442 | 443 | children: Vec, 444 | } 445 | 446 | #[wasm_bindgen] 447 | impl Layout { 448 | fn new(allocator: &Allocator, node: taffy::node::Node) -> Layout { 449 | let taffy = allocator.taffy.borrow(); 450 | let layout = taffy.layout(node).unwrap(); 451 | let children = taffy.children(node).unwrap(); 452 | 453 | Layout { 454 | width: layout.size.width, 455 | height: layout.size.height, 456 | x: layout.location.x, 457 | y: layout.location.y, 458 | childCount: children.len(), 459 | children: children 460 | .into_iter() 461 | .map(|child| Layout::new(allocator, child)) 462 | .collect(), 463 | } 464 | } 465 | 466 | #[wasm_bindgen] 467 | pub fn child(&self, at: usize) -> Layout { 468 | self.children[at].clone() 469 | } 470 | } 471 | 472 | #[wasm_bindgen] 473 | #[derive(Clone)] 474 | pub struct Allocator { 475 | taffy: Rc>, 476 | } 477 | 478 | #[wasm_bindgen] 479 | impl Allocator { 480 | #[wasm_bindgen(constructor)] 481 | pub fn new() -> Self { 482 | Self { 483 | taffy: Rc::new(RefCell::new(taffy::Taffy::new())), 484 | } 485 | } 486 | } 487 | 488 | #[wasm_bindgen] 489 | pub struct Node { 490 | allocator: Allocator, 491 | node: taffy::node::Node, 492 | style: JsValue, 493 | 494 | #[wasm_bindgen(readonly)] 495 | pub childCount: usize, 496 | } 497 | 498 | #[wasm_bindgen] 499 | impl Node { 500 | #[wasm_bindgen(constructor)] 501 | pub fn new(allocator: &Allocator, style: &JsValue) -> Self { 502 | Self { 503 | allocator: allocator.clone(), 504 | node: allocator 505 | .taffy 506 | .borrow_mut() 507 | .new_leaf(parse_style(&style)) 508 | .unwrap(), 509 | style: style.clone(), 510 | childCount: 0, 511 | } 512 | } 513 | 514 | #[wasm_bindgen(js_name = setMeasure)] 515 | pub fn set_measure(&mut self, measure: &JsValue) { 516 | let _measure = Function::from(measure.clone()); 517 | 518 | self.allocator 519 | .taffy 520 | .borrow_mut() 521 | .set_measure( 522 | self.node, 523 | // Some(taffy::node::MeasureFunc::Boxed(Box::new( 524 | // move |constraints| { 525 | // use taffy::number::OrElse; 526 | 527 | // let widthConstraint = 528 | // if let taffy::number::Number::Defined(val) = constraints.width { 529 | // val.into() 530 | // } else { 531 | // JsValue::UNDEFINED 532 | // }; 533 | 534 | // let heightConstaint = 535 | // if let taffy::number::Number::Defined(val) = constraints.height { 536 | // val.into() 537 | // } else { 538 | // JsValue::UNDEFINED 539 | // }; 540 | 541 | // if let Ok(result) = 542 | // measure.call2(&JsValue::UNDEFINED, &widthConstraint, &heightConstaint) 543 | // { 544 | // let width = get_f32(&result, "width"); 545 | // let height = get_f32(&result, "height"); 546 | 547 | // if width.is_some() && height.is_some() { 548 | // return taffy::geometry::Size { 549 | // width: width.unwrap(), 550 | // height: height.unwrap(), 551 | // }; 552 | // } 553 | // } 554 | 555 | // constraints.map(|v| v.or_else(0.0)) 556 | // }, 557 | // ))), 558 | None, 559 | ) 560 | .unwrap(); 561 | } 562 | 563 | #[wasm_bindgen(js_name = addChild)] 564 | pub fn add_child(&mut self, child: &Node) { 565 | self.allocator 566 | .taffy 567 | .borrow_mut() 568 | .add_child(self.node, child.node) 569 | .unwrap(); 570 | self.childCount += 1; 571 | } 572 | 573 | #[wasm_bindgen(js_name = removeChild)] 574 | pub fn remove_child(&mut self, child: &Node) { 575 | self.allocator 576 | .taffy 577 | .borrow_mut() 578 | .remove_child(self.node, child.node) 579 | .unwrap(); 580 | self.childCount -= 1; 581 | } 582 | 583 | #[wasm_bindgen(js_name = replaceChildAtIndex)] 584 | pub fn replace_child_at_index(&mut self, index: usize, child: &Node) { 585 | self.allocator 586 | .taffy 587 | .borrow_mut() 588 | .replace_child_at_index(self.node, index, child.node) 589 | .unwrap(); 590 | } 591 | 592 | #[wasm_bindgen(js_name = removeChildAtIndex)] 593 | pub fn remove_child_at_index(&mut self, index: usize) { 594 | self.allocator 595 | .taffy 596 | .borrow_mut() 597 | .remove_child_at_index(self.node, index) 598 | .unwrap(); 599 | self.childCount -= 1; 600 | } 601 | 602 | #[wasm_bindgen(js_name = getStyle)] 603 | pub fn get_style(&self) -> JsValue { 604 | self.style.clone() 605 | } 606 | 607 | #[wasm_bindgen(js_name = setStyle)] 608 | pub fn set_style(&mut self, style: &JsValue) { 609 | self.allocator 610 | .taffy 611 | .borrow_mut() 612 | .set_style(self.node, parse_style(style)) 613 | .unwrap(); 614 | self.style = style.clone(); 615 | } 616 | 617 | #[wasm_bindgen(js_name = markDirty)] 618 | pub fn mark_dirty(&mut self) { 619 | self.allocator 620 | .taffy 621 | .borrow_mut() 622 | .mark_dirty(self.node) 623 | .unwrap() 624 | } 625 | 626 | #[wasm_bindgen(js_name = isDirty)] 627 | pub fn is_dirty(&self) -> bool { 628 | self.allocator.taffy.borrow().dirty(self.node).unwrap() 629 | } 630 | 631 | #[wasm_bindgen(js_name = isChildless)] 632 | pub fn is_childless(&mut self) -> bool { 633 | self.allocator.taffy.borrow_mut().is_childless(self.node) 634 | } 635 | 636 | #[wasm_bindgen(js_name = computeLayout)] 637 | pub fn compute_layout(&mut self, size: &JsValue) -> Layout { 638 | self.allocator 639 | .taffy 640 | .borrow_mut() 641 | .compute_layout( 642 | self.node, 643 | taffy::geometry::Size { 644 | width: get_available_space(size, "width"), 645 | height: get_available_space(size, "height"), 646 | }, 647 | ) 648 | .unwrap(); 649 | Layout::new(&self.allocator, self.node) 650 | } 651 | } 652 | 653 | fn parse_style(style: &JsValue) -> taffy::style::Style { 654 | taffy::style::Style { 655 | display: get_i32(style, "display") 656 | .map(|i| Display::from(i).into()) 657 | .unwrap_or_default(), 658 | position: get_i32(style, "position") 659 | .map(|i| Position::from(i).into()) 660 | .unwrap_or_default(), 661 | flex_direction: get_i32(style, "flexDirection") 662 | .map(|i| FlexDirection::from(i).into()) 663 | .unwrap_or_default(), 664 | flex_wrap: get_i32(style, "flexWrap") 665 | .map(|i| FlexWrap::from(i).into()) 666 | .unwrap_or_default(), 667 | align_items: get_i32(style, "alignItems") 668 | .map(|i| Some(AlignItems::from(i).into())) 669 | .unwrap_or_default(), 670 | align_self: get_i32(style, "alignSelf") 671 | .map(|i| Some(AlignSelf::from(i).into())) 672 | .unwrap_or_default(), 673 | align_content: get_i32(style, "alignContent") 674 | .map(|i| Some(AlignContent::from(i).into())) 675 | .unwrap_or_default(), 676 | justify_content: get_i32(style, "justifyContent") 677 | .map(|i| Some(JustifyContent::from(i).into())) 678 | .unwrap_or_default(), 679 | justify_self: get_i32(style, "justifySelf") 680 | .map(|i| Some(JustifySelf::from(i).into())) 681 | .unwrap_or_default(), 682 | justify_items: get_i32(style, "justifyItems") 683 | .map(|i| Some(JustifyItems::from(i).into())) 684 | .unwrap_or_default(), 685 | grid_template_rows: Default::default(), 686 | grid_template_columns: Default::default(), 687 | grid_auto_rows: Default::default(), 688 | grid_auto_columns: Default::default(), 689 | grid_auto_flow: get_i32(style, "gridAutoFlow") 690 | .map(|i| GridAutoFlow::from(i).into()) 691 | .unwrap_or_default(), 692 | grid_row: Default::default(), 693 | grid_column: Default::default(), 694 | inset: taffy::geometry::Rect { 695 | left: get_length_percentage_auto_dimension(style, "insetLeft"), 696 | right: get_length_percentage_auto_dimension(style, "insetRight"), 697 | top: get_length_percentage_auto_dimension(style, "insetTop"), 698 | bottom: get_length_percentage_auto_dimension(style, "insetBottom"), 699 | }, 700 | margin: taffy::geometry::Rect { 701 | left: get_length_percentage_auto_dimension(style, "marginLeft"), 702 | right: get_length_percentage_auto_dimension(style, "marginRight"), 703 | top: get_length_percentage_auto_dimension(style, "marginTop"), 704 | bottom: get_length_percentage_auto_dimension(style, "marginBottom"), 705 | }, 706 | 707 | padding: taffy::geometry::Rect { 708 | left: get_length_percentage_dimension(style, "paddingLeft"), 709 | right: get_length_percentage_dimension(style, "paddingRight"), 710 | top: get_length_percentage_dimension(style, "paddingTop"), 711 | bottom: get_length_percentage_dimension(style, "paddingBottom"), 712 | }, 713 | 714 | border: taffy::geometry::Rect { 715 | left: get_length_percentage_dimension(style, "borderLeft"), 716 | right: get_length_percentage_dimension(style, "borderRight"), 717 | top: get_length_percentage_dimension(style, "borderTop"), 718 | bottom: get_length_percentage_dimension(style, "borderBottom"), 719 | }, 720 | 721 | flex_grow: get_f32(style, "flexGrow").unwrap_or(0.0), 722 | flex_shrink: get_f32(style, "flexShrink").unwrap_or(1.0), 723 | flex_basis: get_dimension(style, "flexBasis"), 724 | 725 | gap: taffy::geometry::Size { 726 | width: get_length_percentage_dimension(style, "gapWidth"), 727 | height: get_length_percentage_dimension(style, "gapHeight"), 728 | }, 729 | 730 | size: taffy::geometry::Size { 731 | width: get_size_dimension(style, "width"), 732 | height: get_size_dimension(style, "height"), 733 | }, 734 | 735 | min_size: taffy::geometry::Size { 736 | width: get_size_dimension(style, "minWidth"), 737 | height: get_size_dimension(style, "minHeight"), 738 | }, 739 | 740 | max_size: taffy::geometry::Size { 741 | width: get_size_dimension(style, "maxWidth"), 742 | height: get_size_dimension(style, "maxHeight"), 743 | }, 744 | 745 | aspect_ratio: get_f32(style, "aspectRatio"), 746 | } 747 | } 748 | 749 | fn get_size_dimension(obj: &JsValue, key: &str) -> taffy::style::Dimension { 750 | let dimension = get_dimension(obj, key); 751 | match dimension { 752 | taffy::style::Dimension::Auto => taffy::style::Dimension::Auto, 753 | _ => dimension, 754 | } 755 | } 756 | 757 | fn get_dimension(obj: &JsValue, key: &str) -> taffy::style::Dimension { 758 | if has_key(obj, key) { 759 | if let Ok(val) = Reflect::get(obj, &key.into()) { 760 | if let Some(number) = val.as_f64() { 761 | return taffy::style::Dimension::Points(number as f32); 762 | } 763 | if let Some(string) = val.as_string() { 764 | if string == "auto" { 765 | return taffy::style::Dimension::Auto; 766 | } 767 | if let Ok(number) = string.parse::() { 768 | return taffy::style::Dimension::Points(number); 769 | } 770 | if string.ends_with('%') { 771 | let len = string.len(); 772 | if let Ok(number) = string[..len - 1].parse::() { 773 | return taffy::style::Dimension::Percent(number / 100.0); 774 | } 775 | } 776 | } 777 | } 778 | } 779 | taffy::style::Dimension::Auto 780 | } 781 | 782 | fn get_available_space(obj: &JsValue, key: &str) -> taffy::style::AvailableSpace { 783 | if has_key(obj, key) { 784 | if let Ok(val) = Reflect::get(obj, &key.into()) { 785 | if let Some(number) = val.as_f64() { 786 | return taffy::style::AvailableSpace::Definite(number as f32); 787 | } 788 | if let Some(string) = val.as_string() { 789 | if string == "min" || string == "minContent" { 790 | return taffy::style::AvailableSpace::MinContent; 791 | } 792 | if string == "max" || string == "maxContent" { 793 | return taffy::style::AvailableSpace::MaxContent; 794 | } 795 | if let Ok(number) = string.parse::() { 796 | return taffy::style::AvailableSpace::Definite(number); 797 | } 798 | } 799 | } 800 | } 801 | taffy::style::AvailableSpace::ZERO 802 | } 803 | 804 | fn get_length_percentage_auto_dimension( 805 | obj: &JsValue, 806 | key: &str, 807 | ) -> taffy::style::LengthPercentageAuto { 808 | if has_key(obj, key) { 809 | if let Ok(val) = Reflect::get(obj, &key.into()) { 810 | if let Some(number) = val.as_f64() { 811 | return taffy::style::LengthPercentageAuto::Points(number as f32); 812 | } 813 | if let Some(string) = val.as_string() { 814 | if string == "auto" { 815 | return taffy::style::LengthPercentageAuto::Auto; 816 | } 817 | if let Ok(number) = string.parse::() { 818 | return taffy::style::LengthPercentageAuto::Points(number); 819 | } 820 | if string.ends_with('%') { 821 | let len = string.len(); 822 | if let Ok(number) = string[..len - 1].parse::() { 823 | return taffy::style::LengthPercentageAuto::Percent(number / 100.0); 824 | } 825 | } 826 | } 827 | } 828 | } 829 | taffy::style::LengthPercentageAuto::ZERO 830 | } 831 | 832 | fn get_length_percentage_dimension(obj: &JsValue, key: &str) -> taffy::style::LengthPercentage { 833 | if has_key(obj, key) { 834 | if let Ok(val) = Reflect::get(obj, &key.into()) { 835 | if let Some(number) = val.as_f64() { 836 | return taffy::style::LengthPercentage::Points(number as f32); 837 | } 838 | if let Some(string) = val.as_string() { 839 | if let Ok(number) = string.parse::() { 840 | return taffy::style::LengthPercentage::Points(number); 841 | } 842 | if string.ends_with('%') { 843 | let len = string.len(); 844 | if let Ok(number) = string[..len - 1].parse::() { 845 | return taffy::style::LengthPercentage::Percent(number / 100.0); 846 | } 847 | } 848 | } 849 | } 850 | } 851 | taffy::style::LengthPercentage::ZERO 852 | } 853 | 854 | fn get_i32(obj: &JsValue, key: &str) -> Option { 855 | if has_key(obj, key) { 856 | if let Ok(val) = Reflect::get(obj, &key.into()) { 857 | return val.as_f64().map(|v| v as i32); 858 | } 859 | } 860 | None 861 | } 862 | 863 | fn get_f32(obj: &JsValue, key: &str) -> Option { 864 | if has_key(obj, key) { 865 | if let Ok(val) = Reflect::get(obj, &key.into()) { 866 | return val.as_f64().map(|v| v as f32); 867 | } 868 | } 869 | None 870 | } 871 | 872 | fn has_key(obj: &JsValue, key: &str) -> bool { 873 | if let Ok(exists) = Reflect::has(obj, &key.into()) { 874 | exists 875 | } else { 876 | false 877 | } 878 | } 879 | --------------------------------------------------------------------------------