diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..1be0c5be0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,40 @@ +--- +name: Bug report +about: Report a problem encountered while using azul +labels: bug +--- + +## Description + + +## Version / OS + + +* azul version: + + +* Operating system: + + +* Windowing system (X11 or Wayland, Linux only): + + +## Steps to Reproduce + + + +## Additional Information + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..a1d2b2bf2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,29 @@ +--- +name: Feature request +about: Suggest an idea to improve azul +labels: feature-request +--- + +## Description + + +## Describe your solution + + +## Are there alternatives or drawbacks? + + +## Is this a breaking change + + +## Additional notes + diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 000000000..3deb331f6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,6 @@ +--- +name: Question +about: Ask a question / problem when using azul +labels: question +--- + diff --git a/.gitignore b/.gitignore index 96c17edab..90ac5de9c 100755 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ target/ **/*.rs.bk -Cargo.lock *.pdf *.sublime-project .vscode/* -*.log \ No newline at end of file +*.log +.idea/* diff --git a/.travis.yml b/.travis.yml index 13cce64d7..f5c7e17e3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,42 +1,47 @@ language: rust - -sudo: false - +cache: cargo +sudo: required rust: - - stable - - beta - - nightly + - 1.31.0 + +os: + - linux +# - osx matrix: - allow_failures: - - rust: nightly + fast_finish: true + +notifications: + email: false + +# We can't test OpenGL 3.2 on Travis, the shader compilation fails +# because glium does a check first if it has a OGL 3.2 context +script: + - cargo check --verbose --all-features + - cargo check --verbose --examples + - cargo check --no-default-features + - cargo check --no-default-features --features="svg" + - cargo check --no-default-features --features="image_loading" + - cargo check --no-default-features --features="logging" + - cargo check --verbose --release --all-features + - RUST_BACKTRACE=1 cargo test --verbose --all-features -env: - global: - - RUSTFLAGS="-C link-dead-code" +install: + - PATH=$PATH:/home/travis/.cargo/bin addons: apt: + update: true packages: - - libcurl4-openssl-dev - - libelf-dev - - libdw-dev - - cmake - - gcc - - binutils-dev - - libiberty-dev + - libcurl4-openssl-dev # for kcov + - libelf-dev # for kcov + - libdw-dev # for kcov + - cmake # for kcov + - binutils-dev # for kcov + - libiberty-dev # for kcov after_success: | wget https://github.com/SimonKagstrom/kcov/archive/master.tar.gz && - tar xzf master.tar.gz && - cd kcov-master && - mkdir build && - cd build && - cmake .. && - make && - make install DESTDIR=../../kcov-build && - cd ../.. && - rm -rf kcov-master && - for file in target/debug/azul-*[^\.d]; do mkdir -p "target/cov/$(basename $file)"; ./kcov-build/usr/local/bin/kcov --exclude-pattern=/.cargo,/usr/lib --verify "target/cov/$(basename $file)" "$file"; done && - bash <(curl -s https://codecov.io/bash) && - echo "Uploaded code coverage" \ No newline at end of file + tar xzf master.tar.gz && mkdir kcov-master/build && cd kcov-master/build && cmake .. && make && + sudo make install && cd ../.. && + kcov --verify --coveralls-id=$TRAVIS_JOB_ID --exclude-pattern=/.cargo target/kcov $(find target/debug -maxdepth 1 -executable -name azul-\*) \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 000000000..b95f90101 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1635 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "adler32" +version = "1.0.3" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" + +[[package]] +name = "andrew" +version = "0.2.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "bitflags 1.0.4 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "line_drawing 0.7.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "rusttype 0.7.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "walkdir 2.2.7 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "xdg 2.2.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "xml-rs 0.8.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "android_glue" +version = "0.2.3" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" + +[[package]] +name = "app_units" +version = "0.7.1" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "num-traits 0.2.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "serde 1.0.80 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "approx" +version = "0.3.1" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "num-traits 0.2.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "arrayvec" +version = "0.4.8" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "nodrop 0.1.13 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "azul" +version = "0.1.0" +dependencies = [ + "azul-css 0.1.0", + "azul-css-parser 0.1.0", + "azul-dependencies 0.1.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "azul-native-style 0.1.0", + "serde 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "azul-css" +version = "0.1.0" + +[[package]] +name = "azul-css-parser" +version = "0.1.0" +dependencies = [ + "azul-css 0.1.0", + "serde 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)", + "simplecss 0.1.0 (git+https://github.com/fschutt/simplecss.git?rev=0bf3b7800f49a4524b889687d7761b163e8b4101)", +] + +[[package]] +name = "azul-dependencies" +version = "0.1.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "app_units 0.7.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "backtrace 0.3.12 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "clipboard2 0.1.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "euclid 0.19.4 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "fern 0.5.7 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "font-loader 0.8.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "gleam 0.6.8 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "glium 0.22.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "harfbuzz-sys 0.3.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "image 0.20.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "lazy_static 1.2.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "log 0.4.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "lyon 0.11.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "rusttype 0.7.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "serde 1.0.80 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "stb_truetype 0.2.2 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "tinyfiledialogs 3.3.5 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "unicode-normalization 0.1.7 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "usvg 0.3.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "webrender 0.57.2 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "xmlparser 0.6.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "azul-native-style" +version = "0.1.0" +dependencies = [ + "azul-css 0.1.0", + "azul-css-parser 0.1.0", +] + +[[package]] +name = "backtrace" +version = "0.3.12" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "backtrace-sys 0.1.24 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "cfg-if 0.1.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "libc 0.2.45 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "rustc-demangle 0.1.9 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "winapi 0.3.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "backtrace-sys" +version = "0.1.24" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "cc 1.0.26 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "libc 0.2.45 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "base64" +version = "0.9.3" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "byteorder 1.2.7 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "safemem 0.3.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "binary-space-partition" +version = "0.1.2" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" + +[[package]] +name = "bincode" +version = "1.0.1" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "byteorder 1.2.7 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "serde 1.0.80 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "bitflags" +version = "1.0.4" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" + +[[package]] +name = "bitflags" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "byteorder" +version = "1.2.7" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" + +[[package]] +name = "cc" +version = "1.0.26" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" + +[[package]] +name = "cfg-if" +version = "0.1.6" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" + +[[package]] +name = "cfg-if" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "cgl" +version = "0.2.3" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "gleam 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.45 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "clipboard-win" +version = "2.1.2" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "winapi 0.3.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "clipboard2" +version = "0.1.1" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "clipboard-win 2.1.2 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "objc 0.2.5 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "objc-foundation 0.1.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "objc_id 0.1.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "x11-clipboard 0.3.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "cmake" +version = "0.1.35" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "cc 1.0.26 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "cocoa" +version = "0.18.4" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "block 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "core-foundation 0.6.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "core-graphics 0.17.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.45 (registry+https://github.com/rust-lang/crates.io-index)", + "objc 0.2.5 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "color_quant" +version = "1.0.1" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" + +[[package]] +name = "core-foundation" +version = "0.6.3" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "core-foundation-sys 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.45 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "core-foundation-sys" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "core-graphics" +version = "0.17.3" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "core-foundation 0.6.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.45 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "core-text" +version = "13.1.1" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "core-foundation 0.6.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "core-graphics 0.17.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.45 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "crc32fast" +version = "1.1.2" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "cfg-if 0.1.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "crossbeam-deque" +version = "0.2.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "crossbeam-epoch 0.3.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "crossbeam-utils 0.2.2 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.3.1" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "arrayvec 0.4.8 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "cfg-if 0.1.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "crossbeam-utils 0.2.2 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "lazy_static 1.2.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "memoffset 0.2.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "nodrop 0.1.13 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "scopeguard 0.3.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "crossbeam-utils" +version = "0.2.2" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "cfg-if 0.1.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "deflate" +version = "0.7.19" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "adler32 1.0.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "byteorder 1.2.7 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "dlib" +version = "0.4.1" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "libloading 0.5.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "downcast-rs" +version = "1.0.3" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" + +[[package]] +name = "dwrote" +version = "0.6.3" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "lazy_static 1.2.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "libc 0.2.45 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "serde 1.0.80 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "serde_derive 1.0.80 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "winapi 0.3.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "either" +version = "1.5.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" + +[[package]] +name = "euclid" +version = "0.19.4" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "num-traits 0.2.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "serde 1.0.80 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "expat-sys" +version = "2.1.6" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "cmake 0.1.35 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "pkg-config 0.3.14 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "fern" +version = "0.5.7" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "log 0.4.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "float-cmp" +version = "0.4.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "fnv" +version = "1.0.6" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" + +[[package]] +name = "font-loader" +version = "0.8.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "core-foundation 0.6.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "core-text 13.1.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "libc 0.2.45 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "servo-fontconfig 0.4.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "winapi 0.3.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "freetype" +version = "0.4.1" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "libc 0.2.45 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "servo-freetype-sys 4.0.5 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "byteorder 1.2.7 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "gif" +version = "0.10.1" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "color_quant 1.0.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "lzw 0.10.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "gl_generator" +version = "0.10.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "khronos_api 3.0.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "log 0.4.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "xml-rs 0.8.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "gl_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "khronos_api 3.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "xml-rs 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "gleam" +version = "0.6.8" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "gl_generator 0.10.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "gleam" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "gl_generator 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "glium" +version = "0.22.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "fnv 1.0.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "gl_generator 0.10.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "glutin 0.19.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "lazy_static 1.2.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "smallvec 0.6.7 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "glutin" +version = "0.19.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "android_glue 0.2.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "cgl 0.2.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "cocoa 0.18.4 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "core-foundation 0.6.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "core-graphics 0.17.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "gl_generator 0.10.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "lazy_static 1.2.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "libc 0.2.45 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "libloading 0.5.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "objc 0.2.5 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "osmesa-sys 0.1.2 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "shared_library 0.1.9 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "wayland-client 0.23.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "winapi 0.3.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "winit 0.18.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "x11-dl 2.18.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "harfbuzz-sys" +version = "0.3.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "cmake 0.1.35 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "freetype 0.4.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "pkg-config 0.3.14 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "image" +version = "0.20.1" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "byteorder 1.2.7 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "gif 0.10.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "jpeg-decoder 0.1.15 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "lzw 0.10.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "num-iter 0.1.37 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "num-rational 0.2.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "num-traits 0.2.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "png 0.12.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "scoped_threadpool 0.1.9 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "tiff 0.2.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "inflate" +version = "0.4.4" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "adler32 1.0.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "jpeg-decoder" +version = "0.1.15" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "byteorder 1.2.7 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "rayon 1.0.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "khronos_api" +version = "3.0.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" + +[[package]] +name = "khronos_api" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "lazy_static" +version = "1.2.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" + +[[package]] +name = "libc" +version = "0.2.45" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" + +[[package]] +name = "libc" +version = "0.2.45" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libflate" +version = "0.1.19" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "adler32 1.0.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "byteorder 1.2.7 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "crc32fast 1.1.2 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "libloading" +version = "0.5.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "cc 1.0.26 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "winapi 0.3.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "line_drawing" +version = "0.7.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "num-traits 0.2.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "log" +version = "0.4.6" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "cfg-if 0.1.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "log" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "lyon" +version = "0.11.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "lyon_algorithms 0.11.2 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "lyon_tessellation 0.11.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "lyon_algorithms" +version = "0.11.2" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "lyon_path 0.11.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "lyon_geom" +version = "0.11.1" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "arrayvec 0.4.8 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "euclid 0.19.4 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "num-traits 0.2.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "lyon_geom" +version = "0.12.2" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "arrayvec 0.4.8 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "euclid 0.19.4 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "num-traits 0.2.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "lyon_path" +version = "0.11.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "lyon_geom 0.11.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "lyon_tessellation" +version = "0.11.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "lyon_path 0.11.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "sid 0.5.2 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "lzw" +version = "0.10.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.45 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "memmap" +version = "0.7.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "libc 0.2.45 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "winapi 0.3.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "memoffset" +version = "0.2.1" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" + +[[package]] +name = "nix" +version = "0.12.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "bitflags 1.0.4 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "cc 1.0.26 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "cfg-if 0.1.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "libc 0.2.45 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "void 1.0.2 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "nodrop" +version = "0.1.13" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" + +[[package]] +name = "num-derive" +version = "0.2.3" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "num-traits 0.2.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "proc-macro2 0.4.24 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "quote 0.6.10 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "syn 0.15.22 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "num-integer" +version = "0.1.39" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "num-traits 0.2.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "num-iter" +version = "0.1.37" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "num-integer 0.1.39 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "num-traits 0.2.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "num-rational" +version = "0.2.1" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "num-integer 0.1.39 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "num-traits 0.2.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "num-traits" +version = "0.2.6" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" + +[[package]] +name = "num-traits" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "num_cpus" +version = "1.9.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "libc 0.2.45 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "objc" +version = "0.2.5" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "malloc_buf 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "block 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "objc 0.2.5 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "objc_id 0.1.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "objc 0.2.5 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "ordered-float" +version = "1.0.1" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "num-traits 0.2.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "osmesa-sys" +version = "0.1.2" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "shared_library 0.1.9 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "percent-encoding" +version = "1.0.1" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" + +[[package]] +name = "phf" +version = "0.7.23" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "phf_shared 0.7.23 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "phf_shared" +version = "0.7.23" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "siphasher 0.2.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "pkg-config" +version = "0.3.14" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" + +[[package]] +name = "plane-split" +version = "0.13.3" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "binary-space-partition 0.1.2 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "euclid 0.19.4 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "log 0.4.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "num-traits 0.2.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "png" +version = "0.12.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "bitflags 1.0.4 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "deflate 0.7.19 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "inflate 0.4.4 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "num-iter 0.1.37 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "proc-macro2" +version = "0.4.24" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "unicode-xid 0.1.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "proc-macro2" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "quote" +version = "0.6.10" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "proc-macro2 0.4.24 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "quote" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rayon" +version = "1.0.3" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "crossbeam-deque 0.2.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "either 1.5.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "rayon-core 1.4.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "rayon-core" +version = "1.4.1" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "crossbeam-deque 0.2.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "lazy_static 1.2.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "libc 0.2.45 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "num_cpus 1.9.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "rctree" +version = "0.2.1" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" + +[[package]] +name = "roxmltree" +version = "0.1.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "xmlparser 0.6.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.9" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" + +[[package]] +name = "rusttype" +version = "0.7.3" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "approx 0.3.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "arrayvec 0.4.8 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "ordered-float 1.0.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "stb_truetype 0.2.2 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "safemem" +version = "0.3.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" + +[[package]] +name = "same-file" +version = "1.0.4" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "winapi-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "scoped_threadpool" +version = "0.1.9" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" + +[[package]] +name = "scopeguard" +version = "0.3.3" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" + +[[package]] +name = "serde" +version = "1.0.80" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "serde_derive 1.0.80 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "serde" +version = "1.0.80" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde_bytes" +version = "0.10.4" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "serde 1.0.80 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "serde_derive" +version = "1.0.80" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "proc-macro2 0.4.24 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "quote 0.6.10 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "syn 0.15.22 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "serde_derive" +version = "1.0.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "servo-fontconfig" +version = "0.4.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "libc 0.2.45 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "servo-fontconfig-sys 4.0.7 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "servo-fontconfig-sys" +version = "4.0.7" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "expat-sys 2.1.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "pkg-config 0.3.14 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "servo-freetype-sys 4.0.5 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "servo-freetype-sys" +version = "4.0.5" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "cmake 0.1.35 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "pkg-config 0.3.14 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "shared_library" +version = "0.1.9" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "lazy_static 1.2.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "libc 0.2.45 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "sid" +version = "0.5.2" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "num-traits 0.2.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "simplecss" +version = "0.1.0" +source = "git+https://github.com/fschutt/simplecss.git?rev=0bf3b7800f49a4524b889687d7761b163e8b4101#0bf3b7800f49a4524b889687d7761b163e8b4101" + +[[package]] +name = "simplecss" +version = "0.1.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" + +[[package]] +name = "siphasher" +version = "0.2.3" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" + +[[package]] +name = "slab" +version = "0.4.1" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" + +[[package]] +name = "smallvec" +version = "0.6.7" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "unreachable 1.0.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "smithay-client-toolkit" +version = "0.6.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "andrew 0.2.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "bitflags 1.0.4 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "dlib 0.4.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "lazy_static 1.2.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "memmap 0.7.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "nix 0.12.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "wayland-client 0.23.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "wayland-protocols 0.23.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "stb_truetype" +version = "0.2.2" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "byteorder 1.2.7 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "svgdom" +version = "0.14.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "log 0.4.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "roxmltree 0.1.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "simplecss 0.1.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "slab 0.4.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "svgtypes 0.2.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "svgtypes" +version = "0.2.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "float-cmp 0.4.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "log 0.4.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "phf 0.7.23 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "xmlparser 0.6.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "syn" +version = "0.15.22" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "proc-macro2 0.4.24 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "quote 0.6.10 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "unicode-xid 0.1.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "syn" +version = "0.15.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "thread_profiler" +version = "0.1.3" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" + +[[package]] +name = "tiff" +version = "0.2.1" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "byteorder 1.2.7 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "lzw 0.10.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "num-derive 0.2.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "num-traits 0.2.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "time" +version = "0.1.40" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "libc 0.2.45 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "winapi 0.3.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "tinyfiledialogs" +version = "3.3.5" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "cc 1.0.26 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "libc 0.2.45 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.7" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" + +[[package]] +name = "unicode-segmentation" +version = "1.2.1" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" + +[[package]] +name = "unicode-xid" +version = "0.1.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" + +[[package]] +name = "unicode-xid" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unreachable" +version = "1.0.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "void 1.0.2 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "usvg" +version = "0.3.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "base64 0.9.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "libflate 0.1.19 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "log 0.4.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "lyon_geom 0.12.2 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "rctree 0.2.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "svgdom 0.14.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "unicode-segmentation 1.2.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "void" +version = "1.0.2" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" + +[[package]] +name = "walkdir" +version = "2.2.7" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "same-file 1.0.4 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "winapi 0.3.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "winapi-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "wayland-client" +version = "0.23.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "bitflags 1.0.4 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "downcast-rs 1.0.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "libc 0.2.45 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "nix 0.12.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "wayland-commons 0.23.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "wayland-scanner 0.23.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "wayland-sys 0.23.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "wayland-commons" +version = "0.23.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "nix 0.12.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "wayland-sys 0.23.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "wayland-protocols" +version = "0.23.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "bitflags 1.0.4 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "wayland-client 0.23.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "wayland-commons 0.23.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "wayland-scanner 0.23.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "wayland-scanner" +version = "0.23.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "proc-macro2 0.4.24 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "quote 0.6.10 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "xml-rs 0.8.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "wayland-sys" +version = "0.23.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "dlib 0.4.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "lazy_static 1.2.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "webrender" +version = "0.57.2" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "app_units 0.7.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "bincode 1.0.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "bitflags 1.0.4 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "byteorder 1.2.7 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "cfg-if 0.1.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "core-foundation 0.6.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "core-graphics 0.17.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "core-text 13.1.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "dwrote 0.6.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "freetype 0.4.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "fxhash 0.2.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "gleam 0.6.8 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "lazy_static 1.2.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "log 0.4.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "num-traits 0.2.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "plane-split 0.13.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "rayon 1.0.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "smallvec 0.6.7 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "thread_profiler 0.1.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "time 0.1.40 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "webrender_api 0.57.2 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "webrender_api" +version = "0.57.2" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "app_units 0.7.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "bincode 1.0.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "bitflags 1.0.4 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "byteorder 1.2.7 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "core-foundation 0.6.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "core-graphics 0.17.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "dwrote 0.6.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "euclid 0.19.4 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "serde 1.0.80 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "serde_bytes 0.10.4 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "serde_derive 1.0.80 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "time 0.1.40 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "winapi" +version = "0.3.6" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "winapi-i686-pc-windows-gnu 0.4.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "winapi-x86_64-pc-windows-gnu 0.4.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "winapi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi-util" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winit" +version = "0.18.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "android_glue 0.2.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "cocoa 0.18.4 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "core-foundation 0.6.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "core-graphics 0.17.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "image 0.20.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "lazy_static 1.2.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "libc 0.2.45 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "log 0.4.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "objc 0.2.5 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "percent-encoding 1.0.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "smithay-client-toolkit 0.6.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "wayland-client 0.23.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "winapi 0.3.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "x11-dl 2.18.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "x11-clipboard" +version = "0.3.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "xcb 0.8.2 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "x11-dl" +version = "2.18.3" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "lazy_static 1.2.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "libc 0.2.45 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "pkg-config 0.3.14 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "xcb" +version = "0.8.2" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" +dependencies = [ + "libc 0.2.45 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", + "log 0.4.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)", +] + +[[package]] +name = "xdg" +version = "2.2.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" + +[[package]] +name = "xml-rs" +version = "0.8.0" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" + +[[package]] +name = "xml-rs" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "xmlparser" +version = "0.6.1" +source = "git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7#380b7e7cba8b728a3fc89fe28952e3b07aa624e7" + +[metadata] +"checksum adler32 1.0.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum andrew 0.2.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum android_glue 0.2.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum app_units 0.7.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum approx 0.3.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum arrayvec 0.4.8 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum azul-dependencies 0.1.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum backtrace 0.3.12 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum backtrace-sys 0.1.24 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum base64 0.9.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum binary-space-partition 0.1.2 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum bincode 1.0.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum bitflags 1.0.4 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12" +"checksum block 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" +"checksum byteorder 1.2.7 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum cc 1.0.26 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum cfg-if 0.1.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "082bb9b28e00d3c9d39cc03e64ce4cea0f1bb9b3fde493f0cbc008472d22bdf4" +"checksum cgl 0.2.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum clipboard-win 2.1.2 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum clipboard2 0.1.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum cmake 0.1.35 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum cocoa 0.18.4 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum color_quant 1.0.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum core-foundation 0.6.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum core-foundation-sys 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b" +"checksum core-graphics 0.17.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum core-text 13.1.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum crc32fast 1.1.2 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum crossbeam-deque 0.2.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum crossbeam-epoch 0.3.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum crossbeam-utils 0.2.2 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum deflate 0.7.19 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum dlib 0.4.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum downcast-rs 1.0.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum dwrote 0.6.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum either 1.5.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum euclid 0.19.4 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum expat-sys 2.1.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum fern 0.5.7 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum float-cmp 0.4.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum fnv 1.0.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum font-loader 0.8.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +"checksum foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +"checksum freetype 0.4.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum fxhash 0.2.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum gif 0.10.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum gl_generator 0.10.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum gl_generator 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a0ffaf173cf76c73a73e080366bf556b4776ece104b06961766ff11449f38604" +"checksum gleam 0.6.8 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum gleam 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)" = "4b47f5b15742aee359c7895ab98cf2cceecc89bb4feb6f4e42f802d7899877da" +"checksum glium 0.22.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum glutin 0.19.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum harfbuzz-sys 0.3.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum image 0.20.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum inflate 0.4.4 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum jpeg-decoder 0.1.15 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum khronos_api 3.0.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum khronos_api 3.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "62237e6d326bd5871cd21469323bf096de81f1618cd82cbaf5d87825335aeb49" +"checksum lazy_static 1.2.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum libc 0.2.45 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum libc 0.2.45 (registry+https://github.com/rust-lang/crates.io-index)" = "2d2857ec59fadc0773853c664d2d18e7198e83883e7060b63c924cb077bd5c74" +"checksum libflate 0.1.19 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum libloading 0.5.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum line_drawing 0.7.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum log 0.4.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c84ec4b527950aa83a329754b01dbe3f58361d1c5efacd1f6d68c494d08a17c6" +"checksum lyon 0.11.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum lyon_algorithms 0.11.2 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum lyon_geom 0.11.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum lyon_geom 0.12.2 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum lyon_path 0.11.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum lyon_tessellation 0.11.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum lzw 0.10.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum malloc_buf 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +"checksum memmap 0.7.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum memoffset 0.2.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum nix 0.12.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum nodrop 0.1.13 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum num-derive 0.2.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum num-integer 0.1.39 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum num-iter 0.1.37 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum num-rational 0.2.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum num-traits 0.2.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0b3a5d7cc97d6d30d8b9bc8fa19bf45349ffe46241e8816f50f62f6d6aaabee1" +"checksum num_cpus 1.9.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum objc 0.2.5 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum objc-foundation 0.1.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum objc_id 0.1.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum ordered-float 1.0.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum osmesa-sys 0.1.2 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum percent-encoding 1.0.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum phf 0.7.23 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum phf_shared 0.7.23 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum pkg-config 0.3.14 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum plane-split 0.13.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum png 0.12.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum proc-macro2 0.4.24 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)" = "4d317f9caece796be1980837fd5cb3dfec5613ebdb04ad0956deea83ce168915" +"checksum quote 0.6.10 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)" = "cdd8e04bd9c52e0342b406469d494fcb033be4bdbe5c606016defbb1681411e1" +"checksum rayon 1.0.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum rayon-core 1.4.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum rctree 0.2.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum roxmltree 0.1.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum rustc-demangle 0.1.9 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum rusttype 0.7.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum safemem 0.3.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum same-file 1.0.4 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum scoped_threadpool 0.1.9 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum scopeguard 0.3.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum serde 1.0.80 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum serde 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)" = "15c141fc7027dd265a47c090bf864cf62b42c4d228bbcf4e51a0c9e2b0d3f7ef" +"checksum serde_bytes 0.10.4 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum serde_derive 1.0.80 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum serde_derive 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)" = "633e97856567e518b59ffb2ad7c7a4fd4c5d91d9c7f32dd38a27b2bf7e8114ea" +"checksum servo-fontconfig 0.4.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum servo-fontconfig-sys 4.0.7 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum servo-freetype-sys 4.0.5 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum shared_library 0.1.9 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum sid 0.5.2 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum simplecss 0.1.0 (git+https://github.com/fschutt/simplecss.git?rev=0bf3b7800f49a4524b889687d7761b163e8b4101)" = "" +"checksum simplecss 0.1.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum siphasher 0.2.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum slab 0.4.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum smallvec 0.6.7 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum smithay-client-toolkit 0.6.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum stb_truetype 0.2.2 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum svgdom 0.14.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum svgtypes 0.2.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum syn 0.15.22 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)" = "f92e629aa1d9c827b2bb8297046c1ccffc57c99b947a680d3ccff1f136a3bee9" +"checksum thread_profiler 0.1.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum tiff 0.2.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum time 0.1.40 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum tinyfiledialogs 3.3.5 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum unicode-normalization 0.1.7 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum unicode-segmentation 1.2.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum unicode-xid 0.1.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" +"checksum unreachable 1.0.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum usvg 0.3.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum void 1.0.2 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum walkdir 2.2.7 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum wayland-client 0.23.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum wayland-commons 0.23.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum wayland-protocols 0.23.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum wayland-scanner 0.23.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum wayland-sys 0.23.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum webrender 0.57.2 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum webrender_api 0.57.2 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum winapi 0.3.6 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "92c1eb33641e276cfa214a0522acad57be5c56b10cb348b3c5117db75f3ac4b0" +"checksum winapi-i686-pc-windows-gnu 0.4.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +"checksum winapi-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "afc5508759c5bf4285e61feb862b6083c8480aec864fa17a81fdec6f69b461ab" +"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +"checksum winit 0.18.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum x11-clipboard 0.3.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum x11-dl 2.18.3 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum xcb 0.8.2 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum xdg 2.2.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum xml-rs 0.8.0 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" +"checksum xml-rs 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "541b12c998c5b56aa2b4e6f18f03664eef9a4fd0a246a55594efae6cc2d964b5" +"checksum xmlparser 0.6.1 (git+https://github.com/maps4print/azul-dependencies?rev=380b7e7cba8b728a3fc89fe28952e3b07aa624e7)" = "" diff --git a/Cargo.toml b/Cargo.toml index a096aa2b2..26a3b15f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,7 @@ -[package] -name = "azul" -version = "0.1.0" -authors = ["Felix Schütt "] - -[dependencies] -webrender = { git = "https://github.com/servo/webrender", rev = "2083e83d958dd4a230ccae5c518e4bc8fbf88009" } -cassowary = "0.3.0" -simplecss = "0.1.0" -twox-hash = "1.1.0" -glium = "0.20.0" -gleam = "0.4.20" -euclid = "0.17" +[workspace] +members = [ + "azul", + "azul-css", + "azul-css-parser", + "azul-native-style", +] diff --git a/LICENSE b/LICENSE index 42f24e5e6..04b25241e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,20 @@ Copyright 2017 Maps4Print Einzelunternehmung -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index dc5a44ba0..4306b0adb 100644 --- a/README.md +++ b/README.md @@ -1,62 +1,253 @@ -# azul - -[![LICENSE](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) -[![Build Status](https://travis-ci.org/maps4print/azul.svg?branch=master)](https://travis-ci.org/maps4print/azul) -[![Coverage Status](https://coveralls.io/repos/github/maps4print/azul/badge.svg?branch=master)](https://coveralls.io/github/maps4print/azul?branch=master) -[![Rust Compiler Version](https://img.shields.io/badge/rustc-1.23%20stable-blue.svg)]() - -azul is a stylable GUI framework using `webrender` and `limn-layout` for rendering - -## Design - -azul is a library, that, in difference to pretty much all other GUI libraries -uses a functional, data-driven design. `azul` requires your application data to -serialize itself into a user interface. Due to CSS stylesheets, your application can -be styled however you want. - -That said, `azul` is probably not the most efficient UI library. - -![azul design diagram](https://i.imgur.com/M5NGnBk.png) - -## Goals - -This library is not done yet. Once it is done, it should support the following: - -- Basic elements - - Label - - List Box - - Checkbox - - Radio - - Three-state checkbox - - Dropdown - - Button - - Menu - - Either / Or checkbox - - GlImage - - Ordered list (1. 2. 3.) - - Unordered list - -- OpenGL helpers - - Rectangle - - Rectangle with borders - - Circle - - Dashed / dotted circles - -- Layout (parent) - - direction (horizontal, vertical, horizontal-reverse, vertical-reverse) - - wrap (nowrap, wrap, wrap-reverse) - - justify-content: start, end, center, space-between, space-around, space-evenly - - align-items: start, end, center, stretch - - align-content: start, end, center, stretch, space-between, space-around - -- Layout (child) - - order: `number` - -- Media rules - - query window width & height - -## Use-cases - -The goal is to be used in desktop applications that require special rendering -(ex. image / vector editors) as well as games. Currently the backend is tied to -OpenGL. + +# Azul - Desktop GUI framework + +## WARNING: The features advertised in this README may not work yet. + + +[![Build Status Linux / macOS](https://travis-ci.org/maps4print/azul.svg?branch=master)](https://travis-ci.org/maps4print/azul) +[![Build status Windows](https://ci.appveyor.com/api/projects/status/p487hewqh6bxeucv?svg=true)](https://ci.appveyor.com/project/fschutt/azul) +[![Coverage Status](https://coveralls.io/repos/github/maps4print/azul/badge.svg?branch=master)](https://coveralls.io/github/maps4print/azul?branch=master) +[![LICENSE](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Rust Compiler Version](https://img.shields.io/badge/rustc-1.31%20stable-blue.svg)]() + + +> Azul is a free, functional, immediate mode GUI framework that is built on the Mozilla WebRender rendering engine for rapid development +of desktop applications that are written in Rust and use a CSS / DOM model for layout and styling. + +###### [Website](https://azul.rs/) | [Tutorial / user guide](https://github.com/maps4print/azul/wiki) | [Video demo](https://www.youtube.com/watch?v=kWL0ehf4wwI) | [Discord Chat](https://discord.gg/nxUmsCG) + +## About + +Azul is a library for creating graphical user interfaces or GUIs in Rust. It mixes +paradigms from functional, immediate mode GUI programming commonly found in games +and game engines with an API suitable for developing desktop applications. +Instead of focusing on an object-oriented approach to GUI programming ("a button +is an object"), it focuses on combining objects by composition ("a button is a function") +and achieves complex layouts by composing widgets into a larger DOM tree. + +Azul separates the concerns of business logic / callbacks, data model and UI +rendering / styling by not letting the UI / rendering logic have mutable access +to the application data. Widgets of your user interface are seen as a "view" into +your applications data, they are not "objects that manage their own state", like +in so many other toolkits. Widgets are simply functions that render a certain state, +more complex widgets combine buttons by calling a function multiple times. + +The generated DOM itself is immutable and gets re-generated every frame. This makes testing +and debugging very easy, since the UI is a pure function, mapping from a specific application +state into a visual interface. For layouting, Azul features a custom CSS-like layout engine, +which closely follows the CSS flexbox model. + +## Hello World + +Here is what a Hello World application in Azul looks like: + +![Hello World Application](https://raw.githubusercontent.com/maps4print/azul/master/doc/azul_hello_world_button.png) + +This application is created by the following code: + +```rust +extern crate azul; + +use azul::{ + prelude::*, + widgets::{button::Button, label::Label}, +}; + +struct DataModel { + counter: usize, +} + +impl Layout for DataModel { + // Model renders View + fn layout(&self, _: LayoutInfo) -> Dom { + let label = Label::new(format!("{}", self.counter)).dom(); + let button = Button::with_label("Update counter") + .dom() + .with_callback(On::MouseUp, Callback(update_counter)); + + Dom::new(NodeType::Div).with_child(label).with_child(button) + } +} + +// View updates Model +fn update_counter( + app_state: &mut AppState, + _event: &mut CallbackInfo, +) -> UpdateScreen { + app_state.data.modify(|state| state.counter += 1); + Redraw +} + +fn main() { + let mut app = App::new(DataModel { counter: 0 }, AppConfig::default()).unwrap(); + let window = app + .create_window(WindowCreateOptions::default(), css::native()) + .unwrap(); + app.run(window).unwrap(); +} +``` + +[Read more about the Hello-World application ...](https://github.com/maps4print/azul/wiki/A-simple-counter) + +## Programming model + +In order to comply with Rust's mutability rules, the application lifecycle in Azul +consists of three states that are called over and over again. The framework determines +exactly when a repaint is necessary, you don't need to worry about manually repainting +your UI: + +![Azul callback model](https://raw.githubusercontent.com/maps4print/azul/master/doc/azul_event_model.png) + +Azul works through composition instead of inheritance - widgets are composed of other +widgets, instead of inheriting from them (since Rust does not support inheritance). +The main `layout()` function of a production-ready application could look something +like this: + +```rust +impl Layout for DataModel { + fn layout(&self, _info: LayoutInfo) -> Dom { + match self.state { + LoginScreen => { + Dom::new(NodeType::Div).with_id("login_screen") + .with_child(render_hello_mgs()) + .with_child(render_login_with_button()) + .with_child(render_password()) + .with_child(render_username_field()) + }, + EmailList(emails) => { + Dom::new(NodeType::Div).with_id("email_list_container") + .with_child(render_task_bar()) + .with_child(emails.iter().map(render_email).collect()) + .with_child(render_status_bar()) + } + } + } +} +``` + +One defining feature is that Azul automatically determines when a UI repaint is +necessary and therefore you don't need to worry about manually redrawing your UI. + +[Read more about the programming model ...](https://github.com/maps4print/azul/wiki/Getting-Started) + +## Features + +### Easy two-way data binding + +When programming reusable and common UI elements, such as lists, tables or sliders +you don't want the user having to write code to update the UI state of these widgets. +Previously, this could only be solved by inheritance, but due to Azul's unique +architecture, it is possible to create widgets that update themselves purely by +composition, for example: + +```rust +struct DataModel { + text_input: TextInputState, +} + +impl Layout for DataModel { + fn layout(&self, info: LayoutInfo) -> Dom { + // Create a new text input field + TextInput::new() + // ... bind it to self.text_input - will automatically update + .bind(info.window, &self.text_input, &self) + // ... and render it in the UI + .dom(&self.text_input) + .with_callback(On::KeyUp, Callback(print_text_field)) + } +} + +fn print_text_field(app_state: &mut AppState, _event: &mut CallbackInfo) -> UpdateScreen { + println!("You've typed: {}", app_state.data.lock().unwrap().text_input.text); + DontRedraw +} +``` + +[Read more about two-way data binding ...](https://github.com/maps4print/azul/wiki/Two-way-data-binding) + +### CSS styling & layout engine + +Azul features a CSS-like layout and styling engine that is modeled after the +flexbox model - i.e. by default, every element will try to stretch to the dimensions +of its parent. The layout itself is handled by a simple and fast flexbox layout solver. + +[Read more about CSS styling ...](https://github.com/maps4print/azul/wiki/Styling-your-application-with-CSS) + +### Asynchronous UI programming + +Azul features multiple ways of preventing your UI from being blocked, such as +"Tasks" (threads that are managed by the Azul runtime) and "Daemons" +(callback functions that can be optionally used as timers or timeouts). + +[Read more about async IO ...](https://github.com/maps4print/azul/wiki/Timers,-daemons,-tasks-and-async-IO) + +### SVG / GPU-accelerated 2D Vector drawing + +For drawing non-rectangular shapes, such as triangles, circles, polygons or SVG files, +Azul provides a GPU-accelerated 2D renderer, featuring lines drawing (incl. bezier curves), +rects, circles, arbitrary polygons, text (incl. translation / rotation and text-on-curve +positioning), hit-testing texts, caching and an (optional) SVG parsing module. + +![Azul SVG Tiger drawing](https://raw.githubusercontent.com/maps4print/azul/master/doc/azul_svg_tiger.png) + +[Read more about SVG drawing ...](https://github.com/maps4print/azul/wiki/SVG-drawing) + +### OpenGL API + +While Azul can't help you (yet) with 3D content, it does provide easy ways to hook +into the OpenGL context of the running application - you can draw everything you +want to an OpenGL texture, which will then be composited into the frame using +WebRender. + +[Read more about OpenGL drawing ...](https://github.com/maps4print/azul/wiki/OpenGL-drawing) + +### UI Testing + +Due to the separation of the UI, the data model and the callbacks, Azul applications +are very easy to test: + +```rust +#[test] +fn test_it_should_increase_the_counter() { + let mut initial_state = AppState::new(DataModel { counter: 0 }); + let expected_state = AppState::new(DataModel { counter: 1 }); + update_counter(&mut initial_state, &mut CallbackInfo::mock()); + assert_eq!(initial_state, expected_state); +} +``` + +[Read more about testing ...](https://github.com/maps4print/azul/wiki/Unit-testing) + +## Performance + +A default window, with no fonts or images added takes up roughly 23MB of RAM and +5MB in binary size. This usage can go up once you load more images and fonts, since +Azul has to load and keep the images in RAM. + +The frame time (i.e. the time necessary to draw a single frame, including layout) +lies between 2 - 5 milliseconds, which equals roughly 200 - 500 frames per second. +However, Azul limits this frame time and **only redraws the window when absolutely +necessary**, in order to not waste the users battery life. + +The startup time depends on how many fonts / images you add on startup, the +default time is between 100 and 200 ms for an app with no images and a single font. + +While Azul can run in software rendering mode (automatically switching to the +built-in OSMesa), it isn't intended to run on microcontrollers or devices with +extremely low memory requirements. + +## Thanks + +Several projects have helped severely during the development and should be credited: + +- Chris Tollidays [limn](https://github.com/christolliday/limn) framework has helped + a lot with discovering undocumented parts of WebRender. +- Nicolas Silva for his work on [lyon](https://github.com/nical/lyon) - without this, + the SVG renderer wouldn't have been possible + +## License + +This library is MIT-licensed. It was developed by [Maps4Print](http://maps4print.com/), +for quickly prototyping and producing desktop GUI cross-platform applications, +such as vector or photo editors. + +For licensing questions, please contact opensource@maps4print.com diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 000000000..507c70142 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,48 @@ +environment: + global: + PROJECT_NAME: azul + matrix: + # Disable windows-gnu because appveyor has problems installing CMAKE correctly + # (CMAKE_C_COMPILER not set) + # - TARGET: i686-pc-windows-gnu + # CHANNEL: stable + - TARGET: i686-pc-windows-msvc + CHANNEL: stable + # This target is commented out because for some reason appveyor only ships + # GCC in 32-bit mode, so when compiling miniz.c (require for zipping), it'll + # fail although the build itself will work fine (because nobody uses a 32-bit + # compiler anymore in the real world). Just use a 64-bit compiler and + # everything will work fine. + # + # - TARGET: x86_64-pc-windows-gnu + # CHANNEL: stable + - TARGET: x86_64-pc-windows-msvc + CHANNEL: stable + +branches: + only: + - master + +# Install Rust and Cargo +# Based on https://github.com/rust-lang/libc/blob/master/appveyor.yml +install: + - appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe + - rustup-init -yv --default-toolchain %channel% --default-host %target% + - set PATH=%PATH%;%USERPROFILE%\.cargo\bin;C:\tools\mingw64\bin;C:\MinGW\bin; + - set PATH=%PATH:C:\Program Files\Git\usr\bin;=% # ignore other MinGW shells, use C:\MinGW + - gcc -v + - rustc -vV + - cargo -vV + +# 'cargo test' takes care of building for us, so disable Appveyor's build stage. +# This prevents the "directory does not contain a project or solution file" error. +# source: https://github.com/starkat99/appveyor-rust/blob/master/appveyor.yml#L113 +build: false + +# Equivalent to Travis' `script` phase +test_script: + - cargo check --verbose --examples + - cargo check --no-default-features + - cargo check --no-default-features --features="svg" + - cargo check --no-default-features --features="logging" + - cargo test --verbose diff --git a/assets/fonts/KoHo-Light.ttf b/assets/fonts/KoHo-Light.ttf new file mode 100644 index 000000000..b5bb199a2 Binary files /dev/null and b/assets/fonts/KoHo-Light.ttf differ diff --git a/assets/fonts/OFL.txt b/assets/fonts/OFL.txt new file mode 100644 index 000000000..8c51e1686 --- /dev/null +++ b/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2018 The KoHo Project Authors (https://github.com/cadsondemak/Koho) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/fonts/weblysleekuil.ttf b/assets/fonts/weblysleekuil.ttf new file mode 100644 index 000000000..a4666b534 Binary files /dev/null and b/assets/fonts/weblysleekuil.ttf differ diff --git a/assets/images/azul_logo_full_min.svg.png b/assets/images/azul_logo_full_min.svg.png new file mode 100644 index 000000000..b9762e8de Binary files /dev/null and b/assets/images/azul_logo_full_min.svg.png differ diff --git a/assets/images/cat_image.jpg b/assets/images/cat_image.jpg new file mode 100644 index 000000000..e32f90627 Binary files /dev/null and b/assets/images/cat_image.jpg differ diff --git a/assets/images/favicon.ico b/assets/images/favicon.ico new file mode 100644 index 000000000..3354f2f3e Binary files /dev/null and b/assets/images/favicon.ico differ diff --git a/assets/native_linux.css b/assets/native_linux.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/assets/native_macos.css b/assets/native_macos.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/assets/native_windows.css b/assets/native_windows.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/assets/svg/test.svg b/assets/svg/test.svg new file mode 100644 index 000000000..c355c65ae --- /dev/null +++ b/assets/svg/test.svg @@ -0,0 +1,106 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + Hello + + diff --git a/assets/svg/tiger.svg b/assets/svg/tiger.svg new file mode 100644 index 000000000..679edec2e --- /dev/null +++ b/assets/svg/tiger.svg @@ -0,0 +1,725 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/azul-css-parser/Cargo.toml b/azul-css-parser/Cargo.toml new file mode 100644 index 000000000..f603b7242 --- /dev/null +++ b/azul-css-parser/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "azul-css-parser" +version = "0.1.0" +authors = ["Felix Schütt "] +license = "MIT" +description = ''' + CSS-compatible parser for the Azul GUI framework +''' +documentation = "https://docs.rs/azul" +homepage = "https://azul.rs/" +keywords = ["gui", "GUI", "user interface", "svg", "graphics", "css" ] +categories = ["gui"] +repository = "https://github.com/maps4print/azul" +readme = "README.md" +exclude = ["assets/*", "doc/*", "examples/*"] +autoexamples = false + +[dependencies] +azul-css = { path = "../azul-css", default-features = false } +simplecss = { git = "https://github.com/fschutt/simplecss.git", rev = "0bf3b7800f49a4524b889687d7761b163e8b4101" } +serde = { version = "1", default-features = false, optional = true } + +[features] +default = [] +serde_serialization = ["serde"] \ No newline at end of file diff --git a/azul-css-parser/src/css.rs b/azul-css-parser/src/css.rs new file mode 100644 index 000000000..cdbf1645c --- /dev/null +++ b/azul-css-parser/src/css.rs @@ -0,0 +1,851 @@ +//! High-level types and functions related to CSS parsing +use std::{ + num::ParseIntError, + fmt, +}; +pub use simplecss::Error as CssSyntaxError; +use simplecss::Tokenizer; + +use crate::css_parser; +pub use crate::css_parser::CssParsingError; +use azul_css::{ + Css, CssDeclaration, Stylesheet, + DynamicCssProperty, DynamicCssPropertyDefault, + CssPropertyType, CssRuleBlock, CssPath, CssPathSelector, + CssNthChildSelector, CssPathPseudoSelector, CssNthChildSelector::*, + NodeTypePath, NodeTypePathParseError, +}; + +/// Error that can happen during the parsing of a CSS value +#[derive(Debug, Clone, PartialEq)] +pub struct CssParseError<'a> { + pub error: CssParseErrorInner<'a>, + pub location: ErrorLocation, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum CssParseErrorInner<'a> { + /// A hard error in the CSS syntax + ParseError(CssSyntaxError), + /// Braces are not balanced properly + UnclosedBlock, + /// Invalid syntax, such as `#div { #div: "my-value" }` + MalformedCss, + /// Error parsing dynamic CSS property, such as + /// `#div { width: {{ my_id }} /* no default case */ }` + DynamicCssParseError(DynamicCssParseError<'a>), + /// Error while parsing a pseudo selector (like `:aldkfja`) + PseudoSelectorParseError(CssPseudoSelectorParseError<'a>), + /// The path has to be either `*`, `div`, `p` or something like that + NodeTypePath(NodeTypePathParseError<'a>), + /// A certain property has an unknown key, for example: `alsdfkj: 500px` = `unknown CSS key "alsdfkj: 500px"` + UnknownPropertyKey(&'a str, &'a str), +} + +impl_display!{ CssParseErrorInner<'a>, { + ParseError(e) => format!("Parse Error: {:?}", e), + UnclosedBlock => "Unclosed block", + MalformedCss => "Malformed Css", + DynamicCssParseError(e) => format!("Error parsing dynamic CSS property: {}", e), + PseudoSelectorParseError(e) => format!("Failed to parse pseudo-selector: {}", e), + NodeTypePath(e) => format!("Failed to parse CSS selector path: {}", e), + UnknownPropertyKey(k, v) => format!("Unknown CSS key: \"{}: {}\"", k, v), +}} + +impl<'a> From for CssParseErrorInner<'a> { + fn from(e: CssSyntaxError) -> Self { + CssParseErrorInner::ParseError(e) + } +} + +impl_from! { DynamicCssParseError<'a>, CssParseErrorInner::DynamicCssParseError } +impl_from! { NodeTypePathParseError<'a>, CssParseErrorInner::NodeTypePath } +impl_from! { CssPseudoSelectorParseError<'a>, CssParseErrorInner::PseudoSelectorParseError } + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CssPseudoSelectorParseError<'a> { + EmptyNthChild, + UnknownSelector(&'a str, Option<&'a str>), + InvalidNthChildPattern(&'a str), + InvalidNthChild(ParseIntError), +} + +impl<'a> From for CssPseudoSelectorParseError<'a> { + fn from(e: ParseIntError) -> Self { CssPseudoSelectorParseError::InvalidNthChild(e) } +} + +impl_display! { CssPseudoSelectorParseError<'a>, { + EmptyNthChild => format!("\ + Empty :nth-child() selector - nth-child() must at least take a number, \ + a pattern (such as \"2n+3\") or the values \"even\" or \"odd\"." + ), + UnknownSelector(selector, value) => { + let format_str = match value { + Some(v) => format!("{}({})", selector, v), + None => format!("{}", selector), + }; + format!("Invalid or unknown CSS pseudo-selector: ':{}'", format_str) + }, + InvalidNthChildPattern(selector) => format!( + "Invalid pseudo-selector :{} - value has to be a \ + number, \"even\" or \"odd\" or a pattern such as \"2n+3\"", selector + ), + InvalidNthChild(e) => format!("Invalid :nth-child pseudo-selector: ':{}'", e), +}} + +/// "selector" contains the actual selector such as "nth-child" while "value" contains +/// an optional value - for example "nth-child(3)" would be: selector: "nth-child", value: "3". +fn pseudo_selector_from_str<'a>(selector: &'a str, value: Option<&'a str>) +-> Result> +{ + match selector { + "first" => Ok(CssPathPseudoSelector::First), + "last" => Ok(CssPathPseudoSelector::Last), + "hover" => Ok(CssPathPseudoSelector::Hover), + "active" => Ok(CssPathPseudoSelector::Active), + "focus" => Ok(CssPathPseudoSelector::Focus), + "nth-child" => { + let value = value.ok_or(CssPseudoSelectorParseError::EmptyNthChild)?; + let parsed = parse_nth_child_selector(value)?; + Ok(CssPathPseudoSelector::NthChild(parsed)) + }, + _ => { + Err(CssPseudoSelectorParseError::UnknownSelector(selector, value)) + }, + } +} + +/// Parses the inner value of the `:nth-child` selector, including numbers and patterns. +/// +/// I.e.: `"2n+3"` -> `Pattern { repeat: 2, offset: 3 }` +fn parse_nth_child_selector<'a>(value: &'a str) -> Result> { + + let value = value.trim(); + + if value.is_empty() { + return Err(CssPseudoSelectorParseError::EmptyNthChild); + } + + if let Ok(number) = value.parse::() { + return Ok(Number(number)); + } + + // If the value is not a number + match value.as_ref() { + "even" => Ok(Even), + "odd" => Ok(Odd), + other => parse_nth_child_pattern(value), + } +} + +/// Parses the pattern between the braces of a "nth-child" (such as "2n+3"). +fn parse_nth_child_pattern<'a>(value: &'a str) -> Result> { + + let value = value.trim(); + + if value.is_empty() { + return Err(CssPseudoSelectorParseError::EmptyNthChild); + } + + // TODO: Test for "+" + let repeat = value.split("n").next() + .ok_or(CssPseudoSelectorParseError::InvalidNthChildPattern(value))? + .trim() + .parse::()?; + + // In a "2n+3" form, the first .next() yields the "2n", the second .next() yields the "3" + let mut offset_iterator = value.split("+"); + + // has to succeed, since the string is verified to not be empty + offset_iterator.next().unwrap(); + + let offset = match offset_iterator.next() { + Some(offset_string) => { + let offset_string = offset_string.trim(); + if offset_string.is_empty() { + return Err(CssPseudoSelectorParseError::InvalidNthChildPattern(value)); + } else { + offset_string.parse::()? + } + }, + None => 0, + }; + + Ok(Pattern { repeat, offset }) +} + +#[test] +fn test_css_pseudo_selector_parse() { + + use self::CssPathPseudoSelector::*; + use self::CssPseudoSelectorParseError::*; + + let ok_res = [ + (("first", None), First), + (("last", None), Last), + (("hover", None), Hover), + (("active", None), Active), + (("focus", None), Focus), + (("nth-child", Some("4")), NthChild(Number(4))), + (("nth-child", Some("even")), NthChild(Even)), + (("nth-child", Some("odd")), NthChild(Odd)), + (("nth-child", Some("5n")), NthChild(Pattern { repeat: 5, offset: 0 })), + (("nth-child", Some("2n+3")), NthChild(Pattern { repeat: 2, offset: 3 })), + ]; + + let err = [ + (("asdf", None), UnknownSelector("asdf", None)), + (("", None), UnknownSelector("", None)), + (("nth-child", Some("2n+")), InvalidNthChildPattern("2n+")), + // Can't test for ParseIntError because the fields are private. + // This is an example on why you shouldn't use std::error::Error! + ]; + + for ((selector, val), a) in &ok_res { + assert_eq!(pseudo_selector_from_str(selector, *val), Ok(*a)); + } + + for ((selector, val), e) in &err { + assert_eq!(pseudo_selector_from_str(selector, *val), Err(e.clone())); + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct ErrorLocation { + pub line: usize, + pub column: usize, +} + +impl<'a> fmt::Display for CssParseError<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "CSS error at line {}:{}: {}", self.location.line, self.location.column, self.error) + } +} + +pub fn new_from_str<'a>(css_string: &'a str) -> Result> { + let mut tokenizer = Tokenizer::new(css_string); + match new_from_str_inner(css_string, &mut tokenizer) { + Ok(stylesheet) => Ok(Css { + stylesheets: vec![ + stylesheet + ], + }), + Err(e) => { + let error_location = tokenizer.pos().saturating_sub(1); + let line_number: usize = css_string[0..error_location].lines().count(); + + // Rust doesn't count "\n" as a character, so we have to add the line number count on top + let total_characters: usize = css_string[0..error_location].lines().take(line_number.saturating_sub(1)).map(|line| line.chars().count()).sum(); + let total_characters = total_characters + line_number; + /*println!("line_number: {} error location: {}, total characters: {}", line_number, + error_location, total_characters);*/ + let characters_in_line = (error_location + 2) - total_characters; + + let error_location = ErrorLocation { + line: line_number, + column: characters_in_line, + }; + + Err(CssParseError { + error: e, + location: error_location, + }) + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum CssPathParseError<'a> { + EmptyPath, + /// Invalid item encountered in string (for example a "{", "}") + InvalidTokenEncountered(&'a str), + UnexpectedEndOfStream(&'a str), + SyntaxError(CssSyntaxError), + /// The path has to be either `*`, `div`, `p` or something like that + NodeTypePath(NodeTypePathParseError<'a>), + /// Error while parsing a pseudo selector (like `:aldkfja`) + PseudoSelectorParseError(CssPseudoSelectorParseError<'a>), +} + +impl_from! { NodeTypePathParseError<'a>, CssPathParseError::NodeTypePath } +impl_from! { CssPseudoSelectorParseError<'a>, CssPathParseError::PseudoSelectorParseError } + +impl<'a> From for CssPathParseError<'a> { + fn from(e: CssSyntaxError) -> Self { + CssPathParseError::SyntaxError(e) + } +} + +/// Parses a CSS path from a string (only the path,.no commas allowed) +/// +/// ```rust +/// # extern crate azul_css; +/// # extern crate azul_css_parser; +/// # use azul_css_parser::parse_css_path; +/// # use azul_css::{ +/// # CssPathSelector::*, CssPathPseudoSelector::*, CssPath, +/// # NodeTypePath::*, CssNthChildSelector::* +/// # }; +/// +/// assert_eq!( +/// parse_css_path("* div #my_id > .class:nth-child(2)"), +/// Ok(CssPath { selectors: vec![ +/// Global, +/// Type(Div), +/// Children, +/// Id("my_id".to_string()), +/// DirectChildren, +/// Class("class".to_string()), +/// PseudoSelector(NthChild(Number(2))), +/// ]}) +/// ); +/// ``` +pub fn parse_css_path<'a>(input: &'a str) -> Result> { + use simplecss::{Token, Combinator}; + let input = input.trim(); + if input.is_empty() { + return Err(CssPathParseError::EmptyPath); + } + let mut tokenizer = Tokenizer::new(input); + let mut selectors = Vec::new(); + + loop { + let token = tokenizer.parse_next()?; + match token { + Token::UniversalSelector => { + selectors.push(CssPathSelector::Global); + }, + Token::TypeSelector(div_type) => { + selectors.push(CssPathSelector::Type(NodeTypePath::from_str(div_type)?)); + }, + Token::IdSelector(id) => { + selectors.push(CssPathSelector::Id(id.to_string())); + }, + Token::ClassSelector(class) => { + selectors.push(CssPathSelector::Class(class.to_string())); + }, + Token::Combinator(Combinator::GreaterThan) => { + selectors.push(CssPathSelector::DirectChildren); + }, + Token::Combinator(Combinator::Space) => { + selectors.push(CssPathSelector::Children); + }, + Token::PseudoClass { selector, value } => { + selectors.push(CssPathSelector::PseudoSelector(pseudo_selector_from_str(selector, value)?)); + }, + Token::EndOfStream => { + break; + } + _ => { + return Err(CssPathParseError::InvalidTokenEncountered(input)); + } + } + } + + if !selectors.is_empty() { + Ok(CssPath { selectors }) + } else { + Err(CssPathParseError::EmptyPath) + } +} + +/// Parses a CSS string (single-threaded) and returns the parsed rules in blocks +fn new_from_str_inner<'a>(css_string: &'a str, tokenizer: &mut Tokenizer<'a>) -> Result> { + use simplecss::{Token, Combinator}; + + let mut css_blocks = Vec::new(); + + // Used for error checking / checking for closed braces + let mut parser_in_block = false; + let mut block_nesting = 0_usize; + + // Current css paths (i.e. `div#id, .class, p` are stored here - + // when the block is finished, all `current_rules` gets duplicated with + // one path corresponding to one set of rules each). + let mut current_paths = Vec::new(); + // Current CSS declarations + let mut current_rules = Vec::new(); + // Keep track of the current path during parsing + let mut last_path = Vec::new(); + + let css_property_map = azul_css::get_css_key_map(); + loop { + let token = tokenizer.parse_next()?; + match token { + Token::BlockStart => { + if parser_in_block { + // multi-nested CSS blocks are currently not supported + return Err(CssParseErrorInner::MalformedCss); + } + parser_in_block = true; + block_nesting += 1; + current_paths.push(last_path.clone()); + last_path.clear(); + }, + Token::Comma => { + current_paths.push(last_path.clone()); + last_path.clear(); + }, + Token::BlockEnd => { + block_nesting -= 1; + if !parser_in_block { + return Err(CssParseErrorInner::MalformedCss); + } + parser_in_block = false; + for path in current_paths.drain(..) { + css_blocks.push(CssRuleBlock { + path: CssPath { selectors: path }, + declarations: current_rules.clone(), + }) + } + current_rules.clear(); + last_path.clear(); // technically unnecessary, but just to be sure + }, + + // tokens that adjust the last_path + Token::UniversalSelector => { + if parser_in_block { + return Err(CssParseErrorInner::MalformedCss); + } + last_path.push(CssPathSelector::Global); + }, + Token::TypeSelector(div_type) => { + if parser_in_block { + return Err(CssParseErrorInner::MalformedCss); + } + last_path.push(CssPathSelector::Type(NodeTypePath::from_str(div_type)?)); + }, + Token::IdSelector(id) => { + if parser_in_block { + return Err(CssParseErrorInner::MalformedCss); + } + last_path.push(CssPathSelector::Id(id.to_string())); + }, + Token::ClassSelector(class) => { + if parser_in_block { + return Err(CssParseErrorInner::MalformedCss); + } + last_path.push(CssPathSelector::Class(class.to_string())); + }, + Token::Combinator(Combinator::GreaterThan) => { + if parser_in_block { + return Err(CssParseErrorInner::MalformedCss); + } + last_path.push(CssPathSelector::DirectChildren); + }, + Token::Combinator(Combinator::Space) => { + if parser_in_block { + return Err(CssParseErrorInner::MalformedCss); + } + last_path.push(CssPathSelector::Children); + }, + Token::PseudoClass { selector, value } => { + if parser_in_block { + return Err(CssParseErrorInner::MalformedCss); + } + last_path.push(CssPathSelector::PseudoSelector(pseudo_selector_from_str(selector, value)?)); + }, + Token::Declaration(key, val) => { + if !parser_in_block { + return Err(CssParseErrorInner::MalformedCss); + } + + let parsed_key = CssPropertyType::from_str(key, &css_property_map) + .ok_or(CssParseErrorInner::UnknownPropertyKey(key, val))?; + + current_rules.push(determine_static_or_dynamic_css_property(parsed_key, val)?); + }, + Token::EndOfStream => { + break; + }, + _ => { + // attributes, lang-attributes and @keyframes are not supported + } + } + } + + // non-even number of blocks + if block_nesting != 0 { + return Err(CssParseErrorInner::UnclosedBlock); + } + + Ok(css_blocks.into()) +} + +/// Error that can happen during `css_parser::parse_key_value_pair` +#[derive(Debug, Clone, PartialEq)] +pub enum DynamicCssParseError<'a> { + /// The braces of a dynamic CSS property aren't closed or unbalanced, i.e. ` [[ ` + UnclosedBraces, + /// There is a valid dynamic css property, but no default case + NoDefaultCase, + /// The dynamic CSS property has no ID, i.e. `[[ 400px ]]` + NoId, + /// The ID may not start with a number or be a CSS property itself + InvalidId, + /// Dynamic css property braces are empty, i.e. `[[ ]]` + EmptyBraces, + /// Unexpected value when parsing the string + UnexpectedValue(CssParsingError<'a>), +} + +impl_display!{ DynamicCssParseError<'a>, { + UnclosedBraces => "The braces of a dynamic CSS property aren't closed or unbalanced, i.e. ` [[ `", + NoDefaultCase => "There is a valid dynamic css property, but no default case", + NoId => "The dynamic CSS property has no ID, i.e. [[ 400px ]]", + InvalidId => "The ID may not start with a number or be a CSS property itself", + EmptyBraces => "Dynamic css property braces are empty, i.e. `[[ ]]`", + UnexpectedValue(e) => format!("Unexpected value: {}", e), +}} + +impl<'a> From> for DynamicCssParseError<'a> { + fn from(e: CssParsingError<'a>) -> Self { + DynamicCssParseError::UnexpectedValue(e) + } +} + +pub const START_BRACE: &str = "[["; +pub const END_BRACE: &str = "]]"; + +/// Determine if a Css property is static (immutable) or if it can change +/// during the runtime of the program +pub fn determine_static_or_dynamic_css_property<'a>(key: CssPropertyType, value: &'a str) +-> Result> +{ + let value = value.trim(); + + let is_starting_with_braces = value.starts_with(START_BRACE); + let is_ending_with_braces = value.ends_with(END_BRACE); + + match (is_starting_with_braces, is_ending_with_braces) { + (true, false) | (false, true) => { + Err(DynamicCssParseError::UnclosedBraces) + }, + (true, true) => { + parse_dynamic_css_property(key, value).and_then(|val| Ok(CssDeclaration::Dynamic(val))) + }, + (false, false) => { + Ok(CssDeclaration::Static(css_parser::parse_key_value_pair(key, value)?)) + } + } +} + +pub fn parse_dynamic_css_property<'a>(key: CssPropertyType, value: &'a str) -> Result> { + use std::char; + + // "[[ id | 400px ]]" => "id | 400px" + let value = value.trim_start_matches(START_BRACE); + let value = value.trim_end_matches(END_BRACE); + let value = value.trim(); + + let mut pipe_split = value.splitn(2, "|"); + let dynamic_id = pipe_split.next(); + let default_case = pipe_split.next(); + + // note: dynamic_id will always be Some(), which is why the + let (default_case, dynamic_id) = match (default_case, dynamic_id) { + (Some(default), Some(id)) => (default, id), + (None, Some(id)) => { + if id.trim().is_empty() { + return Err(DynamicCssParseError::EmptyBraces); + } else if css_parser::parse_key_value_pair(key, id).is_ok() { + // if there is an ID, but the ID is a CSS value + return Err(DynamicCssParseError::NoId); + } else { + return Err(DynamicCssParseError::NoDefaultCase); + } + }, + (None, None) | (Some(_), None) => unreachable!(), // iterator would be broken if this happened + }; + + let dynamic_id = dynamic_id.trim(); + let default_case = default_case.trim(); + + match (dynamic_id.is_empty(), default_case.is_empty()) { + (true, true) => return Err(DynamicCssParseError::EmptyBraces), + (true, false) => return Err(DynamicCssParseError::NoId), + (false, true) => return Err(DynamicCssParseError::NoDefaultCase), + (false, false) => { /* everything OK */ } + } + + if dynamic_id.starts_with(char::is_numeric) || + css_parser::parse_key_value_pair(key, dynamic_id).is_ok() { + return Err(DynamicCssParseError::InvalidId); + } + + let default_case_parsed = match default_case { + "auto" => DynamicCssPropertyDefault::Auto, + other => DynamicCssPropertyDefault::Exact(css_parser::parse_key_value_pair(key, other)?), + }; + + Ok(DynamicCssProperty { + property_type: key, + dynamic_id: dynamic_id.to_string(), + default: default_case_parsed, + }) +} + +#[test] +fn test_detect_static_or_dynamic_property() { + use azul_css::{CssProperty, StyleTextAlignmentHorz}; + use crate::css_parser::InvalidValueErr; + assert_eq!( + determine_static_or_dynamic_css_property(CssPropertyType::TextAlign, " center "), + Ok(CssDeclaration::Static(CssProperty::TextAlign(StyleTextAlignmentHorz::Center))) + ); + + assert_eq!( + determine_static_or_dynamic_css_property(CssPropertyType::TextAlign, "[[ 400px ]]"), + Err(DynamicCssParseError::NoDefaultCase) + ); + + assert_eq!(determine_static_or_dynamic_css_property(CssPropertyType::TextAlign, "[[ 400px"), + Err(DynamicCssParseError::UnclosedBraces) + ); + + assert_eq!( + determine_static_or_dynamic_css_property(CssPropertyType::TextAlign, "[[ 400px | center ]]"), + Err(DynamicCssParseError::InvalidId) + ); + + assert_eq!( + determine_static_or_dynamic_css_property(CssPropertyType::TextAlign, "[[ hello | center ]]"), + Ok(CssDeclaration::Dynamic(DynamicCssProperty { + property_type: CssPropertyType::TextAlign, + default: DynamicCssPropertyDefault::Exact(CssProperty::TextAlign(StyleTextAlignmentHorz::Center)), + dynamic_id: String::from("hello"), + })) + ); + + assert_eq!( + determine_static_or_dynamic_css_property(CssPropertyType::TextAlign, "[[ hello | auto ]]"), + Ok(CssDeclaration::Dynamic(DynamicCssProperty { + property_type: CssPropertyType::TextAlign, + default: DynamicCssPropertyDefault::Auto, + dynamic_id: String::from("hello"), + })) + ); + + assert_eq!( + determine_static_or_dynamic_css_property(CssPropertyType::TextAlign, "[[ abc | hello ]]"), + Err(DynamicCssParseError::UnexpectedValue( + CssParsingError::InvalidValueErr(InvalidValueErr("hello")) + )) + ); + + assert_eq!( + determine_static_or_dynamic_css_property(CssPropertyType::TextAlign, "[[ ]]"), + Err(DynamicCssParseError::EmptyBraces) + ); + assert_eq!( + determine_static_or_dynamic_css_property(CssPropertyType::TextAlign, "[[]]"), + Err(DynamicCssParseError::EmptyBraces) + ); + + + assert_eq!( + determine_static_or_dynamic_css_property(CssPropertyType::TextAlign, "[[ center ]]"), + Err(DynamicCssParseError::NoId) + ); + + assert_eq!( + determine_static_or_dynamic_css_property(CssPropertyType::TextAlign, "[[ hello | ]]"), + Err(DynamicCssParseError::NoDefaultCase) + ); + + // debatable if this is a suitable error for this case: + assert_eq!( + determine_static_or_dynamic_css_property(CssPropertyType::TextAlign, "[[ | ]]"), + Err(DynamicCssParseError::EmptyBraces) + ); +} + +#[test] +fn test_css_parse_1() { + + use azul_css::{ColorU, StyleBackground, NodeTypePath, CssProperty}; + + let parsed_css = new_from_str(" + div#my_id .my_class:first { + background-color: red; + } + ").unwrap(); + + let expected_css_rules = vec![ + CssRuleBlock { + path: CssPath { + selectors: vec![ + CssPathSelector::Type(NodeTypePath::Div), + CssPathSelector::Id(String::from("my_id")), + CssPathSelector::Children, + // NOTE: This is technically wrong, the space between "#my_id" + // and ".my_class" is important, but gets ignored for now + CssPathSelector::Class(String::from("my_class")), + CssPathSelector::PseudoSelector(CssPathPseudoSelector::First), + ], + }, + declarations: vec![CssDeclaration::Static(CssProperty::Background(StyleBackground::Color(ColorU { r: 255, g: 0, b: 0, a: 255 })))], + } + ]; + + assert_eq!(parsed_css, Css { stylesheets: vec![expected_css_rules.into()] }); +} + +#[test] +fn test_css_simple_selector_parse() { + use self::CssPathSelector::*; + use azul_css::NodeTypePath; + let css = "div#id.my_class > p .new { }"; + let parsed = vec![ + Type(NodeTypePath::Div), + Id("id".into()), + Class("my_class".into()), + DirectChildren, + Type(NodeTypePath::P), + Children, + Class("new".into()) + ]; + assert_eq!(new_from_str(css).unwrap(), Css { + stylesheets: vec![Stylesheet { + rules: vec![CssRuleBlock { + path: CssPath { selectors: parsed }, + declarations: Vec::new(), + }], + }], + }); +} + +#[cfg(test)] +mod stylesheet_parse { + + use azul_css::*; + use super::*; + + fn test_css(css: &str, expected: Vec) { + let css = new_from_str(css).unwrap(); + assert_eq!(css, Css { stylesheets: vec![expected.into()] }); + } + + // Tests that an element with a single class always gets the CSS element applied properly + #[test] + fn test_apply_css_pure_class() { + + let red = CssProperty::Background(StyleBackground::Color(ColorU { r: 255, g: 0, b: 0, a: 255 })); + let blue = CssProperty::Background(StyleBackground::Color(ColorU { r: 0, g: 0, b: 255, a: 255 })); + let black = CssProperty::Background(StyleBackground::Color(ColorU { r: 0, g: 0, b: 0, a: 255 })); + + // Simple example + { + let css_1 = ".my_class { background-color: red; }"; + let expected_rules = vec![ + CssRuleBlock { + path: CssPath { selectors: vec![CssPathSelector::Class("my_class".into())] }, + declarations: vec![ + CssDeclaration::Static(red.clone()) + ], + }, + ]; + test_css(css_1, expected_rules); + } + + // Slightly more complex example + { + let css_2 = "#my_id { background-color: red; } .my_class { background-color: blue; }"; + let expected_rules = vec![ + CssRuleBlock { + path: CssPath { selectors: vec![CssPathSelector::Id("my_id".into())] }, + declarations: vec![CssDeclaration::Static(red.clone())] + }, + CssRuleBlock { + path: CssPath { selectors: vec![CssPathSelector::Class("my_class".into())] }, + declarations: vec![CssDeclaration::Static(blue.clone())] + }, + ]; + test_css(css_2, expected_rules); + } + + // Even more complex example + { + let css_3 = "* { background-color: black; } .my_class#my_id { background-color: red; } .my_class { background-color: blue; }"; + let expected_rules = vec![ + CssRuleBlock { + path: CssPath { selectors: vec![CssPathSelector::Global] }, + declarations: vec![CssDeclaration::Static(black.clone())] + }, + CssRuleBlock { + path: CssPath { selectors: vec![CssPathSelector::Class("my_class".into()), CssPathSelector::Id("my_id".into())] }, + declarations: vec![CssDeclaration::Static(red.clone())] + }, + CssRuleBlock { + path: CssPath { selectors: vec![CssPathSelector::Class("my_class".into())] }, + declarations: vec![CssDeclaration::Static(blue.clone())] + }, + ]; + test_css(css_3, expected_rules); + } + } +} + +// Assert that order of the style rules is correct (in same order as provided in CSS form) +#[test] +fn test_multiple_rules() { + use azul_css::*; + use self::CssPathSelector::*; + + let parsed_css = new_from_str(" + * { } + * div.my_class#my_id { } + * div#my_id { } + * #my_id { } + div.my_class.specific#my_id { } + ").unwrap(); + + let expected_rules = vec![ + // Rules are sorted by order of appearance in source string + CssRuleBlock { path: CssPath { selectors: vec![Global] }, declarations: Vec::new() }, + CssRuleBlock { path: CssPath { selectors: vec![Global, Type(NodeTypePath::Div), Class("my_class".into()), Id("my_id".into())] }, declarations: Vec::new() }, + CssRuleBlock { path: CssPath { selectors: vec![Global, Type(NodeTypePath::Div), Id("my_id".into())] }, declarations: Vec::new() }, + CssRuleBlock { path: CssPath { selectors: vec![Global, Id("my_id".into())] }, declarations: Vec::new() }, + CssRuleBlock { path: CssPath { selectors: vec![Type(NodeTypePath::Div), Class("my_class".into()), Class("specific".into()), Id("my_id".into())] }, declarations: Vec::new() }, + ]; + + assert_eq!(parsed_css, Css { stylesheets: vec![expected_rules.into()] }); +} + +#[test] +fn test_case_issue_93() { + + use azul_css::*; + use self::CssPathSelector::*; + + let parsed_css = new_from_str(" + .tabwidget-tab-label { + color: #FFFFFF; + } + + .tabwidget-tab.active .tabwidget-tab-label { + color: #000000; + } + + .tabwidget-tab.active .tabwidget-tab-close { + color: #FF0000; + } + ").unwrap(); + + fn declaration(classes: &[CssPathSelector], color: ColorU) -> CssRuleBlock { + CssRuleBlock { + path: CssPath { selectors: classes.to_vec() }, + declarations: vec![ + CssDeclaration::Static(CssProperty::TextColor(StyleTextColor(color))), + ], + } + } + + let expected_rules = vec![ + declaration(&[Class("tabwidget-tab-label".into())], ColorU { r: 255, g: 255, b: 255, a: 255 }), + declaration(&[Class("tabwidget-tab".into()), Class("active".into()), Children, Class("tabwidget-tab-label".into())], ColorU { r: 0, g: 0, b: 0, a: 255 }), + declaration(&[Class("tabwidget-tab".into()), Class("active".into()), Children, Class("tabwidget-tab-close".into())], ColorU { r: 255, g: 0, b: 0, a: 255 }), + ]; + + assert_eq!(parsed_css, Css { stylesheets: vec![expected_rules.into()] }); +} \ No newline at end of file diff --git a/azul-css-parser/src/css_parser.rs b/azul-css-parser/src/css_parser.rs new file mode 100644 index 000000000..ddace55fb --- /dev/null +++ b/azul-css-parser/src/css_parser.rs @@ -0,0 +1,2879 @@ +//! Contains utilities to convert strings (CSS strings) to servo types + +use std::num::{ParseIntError, ParseFloatError}; +use azul_css::{ + CssPropertyType, + StyleTextAlignmentHorz, Overflow, + LayoutAlignItems, LayoutAlignContent, LayoutJustifyContent, Shape, + LayoutWrap, LayoutDirection, LayoutPosition, CssProperty, LayoutOverflow, + StyleFontFamily, StyleFontSize, StyleLineHeight, LayoutFlexShrink, LayoutFlexGrow, + LayoutLeft, LayoutRight, LayoutTop, LayoutBottom, StyleCursor, StyleWordSpacing, StyleTabWidth, + LayoutMaxHeight, LayoutMinHeight, LayoutHeight, LayoutMaxWidth, LayoutMinWidth, LayoutWidth, + StyleBorderRadius, PixelValue, PercentageValue, FloatValue, + ColorU, LayoutMargin, StyleLetterSpacing, StyleTextColor, StyleBackground, StyleBoxShadow, + GradientStopPre, RadialGradient, StyleBackgroundSize, StyleBackgroundRepeat, + DirectionCorner, StyleBorder, Direction, CssImageId, LinearGradient, + BoxShadowPreDisplayItem, BorderStyle, LayoutPadding, StyleBorderSide, BorderRadius, PixelSize, + BackgroundType, + + SizeMetric, BoxShadowClipMode, ExtendMode, FontId, +}; + +/// A parser that can accept a list of items and mappings +macro_rules! multi_type_parser { + ($fn:ident, $return_str:expr, $return:ident, $import_str:expr, $([$identifier_string:expr, $enum_type:ident, $parse_str:expr]),+) => { + #[doc = "Parses a `"] + #[doc = $return_str] + #[doc = "` attribute from a `&str`"] + #[doc = ""] + #[doc = "# Example"] + #[doc = ""] + #[doc = "```rust"] + #[doc = $import_str] + $( + #[doc = $parse_str] + )+ + #[doc = "```"] + pub fn $fn<'a>(input: &'a str) + -> Result<$return, InvalidValueErr<'a>> + { + let input = input.trim(); + match input { + $( + $identifier_string => Ok($return::$enum_type), + )+ + _ => Err(InvalidValueErr(input)), + } + } + }; + ($fn:ident, $return:ident, $([$identifier_string:expr, $enum_type:ident]),+) => { + multi_type_parser!($fn, stringify!($return), $return, + concat!( + "# extern crate azul_css;", "\r\n", + "# extern crate azul_css_parser;", "\r\n", + "# use azul_css_parser::", stringify!($fn), ";", "\r\n", + "# use azul_css::", stringify!($return), ";" + ), + $([ + $identifier_string, $enum_type, + concat!("assert_eq!(", stringify!($fn), "(\"", $identifier_string, "\"), Ok(", stringify!($return), "::", stringify!($enum_type), "));") + ]),+ + ); + }; +} + +macro_rules! typed_pixel_value_parser { + ($fn:ident, $fn_str:expr, $return:ident, $return_str:expr, $import_str:expr, $test_str:expr) => { + #[doc = "Parses a `"] + #[doc = $return_str] + #[doc = "` attribute from a `&str`"] + #[doc = ""] + #[doc = "# Example"] + #[doc = ""] + #[doc = "```rust"] + #[doc = $import_str] + #[doc = $test_str] + #[doc = "```"] + pub fn $fn<'a>(input: &'a str) -> Result<$return, PixelParseError<'a>> { + parse_pixel_value(input).and_then(|e| Ok($return(e))) + } + }; + ($fn:ident, $return:ident) => { + typed_pixel_value_parser!($fn, stringify!($fn), $return, stringify!($return), + concat!( + "# extern crate azul_css;", "\r\n", + "# extern crate azul_css_parser;", "\r\n", + "# use azul_css_parser::", stringify!($fn), ";", "\r\n", + "# use azul_css::{PixelValue, ", stringify!($return), "};" + ), + concat!("assert_eq!(", stringify!($fn), "(\"5px\"), Ok(", stringify!($return), "(PixelValue::px(5.0))));") + ); + }; +} + +/// Main parsing function, takes a stringified key / value pair and either +/// returns the parsed value or an error +/// +/// ```rust +/// # extern crate azul_css_parser; +/// # extern crate azul_css; +/// # use azul_css_parser; +/// # use azul_css::{LayoutWidth, PixelValue, CssPropertyType, CssProperty}; +/// assert_eq!( +/// azul_css_parser::parse_key_value_pair(CssPropertyType::Width, "500px"), +/// Ok(CssProperty::Width(LayoutWidth(PixelValue::px(500.0)))) +/// ) +/// ``` +pub fn parse_key_value_pair<'a>(key: CssPropertyType, value: &'a str) -> Result> { + use self::CssPropertyType::*; + let value = value.trim(); + match key { + Background => Ok(parse_style_background(value)?.into()), + BackgroundColor => Ok(StyleBackground::Color(parse_css_color(value)?).into()), + BackgroundImage => Ok(StyleBackground::Image(parse_image(value)?).into()), + BackgroundSize => Ok(parse_style_background_size(value)?.into()), + BackgroundRepeat => Ok(parse_style_background_repeat(value)?.into()), + + TextColor => Ok(parse_style_text_color(value)?.into()), + BorderRadius => Ok(parse_style_border_radius(value)?.into()), + FontSize => Ok(parse_style_font_size(value)?.into()), + FontFamily => Ok(parse_style_font_family(value)?.into()), + LetterSpacing => Ok(parse_style_letter_spacing(value)?.into()), + WordSpacing => Ok(parse_style_word_spacing(value)?.into()), + TabWidth => Ok(parse_style_tab_width(value)?.into()), + LineHeight => Ok(parse_style_line_height(value)?.into()), + Cursor => Ok(parse_style_cursor(value)?.into()), + + Border => Ok(StyleBorder::all(parse_css_border(value)?).into()), + BorderTop => Ok(border_parser::parse_top(value)?.into()), + BorderBottom => Ok(border_parser::parse_bottom(value)?.into()), + BorderLeft => Ok(border_parser::parse_left(value)?.into()), + BorderRight => Ok(border_parser::parse_right(value)?.into()), + + Width => Ok(parse_layout_width(value)?.into()), + Height => Ok(parse_layout_height(value)?.into()), + MinWidth => Ok(parse_layout_min_width(value)?.into()), + MinHeight => Ok(parse_layout_min_height(value)?.into()), + MaxWidth => Ok(parse_layout_max_width(value)?.into()), + MaxHeight => Ok(parse_layout_max_height(value)?.into()), + + Position => Ok(parse_layout_position(value)?.into()), + Top => Ok(parse_layout_top(value)?.into()), + Right => Ok(parse_layout_right(value)?.into()), + Left => Ok(parse_layout_left(value)?.into()), + Bottom => Ok(parse_layout_bottom(value)?.into()), + TextAlign => Ok(parse_layout_text_align(value)?.into()), + + BoxShadow => Ok(StyleBoxShadow::all(parse_css_box_shadow(value)?).into()), + BoxShadowTop => Ok(box_shadow_parser::parse_top(value)?.into()), + BoxShadowBottom => Ok(box_shadow_parser::parse_bottom(value)?.into()), + BoxShadowLeft => Ok(box_shadow_parser::parse_left(value)?.into()), + BoxShadowRight => Ok(box_shadow_parser::parse_right(value)?.into()), + + Padding => Ok(parse_layout_padding(value)?.into()), + PaddingTop => Ok(layout_padding_parser::parse_top(value)?.into()), + PaddingBottom => Ok(layout_padding_parser::parse_bottom(value)?.into()), + PaddingLeft => Ok(layout_padding_parser::parse_left(value)?.into()), + PaddingRight => Ok(layout_padding_parser::parse_right(value)?.into()), + + Margin => Ok(parse_layout_margin(value)?.into()), + MarginTop => Ok(layout_margin_parser::parse_top(value)?.into()), + MarginBottom => Ok(layout_margin_parser::parse_bottom(value)?.into()), + MarginLeft => Ok(layout_margin_parser::parse_left(value)?.into()), + MarginRight => Ok(layout_margin_parser::parse_right(value)?.into()), + + FlexWrap => Ok(parse_layout_wrap(value)?.into()), + FlexDirection => Ok(parse_layout_direction(value)?.into()), + FlexGrow => Ok(parse_layout_flex_grow(value)?.into()), + FlexShrink => Ok(parse_layout_flex_shrink(value)?.into()), + + JustifyContent => Ok(parse_layout_justify_content(value)?.into()), + AlignItems => Ok(parse_layout_align_items(value)?.into()), + AlignContent => Ok(parse_layout_align_content(value)?.into()), + + Overflow => { + let overflow_both_directions = parse_layout_text_overflow(value)?; + Ok(LayoutOverflow { + horizontal: Some(overflow_both_directions), + vertical: Some(overflow_both_directions), + }.into()) + }, + OverflowX => { + let overflow_x = parse_layout_text_overflow(value)?; + Ok(LayoutOverflow { + horizontal: Some(overflow_x), + vertical: None, + }.into()) + }, + OverflowY => { + let overflow_y = parse_layout_text_overflow(value)?; + Ok(LayoutOverflow { + horizontal: None, + vertical: Some(overflow_y), + }.into()) + }, + } +} + +/// Error containing all sub-errors that could happen during CSS parsing +/// +/// Usually we want to crash on the first error, to notify the user of the problem. +#[derive(Clone, PartialEq)] +pub enum CssParsingError<'a> { + CssBorderParseError(CssBorderParseError<'a>), + CssShadowParseError(CssShadowParseError<'a>), + InvalidValueErr(InvalidValueErr<'a>), + PixelParseError(PixelParseError<'a>), + PercentageParseError(PercentageParseError), + CssImageParseError(CssImageParseError<'a>), + CssStyleFontFamilyParseError(CssStyleFontFamilyParseError<'a>), + CssBackgroundParseError(CssBackgroundParseError<'a>), + CssColorParseError(CssColorParseError<'a>), + CssStyleBorderRadiusParseError(CssStyleBorderRadiusParseError<'a>), + PaddingParseError(LayoutPaddingParseError<'a>), + MarginParseError(LayoutMarginParseError<'a>), + FlexShrinkParseError(FlexShrinkParseError<'a>), + FlexGrowParseError(FlexGrowParseError<'a>), +} + +impl_debug_as_display!(CssParsingError<'a>); +impl_display!{ CssParsingError<'a>, { + CssStyleBorderRadiusParseError(e) => format!("Invalid border-radius: {}", e), + CssBorderParseError(e) => format!("Invalid border property: {}", e), + CssShadowParseError(e) => format!("Invalid shadow: \"{}\"", e), + InvalidValueErr(e) => format!("\"{}\"", e.0), + PixelParseError(e) => format!("{}", e), + PercentageParseError(e) => format!("{}", e), + CssImageParseError(e) => format!("{}", e), + CssStyleFontFamilyParseError(e) => format!("{}", e), + CssBackgroundParseError(e) => format!("{}", e), + CssColorParseError(e) => format!("{}", e), + PaddingParseError(e) => format!("{}", e), + MarginParseError(e) => format!("{}", e), + FlexShrinkParseError(e) => format!("{}", e), + FlexGrowParseError(e) => format!("{}", e), +}} + +impl_from!(CssBorderParseError<'a>, CssParsingError::CssBorderParseError); +impl_from!(CssShadowParseError<'a>, CssParsingError::CssShadowParseError); +impl_from!(CssColorParseError<'a>, CssParsingError::CssColorParseError); +impl_from!(InvalidValueErr<'a>, CssParsingError::InvalidValueErr); +impl_from!(PixelParseError<'a>, CssParsingError::PixelParseError); +impl_from!(CssImageParseError<'a>, CssParsingError::CssImageParseError); +impl_from!(CssStyleFontFamilyParseError<'a>, CssParsingError::CssStyleFontFamilyParseError); +impl_from!(CssBackgroundParseError<'a>, CssParsingError::CssBackgroundParseError); +impl_from!(CssStyleBorderRadiusParseError<'a>, CssParsingError::CssStyleBorderRadiusParseError); +impl_from!(LayoutPaddingParseError<'a>, CssParsingError::PaddingParseError); +impl_from!(LayoutMarginParseError<'a>, CssParsingError::MarginParseError); +impl_from!(FlexShrinkParseError<'a>, CssParsingError::FlexShrinkParseError); +impl_from!(FlexGrowParseError<'a>, CssParsingError::FlexGrowParseError); + +impl<'a> From for CssParsingError<'a> { + fn from(e: PercentageParseError) -> Self { + CssParsingError::PercentageParseError(e) + } +} + +/// Simple "invalid value" error, used for +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct InvalidValueErr<'a>(pub &'a str); + +#[derive(Clone, PartialEq)] +pub enum CssStyleBorderRadiusParseError<'a> { + TooManyValues(&'a str), + PixelParseError(PixelParseError<'a>), +} + +impl_debug_as_display!(CssStyleBorderRadiusParseError<'a>); +impl_display!{ CssStyleBorderRadiusParseError<'a>, { + TooManyValues(val) => format!("Too many values: \"{}\"", val), + PixelParseError(e) => format!("{}", e), +}} + +impl_from!(PixelParseError<'a>, CssStyleBorderRadiusParseError::PixelParseError); + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum CssColorComponent { + Red, + Green, + Blue, + Hue, + Saturation, + Lightness, + Alpha, +} + +#[derive(Clone, PartialEq)] +pub enum CssColorParseError<'a> { + InvalidColor(&'a str), + InvalidFunctionName(&'a str), + InvalidColorComponent(u8), + IntValueParseErr(ParseIntError), + FloatValueParseErr(ParseFloatError), + FloatValueOutOfRange(f32), + MissingColorComponent(CssColorComponent), + ExtraArguments(&'a str), + UnclosedColor(&'a str), + EmptyInput, + DirectionParseError(CssDirectionParseError<'a>), + UnsupportedDirection(&'a str), + InvalidPercentage(PercentageParseError), +} + +impl_debug_as_display!(CssColorParseError<'a>); +impl_display!{CssColorParseError<'a>, { + InvalidColor(i) => format!("Invalid CSS color: \"{}\"", i), + InvalidFunctionName(i) => format!("Invalid function name, expected one of: \"rgb\", \"rgba\", \"hsl\", \"hsla\" got: \"{}\"", i), + InvalidColorComponent(i) => format!("Invalid color component when parsing CSS color: \"{}\"", i), + IntValueParseErr(e) => format!("CSS color component: Value not in range between 00 - FF: \"{}\"", e), + FloatValueParseErr(e) => format!("CSS color component: Value cannot be parsed as floating point number: \"{}\"", e), + FloatValueOutOfRange(v) => format!("CSS color component: Value not in range between 0.0 - 1.0: \"{}\"", v), + MissingColorComponent(c) => format!("CSS color is missing {:?} component", c), + ExtraArguments(a) => format!("Extra argument to CSS color: \"{}\"", a), + EmptyInput => format!("Empty color string."), + UnclosedColor(i) => format!("Unclosed color: \"{}\"", i), + DirectionParseError(e) => format!("Could not parse direction argument for CSS color: \"{}\"", e), + UnsupportedDirection(d) => format!("Unsupported direction type for CSS color: \"{}\"", d), + InvalidPercentage(p) => format!("Invalid percentage when parsing CSS color: \"{}\"", p), +}} + +impl<'a> From for CssColorParseError<'a> { + fn from(e: ParseIntError) -> Self { + CssColorParseError::IntValueParseErr(e) + } +} + +impl<'a> From for CssColorParseError<'a> { + fn from(e: ParseFloatError) -> Self { + CssColorParseError::FloatValueParseErr(e) + } +} + +impl_from!(CssDirectionParseError<'a>, CssColorParseError::DirectionParseError); + +#[derive(Copy, Clone, PartialEq)] +pub enum CssImageParseError<'a> { + UnclosedQuotes(&'a str), +} + +impl_debug_as_display!(CssImageParseError<'a>); +impl_display!{CssImageParseError<'a>, { + UnclosedQuotes(e) => format!("Unclosed quotes: \"{}\"", e), +}} + +/// String has unbalanced `'` or `"` quotation marks +#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)] +pub struct UnclosedQuotesError<'a>(pub &'a str); + +impl<'a> From> for CssImageParseError<'a> { + fn from(err: UnclosedQuotesError<'a>) -> Self { + CssImageParseError::UnclosedQuotes(err.0) + } +} + +#[derive(Clone, PartialEq)] +pub enum CssBorderParseError<'a> { + MissingThickness(&'a str), + InvalidBorderStyle(InvalidValueErr<'a>), + InvalidBorderDeclaration(&'a str), + ThicknessParseError(PixelParseError<'a>), + ColorParseError(CssColorParseError<'a>), +} +impl_debug_as_display!(CssBorderParseError<'a>); +impl_display!{ CssBorderParseError<'a>, { + MissingThickness(e) => format!("Missing border thickness: \"{}\"", e), + InvalidBorderStyle(e) => format!("Invalid style: {}", e.0), + InvalidBorderDeclaration(e) => format!("Invalid declaration: \"{}\"", e), + ThicknessParseError(e) => format!("Invalid thickness: {}", e), + ColorParseError(e) => format!("Invalid color: {}", e), +}} + +#[derive(Clone, PartialEq)] +pub enum CssShadowParseError<'a> { + InvalidSingleStatement(&'a str), + TooManyComponents(&'a str), + ValueParseErr(PixelParseError<'a>), + ColorParseError(CssColorParseError<'a>), +} +impl_debug_as_display!(CssShadowParseError<'a>); +impl_display!{ CssShadowParseError<'a>, { + InvalidSingleStatement(e) => format!("Invalid single statement: \"{}\"", e), + TooManyComponents(e) => format!("Too many components: \"{}\"", e), + ValueParseErr(e) => format!("Invalid value: {}", e), + ColorParseError(e) => format!("Invalid color-value: {}", e), +}} + +impl_from!(PixelParseError<'a>, CssShadowParseError::ValueParseErr); +impl_from!(CssColorParseError<'a>, CssShadowParseError::ColorParseError); + +/// parse the border-radius like "5px 10px" or "5px 10px 6px 10px" +pub fn parse_style_border_radius<'a>(input: &'a str) +-> Result> +{ + let mut components = input.split_whitespace(); + let len = components.clone().count(); + + match len { + 1 => { + // One value - border-radius: 15px; + // (the value applies to all four corners, which are rounded equally: + + let uniform_radius = parse_pixel_value(components.next().unwrap())?; + Ok(StyleBorderRadius(BorderRadius::uniform(PixelSize::new(uniform_radius, uniform_radius)))) + }, + 2 => { + // Two values - border-radius: 15px 50px; + // (first value applies to top-left and bottom-right corners, + // and the second value applies to top-right and bottom-left corners): + + let top_left_bottom_right = parse_pixel_value(components.next().unwrap())?; + let top_right_bottom_left = parse_pixel_value(components.next().unwrap())?; + + Ok(StyleBorderRadius(BorderRadius { + top_left: PixelSize::new(top_left_bottom_right, top_left_bottom_right), + bottom_right: PixelSize::new(top_left_bottom_right, top_left_bottom_right), + top_right: PixelSize::new(top_right_bottom_left, top_right_bottom_left), + bottom_left: PixelSize::new(top_right_bottom_left, top_right_bottom_left), + })) + }, + 3 => { + // Three values - border-radius: 15px 50px 30px; + // (first value applies to top-left corner, + // second value applies to top-right and bottom-left corners, + // and third value applies to bottom-right corner): + let top_left = parse_pixel_value(components.next().unwrap())?; + let top_right_bottom_left = parse_pixel_value(components.next().unwrap())?; + let bottom_right = parse_pixel_value(components.next().unwrap())?; + + Ok(StyleBorderRadius(BorderRadius { + top_left: PixelSize::new(top_left, top_left), + bottom_right: PixelSize::new(bottom_right, bottom_right), + top_right: PixelSize::new(top_right_bottom_left, top_right_bottom_left), + bottom_left: PixelSize::new(top_right_bottom_left, top_right_bottom_left), + })) + } + 4 => { + // Four values - border-radius: 15px 50px 30px 5px; + // (first value applies to top-left corner, + // second value applies to top-right corner, + // third value applies to bottom-right corner, + // fourth value applies to bottom-left corner) + let top_left = parse_pixel_value(components.next().unwrap())?; + let top_right = parse_pixel_value(components.next().unwrap())?; + let bottom_right = parse_pixel_value(components.next().unwrap())?; + let bottom_left = parse_pixel_value(components.next().unwrap())?; + + Ok(StyleBorderRadius(BorderRadius { + top_left: PixelSize::new(top_left, top_left), + bottom_right: PixelSize::new(bottom_right, bottom_right), + top_right: PixelSize::new(top_right, top_right), + bottom_left: PixelSize::new(bottom_left, bottom_left), + })) + }, + _ => { + Err(CssStyleBorderRadiusParseError::TooManyValues(input)) + } + } +} + +#[derive(Clone, PartialEq)] +pub enum PixelParseError<'a> { + EmptyString, + NoValueGiven(&'a str), + UnsupportedMetric(f32, String, &'a str), + ValueParseErr(ParseFloatError, String), +} + +impl_debug_as_display!(PixelParseError<'a>); + +impl_display!{ PixelParseError<'a>, { + EmptyString => format!("Missing [px / pt / em] value"), + NoValueGiven(input) => format!("Expected floating-point pixel value, got: \"{}\"", input), + UnsupportedMetric(_, metric, input) => format!("Could not parse \"{}\": Metric \"{}\" is not (yet) implemented.", input, metric), + ValueParseErr(err, number_str) => format!("Could not parse \"{}\" as floating-point value: \"{}\"", number_str, err), +}} + +/// parse a single value such as "15px" +pub fn parse_pixel_value<'a>(input: &'a str) +-> Result> +{ + let input = input.trim(); + + if input.is_empty() { + return Err(PixelParseError::EmptyString); + } + + let is_part_of_number = |ch: &char| ch.is_numeric() || *ch == '.' || *ch == '-'; + + // You can't sub-string pixel values, have to call collect() here! + let number_str = input.chars().take_while(is_part_of_number).collect::(); + let unit_str = input.chars().filter(|ch| !is_part_of_number(ch)).collect::(); + + if number_str.is_empty() { + return Err(PixelParseError::NoValueGiven(input)); + } + + let number = number_str.parse::().map_err(|e| PixelParseError::ValueParseErr(e, number_str))?; + + let unit = match unit_str.as_str() { + "px" => SizeMetric::Px, + "em" => SizeMetric::Em, + "pt" => SizeMetric::Pt, + _ => return Err(PixelParseError::UnsupportedMetric(number, unit_str, input)), + }; + + Ok(PixelValue::from_metric(unit, number)) +} + +#[derive(Clone, PartialEq, Eq)] +pub enum PercentageParseError { + ValueParseErr(ParseFloatError), + NoPercentSign +} + +impl_debug_as_display!(PercentageParseError); +impl_from!(ParseFloatError, PercentageParseError::ValueParseErr); + +impl_display! { PercentageParseError, { + ValueParseErr(e) => format!("\"{}\"", e), + NoPercentSign => format!("No percent sign after number"), +}} + +// Parse "1.2" or "120%" (similar to parse_pixel_value) +pub fn parse_percentage_value(input: &str) +-> Result +{ + let mut split_pos = 0; + for (idx, ch) in input.char_indices() { + if ch.is_numeric() || ch == '.' { + split_pos = idx; + } + } + + split_pos += 1; + + let unit = &input[split_pos..]; + let mut number = input[..split_pos].parse::().map_err(|e| PercentageParseError::ValueParseErr(e))?; + + if unit == "%" { + number /= 100.0; + } + + Ok(PercentageValue::new(number)) +} + +/// Parse any valid CSS color, INCLUDING THE HASH +/// +/// "blue" -> "00FF00" -> ColorF { r: 0, g: 255, b: 0 }) +/// "#00FF00" -> ColorF { r: 0, g: 255, b: 0 }) +pub fn parse_css_color<'a>(input: &'a str) +-> Result> +{ + let input = input.trim(); + if input.starts_with('#') { + parse_color_no_hash(&input[1..]) + } else { + use self::ParenthesisParseError::*; + + match parse_parentheses(input, &["rgba", "rgb", "hsla", "hsl"]) { + Ok((stopword, inner_value)) => { + match stopword { + "rgba" => parse_color_rgb(inner_value, true), + "rgb" => parse_color_rgb(inner_value, false), + "hsla" => parse_color_hsl(inner_value, true), + "hsl" => parse_color_hsl(inner_value, false), + _ => unreachable!(), + } + }, + Err(e) => match e { + UnclosedBraces => Err(CssColorParseError::UnclosedColor(input)), + EmptyInput => Err(CssColorParseError::EmptyInput), + StopWordNotFound(stopword) => Err(CssColorParseError::InvalidFunctionName(stopword)), + NoClosingBraceFound => Err(CssColorParseError::UnclosedColor(input)), + NoOpeningBraceFound => parse_color_builtin(input), + }, + } + } +} + +pub fn parse_float_value(input: &str) +-> Result +{ + Ok(FloatValue::new(input.trim().parse::()?)) +} + +pub fn parse_style_text_color<'a>(input: &'a str) +-> Result> +{ + parse_css_color(input).and_then(|ok| Ok(StyleTextColor(ok))) +} + +/// Parse a built-in background color +/// +/// "blue" -> "00FF00" -> ColorF { r: 0, g: 255, b: 0 }) +pub fn parse_color_builtin<'a>(input: &'a str) +-> Result> +{ + let (r, g, b, a) = match input { + "AliceBlue" | "alice-blue" => (240, 248, 255, 255), + "AntiqueWhite" | "antique-white" => (250, 235, 215, 255), + "Aqua" | "aqua" => ( 0, 255, 255, 255), + "Aquamarine" | "aquamarine" => (127, 255, 212, 255), + "Azure" | "azure" => (240, 255, 255, 255), + "Beige" | "beige" => (245, 245, 220, 255), + "Bisque" | "bisque" => (255, 228, 196, 255), + "Black" | "black" => ( 0, 0, 0, 255), + "BlanchedAlmond" | "blanched-almond" => (255, 235, 205, 255), + "Blue" | "blue" => ( 0, 0, 255, 255), + "BlueViolet" | "blue-violet" => (138, 43, 226, 255), + "Brown" | "brown" => (165, 42, 42, 255), + "BurlyWood" | "burly-wood" => (222, 184, 135, 255), + "CadetBlue" | "cadet-blue" => ( 95, 158, 160, 255), + "Chartreuse" | "chartreuse" => (127, 255, 0, 255), + "Chocolate" | "chocolate" => (210, 105, 30, 255), + "Coral" | "coral" => (255, 127, 80, 255), + "CornflowerBlue" | "cornflower-blue" => (100, 149, 237, 255), + "Cornsilk" | "cornsilk" => (255, 248, 220, 255), + "Crimson" | "crimson" => (220, 20, 60, 255), + "Cyan" | "cyan" => ( 0, 255, 255, 255), + "DarkBlue" | "dark-blue" => ( 0, 0, 139, 255), + "DarkCyan" | "dark-cyan" => ( 0, 139, 139, 255), + "DarkGoldenRod" | "dark-golden-rod" => (184, 134, 11, 255), + "DarkGray" | "dark-gray" => (169, 169, 169, 255), + "DarkGrey" | "dark-grey" => (169, 169, 169, 255), + "DarkGreen" | "dark-green" => ( 0, 100, 0, 255), + "DarkKhaki" | "dark-khaki" => (189, 183, 107, 255), + "DarkMagenta" | "dark-magenta" => (139, 0, 139, 255), + "DarkOliveGreen" | "dark-olive-green" => ( 85, 107, 47, 255), + "DarkOrange" | "dark-orange" => (255, 140, 0, 255), + "DarkOrchid" | "dark-orchid" => (153, 50, 204, 255), + "DarkRed" | "dark-red" => (139, 0, 0, 255), + "DarkSalmon" | "dark-salmon" => (233, 150, 122, 255), + "DarkSeaGreen" | "dark-sea-green" => (143, 188, 143, 255), + "DarkSlateBlue" | "dark-slate-blue" => ( 72, 61, 139, 255), + "DarkSlateGray" | "dark-slate-gray" => ( 47, 79, 79, 255), + "DarkSlateGrey" | "dark-slate-grey" => ( 47, 79, 79, 255), + "DarkTurquoise" | "dark-turquoise" => ( 0, 206, 209, 255), + "DarkViolet" | "dark-violet" => (148, 0, 211, 255), + "DeepPink" | "deep-pink" => (255, 20, 147, 255), + "DeepSkyBlue" | "deep-sky-blue" => ( 0, 191, 255, 255), + "DimGray" | "dim-gray" => (105, 105, 105, 255), + "DimGrey" | "dim-grey" => (105, 105, 105, 255), + "DodgerBlue" | "dodger-blue" => ( 30, 144, 255, 255), + "FireBrick" | "fire-brick" => (178, 34, 34, 255), + "FloralWhite" | "floral-white" => (255, 250, 240, 255), + "ForestGreen" | "forest-green" => ( 34, 139, 34, 255), + "Fuchsia" | "fuchsia" => (255, 0, 255, 255), + "Gainsboro" | "gainsboro" => (220, 220, 220, 255), + "GhostWhite" | "ghost-white" => (248, 248, 255, 255), + "Gold" | "gold" => (255, 215, 0, 255), + "GoldenRod" | "golden-rod" => (218, 165, 32, 255), + "Gray" | "gray" => (128, 128, 128, 255), + "Grey" | "grey" => (128, 128, 128, 255), + "Green" | "green" => ( 0, 128, 0, 255), + "GreenYellow" | "green-yellow" => (173, 255, 47, 255), + "HoneyDew" | "honey-dew" => (240, 255, 240, 255), + "HotPink" | "hot-pink" => (255, 105, 180, 255), + "IndianRed" | "indian-red" => (205, 92, 92, 255), + "Indigo" | "indigo" => ( 75, 0, 130, 255), + "Ivory" | "ivory" => (255, 255, 240, 255), + "Khaki" | "khaki" => (240, 230, 140, 255), + "Lavender" | "lavender" => (230, 230, 250, 255), + "LavenderBlush" | "lavender-blush" => (255, 240, 245, 255), + "LawnGreen" | "lawn-green" => (124, 252, 0, 255), + "LemonChiffon" | "lemon-chiffon" => (255, 250, 205, 255), + "LightBlue" | "light-blue" => (173, 216, 230, 255), + "LightCoral" | "light-coral" => (240, 128, 128, 255), + "LightCyan" | "light-cyan" => (224, 255, 255, 255), + "LightGoldenRodYellow" | "light-golden-rod-yellow" => (250, 250, 210, 255), + "LightGray" | "light-gray" => (211, 211, 211, 255), + "LightGrey" | "light-grey" => (144, 238, 144, 255), + "LightGreen" | "light-green" => (211, 211, 211, 255), + "LightPink" | "light-pink" => (255, 182, 193, 255), + "LightSalmon" | "light-salmon" => (255, 160, 122, 255), + "LightSeaGreen" | "light-sea-green" => ( 32, 178, 170, 255), + "LightSkyBlue" | "light-sky-blue" => (135, 206, 250, 255), + "LightSlateGray" | "light-slate-gray" => (119, 136, 153, 255), + "LightSlateGrey" | "light-slate-grey" => (119, 136, 153, 255), + "LightSteelBlue" | "light-steel-blue" => (176, 196, 222, 255), + "LightYellow" | "light-yellow" => (255, 255, 224, 255), + "Lime" | "lime" => ( 0, 255, 0, 255), + "LimeGreen" | "lime-green" => ( 50, 205, 50, 255), + "Linen" | "linen" => (250, 240, 230, 255), + "Magenta" | "magenta" => (255, 0, 255, 255), + "Maroon" | "maroon" => (128, 0, 0, 255), + "MediumAquaMarine" | "medium-aqua-marine" => (102, 205, 170, 255), + "MediumBlue" | "medium-blue" => ( 0, 0, 205, 255), + "MediumOrchid" | "medium-orchid" => (186, 85, 211, 255), + "MediumPurple" | "medium-purple" => (147, 112, 219, 255), + "MediumSeaGreen" | "medium-sea-green" => ( 60, 179, 113, 255), + "MediumSlateBlue" | "medium-slate-blue" => (123, 104, 238, 255), + "MediumSpringGreen" | "medium-spring-green" => ( 0, 250, 154, 255), + "MediumTurquoise" | "medium-turquoise" => ( 72, 209, 204, 255), + "MediumVioletRed" | "medium-violet-red" => (199, 21, 133, 255), + "MidnightBlue" | "midnight-blue" => ( 25, 25, 112, 255), + "MintCream" | "mint-cream" => (245, 255, 250, 255), + "MistyRose" | "misty-rose" => (255, 228, 225, 255), + "Moccasin" | "moccasin" => (255, 228, 181, 255), + "NavajoWhite" | "navajo-white" => (255, 222, 173, 255), + "Navy" | "navy" => ( 0, 0, 128, 255), + "OldLace" | "old-lace" => (253, 245, 230, 255), + "Olive" | "olive" => (128, 128, 0, 255), + "OliveDrab" | "olive-drab" => (107, 142, 35, 255), + "Orange" | "orange" => (255, 165, 0, 255), + "OrangeRed" | "orange-red" => (255, 69, 0, 255), + "Orchid" | "orchid" => (218, 112, 214, 255), + "PaleGoldenRod" | "pale-golden-rod" => (238, 232, 170, 255), + "PaleGreen" | "pale-green" => (152, 251, 152, 255), + "PaleTurquoise" | "pale-turquoise" => (175, 238, 238, 255), + "PaleVioletRed" | "pale-violet-red" => (219, 112, 147, 255), + "PapayaWhip" | "papaya-whip" => (255, 239, 213, 255), + "PeachPuff" | "peach-puff" => (255, 218, 185, 255), + "Peru" | "peru" => (205, 133, 63, 255), + "Pink" | "pink" => (255, 192, 203, 255), + "Plum" | "plum" => (221, 160, 221, 255), + "PowderBlue" | "powder-blue" => (176, 224, 230, 255), + "Purple" | "purple" => (128, 0, 128, 255), + "RebeccaPurple" | "rebecca-purple" => (102, 51, 153, 255), + "Red" | "red" => (255, 0, 0, 255), + "RosyBrown" | "rosy-brown" => (188, 143, 143, 255), + "RoyalBlue" | "royal-blue" => ( 65, 105, 225, 255), + "SaddleBrown" | "saddle-brown" => (139, 69, 19, 255), + "Salmon" | "salmon" => (250, 128, 114, 255), + "SandyBrown" | "sandy-brown" => (244, 164, 96, 255), + "SeaGreen" | "sea-green" => ( 46, 139, 87, 255), + "SeaShell" | "sea-shell" => (255, 245, 238, 255), + "Sienna" | "sienna" => (160, 82, 45, 255), + "Silver" | "silver" => (192, 192, 192, 255), + "SkyBlue" | "sky-blue" => (135, 206, 235, 255), + "SlateBlue" | "slate-blue" => (106, 90, 205, 255), + "SlateGray" | "slate-gray" => (112, 128, 144, 255), + "SlateGrey" | "slate-grey" => (112, 128, 144, 255), + "Snow" | "snow" => (255, 250, 250, 255), + "SpringGreen" | "spring-green" => ( 0, 255, 127, 255), + "SteelBlue" | "steel-blue" => ( 70, 130, 180, 255), + "Tan" | "tan" => (210, 180, 140, 255), + "Teal" | "teal" => ( 0, 128, 128, 255), + "Thistle" | "thistle" => (216, 191, 216, 255), + "Tomato" | "tomato" => (255, 99, 71, 255), + "Turquoise" | "turquoise" => ( 64, 224, 208, 255), + "Violet" | "violet" => (238, 130, 238, 255), + "Wheat" | "wheat" => (245, 222, 179, 255), + "White" | "white" => (255, 255, 255, 255), + "WhiteSmoke" | "white-smoke" => (245, 245, 245, 255), + "Yellow" | "yellow" => (255, 255, 0, 255), + "YellowGreen" | "yellow-green" => (154, 205, 50, 255), + "Transparent" | "transparent" => (255, 255, 255, 0), + _ => { return Err(CssColorParseError::InvalidColor(input)); } + }; + Ok(ColorU { r, g, b, a }) +} + +/// Parse a color of the form 'rgb([0-255], [0-255], [0-255])', or 'rgba([0-255], [0-255], [0-255], +/// [0.0-1.0])' without the leading 'rgb[a](' or trailing ')'. Alpha defaults to 255. +pub fn parse_color_rgb<'a>(input: &'a str, parse_alpha: bool) +-> Result> +{ + let mut components = input.split(',').map(|c| c.trim()); + let rgb_color = parse_color_rgb_components(&mut components)?; + let a = if parse_alpha { + parse_alpha_component(&mut components)? + } else { + 255 + }; + if let Some(arg) = components.next() { + return Err(CssColorParseError::ExtraArguments(arg)); + } + Ok(ColorU { a, ..rgb_color }) +} + +/// Parse the color components passed as arguments to an rgb(...) CSS color. +pub fn parse_color_rgb_components<'a>(components: &mut Iterator) +-> Result> +{ + #[inline] + fn component_from_str<'a>(components: &mut Iterator, which: CssColorComponent) + -> Result> + { + let c = components.next().ok_or(CssColorParseError::MissingColorComponent(which))?; + if c.is_empty() { + return Err(CssColorParseError::MissingColorComponent(which)); + } + let c = c.parse::()?; + Ok(c) + } + + Ok(ColorU { + r: component_from_str(components, CssColorComponent::Red)?, + g: component_from_str(components, CssColorComponent::Green)?, + b: component_from_str(components, CssColorComponent::Blue)?, + a: 255 + }) +} + +/// Parse a color of the form 'hsl([0.0-360.0]deg, [0-100]%, [0-100]%)', or 'hsla([0.0-360.0]deg, [0-100]%, [0-100]%, [0.0-1.0])' without the leading 'hsl[a](' or trailing ')'. Alpha defaults to 255. +pub fn parse_color_hsl<'a>(input: &'a str, parse_alpha: bool) +-> Result> +{ + let mut components = input.split(',').map(|c| c.trim()); + let rgb_color = parse_color_hsl_components(&mut components)?; + let a = if parse_alpha { + parse_alpha_component(&mut components)? + } else { + 255 + }; + if let Some(arg) = components.next() { + return Err(CssColorParseError::ExtraArguments(arg)); + } + Ok(ColorU { a, ..rgb_color }) +} + +/// Parse the color components passed as arguments to an hsl(...) CSS color. +pub fn parse_color_hsl_components<'a>(components: &mut Iterator) +-> Result> +{ + #[inline] + fn angle_from_str<'a>(components: &mut Iterator, which: CssColorComponent) + -> Result> + { + let c = components.next().ok_or(CssColorParseError::MissingColorComponent(which))?; + if c.is_empty() { + return Err(CssColorParseError::MissingColorComponent(which)); + } + let dir = parse_direction(c)?; + match dir { + Direction::Angle(deg) => Ok(deg.get()), + Direction::FromTo(_, _) => return Err(CssColorParseError::UnsupportedDirection(c)), + } + } + + #[inline] + fn percent_from_str<'a>(components: &mut Iterator, which: CssColorComponent) + -> Result> + { + let c = components.next().ok_or(CssColorParseError::MissingColorComponent(which))?; + if c.is_empty() { + return Err(CssColorParseError::MissingColorComponent(which)); + } + + let parsed_percent = parse_percentage(c).map_err(|e| CssColorParseError::InvalidPercentage(e))?; + + Ok(parsed_percent.get()) + } + + /// Adapted from [https://en.wikipedia.org/wiki/HSL_and_HSV#Converting_to_RGB] + #[inline] + fn hsl_to_rgb<'a>(h: f32, s: f32, l: f32) -> (u8, u8, u8) { + let s = s / 100.0; + let l = l / 100.0; + let c = (1.0 - (2.0 * l - 1.0).abs()) * s; + let h = h / 60.0; + let x = c * (1.0 - ((h % 2.0) - 1.0).abs()); + let (r1, g1, b1) = match h as u8 { + 0 => (c, x, 0.0), + 1 => (x, c, 0.0), + 2 => (0.0, c, x), + 3 => (0.0, x, c), + 4 => (x, 0.0, c), + 5 => (c, 0.0, x), + _ => { + unreachable!(); + } + }; + let m = l - c / 2.0; + ( + ((r1 + m) * 256.0).min(255.0) as u8, + ((g1 + m) * 256.0).min(255.0) as u8, + ((b1 + m) * 256.0).min(255.0) as u8, + ) + } + + let (h, s, l) = ( + angle_from_str(components, CssColorComponent::Hue)?, + percent_from_str(components, CssColorComponent::Saturation)?, + percent_from_str(components, CssColorComponent::Lightness)?, + ); + + let (r, g, b) = hsl_to_rgb(h, s, l); + + Ok(ColorU { r, g, b, a: 255 }) +} + +fn parse_alpha_component<'a>(components: &mut Iterator) -> Result> { + let a = components.next().ok_or(CssColorParseError::MissingColorComponent(CssColorComponent::Alpha))?; + if a.is_empty() { + return Err(CssColorParseError::MissingColorComponent(CssColorComponent::Alpha)); + } + let a = a.parse::()?; + if a < 0.0 || a > 1.0 { + return Err(CssColorParseError::FloatValueOutOfRange(a)); + } + let a = (a * 256.0).min(255.0) as u8; + Ok(a) +} + + +/// Parse a background color, WITHOUT THE HASH +/// +/// "00FFFF" -> ColorF { r: 0, g: 255, b: 255}) +pub fn parse_color_no_hash<'a>(input: &'a str) +-> Result> +{ + #[inline] + fn from_hex<'a>(c: u8) -> Result> { + match c { + b'0' ... b'9' => Ok(c - b'0'), + b'a' ... b'f' => Ok(c - b'a' + 10), + b'A' ... b'F' => Ok(c - b'A' + 10), + _ => Err(CssColorParseError::InvalidColorComponent(c)) + } + } + + match input.len() { + 3 => { + let mut input_iter = input.chars(); + + let r = input_iter.next().unwrap() as u8; + let g = input_iter.next().unwrap() as u8; + let b = input_iter.next().unwrap() as u8; + + let r = from_hex(r)? * 16 + from_hex(r)?; + let g = from_hex(g)? * 16 + from_hex(g)?; + let b = from_hex(b)? * 16 + from_hex(b)?; + + Ok(ColorU { + r: r, + g: g, + b: b, + a: 255, + }) + }, + 4 => { + let mut input_iter = input.chars(); + + let r = input_iter.next().unwrap() as u8; + let g = input_iter.next().unwrap() as u8; + let b = input_iter.next().unwrap() as u8; + let a = input_iter.next().unwrap() as u8; + + let r = from_hex(r)? * 16 + from_hex(r)?; + let g = from_hex(g)? * 16 + from_hex(g)?; + let b = from_hex(b)? * 16 + from_hex(b)?; + let a = from_hex(a)? * 16 + from_hex(a)?; + + Ok(ColorU { + r: r, + g: g, + b: b, + a: a, + }) + }, + 6 => { + let input = u32::from_str_radix(input, 16).map_err(|e| CssColorParseError::IntValueParseErr(e))?; + Ok(ColorU { + r: ((input >> 16) & 255) as u8, + g: ((input >> 8) & 255) as u8, + b: (input & 255) as u8, + a: 255, + }) + }, + 8 => { + let input = u32::from_str_radix(input, 16).map_err(|e| CssColorParseError::IntValueParseErr(e))?; + Ok(ColorU { + r: ((input >> 24) & 255) as u8, + g: ((input >> 16) & 255) as u8, + b: ((input >> 8) & 255) as u8, + a: (input & 255) as u8, + }) + }, + _ => { Err(CssColorParseError::InvalidColor(input)) } + } +} + +macro_rules! parse_x {($struct_name:ident, $error_name:ident, $fn_name:ident, $field:ident, $body:ident) => ( +pub fn $fn_name<'a>(input: &'a str) -> Result<$struct_name, $error_name> { + let value = $body(input)?; + Ok($struct_name { + $field: Some(value), + .. Default::default() + }) +})} + +macro_rules! parse_tblr {($mod_name:ident, $struct_name:ident, $error_name:ident, $parse_fn:ident) => ( +mod $mod_name { + use super::*; + parse_x!($struct_name, $error_name, parse_left, left, $parse_fn); + parse_x!($struct_name, $error_name, parse_right, right, $parse_fn); + parse_x!($struct_name, $error_name, parse_bottom, bottom, $parse_fn); + parse_x!($struct_name, $error_name, parse_top, top, $parse_fn); +})} + +parse_tblr!(layout_padding_parser, LayoutPadding, LayoutPaddingParseError, parse_pixel_value); +parse_tblr!(layout_margin_parser, LayoutMargin, LayoutMarginParseError, parse_pixel_value); + +#[derive(Debug, Clone, PartialEq)] +pub enum LayoutPaddingParseError<'a> { + PixelParseError(PixelParseError<'a>), + TooManyValues, + TooFewValues, +} + +impl_display!{ LayoutPaddingParseError<'a>, { + PixelParseError(e) => format!("Could not parse pixel value: {}", e), + TooManyValues => format!("Too many values - padding property has a maximum of 4 values."), + TooFewValues => format!("Too few values - padding property has a minimum of 1 value."), +}} + +impl_from!(PixelParseError<'a>, LayoutPaddingParseError::PixelParseError); + +/// Parse a padding value such as +/// +/// "10px 10px" +pub fn parse_layout_padding<'a>(input: &'a str) +-> Result +{ + let mut input_iter = input.split_whitespace(); + let first = parse_pixel_value(input_iter.next().ok_or(LayoutPaddingParseError::TooFewValues)?)?; + let second = parse_pixel_value(match input_iter.next() { + Some(s) => s, + None => return Ok(LayoutPadding { + top: Some(first), + bottom: Some(first), + left: Some(first), + right: Some(first), + }), + })?; + let third = parse_pixel_value(match input_iter.next() { + Some(s) => s, + None => return Ok(LayoutPadding { + top: Some(first), + bottom: Some(first), + left: Some(second), + right: Some(second), + }), + })?; + let fourth = parse_pixel_value(match input_iter.next() { + Some(s) => s, + None => return Ok(LayoutPadding { + top: Some(first), + left: Some(second), + right: Some(second), + bottom: Some(third), + }), + })?; + + if input_iter.next().is_some() { + return Err(LayoutPaddingParseError::TooManyValues); + } + + Ok(LayoutPadding { + top: Some(first), + right: Some(second), + bottom: Some(third), + left: Some(fourth), + }) +} + +#[derive(Debug, Clone, PartialEq)] +pub enum LayoutMarginParseError<'a> { + PixelParseError(PixelParseError<'a>), + TooManyValues, + TooFewValues, +} + +impl_display!{ LayoutMarginParseError<'a>, { + PixelParseError(e) => format!("Could not parse pixel value: {}", e), + TooManyValues => format!("Too many values - margin property has a maximum of 4 values."), + TooFewValues => format!("Too few values - margin property has a minimum of 1 value."), +}} + +impl_from!(PixelParseError<'a>, LayoutMarginParseError::PixelParseError); + +pub fn parse_layout_margin<'a>(input: &'a str) +-> Result +{ + match parse_layout_padding(input) { + Ok(padding) => { + Ok(LayoutMargin { + top: padding.top, + left: padding.left, + right: padding.right, + bottom: padding.bottom, + }) + }, + Err(LayoutPaddingParseError::PixelParseError(e)) => Err(e.into()), + Err(LayoutPaddingParseError::TooManyValues) => Err(LayoutMarginParseError::TooManyValues), + Err(LayoutPaddingParseError::TooFewValues) => Err(LayoutMarginParseError::TooFewValues), + } +} + +parse_tblr!(border_parser, StyleBorder, CssBorderParseError, parse_css_border); + +const DEFAULT_BORDER_COLOR: ColorU = ColorU { r: 0, g: 0, b: 0, a: 255 }; +// Default border thickness on the web seems to be 3px +const DEFAULT_BORDER_THICKNESS: PixelValue = PixelValue::const_px(3); + +use std::str::CharIndices; + +fn advance_until_next_char(iter: &mut CharIndices) -> Option { + let mut next_char = iter.next()?; + while next_char.1.is_whitespace() { + match iter.next() { + Some(s) => next_char = s, + None => return Some(next_char.0 + 1), + } + } + Some(next_char.0) +} + +/// Advances a CharIndices iterator until the next space is encountered +fn take_until_next_whitespace(iter: &mut CharIndices) -> Option { + let mut next_char = iter.next()?; + while !next_char.1.is_whitespace() { + match iter.next() { + Some(s) => next_char = s, + None => return Some(next_char.0 + 1), + } + } + Some(next_char.0) +} + +/// Parse a CSS border such as +/// +/// "5px solid red" +pub fn parse_css_border<'a>(input: &'a str) +-> Result> +{ + use self::CssBorderParseError::*; + + let input = input.trim(); + + // The first argument can either be a style or a pixel value + + let mut char_iter = input.char_indices(); + let first_arg_end = take_until_next_whitespace(&mut char_iter).ok_or(MissingThickness(input))?; + let first_arg_str = &input[0..first_arg_end]; + + advance_until_next_char(&mut char_iter); + + let second_argument_end = take_until_next_whitespace(&mut char_iter); + let (border_width, border_width_str_end, border_style); + + match second_argument_end { + None => { + // First argument is the one and only argument, therefore has to be a style such as "double" + border_style = parse_border_style(first_arg_str).map_err(|e| InvalidBorderStyle(e))?; + return Ok(StyleBorderSide { + border_style, + border_width: DEFAULT_BORDER_THICKNESS, + border_color: DEFAULT_BORDER_COLOR, + }); + }, + Some(end) => { + // First argument is a pixel value, second argument is the border style + border_width = parse_pixel_value(first_arg_str).map_err(|e| ThicknessParseError(e))?; + let border_style_str = &input[first_arg_end..end]; + border_style = parse_border_style(border_style_str).map_err(|e| InvalidBorderStyle(e))?; + border_width_str_end = end; + } + } + + let border_color_str = &input[border_width_str_end..]; + + // Last argument can be either a hex color or a rgb str + let border_color = parse_css_color(border_color_str).map_err(|e| ColorParseError(e))?; + + Ok(StyleBorderSide { + border_width, + border_style, + border_color, + }) +} + +multi_type_parser!(parse_border_style, BorderStyle, + ["none", None], + ["solid", Solid], + ["double", Double], + ["dotted", Dotted], + ["dashed", Dashed], + ["hidden", Hidden], + ["groove", Groove], + ["ridge", Ridge], + ["inset", Inset], + ["outset", Outset]); + +parse_tblr!(box_shadow_parser, StyleBoxShadow, CssShadowParseError, parse_css_box_shadow); + +/// Parses a CSS box-shadow +pub fn parse_css_box_shadow<'a>(input: &'a str) +-> Result, CssShadowParseError<'a>> +{ + let mut input_iter = input.split_whitespace(); + let count = input_iter.clone().count(); + + let mut box_shadow = BoxShadowPreDisplayItem { + offset: [PixelValue::px(0.0), PixelValue::px(0.0)], + color: ColorU { r: 0, g: 0, b: 0, a: 255 }, + blur_radius: PixelValue::px(0.0), + spread_radius: PixelValue::px(0.0), + clip_mode: BoxShadowClipMode::Outset, + }; + + let last_val = input_iter.clone().rev().next(); + let is_inset = last_val == Some("inset") || last_val == Some("outset"); + + if count > 2 && is_inset { + let l_val = last_val.unwrap(); + if l_val == "outset" { + box_shadow.clip_mode = BoxShadowClipMode::Outset; + } else if l_val == "inset" { + box_shadow.clip_mode = BoxShadowClipMode::Inset; + } + } + + match count { + 1 => { + // box-shadow: none; + match input_iter.next().unwrap() { + "none" => return Ok(None), + _ => return Err(CssShadowParseError::InvalidSingleStatement(input)), + } + }, + 2 => { + // box-shadow: 5px 10px; (h_offset, v_offset) + let h_offset = parse_pixel_value(input_iter.next().unwrap())?; + let v_offset = parse_pixel_value(input_iter.next().unwrap())?; + box_shadow.offset[0] = h_offset; + box_shadow.offset[1] = v_offset; + }, + 3 => { + // box-shadow: 5px 10px inset; (h_offset, v_offset, inset) + let h_offset = parse_pixel_value(input_iter.next().unwrap())?; + let v_offset = parse_pixel_value(input_iter.next().unwrap())?; + box_shadow.offset[0] = h_offset; + box_shadow.offset[1] = v_offset; + + if !is_inset { + // box-shadow: 5px 10px #888888; (h_offset, v_offset, color) + let color = parse_css_color(input_iter.next().unwrap())?; + box_shadow.color = color; + } + }, + 4 => { + let h_offset = parse_pixel_value(input_iter.next().unwrap())?; + let v_offset = parse_pixel_value(input_iter.next().unwrap())?; + box_shadow.offset[0] = h_offset; + box_shadow.offset[1] = v_offset; + + if !is_inset { + let blur = parse_pixel_value(input_iter.next().unwrap())?; + box_shadow.blur_radius = blur.into(); + } + + let color = parse_css_color(input_iter.next().unwrap())?; + box_shadow.color = color; + }, + 5 => { + // box-shadow: 5px 10px 5px 10px #888888; (h_offset, v_offset, blur, spread, color) + // box-shadow: 5px 10px 5px #888888 inset; (h_offset, v_offset, blur, color, inset) + let h_offset = parse_pixel_value(input_iter.next().unwrap())?; + let v_offset = parse_pixel_value(input_iter.next().unwrap())?; + box_shadow.offset[0] = h_offset; + box_shadow.offset[1] = v_offset; + + let blur = parse_pixel_value(input_iter.next().unwrap())?; + box_shadow.blur_radius = blur.into(); + + if !is_inset { + let spread = parse_pixel_value(input_iter.next().unwrap())?; + box_shadow.spread_radius = spread.into(); + } + + let color = parse_css_color(input_iter.next().unwrap())?; + box_shadow.color = color; + }, + 6 => { + // box-shadow: 5px 10px 5px 10px #888888 inset; (h_offset, v_offset, blur, spread, color, inset) + let h_offset = parse_pixel_value(input_iter.next().unwrap())?; + let v_offset = parse_pixel_value(input_iter.next().unwrap())?; + box_shadow.offset[0] = h_offset; + box_shadow.offset[1] = v_offset; + + let blur = parse_pixel_value(input_iter.next().unwrap())?; + box_shadow.blur_radius = blur.into(); + + let spread = parse_pixel_value(input_iter.next().unwrap())?; + box_shadow.spread_radius = spread.into(); + + let color = parse_css_color(input_iter.next().unwrap())?; + box_shadow.color = color; + } + _ => { + return Err(CssShadowParseError::TooManyComponents(input)); + } + } + + Ok(Some(box_shadow)) +} + +#[derive(Clone, PartialEq)] +pub enum CssBackgroundParseError<'a> { + Error(&'a str), + InvalidBackground(ParenthesisParseError<'a>), + UnclosedGradient(&'a str), + NoDirection(&'a str), + TooFewGradientStops(&'a str), + DirectionParseError(CssDirectionParseError<'a>), + GradientParseError(CssGradientStopParseError<'a>), + ShapeParseError(CssShapeParseError<'a>), + ImageParseError(CssImageParseError<'a>), + ColorParseError(CssColorParseError<'a>), +} + +impl_debug_as_display!(CssBackgroundParseError<'a>); +impl_display!{ CssBackgroundParseError<'a>, { + Error(e) => e, + InvalidBackground(val) => format!("Invalid background value: \"{}\"", val), + UnclosedGradient(val) => format!("Unclosed gradient: \"{}\"", val), + NoDirection(val) => format!("Gradient has no direction: \"{}\"", val), + TooFewGradientStops(val) => format!("Failed to parse gradient due to too few gradient steps: \"{}\"", val), + DirectionParseError(e) => format!("Failed to parse gradient direction: \"{}\"", e), + GradientParseError(e) => format!("Failed to parse gradient: {}", e), + ShapeParseError(e) => format!("Failed to parse shape of radial gradient: {}", e), + ImageParseError(e) => format!("Failed to parse image() value: {}", e), + ColorParseError(e) => format!("Failed to parse color value: {}", e), +}} + +impl_from!(ParenthesisParseError<'a>, CssBackgroundParseError::InvalidBackground); +impl_from!(CssDirectionParseError<'a>, CssBackgroundParseError::DirectionParseError); +impl_from!(CssGradientStopParseError<'a>, CssBackgroundParseError::GradientParseError); +impl_from!(CssShapeParseError<'a>, CssBackgroundParseError::ShapeParseError); +impl_from!(CssImageParseError<'a>, CssBackgroundParseError::ImageParseError); +impl_from!(CssColorParseError<'a>, CssBackgroundParseError::ColorParseError); + +// parses a background, such as "linear-gradient(red, green)" +pub fn parse_style_background<'a>(input: &'a str) +-> Result> +{ + match parse_parentheses(input, &[ + "none", "linear-gradient", "repeating-linear-gradient", + "radial-gradient", "repeating-radial-gradient", "image", + ]) { + Ok((background_type, brace_contents)) => { + let background_type = match background_type { + "none" => { return Ok(StyleBackground::NoBackground); }, + "linear-gradient" => BackgroundType::LinearGradient, + "repeating-linear-gradient" => BackgroundType::RepeatingLinearGradient, + "radial-gradient" => BackgroundType::RadialGradient, + "repeating-radial-gradient" => BackgroundType::RepeatingRadialGradient, + "image" => BackgroundType::Image, + _ => { return Ok(StyleBackground::NoBackground); /* unreachable */ }, + }; + + parse_gradient(brace_contents, background_type) + }, + Err(_) => { + Ok(StyleBackground::Color(parse_css_color(input)?)) + } + } +} + +/// Given a string, returns how many characters need to be skipped +fn skip_next_braces(input: &str, target_char: char) -> Option<(usize, bool)> { + + let mut depth = 0; + let mut last_character = 0; + let mut character_was_found = false; + + if input.is_empty() { + return None; + } + + for (idx, ch) in input.char_indices() { + last_character = idx; + match ch { + '(' => { depth += 1; }, + ')' => { depth -= 1; }, + c => { + if c == target_char && depth == 0 { + character_was_found = true; + break; + } + }, + } + } + + if last_character == 0 { + // No more split by `,` + None + } else { + Some((last_character, character_was_found)) + } +} + +// parses a single gradient such as "to right, 50px" +pub fn parse_gradient<'a>(input: &'a str, background_type: BackgroundType) +-> Result> +{ + let input = input.trim(); + + match background_type { + BackgroundType::Image => { return Ok(StyleBackground::Image(parse_image(input)?)); } + BackgroundType::Color => { return Ok(StyleBackground::Color(parse_css_color(input)?)); } + _ => { }, + } + + // Splitting the input by "," doesn't work since rgba() might contain commas + let mut comma_separated_items = Vec::<&str>::new(); + let mut current_input = &input[..]; + + 'outer: loop { + let (skip_next_braces_result, character_was_found) = + match skip_next_braces(¤t_input, ',') { + Some(s) => s, + None => break 'outer, + }; + let new_push_item = if character_was_found { + ¤t_input[..skip_next_braces_result] + } else { + ¤t_input[..] + }; + let new_current_input = ¤t_input[(skip_next_braces_result + 1)..]; + comma_separated_items.push(new_push_item); + current_input = new_current_input; + if !character_was_found { + break 'outer; + } + } + + let mut brace_iterator = comma_separated_items.iter(); + let mut gradient_stop_count = brace_iterator.clone().count(); + + // "50deg", "to right bottom", etc. + let first_brace_item = match brace_iterator.next() { + Some(s) => s, + None => return Err(CssBackgroundParseError::NoDirection(input)), + }; + + // default shape: ellipse + let mut shape = Shape::Ellipse; + // default gradient: from top to bottom + let mut direction = Direction::FromTo(DirectionCorner::Top, DirectionCorner::Bottom); + + let mut first_is_direction = false; + let mut first_is_shape = false; + + let is_linear_gradient = background_type == BackgroundType::LinearGradient || + background_type == BackgroundType::RepeatingLinearGradient; + + let is_radial_gradient = background_type == BackgroundType::RadialGradient || + background_type == BackgroundType::RepeatingRadialGradient; + + if is_linear_gradient { + if let Ok(dir) = parse_direction(first_brace_item) { + direction = dir; + first_is_direction = true; + } + } + + if is_radial_gradient { + if let Ok(sh) = parse_shape(first_brace_item) { + shape = sh; + first_is_shape = true; + } + } + + let mut first_item_doesnt_count = false; + if (is_linear_gradient && first_is_direction) || (is_radial_gradient && first_is_shape) { + gradient_stop_count -= 1; // first item is not a gradient stop + first_item_doesnt_count = true; + } + + if gradient_stop_count < 2 { + return Err(CssBackgroundParseError::TooFewGradientStops(input)); + } + + let mut color_stops = Vec::::with_capacity(gradient_stop_count); + if !first_item_doesnt_count { + color_stops.push(parse_gradient_stop(first_brace_item)?); + } + + for stop in brace_iterator { + color_stops.push(parse_gradient_stop(stop)?); + } + + normalize_color_stops(&mut color_stops); + + match background_type { + BackgroundType::LinearGradient => { + Ok(StyleBackground::LinearGradient(LinearGradient { + direction: direction, + extend_mode: ExtendMode::Clamp, + stops: color_stops, + })) + }, + BackgroundType::RepeatingLinearGradient => { + Ok(StyleBackground::LinearGradient(LinearGradient { + direction: direction, + extend_mode: ExtendMode::Repeat, + stops: color_stops, + })) + }, + BackgroundType::RadialGradient => { + Ok(StyleBackground::RadialGradient(RadialGradient { + shape: shape, + extend_mode: ExtendMode::Clamp, + stops: color_stops, + })) + }, + BackgroundType::RepeatingRadialGradient => { + Ok(StyleBackground::RadialGradient(RadialGradient { + shape: shape, + extend_mode: ExtendMode::Repeat, + stops: color_stops, + })) + }, + BackgroundType::Image | BackgroundType::Color => unreachable!(), + } +} + +// Normalize the percentages of the parsed color stops +pub fn normalize_color_stops(color_stops: &mut Vec) { + + let mut last_stop = PercentageValue::new(0.0); + let mut increase_stop_cnt: Option = None; + + let color_stop_len = color_stops.len(); + 'outer: for i in 0..color_stop_len { + let offset = color_stops[i].offset; + match offset { + Some(s) => { + last_stop = s; + increase_stop_cnt = None; + }, + None => { + let (_, next) = color_stops.split_at_mut(i); + + if let Some(increase_stop_cnt) = increase_stop_cnt { + last_stop = PercentageValue::new(last_stop.get() + increase_stop_cnt); + next[0].offset = Some(last_stop); + continue 'outer; + } + + let mut next_count: u32 = 0; + let mut next_value = None; + + // iterate until we find a value where the offset isn't none + { + let mut next_iter = next.iter(); + next_iter.next(); + 'inner: for next_stop in next_iter { + if let Some(off) = next_stop.offset { + next_value = Some(off); + break 'inner; + } else { + next_count += 1; + } + } + } + + let next_value = next_value.unwrap_or(PercentageValue::new(100.0)); + let increase = (next_value.get() / (next_count as f32)) - (last_stop.get() / (next_count as f32)) ; + increase_stop_cnt = Some(increase); + if next_count == 1 && (color_stop_len - i) == 1 { + next[0].offset = Some(last_stop); + } else { + if i == 0 { + next[0].offset = Some(PercentageValue::new(0.0)); + } else { + next[0].offset = Some(last_stop); + // last_stop += increase; + } + } + } + } + } +} + +impl<'a> From> for CssImageId { + fn from(input: QuoteStripped<'a>) -> Self { + CssImageId(input.0.to_string()) + } +} + +/// A string that has been stripped of the beginning and ending quote +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct QuoteStripped<'a>(pub &'a str); + +pub fn parse_image<'a>(input: &'a str) -> Result> { + Ok(strip_quotes(input)?.into()) +} + +/// Strip quotes from an input, given that both quotes use either `"` or `'`, but not both. +/// +/// # Example +/// +/// ```rust +/// # extern crate azul_css_parser; +/// # use azul_css_parser::{strip_quotes, QuoteStripped, UnclosedQuotesError}; +/// assert_eq!(strip_quotes("\"Helvetica\""), Ok(QuoteStripped("Helvetica"))); +/// assert_eq!(strip_quotes("'Arial'"), Ok(QuoteStripped("Arial"))); +/// assert_eq!(strip_quotes("\"Arial'"), Err(UnclosedQuotesError("\"Arial'"))); +/// ``` +pub fn strip_quotes<'a>(input: &'a str) -> Result, UnclosedQuotesError<'a>> { + let mut double_quote_iter = input.splitn(2, '"'); + double_quote_iter.next(); + let mut single_quote_iter = input.splitn(2, '\''); + single_quote_iter.next(); + + let first_double_quote = double_quote_iter.next(); + let first_single_quote = single_quote_iter.next(); + if first_double_quote.is_some() && first_single_quote.is_some() { + return Err(UnclosedQuotesError(input)); + } + if first_double_quote.is_some() { + let quote_contents = first_double_quote.unwrap(); + if !quote_contents.ends_with('"') { + return Err(UnclosedQuotesError(quote_contents)); + } + Ok(QuoteStripped(quote_contents.trim_end_matches("\""))) + } else if first_single_quote.is_some() { + let quote_contents = first_single_quote.unwrap(); + if!quote_contents.ends_with('\'') { + return Err(UnclosedQuotesError(input)); + } + Ok(QuoteStripped(quote_contents.trim_end_matches("'"))) + } else { + Err(UnclosedQuotesError(input)) + } +} + +#[derive(Clone, PartialEq)] +pub enum CssGradientStopParseError<'a> { + Error(&'a str), + Percentage(PercentageParseError), + ColorParseError(CssColorParseError<'a>), +} + +impl_debug_as_display!(CssGradientStopParseError<'a>); +impl_display!{ CssGradientStopParseError<'a>, { + Error(e) => e, + Percentage(e) => format!("Failed to parse offset percentage: {}", e), + ColorParseError(e) => format!("{}", e), +}} + +impl_from!(CssColorParseError<'a>, CssGradientStopParseError::ColorParseError); + + +// parses "red" , "red 5%" +pub fn parse_gradient_stop<'a>(input: &'a str) +-> Result> +{ + use self::CssGradientStopParseError::*; + + let input = input.trim(); + + // Color functions such as "rgba(...)" can contain spaces, so we parse right-to-left. + let (color_str, percentage_str) = match (input.rfind(')'), input.rfind(char::is_whitespace)) { + (Some(closing_brace), None) if closing_brace < input.len() - 1 => { + // percentage after closing brace, eg. "rgb(...)50%" + (&input[..=closing_brace], Some(&input[(closing_brace + 1)..])) + }, + (None, Some(last_ws)) => { + // percentage after last whitespace, eg. "... 50%" + (&input[..=last_ws], Some(&input[(last_ws + 1)..])) + } + (Some(closing_brace), Some(last_ws)) if closing_brace < last_ws => { + // percentage after last whitespace, eg. "... 50%" + (&input[..=last_ws], Some(&input[(last_ws + 1)..])) + }, + _ => { + // no percentage + (input, None) + }, + }; + + let color = parse_css_color(color_str)?; + let offset = match percentage_str { + None => None, + Some(s) => Some(parse_percentage(s).map_err(|e| Percentage(e))?) + }; + + Ok(GradientStopPre { offset, color: color }) +} + +// parses "5%" -> 5 +pub fn parse_percentage(input: &str) +-> Result +{ + let percent_location = input.rfind('%').ok_or(PercentageParseError::NoPercentSign)?; + let input = &input[..percent_location]; + Ok(PercentageValue::new(input.parse::()?)) +} + +#[derive(Debug, Clone, PartialEq)] +pub enum CssDirectionParseError<'a> { + Error(&'a str), + InvalidArguments(&'a str), + ParseFloat(ParseFloatError), + CornerError(CssDirectionCornerParseError<'a>), +} + +impl_display!{CssDirectionParseError<'a>, { + Error(e) => e, + InvalidArguments(val) => format!("Invalid arguments: \"{}\"", val), + ParseFloat(e) => format!("Invalid value: {}", e), + CornerError(e) => format!("Invalid corner value: {}", e), +}} + +impl<'a> From for CssDirectionParseError<'a> { + fn from(e: ParseFloatError) -> Self { + CssDirectionParseError::ParseFloat(e) + } +} + +impl<'a> From> for CssDirectionParseError<'a> { + fn from(e: CssDirectionCornerParseError<'a>) -> Self { + CssDirectionParseError::CornerError(e) + } +} + +/// Parses an `direction` such as `"50deg"` or `"to right bottom"` (in the context of gradients) +/// +/// # Example +/// +/// ```rust +/// # extern crate azul_css; +/// # extern crate azul_css_parser; +/// # use azul_css_parser::parse_direction; +/// # use azul_css::{Direction, FloatValue}; +/// use azul_css::DirectionCorner::*; +/// +/// assert_eq!(parse_direction("to right bottom"), Ok(Direction::FromTo(TopLeft, BottomRight))); +/// assert_eq!(parse_direction("to right"), Ok(Direction::FromTo(Left, Right))); +/// assert_eq!(parse_direction("50deg"), Ok(Direction::Angle(FloatValue::new(50.0)))); +/// ``` +pub fn parse_direction<'a>(input: &'a str) +-> Result> +{ + use std::f32::consts::PI; + + let input_iter = input.split_whitespace(); + let count = input_iter.clone().count(); + let mut first_input_iter = input_iter.clone(); + // "50deg" | "to" | "right" + let first_input = first_input_iter.next().ok_or(CssDirectionParseError::Error(input))?; + + let deg = { + if first_input.ends_with("grad") { + first_input.split("grad").next().unwrap().parse::()? / 400.0 * 360.0 + } else if first_input.ends_with("rad") { + first_input.split("rad").next().unwrap().parse::()? * 180.0 / PI + } else if first_input.ends_with("deg") || first_input.parse::().is_ok() { + first_input.split("deg").next().unwrap().parse::()? + } else if let Ok(angle) = first_input.parse::() { + angle + } + else { + // if we get here, the input is definitely not an angle + + if first_input != "to" { + return Err(CssDirectionParseError::InvalidArguments(input)); + } + + let second_input = first_input_iter.next().ok_or(CssDirectionParseError::Error(input))?; + let end = parse_direction_corner(second_input)?; + + return match count { + 2 => { + // "to right" + let start = end.opposite(); + Ok(Direction::FromTo(start, end)) + }, + 3 => { + // "to bottom right" + let beginning = end; + let third_input = first_input_iter.next().ok_or(CssDirectionParseError::Error(input))?; + let new_end = parse_direction_corner(third_input)?; + // "Bottom, Right" -> "BottomRight" + let new_end = beginning.combine(&new_end).ok_or(CssDirectionParseError::Error(input))?; + let start = new_end.opposite(); + Ok(Direction::FromTo(start, new_end)) + }, + _ => { Err(CssDirectionParseError::InvalidArguments(input)) } + }; + } + }; + + // clamp the degree to 360 (so 410deg = 50deg) + let mut deg = deg % 360.0; + if deg < 0.0 { + deg = 360.0 + deg; + } + + // now deg is in the range of +0..+360 + debug_assert!(deg >= 0.0 && deg <= 360.0); + + return Ok(Direction::Angle(FloatValue::new(deg))); +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum CssDirectionCornerParseError<'a> { + InvalidDirection(&'a str), +} + +impl_display!{ CssDirectionCornerParseError<'a>, { + InvalidDirection(val) => format!("Invalid direction: \"{}\"", val), +}} + +pub fn parse_direction_corner<'a>(input: &'a str) +-> Result> +{ + match input { + "right" => Ok(DirectionCorner::Right), + "left" => Ok(DirectionCorner::Left), + "top" => Ok(DirectionCorner::Top), + "bottom" => Ok(DirectionCorner::Bottom), + _ => { Err(CssDirectionCornerParseError::InvalidDirection(input))} + } +} + +#[derive(Debug, PartialEq, Copy, Clone)] +pub enum CssShapeParseError<'a> { + ShapeErr(InvalidValueErr<'a>), +} + +impl_display!{CssShapeParseError<'a>, { + ShapeErr(e) => format!("\"{}\"", e.0), +}} + +/// Stylistic options of the rectangle that don't influence the layout +/// (todo: border-box?) +#[derive(Default, Debug, Clone, PartialEq, Hash)] +pub struct RectStyle { + /// Background size of this rectangle + pub background_size: Option, + /// Background repeat of this rectangle + pub background_repeat: Option, + /// Shadow color + pub box_shadow: Option, + /// Gradient (location) + stops + pub background: Option, + /// Border + pub border: Option, + /// Border radius + pub border_radius: Option, + /// Font size + pub font_size: Option, + /// Font name / family + pub font_family: Option, + /// Text color + pub font_color: Option, + /// Text alignment + pub text_align: Option, + /// Text overflow behaviour + pub overflow: Option, + /// `line-height` property + pub line_height: Option, + /// `letter-spacing` property (modifies the width and height) + pub letter_spacing: Option, +} + +typed_pixel_value_parser!(parse_style_letter_spacing, StyleLetterSpacing); +typed_pixel_value_parser!(parse_style_word_spacing, StyleWordSpacing); + +// Layout constraints for a given rectangle, such as "width", "min-width", "height", etc. +#[derive(Default, Debug, Copy, Clone, PartialEq, Hash)] +pub struct RectLayout { + + pub width: Option, + pub height: Option, + pub min_width: Option, + pub min_height: Option, + pub max_width: Option, + pub max_height: Option, + + pub position: Option, + pub top: Option, + pub bottom: Option, + pub right: Option, + pub left: Option, + + pub padding: Option, + pub margin: Option, + + pub direction: Option, + pub wrap: Option, + pub flex_grow: Option, + pub flex_shrink: Option, + pub justify_content: Option, + pub align_items: Option, + pub align_content: Option, +} + +typed_pixel_value_parser!(parse_layout_width, LayoutWidth); +typed_pixel_value_parser!(parse_layout_height, LayoutHeight); + +typed_pixel_value_parser!(parse_layout_min_height, LayoutMinHeight); +typed_pixel_value_parser!(parse_layout_min_width, LayoutMinWidth); +typed_pixel_value_parser!(parse_layout_max_width, LayoutMaxWidth); +typed_pixel_value_parser!(parse_layout_max_height, LayoutMaxHeight); + +typed_pixel_value_parser!(parse_layout_top, LayoutTop); +typed_pixel_value_parser!(parse_layout_bottom, LayoutBottom); +typed_pixel_value_parser!(parse_layout_right, LayoutRight); +typed_pixel_value_parser!(parse_layout_left, LayoutLeft); + +#[derive(Debug, Clone, PartialEq)] +pub enum FlexGrowParseError<'a> { + ParseFloat(ParseFloatError, &'a str), +} + +impl_display!{FlexGrowParseError<'a>, { + ParseFloat(e, orig_str) => format!("flex-grow: Could not parse floating-point value: \"{}\" - Error: \"{}\"", orig_str, e), +}} + +pub fn parse_layout_flex_grow<'a>(input: &'a str) -> Result> { + match parse_float_value(input) { + Ok(o) => Ok(LayoutFlexGrow(o)), + Err(e) => Err(FlexGrowParseError::ParseFloat(e, input)), + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum FlexShrinkParseError<'a> { + ParseFloat(ParseFloatError, &'a str), +} + +impl_display!{FlexShrinkParseError<'a>, { + ParseFloat(e, orig_str) => format!("flex-shrink: Could not parse floating-point value: \"{}\" - Error: \"{}\"", orig_str, e), +}} + +pub fn parse_layout_flex_shrink<'a>(input: &'a str) -> Result> { + match parse_float_value(input) { + Ok(o) => Ok(LayoutFlexShrink(o)), + Err(e) => Err(FlexShrinkParseError::ParseFloat(e, input)), + } +} + +pub fn parse_style_tab_width(input: &str) +-> Result +{ + parse_percentage_value(input).and_then(|e| Ok(StyleTabWidth(e))) +} + +pub fn parse_style_line_height(input: &str) +-> Result +{ + parse_percentage_value(input).and_then(|e| Ok(StyleLineHeight(e))) +} + +typed_pixel_value_parser!(parse_style_font_size, StyleFontSize); + +#[derive(Debug, PartialEq, Copy, Clone)] +pub enum CssStyleFontFamilyParseError<'a> { + InvalidStyleFontFamily(&'a str), + UnclosedQuotes(&'a str), +} + +impl_display!{CssStyleFontFamilyParseError<'a>, { + InvalidStyleFontFamily(val) => format!("Invalid font-family: \"{}\"", val), + UnclosedQuotes(val) => format!("Unclosed quotes: \"{}\"", val), +}} + +impl<'a> From> for CssStyleFontFamilyParseError<'a> { + fn from(err: UnclosedQuotesError<'a>) -> Self { + CssStyleFontFamilyParseError::UnclosedQuotes(err.0) + } +} + +/// Parses a `StyleFontFamily` declaration from a `&str` +/// +/// # Example +/// +/// ```rust +/// # extern crate azul_css; +/// # extern crate azul_css_parser; +/// # use azul_css_parser::parse_style_font_family; +/// # use azul_css::{StyleFontFamily, FontId}; +/// let input = "\"Helvetica\", 'Arial', Times New Roman"; +/// let fonts = vec![ +/// FontId("Helvetica".into()), +/// FontId("Arial".into()), +/// FontId("Times New Roman".into()) +/// ]; +/// +/// assert_eq!(parse_style_font_family(input), Ok(StyleFontFamily { fonts })); +/// ``` +pub fn parse_style_font_family<'a>(input: &'a str) -> Result> { + let multiple_fonts = input.split(','); + let mut fonts = Vec::with_capacity(1); + + for font in multiple_fonts { + let font = font.trim(); + let font = font.trim_matches('\''); + let font = font.trim_matches('\"'); + let font = font.trim(); + fonts.push(FontId(font.into())); + } + + Ok(StyleFontFamily { + fonts: fonts, + }) +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)] +pub enum ParenthesisParseError<'a> { + UnclosedBraces, + NoOpeningBraceFound, + NoClosingBraceFound, + StopWordNotFound(&'a str), + EmptyInput, +} + +impl_display!{ ParenthesisParseError<'a>, { + UnclosedBraces => format!("Unclosed parenthesis"), + NoOpeningBraceFound => format!("Expected value in parenthesis (missing \"(\")"), + NoClosingBraceFound => format!("Missing closing parenthesis (missing \")\")"), + StopWordNotFound(e) => format!("Stopword not found, found: \"{}\"", e), + EmptyInput => format!("Empty parenthesis"), +}} + +/// Checks wheter a given input is enclosed in parentheses, prefixed +/// by a certain number of stopwords. +/// +/// On success, returns what the stopword was + the string inside the braces +/// on failure returns None. +/// +/// ```rust +/// # use azul_css_parser::parse_parentheses; +/// # use azul_css_parser::ParenthesisParseError::*; +/// // Search for the nearest "abc()" brace +/// assert_eq!(parse_parentheses("abc(def(g))", &["abc"]), Ok(("abc", "def(g)"))); +/// assert_eq!(parse_parentheses("abc(def(g))", &["def"]), Err(StopWordNotFound("abc"))); +/// assert_eq!(parse_parentheses("def(ghi(j))", &["def"]), Ok(("def", "ghi(j)"))); +/// assert_eq!(parse_parentheses("abc(def(g))", &["abc", "def"]), Ok(("abc", "def(g)"))); +/// ``` +pub fn parse_parentheses<'a>( + input: &'a str, + stopwords: &[&'static str]) +-> Result<(&'static str, &'a str), ParenthesisParseError<'a>> +{ + use self::ParenthesisParseError::*; + + let input = input.trim(); + if input.is_empty() { + return Err(EmptyInput); + } + + let first_open_brace = input.find('(').ok_or(NoOpeningBraceFound)?; + let found_stopword = &input[..first_open_brace]; + + // CSS does not allow for space between the ( and the stopword, so no .trim() here + let mut validated_stopword = None; + for stopword in stopwords { + if found_stopword == *stopword { + validated_stopword = Some(stopword); + break; + } + } + + let validated_stopword = validated_stopword.ok_or(StopWordNotFound(found_stopword))?; + let last_closing_brace = input.rfind(')').ok_or(NoClosingBraceFound)?; + + Ok((validated_stopword, &input[(first_open_brace + 1)..last_closing_brace])) +} + +multi_type_parser!(parse_style_cursor, StyleCursor, + ["alias", Alias], + ["all-scroll", AllScroll], + ["cell", Cell], + ["col-resize", ColResize], + ["context-menu", ContextMenu], + ["copy", Copy], + ["crosshair", Crosshair], + ["default", Default], + ["e-resize", EResize], + ["ew-resize", EwResize], + ["grab", Grab], + ["grabbing", Grabbing], + ["help", Help], + ["move", Move], + ["n-resize", NResize], + ["ns-resize", NsResize], + ["nesw-resize", NeswResize], + ["nwse-resize", NwseResize], + ["pointer", Pointer], + ["progress", Progress], + ["row-resize", RowResize], + ["s-resize", SResize], + ["se-resize", SeResize], + ["text", Text], + ["unset", Unset], + ["vertical-text", VerticalText], + ["w-resize", WResize], + ["wait", Wait], + ["zoom-in", ZoomIn], + ["zoom-out", ZoomOut]); + +multi_type_parser!(parse_style_background_size, StyleBackgroundSize, + ["contain", Contain], + ["cover", Cover]); + +multi_type_parser!(parse_style_background_repeat, StyleBackgroundRepeat, + ["no-repeat", NoRepeat], + ["repeat", Repeat], + ["repeat-x", RepeatX], + ["repeat-y", RepeatY]); + +multi_type_parser!(parse_layout_direction, LayoutDirection, + ["row", Row], + ["row-reverse", RowReverse], + ["column", Column], + ["column-reverse", ColumnReverse]); + +multi_type_parser!(parse_layout_wrap, LayoutWrap, + ["wrap", Wrap], + ["nowrap", NoWrap]); + +multi_type_parser!(parse_layout_justify_content, LayoutJustifyContent, + ["flex-start", Start], + ["flex-end", End], + ["center", Center], + ["space-between", SpaceBetween], + ["space-around", SpaceAround]); + +multi_type_parser!(parse_layout_align_items, LayoutAlignItems, + ["flex-start", Start], + ["flex-end", End], + ["stretch", Stretch], + ["center", Center]); + +multi_type_parser!(parse_layout_align_content, LayoutAlignContent, + ["flex-start", Start], + ["flex-end", End], + ["stretch", Stretch], + ["center", Center], + ["space-between", SpaceBetween], + ["space-around", SpaceAround]); + +multi_type_parser!(parse_shape, Shape, + ["circle", Circle], + ["ellipse", Ellipse]); + +multi_type_parser!(parse_layout_position, LayoutPosition, + ["static", Static], + ["absolute", Absolute], + ["relative", Relative]); + +multi_type_parser!(parse_layout_text_overflow, Overflow, + ["auto", Auto], + ["scroll", Scroll], + ["visible", Visible], + ["hidden", Hidden]); + +multi_type_parser!(parse_layout_text_align, StyleTextAlignmentHorz, + ["center", Center], + ["left", Left], + ["right", Right]); + +#[cfg(test)] +mod css_tests { + use super::*; + + #[test] + fn test_parse_box_shadow_1() { + assert_eq!(parse_css_box_shadow("none"), Ok(None)); + } + + #[test] + fn test_parse_box_shadow_2() { + assert_eq!(parse_css_box_shadow("5px 10px"), Ok(Some(BoxShadowPreDisplayItem { + offset: [PixelValue::px(5.0), PixelValue::px(10.0)], + color: ColorU { r: 0, g: 0, b: 0, a: 255 }, + blur_radius: PixelValue::px(0.0), + spread_radius: PixelValue::px(0.0), + clip_mode: BoxShadowClipMode::Outset, + }))); + } + + #[test] + fn test_parse_box_shadow_3() { + assert_eq!(parse_css_box_shadow("5px 10px #888888"), Ok(Some(BoxShadowPreDisplayItem { + offset: [PixelValue::px(5.0), PixelValue::px(10.0)], + color: ColorU { r: 136, g: 136, b: 136, a: 255 }, + blur_radius: PixelValue::px(0.0), + spread_radius: PixelValue::px(0.0), + clip_mode: BoxShadowClipMode::Outset, + }))); + } + + #[test] + fn test_parse_box_shadow_4() { + assert_eq!(parse_css_box_shadow("5px 10px inset"), Ok(Some(BoxShadowPreDisplayItem { + offset: [PixelValue::px(5.0), PixelValue::px(10.0)], + color: ColorU { r: 0, g: 0, b: 0, a: 255 }, + blur_radius: PixelValue::px(0.0), + spread_radius: PixelValue::px(0.0), + clip_mode: BoxShadowClipMode::Inset, + }))); + } + + #[test] + fn test_parse_box_shadow_5() { + assert_eq!(parse_css_box_shadow("5px 10px outset"), Ok(Some(BoxShadowPreDisplayItem { + offset: [PixelValue::px(5.0), PixelValue::px(10.0)], + color: ColorU { r: 0, g: 0, b: 0, a: 255 }, + blur_radius: PixelValue::px(0.0), + spread_radius: PixelValue::px(0.0), + clip_mode: BoxShadowClipMode::Outset, + }))); + } + + #[test] + fn test_parse_box_shadow_6() { + assert_eq!(parse_css_box_shadow("5px 10px 5px #888888"), Ok(Some(BoxShadowPreDisplayItem { + offset: [PixelValue::px(5.0), PixelValue::px(10.0)], + color: ColorU { r: 136, g: 136, b: 136, a: 255 }, + blur_radius: PixelValue::px(5.0), + spread_radius: PixelValue::px(0.0), + clip_mode: BoxShadowClipMode::Outset, + }))); + } + + #[test] + fn test_parse_box_shadow_7() { + assert_eq!(parse_css_box_shadow("5px 10px #888888 inset"), Ok(Some(BoxShadowPreDisplayItem { + offset: [PixelValue::px(5.0), PixelValue::px(10.0)], + color: ColorU { r: 136, g: 136, b: 136, a: 255 }, + blur_radius: PixelValue::px(0.0), + spread_radius: PixelValue::px(0.0), + clip_mode: BoxShadowClipMode::Inset, + }))); + } + + #[test] + fn test_parse_box_shadow_8() { + assert_eq!(parse_css_box_shadow("5px 10px 5px #888888 inset"), Ok(Some(BoxShadowPreDisplayItem { + offset: [PixelValue::px(5.0), PixelValue::px(10.0)], + color: ColorU { r: 136, g: 136, b: 136, a: 255 }, + blur_radius: PixelValue::px(5.0), + spread_radius: PixelValue::px(0.0), + clip_mode: BoxShadowClipMode::Inset, + }))); + } + + #[test] + fn test_parse_box_shadow_9() { + assert_eq!(parse_css_box_shadow("5px 10px 5px 10px #888888"), Ok(Some(BoxShadowPreDisplayItem { + offset: [PixelValue::px(5.0), PixelValue::px(10.0)], + color: ColorU { r: 136, g: 136, b: 136, a: 255 }, + blur_radius: PixelValue::px(5.0), + spread_radius: PixelValue::px(10.0), + clip_mode: BoxShadowClipMode::Outset, + }))); + } + + #[test] + fn test_parse_box_shadow_10() { + assert_eq!(parse_css_box_shadow("5px 10px 5px 10px #888888 inset"), Ok(Some(BoxShadowPreDisplayItem { + offset: [PixelValue::px(5.0), PixelValue::px(10.0)], + color: ColorU { r: 136, g: 136, b: 136, a: 255 }, + blur_radius: PixelValue::px(5.0), + spread_radius: PixelValue::px(10.0), + clip_mode: BoxShadowClipMode::Inset, + }))); + } + + #[test] + fn test_parse_css_border_1() { + assert_eq!( + parse_css_border("5px solid red"), + Ok(StyleBorderSide { + border_width: PixelValue::px(5.0), + border_style: BorderStyle::Solid, + border_color: ColorU { r: 255, g: 0, b: 0, a: 255 }, + }) + ); + } + + #[test] + fn test_parse_css_border_2() { + assert_eq!( + parse_css_border("double"), + Ok(StyleBorderSide { + border_width: PixelValue::px(3.0), + border_style: BorderStyle::Double, + border_color: ColorU { r: 0, g: 0, b: 0, a: 255 }, + }) + ); + } + + #[test] + fn test_parse_css_border_3() { + assert_eq!( + parse_css_border("1px solid rgb(51, 153, 255)"), + Ok(StyleBorderSide { + border_width: PixelValue::px(1.0), + border_style: BorderStyle::Solid, + border_color: ColorU { r: 51, g: 153, b: 255, a: 255 }, + }) + ); + } + + #[test] + fn test_parse_linear_gradient_1() { + assert_eq!(parse_style_background("linear-gradient(red, yellow)"), + Ok(StyleBackground::LinearGradient(LinearGradient { + direction: Direction::FromTo(DirectionCorner::Top, DirectionCorner::Bottom), + extend_mode: ExtendMode::Clamp, + stops: vec![GradientStopPre { + offset: Some(PercentageValue::new(0.0)), + color: ColorU { r: 255, g: 0, b: 0, a: 255 }, + }, + GradientStopPre { + offset: Some(PercentageValue::new(100.0)), + color: ColorU { r: 255, g: 255, b: 0, a: 255 }, + }], + }))); + } + + #[test] + fn test_parse_linear_gradient_2() { + assert_eq!(parse_style_background("linear-gradient(red, lime, blue, yellow)"), + Ok(StyleBackground::LinearGradient(LinearGradient { + direction: Direction::FromTo(DirectionCorner::Top, DirectionCorner::Bottom), + extend_mode: ExtendMode::Clamp, + stops: vec![GradientStopPre { + offset: Some(PercentageValue::new(0.0)), + color: ColorU { r: 255, g: 0, b: 0, a: 255 }, + }, + GradientStopPre { + offset: Some(PercentageValue::new(33.333332)), + color: ColorU { r: 0, g: 255, b: 0, a: 255 }, + }, + GradientStopPre { + offset: Some(PercentageValue::new(66.666664)), + color: ColorU { r: 0, g: 0, b: 255, a: 255 }, + }, + GradientStopPre { + offset: Some(PercentageValue::new(99.9999)), // note: not 100%, but close enough + color: ColorU { r: 255, g: 255, b: 0, a: 255 }, + }], + }))); + } + + #[test] + fn test_parse_linear_gradient_3() { + assert_eq!(parse_style_background("repeating-linear-gradient(50deg, blue, yellow, #00FF00)"), + Ok(StyleBackground::LinearGradient(LinearGradient { + direction: Direction::Angle(50.0.into()), + extend_mode: ExtendMode::Repeat, + stops: vec![ + GradientStopPre { + offset: Some(PercentageValue::new(0.0)), + color: ColorU { r: 0, g: 0, b: 255, a: 255 }, + }, + GradientStopPre { + offset: Some(PercentageValue::new(50.0)), + color: ColorU { r: 255, g: 255, b: 0, a: 255 }, + }, + GradientStopPre { + offset: Some(PercentageValue::new(100.0)), + color: ColorU { r: 0, g: 255, b: 0, a: 255 }, + }], + }))); + } + + #[test] + fn test_parse_linear_gradient_4() { + assert_eq!(parse_style_background("linear-gradient(to bottom right, red, yellow)"), + Ok(StyleBackground::LinearGradient(LinearGradient { + direction: Direction::FromTo(DirectionCorner::TopLeft, DirectionCorner::BottomRight), + extend_mode: ExtendMode::Clamp, + stops: vec![GradientStopPre { + offset: Some(PercentageValue::new(0.0)), + color: ColorU { r: 255, g: 0, b: 0, a: 255 }, + }, + GradientStopPre { + offset: Some(PercentageValue::new(100.0)), + color: ColorU { r: 255, g: 255, b: 0, a: 255 }, + }], + }) + )); + } + + #[test] + fn test_parse_linear_gradient_5() { + assert_eq!(parse_style_background("linear-gradient(0.42rad, red, yellow)"), + Ok(StyleBackground::LinearGradient(LinearGradient { + direction: Direction::Angle(FloatValue::new(24.0642)), + extend_mode: ExtendMode::Clamp, + stops: vec![GradientStopPre { + offset: Some(PercentageValue::new(0.0)), + color: ColorU { r: 255, g: 0, b: 0, a: 255 }, + }, + GradientStopPre { + offset: Some(PercentageValue::new(100.0)), + color: ColorU { r: 255, g: 255, b: 0, a: 255 }, + }], + }))); + } + + #[test] + fn test_parse_linear_gradient_6() { + assert_eq!(parse_style_background("linear-gradient(12.93grad, red, yellow)"), + Ok(StyleBackground::LinearGradient(LinearGradient { + direction: Direction::Angle(FloatValue::new(11.637)), + extend_mode: ExtendMode::Clamp, + stops: vec![GradientStopPre { + offset: Some(PercentageValue::new(0.0)), + color: ColorU { r: 255, g: 0, b: 0, a: 255 }, + }, + GradientStopPre { + offset: Some(PercentageValue::new(100.0)), + color: ColorU { r: 255, g: 255, b: 0, a: 255 }, + }], + }))); + } + + #[test] + fn test_parse_linear_gradient_7() { + assert_eq!(parse_style_background("linear-gradient(to right, rgba(255,0, 0,1) 0%,rgba(0,0,0, 0) 100%)"), + Ok(StyleBackground::LinearGradient(LinearGradient { + direction: Direction::FromTo(DirectionCorner::Left, DirectionCorner::Right), + extend_mode: ExtendMode::Clamp, + stops: vec![GradientStopPre { + offset: Some(PercentageValue::new(0.0)), + color: ColorU { r: 255, g: 0, b: 0, a: 255 }, + }, + GradientStopPre { + offset: Some(PercentageValue::new(100.0)), + color: ColorU { r: 0, g: 0, b: 0, a: 0 }, + }], + }) + )); + } + + #[test] + fn test_parse_linear_gradient_8() { + assert_eq!(parse_style_background("linear-gradient(to bottom, rgb(255,0, 0),rgb(0,0,0))"), + Ok(StyleBackground::LinearGradient(LinearGradient { + direction: Direction::FromTo(DirectionCorner::Top, DirectionCorner::Bottom), + extend_mode: ExtendMode::Clamp, + stops: vec![GradientStopPre { + offset: Some(PercentageValue::new(0.0)), + color: ColorU { r: 255, g: 0, b: 0, a: 255 }, + }, + GradientStopPre { + offset: Some(PercentageValue::new(100.0)), + color: ColorU { r: 0, g: 0, b: 0, a: 255 }, + }], + }) + )); + } + + #[test] + fn test_parse_linear_gradient_9() { + assert_eq!(parse_style_background("linear-gradient(10deg, rgb(10, 30, 20), yellow)"), + Ok(StyleBackground::LinearGradient(LinearGradient { + direction: Direction::Angle(FloatValue::new(10.0)), + extend_mode: ExtendMode::Clamp, + stops: vec![GradientStopPre { + offset: Some(PercentageValue::new(0.0)), + color: ColorU { r: 10, g: 30, b: 20, a: 255 }, + }, + GradientStopPre { + offset: Some(PercentageValue::new(100.0)), + color: ColorU { r: 255, g: 255, b: 0, a: 255 }, + }], + }))); + } + + #[test] + fn test_parse_linear_gradient_10() { + assert_eq!(parse_style_background("linear-gradient(50deg, rgba(10, 30, 20, 0.93), hsla(40deg, 80%, 30%, 0.1))"), + Ok(StyleBackground::LinearGradient(LinearGradient { + direction: Direction::Angle(FloatValue::new(50.0)), + extend_mode: ExtendMode::Clamp, + stops: vec![GradientStopPre { + offset: Some(PercentageValue::new(0.0)), + color: ColorU { r: 10, g: 30, b: 20, a: 238 }, + }, + GradientStopPre { + offset: Some(PercentageValue::new(100.0)), + color: ColorU { r: 138, g: 97, b: 15, a: 25 }, + }], + }))); + } + + #[test] + fn test_parse_linear_gradient_11() { + // wacky whitespace on purpose + assert_eq!(parse_style_background("linear-gradient(to bottom,rgb(255,0, 0)0%, rgb( 0 , 255 , 0 ) 10% ,blue 100% )"), + Ok(StyleBackground::LinearGradient(LinearGradient { + direction: Direction::FromTo(DirectionCorner::Top, DirectionCorner::Bottom), + extend_mode: ExtendMode::Clamp, + stops: vec![GradientStopPre { + offset: Some(PercentageValue::new(0.0)), + color: ColorU { r: 255, g: 0, b: 0, a: 255 }, + }, + GradientStopPre { + offset: Some(PercentageValue::new(10.0)), + color: ColorU { r: 0, g: 255, b: 0, a: 255 }, + }, + GradientStopPre { + offset: Some(PercentageValue::new(100.0)), + color: ColorU { r: 0, g: 0, b: 255, a: 255 }, + }], + }) + )); + } + + #[test] + fn test_parse_radial_gradient_1() { + assert_eq!(parse_style_background("radial-gradient(circle, lime, blue, yellow)"), + Ok(StyleBackground::RadialGradient(RadialGradient { + shape: Shape::Circle, + extend_mode: ExtendMode::Clamp, + stops: vec![ + GradientStopPre { + offset: Some(PercentageValue::new(0.0)), + color: ColorU { r: 0, g: 255, b: 0, a: 255 }, + }, + GradientStopPre { + offset: Some(PercentageValue::new(50.0)), + color: ColorU { r: 0, g: 0, b: 255, a: 255 }, + }, + GradientStopPre { + offset: Some(PercentageValue::new(100.0)), + color: ColorU { r: 255, g: 255, b: 0, a: 255 }, + }], + }))); + } + + // This test currently fails, but it's not that important to fix right now + /* + #[test] + fn test_parse_radial_gradient_2() { + assert_eq!(parse_style_background("repeating-radial-gradient(circle, red 10%, blue 50%, lime, yellow)"), + Ok(ParsedGradient::RadialGradient(RadialGradient { + shape: Shape::Circle, + extend_mode: ExtendMode::Repeat, + stops: vec![ + GradientStopPre { + offset: Some(0.1), + color: ColorF { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, + }, + GradientStopPre { + offset: Some(0.5), + color: ColorF { r: 0.0, g: 0.0, b: 1.0, a: 1.0 }, + }, + GradientStopPre { + offset: Some(0.75), + color: ColorF { r: 0.0, g: 1.0, b: 0.0, a: 1.0 }, + }, + GradientStopPre { + offset: Some(1.0), + color: ColorF { r: 1.0, g: 1.0, b: 0.0, a: 1.0 }, + }], + }))); + } + */ + + #[test] + fn test_parse_css_color_1() { + assert_eq!(parse_css_color("#F0F8FF"), Ok(ColorU { r: 240, g: 248, b: 255, a: 255 })); + } + + #[test] + fn test_parse_css_color_2() { + assert_eq!(parse_css_color("#F0F8FF00"), Ok(ColorU { r: 240, g: 248, b: 255, a: 0 })); + } + + #[test] + fn test_parse_css_color_3() { + assert_eq!(parse_css_color("#EEE"), Ok(ColorU { r: 238, g: 238, b: 238, a: 255 })); + } + + #[test] + fn test_parse_css_color_4() { + assert_eq!(parse_css_color("rgb(192, 14, 12)"), Ok(ColorU { r: 192, g: 14, b: 12, a: 255 })); + } + + #[test] + fn test_parse_css_color_5() { + assert_eq!(parse_css_color("rgb(283, 8, 105)"), Err(CssColorParseError::IntValueParseErr("283".parse::().err().unwrap()))); + } + + #[test] + fn test_parse_css_color_6() { + assert_eq!(parse_css_color("rgba(192, 14, 12, 80)"), Err(CssColorParseError::FloatValueOutOfRange(80.0))); + } + + #[test] + fn test_parse_css_color_7() { + assert_eq!(parse_css_color("rgba( 0,127, 255 , 0.25 )"), Ok(ColorU { r: 0, g: 127, b: 255, a: 64 })); + } + + #[test] + fn test_parse_css_color_8() { + assert_eq!(parse_css_color("rgba( 1 ,2,3, 1.0)"), Ok(ColorU { r: 1, g: 2, b: 3, a: 255 })); + } + + #[test] + fn test_parse_css_color_9() { + assert_eq!(parse_css_color("rgb("), Err(CssColorParseError::UnclosedColor("rgb("))); + } + + #[test] + fn test_parse_css_color_10() { + assert_eq!(parse_css_color("rgba("), Err(CssColorParseError::UnclosedColor("rgba("))); + } + + #[test] + fn test_parse_css_color_11() { + assert_eq!(parse_css_color("rgba(123, 36, 92, 0.375"), Err(CssColorParseError::UnclosedColor("rgba(123, 36, 92, 0.375"))); + } + + #[test] + fn test_parse_css_color_12() { + assert_eq!(parse_css_color("rgb()"), Err(CssColorParseError::MissingColorComponent(CssColorComponent::Red))); + } + + #[test] + fn test_parse_css_color_13() { + assert_eq!(parse_css_color("rgb(10)"), Err(CssColorParseError::MissingColorComponent(CssColorComponent::Green))); + } + + #[test] + fn test_parse_css_color_14() { + assert_eq!(parse_css_color("rgb(20, 30)"), Err(CssColorParseError::MissingColorComponent(CssColorComponent::Blue))); + } + + #[test] + fn test_parse_css_color_15() { + assert_eq!(parse_css_color("rgb(30, 40,)"), Err(CssColorParseError::MissingColorComponent(CssColorComponent::Blue))); + } + + #[test] + fn test_parse_css_color_16() { + assert_eq!(parse_css_color("rgba(40, 50, 60)"), Err(CssColorParseError::MissingColorComponent(CssColorComponent::Alpha))); + } + + #[test] + fn test_parse_css_color_17() { + assert_eq!(parse_css_color("rgba(50, 60, 70, )"), Err(CssColorParseError::MissingColorComponent(CssColorComponent::Alpha))); + } + + #[test] + fn test_parse_css_color_18() { + assert_eq!(parse_css_color("hsl(0deg, 100%, 100%)"), Ok(ColorU { r: 255, g: 255, b: 255, a: 255 })); + } + + #[test] + fn test_parse_css_color_19() { + assert_eq!(parse_css_color("hsl(0deg, 100%, 50%)"), Ok(ColorU { r: 255, g: 0, b: 0, a: 255 })); + } + + #[test] + fn test_parse_css_color_20() { + assert_eq!(parse_css_color("hsl(170deg, 50%, 75%)"), Ok(ColorU { r: 160, g: 224, b: 213, a: 255 })); + } + + #[test] + fn test_parse_css_color_21() { + assert_eq!(parse_css_color("hsla(190deg, 50%, 75%, 1.0)"), Ok(ColorU { r: 160, g: 213, b: 224, a: 255 })); + } + + #[test] + fn test_parse_css_color_22() { + assert_eq!(parse_css_color("hsla(120deg, 0%, 25%, 0.25)"), Ok(ColorU { r: 64, g: 64, b: 64, a: 64 })); + } + + #[test] + fn test_parse_css_color_23() { + assert_eq!(parse_css_color("hsla(120deg, 0%, 0%, 0.5)"), Ok(ColorU { r: 0, g: 0, b: 0, a: 128 })); + } + + #[test] + fn test_parse_css_color_24() { + assert_eq!(parse_css_color("hsla(60.9deg, 80.3%, 40%, 0.5)"), Ok(ColorU { r: 182, g: 184, b: 20, a: 128 })); + } + + #[test] + fn test_parse_css_color_25() { + assert_eq!(parse_css_color("hsla(60.9rad, 80.3%, 40%, 0.5)"), Ok(ColorU { r: 45, g: 20, b: 184, a: 128 })); + } + + #[test] + fn test_parse_css_color_26() { + assert_eq!(parse_css_color("hsla(60.9grad, 80.3%, 40%, 0.5)"), Ok(ColorU { r: 184, g: 170, b: 20, a: 128 })); + } + + #[test] + fn test_parse_direction() { + let first_input = "60.9grad"; + let e = FloatValue::new(first_input.split("grad").next().unwrap().parse::().expect("Parseable float") / 400.0 * 360.0); + assert_eq!(e, FloatValue::new(60.9 / 400.0 * 360.0)); + assert_eq!(parse_direction("60.9grad"), Ok(Direction::Angle(FloatValue::new(60.9 / 400.0 * 360.0)))); + } + + #[test] + fn test_parse_float_value() { + assert_eq!(parse_float_value("60.9"), Ok(FloatValue::new(60.9))); + } + + #[test] + fn test_parse_css_color_27() { + assert_eq!(parse_css_color("hsla(240, 0%, 0%, 0.5)"), Ok(ColorU { r: 0, g: 0, b: 0, a: 128 })); + } + + #[test] + fn test_parse_css_color_28() { + assert_eq!(parse_css_color("hsla(240deg, 0, 0%, 0.5)"), Err(CssColorParseError::InvalidPercentage(PercentageParseError::NoPercentSign))); + } + + #[test] + fn test_parse_css_color_29() { + assert_eq!(parse_css_color("hsla(240deg, 0%, 0, 0.5)"), Err(CssColorParseError::InvalidPercentage(PercentageParseError::NoPercentSign))); + } + + #[test] + fn test_parse_css_color_30() { + assert_eq!(parse_css_color("hsla(240deg, 0%, 0%, )"), Err(CssColorParseError::MissingColorComponent(CssColorComponent::Alpha))); + } + + #[test] + fn test_parse_css_color_31() { + assert_eq!(parse_css_color("hsl(, 0%, 0%, )"), Err(CssColorParseError::MissingColorComponent(CssColorComponent::Hue))); + } + + #[test] + fn test_parse_css_color_32() { + assert_eq!(parse_css_color("hsl(240deg , )"), Err(CssColorParseError::MissingColorComponent(CssColorComponent::Saturation))); + } + + #[test] + fn test_parse_css_color_33() { + assert_eq!(parse_css_color("hsl(240deg, 0%, )"), Err(CssColorParseError::MissingColorComponent(CssColorComponent::Lightness))); + } + + #[test] + fn test_parse_css_color_34() { + assert_eq!(parse_css_color("hsl(240deg, 0%, 0%, )"), Err(CssColorParseError::ExtraArguments(""))); + } + + #[test] + fn test_parse_css_color_35() { + assert_eq!(parse_css_color("hsla(240deg, 0%, 0% )"), Err(CssColorParseError::MissingColorComponent(CssColorComponent::Alpha))); + } + + #[test] + fn test_parse_css_color_36() { + assert_eq!(parse_css_color("rgb(255,0, 0)"), Ok(ColorU { r: 255, g: 0, b: 0, a: 255 })); + } + + #[test] + fn test_parse_pixel_value_1() { + assert_eq!(parse_pixel_value("15px"), Ok(PixelValue::px(15.0))); + } + + #[test] + fn test_parse_pixel_value_2() { + assert_eq!(parse_pixel_value("1.2em"), Ok(PixelValue::em(1.2))); + } + + #[test] + fn test_parse_pixel_value_3() { + assert_eq!(parse_pixel_value("11pt"), Ok(PixelValue::pt(11.0))); + } + + #[test] + fn test_parse_pixel_value_4() { + assert_eq!(parse_pixel_value("aslkfdjasdflk"), Err(PixelParseError::NoValueGiven("aslkfdjasdflk"))); + } + + #[test] + fn test_parse_style_border_radius_1() { + assert_eq!(parse_style_border_radius("15px"), Ok(StyleBorderRadius( + BorderRadius::uniform(PixelSize::new(PixelValue::px(15.0), PixelValue::px(15.0))) + ))); + } + + #[test] + fn test_parse_style_border_radius_2() { + assert_eq!(parse_style_border_radius("15px 50px"), Ok(StyleBorderRadius(BorderRadius { + top_left: PixelSize::new(PixelValue::px(15.0), PixelValue::px(15.0)), + bottom_right: PixelSize::new(PixelValue::px(15.0), PixelValue::px(15.0)), + top_right: PixelSize::new(PixelValue::px(50.0), PixelValue::px(50.0)), + bottom_left: PixelSize::new(PixelValue::px(50.0), PixelValue::px(50.0)), + }))); + } + + #[test] + fn test_parse_style_border_radius_3() { + assert_eq!(parse_style_border_radius("15px 50px 30px"), Ok(StyleBorderRadius(BorderRadius { + top_left: PixelSize::new(PixelValue::px(15.0), PixelValue::px(15.0)), + bottom_right: PixelSize::new(PixelValue::px(30.0), PixelValue::px(30.0)), + top_right: PixelSize::new(PixelValue::px(50.0), PixelValue::px(50.0)), + bottom_left: PixelSize::new(PixelValue::px(50.0), PixelValue::px(50.0)), + }))); + } + + #[test] + fn test_parse_style_border_radius_4() { + assert_eq!(parse_style_border_radius("15px 50px 30px 5px"), Ok(StyleBorderRadius(BorderRadius { + top_left: PixelSize::new(PixelValue::px(15.0), PixelValue::px(15.0)), + bottom_right: PixelSize::new(PixelValue::px(30.0), PixelValue::px(30.0)), + top_right: PixelSize::new(PixelValue::px(50.0), PixelValue::px(50.0)), + bottom_left: PixelSize::new(PixelValue::px(5.0), PixelValue::px(5.0)), + }))); + } + + #[test] + fn test_parse_style_font_family_1() { + assert_eq!(parse_style_font_family("\"Webly Sleeky UI\", monospace"), Ok(StyleFontFamily { + fonts: vec![ + FontId("Webly Sleeky UI".into()), + FontId("monospace".into()), + ] + })); + } + + #[test] + fn test_parse_style_font_family_2() { + assert_eq!(parse_style_font_family("'Webly Sleeky UI'"), Ok(StyleFontFamily { + fonts: vec![ + FontId("Webly Sleeky UI".into()), + ] + })); + } + + #[test] + fn test_parse_background_image() { + assert_eq!(parse_style_background("image(\"Cat 01\")"), Ok(StyleBackground::Image( + CssImageId(String::from("Cat 01")) + ))); + } + + #[test] + fn test_parse_padding_1() { + assert_eq!(parse_layout_padding("10px"), Ok(LayoutPadding { + top: Some(PixelValue::px(10.0)), + right: Some(PixelValue::px(10.0)), + bottom: Some(PixelValue::px(10.0)), + left: Some(PixelValue::px(10.0)), + })); + } + + #[test] + fn test_parse_padding_2() { + assert_eq!(parse_layout_padding("25px 50px"), Ok(LayoutPadding { + top: Some(PixelValue::px(25.0)), + right: Some(PixelValue::px(50.0)), + bottom: Some(PixelValue::px(25.0)), + left: Some(PixelValue::px(50.0)), + })); + } + + #[test] + fn test_parse_padding_3() { + assert_eq!(parse_layout_padding("25px 50px 75px"), Ok(LayoutPadding { + top: Some(PixelValue::px(25.0)), + right: Some(PixelValue::px(50.0)), + left: Some(PixelValue::px(50.0)), + bottom: Some(PixelValue::px(75.0)), + })); + } + + #[test] + fn test_parse_padding_4() { + assert_eq!(parse_layout_padding("25px 50px 75px 100px"), Ok(LayoutPadding { + top: Some(PixelValue::px(25.0)), + right: Some(PixelValue::px(50.0)), + bottom: Some(PixelValue::px(75.0)), + left: Some(PixelValue::px(100.0)), + })); + } +} diff --git a/azul-css-parser/src/hot_reloader.rs b/azul-css-parser/src/hot_reloader.rs new file mode 100644 index 000000000..2e53dafc0 --- /dev/null +++ b/azul-css-parser/src/hot_reloader.rs @@ -0,0 +1,45 @@ +//! Provides an implementation of the HotReloadHandler from the `azul_css` crate, allowing CSS +//! files to be dynamically reloaded at runtime. + +use azul_css::{HotReloadHandler, Css}; +use std::time::Duration; +use std::path::PathBuf; + +pub const DEFAULT_RELOAD_INTERVAL: Duration = Duration::from_millis(500); + +/// Allows dynamic reloading of a CSS file at application runtime. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct HotReloader { + file_path: PathBuf, + reload_interval: Duration, +} + +impl HotReloader { + /// Creates a HotReloader that will load a style directly from the CSS file + /// at the given path. + pub fn new>(file_path: P) -> Self { + Self { file_path: file_path.into(), reload_interval: DEFAULT_RELOAD_INTERVAL } + } + + pub fn with_reload_interval(self, reload_interval: Duration) -> Self { + Self { reload_interval, .. self } + } +} + +impl HotReloadHandler for HotReloader { + fn reload_style(&mut self) -> Result { + use std::fs; + use crate::css; + + let file_name = self.file_path.file_name().map(|os_str| os_str.to_string_lossy()).unwrap_or_default(); + + let reloaded_css = fs::read_to_string(&self.file_path) + .map_err(|e| format!("Io error: Could not load \"{}\" when loading file: \"{}\"", file_name, e))?; + + css::new_from_str(&reloaded_css).map_err(|e| format!("{}: {}", file_name, e)) + } + + fn get_reload_interval(&self) -> Duration { + self.reload_interval + } +} diff --git a/azul-css-parser/src/lib.rs b/azul-css-parser/src/lib.rs new file mode 100644 index 000000000..a4eca0178 --- /dev/null +++ b/azul-css-parser/src/lib.rs @@ -0,0 +1,138 @@ +//! Provides a reference implementation of a style parser for Azul, capable of parsing CSS +//! stylesheets into their respective `Css` counterparts. + +#![doc( + html_logo_url = "https://raw.githubusercontent.com/maps4print/azul/master/assets/images/azul_logo_full_min.svg.png", + html_favicon_url = "https://raw.githubusercontent.com/maps4print/azul/master/assets/images/favicon.ico", +)] + +#![warn(unused_must_use)] +#![deny(unreachable_patterns)] +#![deny(missing_copy_implementations)] +#![allow(unused_variables)] + +extern crate azul_css; +extern crate simplecss; +#[cfg(feature = "serde_serialization")] +extern crate serde; + +#[macro_use] +mod macros; + +mod css_parser; +mod css; +mod hot_reloader; + +pub use crate::css::{ + new_from_str, + parse_css_path, + CssParseError, + CssPathParseError, +}; + +pub use crate::css_parser::*; + +pub use crate::hot_reloader::{ + HotReloader, +}; + + +pub use crate::css_color::CssColor; + +pub mod css_color { + + use azul_css::{ColorU, ColorF}; + use crate::css_parser::{parse_css_color, CssColorParseError}; + + /// CssColor is simply a wrapper around the internal CSS color parsing methods. + /// + /// Sometimes you'd want to load and parse a CSS color, but you don't want to + /// write your own parser for that. Since Azul already has a parser for CSS colors, + /// this API exposes + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] + pub struct CssColor { + internal: ColorU, + } + + impl CssColor { + /// Can parse a CSS color with or without prefixed hash or capitalization, i.e. `#aabbcc` + pub fn from_str<'a>(input: &'a str) -> Result> { + let color = parse_css_color(input)?; + Ok(Self { + internal: color, + }) + } + + /// Returns the internal parsed color, but in a `0.0 - 1.0` range instead of `0 - 255` + pub fn to_color_f(&self) -> ColorF { + self.internal.into() + } + + /// Returns the internal parsed color + pub fn to_color_u(&self) -> ColorU { + self.internal + } + + /// If `prefix_hash` is set to false, you only get the string, without a hash, in lowercase + /// + /// If `self.alpha` is `FF`, it will be omitted from the final result (since `FF` is the default for CSS colors) + pub fn to_string(&self, prefix_hash: bool) -> String { + let prefix = if prefix_hash { "#" } else { "" }; + let alpha = if self.internal.a == 255 { String::new() } else { format!("{:02x}", self.internal.a) }; + format!("{}{:02x}{:02x}{:02x}{}", prefix, self.internal.r, self.internal.g, self.internal.b, alpha) + } + } + + impl From for CssColor { + fn from(color: ColorU) -> Self { + CssColor { internal: color } + } + } + + impl From for CssColor { + fn from(color: ColorF) -> Self { + CssColor { internal: color.into() } + } + } + + impl Into for CssColor { + fn into(self) -> ColorF { + self.to_color_f() + } + } + + impl Into for CssColor { + fn into(self) -> ColorU { + self.to_color_u() + } + } + + impl Into for CssColor { + fn into(self) -> String { + self.to_string(false) + } + } + + #[cfg(feature = "serde_serialization")] + use serde::{de, Serialize, Deserialize, Serializer, Deserializer}; + + #[cfg(feature = "serde_serialization")] + impl Serialize for CssColor { + fn serialize(&self, serializer: S) -> Result + where S: Serializer, + { + let prefix_css_color_with_hash = true; + serializer.serialize_str(&self.to_string(prefix_css_color_with_hash)) + } + } + + #[cfg(feature = "serde_serialization")] + impl<'de> Deserialize<'de> for CssColor { + fn deserialize(deserializer: D) -> Result + where D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + CssColor::from_str(&s).map_err(de::Error::custom) + } + } +} \ No newline at end of file diff --git a/azul-css-parser/src/macros.rs b/azul-css-parser/src/macros.rs new file mode 100644 index 000000000..3e9985422 --- /dev/null +++ b/azul-css-parser/src/macros.rs @@ -0,0 +1,104 @@ +/// Implement `Display` for an enum. +/// +/// Example usage: +/// ``` +/// enum Foo<'a> { +/// Bar(&'a str) +/// Baz(i32) +/// } +/// +/// impl_display!{ Foo<'a>, { +/// Bar(s) => s, +/// Baz(i) => format!("{}", i) +/// }} +/// ``` +macro_rules! impl_display { + // For a type with a lifetime + ($enum:ident<$lt:lifetime>, {$($variant:pat => $fmt_string:expr),+$(,)* }) => { + + impl<$lt> ::std::fmt::Display for $enum<$lt> { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + use self::$enum::*; + match &self { + $( + $variant => write!(f, "{}", $fmt_string), + )+ + } + } + } + + }; + + // For a type without a lifetime + ($enum:ident, {$($variant:pat => $fmt_string:expr),+$(,)* }) => { + + impl ::std::fmt::Display for $enum { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + use self::$enum::*; + match &self { + $( + $variant => write!(f, "{}", $fmt_string), + )+ + } + } + } + + }; +} + +/// Implements `Debug` to use `Display` instead - assumes the that the type has implemented `Display` +macro_rules! impl_debug_as_display { + // For a type with a lifetime + ($enum:ident<$lt:lifetime>) => { + + impl<$lt> ::std::fmt::Debug for $enum<$lt> { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + write!(f, "{}", self) + } + } + + }; + + // For a type without a lifetime + ($enum:ident) => { + + impl ::std::fmt::Debug for $enum { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + write!(f, "{}", self) + } + } + + }; +} + +/// Implement the `From` trait for any type. +/// Example usage: +/// ``` +/// enum MyError<'a> { +/// Bar(BarError<'a>) +/// Foo(FooError<'a>) +/// } +/// +/// impl_from!(BarError<'a>, Error::Bar); +/// impl_from!(BarError<'a>, Error::Bar); +/// +/// ``` +macro_rules! impl_from { + // From a type with a lifetime to a type which also has a lifetime + ($a:ident<$c:lifetime>, $b:ident::$enum_type:ident) => { + impl<$c> From<$a<$c>> for $b<$c> { + fn from(e: $a<$c>) -> Self { + $b::$enum_type(e) + } + } + }; + + // From a type without a lifetime to a type which also does not have a lifetime + ($a:ident, $b:ident::$enum_type:ident) => { + impl From<$a> for $b { + fn from(e: $a) -> Self { + $b::$enum_type(e) + } + } + }; +} diff --git a/azul-css/Cargo.toml b/azul-css/Cargo.toml new file mode 100644 index 000000000..7663bb2dd --- /dev/null +++ b/azul-css/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "azul-css" +version = "0.1.0" +authors = ["Felix Schütt "] +license = "MIT" +description = ''' + Common datatypes used for styling applications + using the Azul desktop GUI framework +''' +documentation = "https://docs.rs/azul" +homepage = "https://azul.rs/" +keywords = ["gui", "GUI", "user interface", "svg", "graphics", "style" ] +categories = ["gui"] +repository = "https://github.com/maps4print/azul" +readme = "README.md" +exclude = ["assets/*", "doc/*", "examples/*"] +autoexamples = false + +[dependencies] diff --git a/azul-css/src/css.rs b/azul-css/src/css.rs new file mode 100644 index 000000000..6ce923602 --- /dev/null +++ b/azul-css/src/css.rs @@ -0,0 +1,435 @@ +//! Types and methods used to describe the style of an application +use crate::css_properties::{CssProperty, CssPropertyType}; +use std::fmt; + +/// Css stylesheet - contains a parsed CSS stylesheet in "rule blocks", +/// i.e. blocks of key-value pairs associated with a selector path. +#[derive(Debug, Default, PartialEq, Clone)] +pub struct Css { + /// One CSS stylesheet can hold more than one sub-stylesheet: + /// For example, when overriding native styles, the `.sort_by_specificy()` function + /// should not mix the two stylesheets during sorting. + pub stylesheets: Vec, +} + +#[derive(Debug, Default, PartialEq, Clone)] +pub struct Stylesheet { + /// The style rules making up the document - for example, de-duplicated CSS rules + pub rules: Vec, +} + +impl From> for Stylesheet { + fn from(rules: Vec) -> Self { + Self { rules } + } +} + +/// Contains one parsed `key: value` pair, static or dynamic +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum CssDeclaration { + /// Static key-value pair, such as `width: 500px` + Static(CssProperty), + /// Dynamic key-value pair with default value, such as `width: [[ my_id | 500px ]]` + Dynamic(DynamicCssProperty), +} + +impl CssDeclaration { + + /// Returns the type of the property (i.e. the CSS key as a typed enum) + pub fn get_type(&self) -> CssPropertyType { + use css::CssDeclaration::*; + match self { + Static(s) => s.get_type(), + Dynamic(d) => d.property_type, + } + } + + /// Determines if the property will be inherited (applied to the children) + /// during the recursive application of the style on the DOM tree + pub fn is_inheritable(&self) -> bool { + use self::CssDeclaration::*; + match self { + Static(s) => s.get_type().is_inheritable(), + Dynamic(d) => d.is_inheritable(), + } + } + + /// Returns whether this rule affects only styling properties or layout + /// properties (that could trigger a re-layout) + pub fn can_trigger_relayout(&self) -> bool { + use self::CssDeclaration::*; + match self { + Static(s) => s.get_type().can_trigger_relayout(), + Dynamic(d) => d.can_trigger_relayout(), + } + } +} + +/// A `DynamicCssProperty` is a type of css property that can be changed on possibly +/// every frame by the Rust code - for example to implement an `On::Hover` behaviour. +/// +/// The syntax for such a property looks like this: +/// +/// ```no_run,ignore +/// #my_div { +/// padding: [[ my_dynamic_property_id | 400px ]]; +/// } +/// ``` +/// +/// Azul will register a dynamic property with the key "my_dynamic_property_id" +/// and the default value of 400px. If the property gets overridden during one frame, +/// the overridden property takes precedence. +/// +/// At runtime the style is immutable (which is a performance optimization - if we +/// can assume that the property never changes at runtime), we can do some optimizations on it. +/// Dynamic style properties can also be used for animations and conditional styles +/// (i.e. `hover`, `focus`, etc.), thereby leading to cleaner code, since all of these +/// special cases now use one single API. +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct DynamicCssProperty { + /// Key for this property + pub property_type: CssPropertyType, + /// The stringified ID of this property, i.e. the `"my_id"` in `width: [[ my_id | 500px ]]`. + pub dynamic_id: String, + /// Default value, used if the css property isn't overridden in this frame + /// i.e. the `500px` in `width: [[ my_id | 500px ]]`. + pub default: DynamicCssPropertyDefault, +} + +/// If this value is set to default, the css property will not exist if it isn't overriden. +/// An example where this is useful is when you want to say something like this: +/// +/// `width: [[ 400px | auto ]];` +/// +/// "If I set this property to width: 400px, then use exactly 400px. Otherwise use whatever the default width is." +/// If this property wouldn't exist, you could only set the default to "0px" or something like +/// that, meaning that if you don't override the property, then you'd set it to 0px - which is +/// different from `auto`, since `auto` has its width determined by how much space there is +/// available in the parent. +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum DynamicCssPropertyDefault { + Exact(CssProperty), + Auto, +} + +impl DynamicCssProperty { + pub fn is_inheritable(&self) -> bool { + // Dynamic style properties should not be inheritable, + // since that could lead to bugs - you set a property in Rust, suddenly + // the wrong UI component starts to react because it was inherited. + false + } + + pub fn can_trigger_relayout(&self) -> bool { + self.property_type.can_trigger_relayout() + } +} + +/// One block of rules that applies a bunch of rules to a "path" in the style, i.e. +/// `div#myid.myclass -> { ("justify-content", "center") }` +#[derive(Debug, Clone, PartialEq)] +pub struct CssRuleBlock { + /// The css path (full selector) of the style ruleset + pub path: CssPath, + /// `"justify-content: center"` => + /// `CssDeclaration::Static(CssProperty::JustifyContent(LayoutJustifyContent::Center))` + pub declarations: Vec, +} + +pub type CssContentGroup<'a> = Vec<&'a CssPathSelector>; + +/// Signifies the type (i.e. the discriminant value) of a DOM node +/// without carrying any of its associated data +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum NodeTypePath { + Div, + P, + Img, + Texture, + IFrame, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum NodeTypePathParseError<'a> { + Invalid(&'a str), +} + +impl<'a> fmt::Display for NodeTypePathParseError<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self { + NodeTypePathParseError::Invalid(e) => write!(f, "Invalid node type: {}", e), + } + } +} + +const NODE_TYPE_PATH_MAP: [(NodeTypePath, &'static str); 5] = [ + (NodeTypePath::Div, "div"), + (NodeTypePath::P, "p"), + (NodeTypePath::Img, "img"), + (NodeTypePath::Texture, "texture"), + (NodeTypePath::IFrame, "iframe"), +]; + +/// Parses the node type from a CSS string such as `"div"` => `NodeTypePath::Div` +impl NodeTypePath { + pub fn from_str(css_key: &str) -> Result { + NODE_TYPE_PATH_MAP.iter() + .find(|(_, k)| css_key == *k) + .and_then(|(v, _)| Some(*v)) + .ok_or(NodeTypePathParseError::Invalid(css_key)) + } +} + +impl fmt::Display for NodeTypePath { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + let display_string = NODE_TYPE_PATH_MAP.iter() + .find(|(v, _)| *self == *v) + .and_then(|(_, k)| Some(*k)) + .unwrap(); + + write!(f, "{}", display_string)?; + Ok(()) + } +} + +/// Represents a full CSS path (i.e. the "div#id.class" selector belonging to +/// a CSS "content group" (the following key-value block)). +/// +/// ```no_run,ignore +/// "#div > .my_class:focus" == +/// [ +/// CssPathSelector::Type(NodeTypePath::Div), +/// CssPathSelector::PseudoSelector(CssPathPseudoSelector::LimitChildren), +/// CssPathSelector::Class("my_class"), +/// CssPathSelector::PseudoSelector(CssPathPseudoSelector::Focus), +/// ] +#[derive(Clone, Hash, Default, PartialEq, Eq, PartialOrd, Ord)] +pub struct CssPath { + pub selectors: Vec, +} + +impl fmt::Display for CssPath { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + for selector in &self.selectors { + write!(f, "{}", selector)?; + } + Ok(()) + } +} + +impl fmt::Debug for CssPath { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + write!(f, "{}", self) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum CssPathSelector { + /// Represents the `*` selector + Global, + /// `div`, `p`, etc. + Type(NodeTypePath), + /// `.something` + Class(String), + /// `#something` + Id(String), + /// `:something` + PseudoSelector(CssPathPseudoSelector), + /// Represents the `>` selector + DirectChildren, + /// Represents the ` ` selector + Children, +} + +impl Default for CssPathSelector { + fn default() -> Self { + CssPathSelector::Global + } +} + +impl fmt::Display for CssPathSelector { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use self::CssPathSelector::*; + match &self { + Global => write!(f, "*"), + Type(n) => write!(f, "{}", n), + Class(c) => write!(f, ".{}", c), + Id(i) => write!(f, "#{}", i), + PseudoSelector(p) => write!(f, ":{}", p), + DirectChildren => write!(f, ">"), + Children => write!(f, " "), + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum CssPathPseudoSelector { + /// `:first` + First, + /// `:last` + Last, + /// `:nth-child` + NthChild(CssNthChildSelector), + /// `:hover` - mouse is over element + Hover, + /// `:active` - mouse is pressed and over element + Active, + /// `:focus` - element has received focus + Focus, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum CssNthChildSelector { + Number(usize), + Even, + Odd, + Pattern { repeat: usize, offset: usize }, +} + +impl fmt::Display for CssNthChildSelector { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use self::CssNthChildSelector::*; + match &self { + Number(u) => write!(f, "{}", u), + Even => write!(f, "even"), + Odd => write!(f, "odd"), + Pattern { repeat, offset } => write!(f, "{}n + {}", repeat, offset), + } + } +} + +impl fmt::Display for CssPathPseudoSelector { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use self::CssPathPseudoSelector::*; + match &self { + First => write!(f, "first"), + Last => write!(f, "last"), + NthChild(u) => write!(f, "nth-child({})", u), + Hover => write!(f, "hover"), + Active => write!(f, "active"), + Focus => write!(f, "focus"), + } + } +} + +impl Css { + + /// Creates a new, empty CSS with no stylesheets + pub fn new() -> Self { + Default::default() + } + + pub fn append(&mut self, css: Self) { + for stylesheet in css.stylesheets { + self.append_stylesheet(stylesheet); + } + } + + pub fn append_stylesheet(&mut self, styles: Stylesheet) { + self.stylesheets.push(styles); + } + + pub fn sort_by_specificity(&mut self) { + for stylesheet in &mut self.stylesheets { + stylesheet.sort_by_specificity() + } + } + + pub fn rules<'a>(&'a self) -> RuleIterator<'a> { + RuleIterator { + current_stylesheet: 0, + current_rule: 0, + css: self, + } + } +} + +pub struct RuleIterator<'a> { + current_stylesheet: usize, + current_rule: usize, + css: &'a Css, +} + +impl<'a> Iterator for RuleIterator<'a> { + type Item = &'a CssRuleBlock; + fn next(&mut self) -> Option<&'a CssRuleBlock> { + let current_stylesheet = self.css.stylesheets.get(self.current_stylesheet)?; + match current_stylesheet.rules.get(self.current_rule) { + Some(s) => { + self.current_rule += 1; + Some(s) + }, + None => { + self.current_rule = 0; + self.current_stylesheet += 1; + self.next() + } + } + } +} + + +impl Stylesheet { + + /// Creates a new stylesheet with no style rules. + pub fn new() -> Self { + Default::default() + } + + /// Sort the style rules by their weight, so that the rules are applied in the correct order. + /// Should always be called when a new style is loaded from an external source. + pub fn sort_by_specificity(&mut self) { + self.rules.sort_by(|a, b| get_specificity(&a.path).cmp(&get_specificity(&b.path))); + } +} + +/// Returns specificity of the given css path. Further information can be found on +/// [the w3 website](http://www.w3.org/TR/selectors/#specificity). +fn get_specificity(path: &CssPath) -> (usize, usize, usize, usize) { + let id_count = path.selectors.iter().filter(|x| if let CssPathSelector::Id(_) = x { true } else { false }).count(); + let class_count = path.selectors.iter().filter(|x| if let CssPathSelector::Class(_) = x { true } else { false }).count(); + let div_count = path.selectors.iter().filter(|x| if let CssPathSelector::Type(_) = x { true } else { false }).count(); + (id_count, class_count, div_count, path.selectors.len()) +} + +#[test] +fn test_specificity() { + use self::CssPathSelector::*; + assert_eq!(get_specificity(&CssPath { selectors: vec![Id("hello".into())] }), (1, 0, 0, 1)); + assert_eq!(get_specificity(&CssPath { selectors: vec![Class("hello".into())] }), (0, 1, 0, 1)); + assert_eq!(get_specificity(&CssPath { selectors: vec![Type(NodeTypePath::Div)] }), (0, 0, 1, 1)); + assert_eq!(get_specificity(&CssPath { selectors: vec![Id("hello".into()), Type(NodeTypePath::Div)] }), (1, 0, 1, 2)); +} + +// Assert that order of the style items is correct (in order of CSS path specificity, lowest-to-highest) +#[test] +fn test_specificity_sort() { + use self::CssPathSelector::*; + use crate::NodeTypePath::*; + + let mut input_style = Stylesheet { + rules: vec![ + // Rules are sorted from lowest-specificity to highest specificity + CssRuleBlock { path: CssPath { selectors: vec![Global] }, declarations: Vec::new() }, + CssRuleBlock { path: CssPath { selectors: vec![Global, Type(Div), Class("my_class".into()), Id("my_id".into())] }, declarations: Vec::new() }, + CssRuleBlock { path: CssPath { selectors: vec![Global, Type(Div), Id("my_id".into())] }, declarations: Vec::new() }, + CssRuleBlock { path: CssPath { selectors: vec![Global, Id("my_id".into())] }, declarations: Vec::new() }, + CssRuleBlock { path: CssPath { selectors: vec![Type(Div), Class("my_class".into()), Class("specific".into()), Id("my_id".into())] }, declarations: Vec::new() }, + ], + }; + + input_style.sort_by_specificity(); + + let expected_style = Stylesheet { + rules: vec![ + // Rules are sorted from lowest-specificity to highest specificity + CssRuleBlock { path: CssPath { selectors: vec![Global] }, declarations: Vec::new() }, + CssRuleBlock { path: CssPath { selectors: vec![Global, Id("my_id".into())] }, declarations: Vec::new() }, + CssRuleBlock { path: CssPath { selectors: vec![Global, Type(Div), Id("my_id".into())] }, declarations: Vec::new() }, + CssRuleBlock { path: CssPath { selectors: vec![Global, Type(Div), Class("my_class".into()), Id("my_id".into())] }, declarations: Vec::new() }, + CssRuleBlock { path: CssPath { selectors: vec![Type(Div), Class("my_class".into()), Class("specific".into()), Id("my_id".into())] }, declarations: Vec::new() }, + ], + }; + + assert_eq!(input_style, expected_style); +} \ No newline at end of file diff --git a/azul-css/src/css_properties.rs b/azul-css/src/css_properties.rs new file mode 100644 index 000000000..b55b58888 --- /dev/null +++ b/azul-css/src/css_properties.rs @@ -0,0 +1,1680 @@ +//! Provides a public API with datatypes used to describe style properties of DOM nodes. + +use std::collections::BTreeMap; +use std::fmt; + +/// Currently hard-coded: Height of one em in pixels +const EM_HEIGHT: f32 = 16.0; +/// WebRender measures in points, not in pixels! +const PT_TO_PX: f32 = 96.0 / 72.0; + +// The following types are present in webrender, however, azul-css should not +// depend on webrender, just to have the same types, azul-css should be a standalone crate. + +/// Only used for calculations: Rectangle (x, y, width, height) in layout space. +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] +pub struct LayoutRect { pub origin: LayoutPoint, pub size: LayoutSize } +/// Only used for calculations: Size (width, height) in layout space. +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] +pub struct LayoutSize { pub width: f32, pub height: f32 } +/// Only used for calculations: Point coordinate (x, y) in layout space. +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] +pub struct LayoutPoint { pub x: f32, pub y: f32 } + +impl LayoutSize { + pub fn new(width: f32, height: f32) -> Self { + Self { + width, + height, + } + } + pub fn zero() -> Self { + Self::new(0.0, 0.0) + } +} + +/// Represents a parsed pair of `5px, 10px` values - useful for border radius calculation +#[derive(Debug, Copy, Clone, PartialEq, Ord, PartialOrd, Eq, Hash)] +pub struct PixelSize { pub width: PixelValue, pub height: PixelValue } + +impl PixelSize { + + pub const fn new(width: PixelValue, height: PixelValue) -> Self { + Self { + width, + height, + } + } + + pub const fn zero() -> Self { + Self::new(PixelValue::const_px(0), PixelValue::const_px(0)) + } +} + +/// Offsets of the border-width calculations +#[derive(Debug, Copy, Clone, PartialEq, Ord, PartialOrd, Eq, Hash)] +pub struct LayoutSideOffsets { + pub top: FloatValue, + pub right: FloatValue, + pub bottom: FloatValue, + pub left: FloatValue, +} + +/// u8-based color, range 0 to 255 (similar to webrenders ColorU) +#[derive(Debug, Copy, Clone, PartialEq, Ord, PartialOrd, Eq, Hash)] +pub struct ColorU { pub r: u8, pub g: u8, pub b: u8, pub a: u8 } + +/// f32-based color, range 0.0 to 1.0 (similar to webrenders ColorF) +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] +pub struct ColorF { pub r: f32, pub g: f32, pub b: f32, pub a: f32 } + +impl From for ColorF { + fn from(input: ColorU) -> ColorF { + ColorF { + r: (input.r as f32) / 255.0, + g: (input.g as f32) / 255.0, + b: (input.b as f32) / 255.0, + a: (input.a as f32) / 255.0, + } + } +} + +impl From for ColorU { + fn from(input: ColorF) -> ColorU { + ColorU { + r: (input.r.min(1.0) * 255.0) as u8, + g: (input.g.min(1.0) * 255.0) as u8, + b: (input.b.min(1.0) * 255.0) as u8, + a: (input.a.min(1.0) * 255.0) as u8, + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Ord, PartialOrd, Eq, Hash)] +pub struct BorderRadius { + pub top_left: PixelSize, + pub top_right: PixelSize, + pub bottom_left: PixelSize, + pub bottom_right: PixelSize, +} + +impl Default for BorderRadius { + fn default() -> Self { + Self::zero() + } +} + +impl BorderRadius { + + pub const fn zero() -> Self { + Self::uniform(PixelSize::zero()) + } + + pub const fn uniform(value: PixelSize) -> Self { + Self { + top_left: value, + top_right: value, + bottom_left: value, + bottom_right: value, + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Ord, PartialOrd, Eq, Hash)] +pub enum BorderDetails { + Normal(NormalBorder), + NinePatch(NinePatchBorder), +} + +/// Represents a normal `border` property (no image border / nine-patch border) +#[derive(Debug, Copy, Clone, PartialEq, Ord, PartialOrd, Eq, Hash)] +pub struct NormalBorder { + pub left: BorderSide, + pub right: BorderSide, + pub top: BorderSide, + pub bottom: BorderSide, + pub radius: Option, +} + +#[derive(Debug, Copy, Clone, PartialEq, Ord, PartialOrd, Eq, Hash)] +pub struct BorderSide { + pub color: ColorU, + pub style: BorderStyle, +} + +/// What direction should a `box-shadow` be clipped in (inset or outset) +#[derive(Debug, Copy, Clone, PartialEq, Ord, PartialOrd, Eq, Hash)] +pub enum BoxShadowClipMode { + Outset, + Inset, +} + +/// Whether a `gradient` should be repeated or clamped to the edges. +#[derive(Debug, Copy, Clone, PartialEq, Ord, PartialOrd, Eq, Hash)] +pub enum ExtendMode { + Clamp, + Repeat, +} + +/// Style of a `border`: solid, double, dash, ridge, etc. +#[derive(Debug, Copy, Clone, PartialEq, Ord, PartialOrd, Eq, Hash)] +pub enum BorderStyle { + None, + Solid, + Double, + Dotted, + Dashed, + Hidden, + Groove, + Ridge, + Inset, + Outset, +} + +#[derive(Debug, Copy, Clone, PartialEq, Ord, PartialOrd, Eq, Hash)] +pub struct NinePatchBorder { + // not implemented or parse-able yet, so no fields! +} + +/// Creates `pt`, `px` and `em` constructors for any struct that has a +/// `PixelValue` as it's self.0 field. +macro_rules! impl_pixel_value {($struct:ident) => ( + impl $struct { + #[inline] + pub fn px(value: f32) -> Self { + $struct(PixelValue::px(value)) + } + + #[inline] + pub fn em(value: f32) -> Self { + $struct(PixelValue::em(value)) + } + + #[inline] + pub fn pt(value: f32) -> Self { + $struct(PixelValue::pt(value)) + } + } + + impl ::std::fmt::Debug for $struct { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + write!(f, "{}({:?})", stringify!($struct), self.0) + } + } +)} + +macro_rules! impl_percentage_value{($struct:ident) => ( + impl ::std::fmt::Debug for $struct { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + write!(f, "{}({:?})", stringify!($struct), self.0) + } + } +)} + +macro_rules! impl_float_value{($struct:ident) => ( + impl ::std::fmt::Debug for $struct { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + write!(f, "{}({:?})", stringify!($struct), self.0) + } + } +)} + +/// Map between CSS keys and a statically typed enum +const CSS_PROPERTY_KEY_MAP: [(CssPropertyType, &'static str);56] = [ + (CssPropertyType::Background, "background"), + (CssPropertyType::BackgroundSize, "background-size"), + (CssPropertyType::BackgroundRepeat, "background-repeat"), + (CssPropertyType::BackgroundColor, "background-color"), + (CssPropertyType::BackgroundImage, "background-image"), + + (CssPropertyType::BorderRadius, "border-radius"), + (CssPropertyType::TextColor, "color"), + (CssPropertyType::FontSize, "font-size"), + (CssPropertyType::FontFamily, "font-family"), + (CssPropertyType::TextAlign, "text-align"), + (CssPropertyType::LetterSpacing, "letter-spacing"), + (CssPropertyType::LineHeight, "line-height"), + (CssPropertyType::WordSpacing, "word-spacing"), + (CssPropertyType::TabWidth, "tab-width"), + (CssPropertyType::Cursor, "cursor"), + (CssPropertyType::Width, "width"), + (CssPropertyType::Height, "height"), + (CssPropertyType::MinWidth, "min-width"), + (CssPropertyType::MinHeight, "min-height"), + (CssPropertyType::MaxWidth, "max-width"), + (CssPropertyType::MaxHeight, "max-height"), + (CssPropertyType::Position, "position"), + (CssPropertyType::Top, "top"), + (CssPropertyType::Right, "right"), + (CssPropertyType::Left, "left"), + (CssPropertyType::Bottom, "bottom"), + (CssPropertyType::FlexWrap, "flex-wrap"), + (CssPropertyType::FlexDirection, "flex-direction"), + (CssPropertyType::FlexGrow, "flex-grow"), + (CssPropertyType::FlexShrink, "flex-shrink"), + (CssPropertyType::JustifyContent, "justify-content"), + (CssPropertyType::AlignItems, "align-items"), + (CssPropertyType::AlignContent, "align-content"), + (CssPropertyType::Overflow, "overflow"), + (CssPropertyType::OverflowX, "overflow-x"), + (CssPropertyType::OverflowY, "overflow-y"), + (CssPropertyType::Padding, "padding"), + (CssPropertyType::PaddingTop, "padding-top"), + (CssPropertyType::PaddingLeft, "padding-left"), + (CssPropertyType::PaddingRight, "padding-right"), + (CssPropertyType::PaddingBottom, "padding-bottom"), + (CssPropertyType::Margin, "margin"), + (CssPropertyType::MarginTop, "margin-top"), + (CssPropertyType::MarginLeft, "margin-left"), + (CssPropertyType::MarginRight, "margin-right"), + (CssPropertyType::MarginBottom, "margin-bottom"), + (CssPropertyType::Border, "border"), + (CssPropertyType::BorderTop, "border-top"), + (CssPropertyType::BorderLeft, "border-left"), + (CssPropertyType::BorderRight, "border-right"), + (CssPropertyType::BorderBottom, "border-bottom"), + (CssPropertyType::BoxShadow, "box-shadow"), + (CssPropertyType::BoxShadowTop, "box-shadow-top"), + (CssPropertyType::BoxShadowLeft, "box-shadow-left"), + (CssPropertyType::BoxShadowRight, "box-shadow-right"), + (CssPropertyType::BoxShadowBottom, "box-shadow-bottom"), +]; + +/// Returns a map useful for parsing the keys of CSS stylesheets +pub fn get_css_key_map() -> BTreeMap<&'static str, CssPropertyType> { + CSS_PROPERTY_KEY_MAP.iter().map(|(v, k)| (*k, *v)).collect() +} + +/// Represents a CSS key (for example `"border-radius"` => `BorderRadius`). +/// You can also derive this key from a `CssProperty` by calling `CssProperty::get_type()`. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum CssPropertyType { + BackgroundColor, + Background, + BackgroundSize, + BackgroundRepeat, + BackgroundImage, + + BorderRadius, + TextColor, + FontSize, + FontFamily, + TextAlign, + LetterSpacing, + WordSpacing, + TabWidth, + LineHeight, + Cursor, + Width, + Height, + MinWidth, + MinHeight, + MaxWidth, + MaxHeight, + Position, + Top, + Right, + Left, + Bottom, + FlexWrap, + FlexDirection, + FlexGrow, + FlexShrink, + JustifyContent, + AlignItems, + AlignContent, + + Overflow, + OverflowX, + OverflowY, + + Padding, + PaddingTop, + PaddingLeft, + PaddingRight, + PaddingBottom, + + Margin, + MarginTop, + MarginLeft, + MarginRight, + MarginBottom, + + Border, + BorderTop, + BorderLeft, + BorderRight, + BorderBottom, + + BoxShadow, + BoxShadowTop, + BoxShadowLeft, + BoxShadowRight, + BoxShadowBottom, +} + +impl CssPropertyType { + + /// Parses a CSS key, such as `width` from a string: + /// + /// # Example + /// + /// ```rust + /// # use azul_css::{CssPropertyType, get_css_key_map}; + /// let map = get_css_key_map(); + /// assert_eq!(Some(CssPropertyType::Width), CssPropertyType::from_str("width", &map)); + /// assert_eq!(Some(CssPropertyType::JustifyContent), CssPropertyType::from_str("justify-content", &map)); + /// assert_eq!(None, CssPropertyType::from_str("asdfasdfasdf", &map)); + /// ``` + pub fn from_str(input: &str, map: &BTreeMap<&'static str, Self>) -> Option { + let input = input.trim(); + map.get(input).and_then(|x| Some(*x)) + } + + /// Returns the original string that was used to construct this `CssPropertyType`. + pub fn to_str(&self, map: &BTreeMap<&'static str, Self>) -> &'static str { + map.iter().find(|(_, v)| *v == self).and_then(|(k, _)| Some(k)).unwrap() + } + + /// Returns whether this property will be inherited during cascading + pub fn is_inheritable(&self) -> bool { + use self::CssPropertyType::*; + match self { + | TextColor + | FontFamily + | FontSize + | LineHeight + | TextAlign => true, + _ => false, + } + } + + /// Returns whether this property can trigger a re-layout (important for incremental layout and caching layouted DOMs). + pub fn can_trigger_relayout(&self) -> bool { + + use self::CssPropertyType::*; + + // Since the border can be larger than the content, + // in which case the content needs to be re-layouted, assume true for Border + + // FontFamily, FontSize, LetterSpacing and LineHeight can affect + // the text layout and therefore the screen layout + + match self { + | BorderRadius + | BackgroundColor + | BackgroundSize + | BackgroundRepeat + | TextColor + | Background + | TextAlign + | BoxShadow + | BoxShadowTop + | BoxShadowLeft + | BoxShadowBottom + | BoxShadowRight + | Cursor => false, + _ => true, + } + } +} + +impl fmt::Display for CssPropertyType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let key = CSS_PROPERTY_KEY_MAP.iter().find(|(v, _)| *v == *self).and_then(|(k, _)| Some(k)).unwrap(); + write!(f, "{}", key) + } +} + +/// Represents one parsed CSS key-value pair, such as `"width: 20px"` => `CssProperty::Width(LayoutWidth::px(20.0))` +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum CssProperty { + BorderRadius(StyleBorderRadius), + BackgroundSize(StyleBackgroundSize), + BackgroundRepeat(StyleBackgroundRepeat), + TextColor(StyleTextColor), + Border(StyleBorder), + Background(StyleBackground), + FontSize(StyleFontSize), + FontFamily(StyleFontFamily), + TextAlign(StyleTextAlignmentHorz), + LetterSpacing(StyleLetterSpacing), + BoxShadow(StyleBoxShadow), + LineHeight(StyleLineHeight), + WordSpacing(StyleWordSpacing), + TabWidth(StyleTabWidth), + Cursor(StyleCursor), + Width(LayoutWidth), + Height(LayoutHeight), + MinWidth(LayoutMinWidth), + MinHeight(LayoutMinHeight), + MaxWidth(LayoutMaxWidth), + MaxHeight(LayoutMaxHeight), + Position(LayoutPosition), + Top(LayoutTop), + Right(LayoutRight), + Left(LayoutLeft), + Bottom(LayoutBottom), + Padding(LayoutPadding), + Margin(LayoutMargin), + FlexWrap(LayoutWrap), + FlexDirection(LayoutDirection), + FlexGrow(LayoutFlexGrow), + FlexShrink(LayoutFlexShrink), + JustifyContent(LayoutJustifyContent), + AlignItems(LayoutAlignItems), + AlignContent(LayoutAlignContent), + Overflow(LayoutOverflow), +} + +impl CssProperty { + + /// Return the type (key) of this property as a statically typed enum + pub fn get_type(&self) -> CssPropertyType { + match &self { + CssProperty::BorderRadius(_) => CssPropertyType::BorderRadius, + CssProperty::BackgroundSize(_) => CssPropertyType::BackgroundSize, + CssProperty::BackgroundRepeat(_) => CssPropertyType::BackgroundRepeat, + CssProperty::TextColor(_) => CssPropertyType::TextColor, + CssProperty::Border(_) => CssPropertyType::Border, + CssProperty::Background(_) => CssPropertyType::Background, + CssProperty::FontSize(_) => CssPropertyType::FontSize, + CssProperty::FontFamily(_) => CssPropertyType::FontFamily, + CssProperty::TextAlign(_) => CssPropertyType::TextAlign, + CssProperty::LetterSpacing(_) => CssPropertyType::LetterSpacing, + CssProperty::WordSpacing(_) => CssPropertyType::WordSpacing, + CssProperty::TabWidth(_) => CssPropertyType::TabWidth, + CssProperty::BoxShadow(_) => CssPropertyType::BoxShadow, + CssProperty::LineHeight(_) => CssPropertyType::LineHeight, + CssProperty::Cursor(_) => CssPropertyType::Cursor, + CssProperty::Width(_) => CssPropertyType::Width, + CssProperty::Height(_) => CssPropertyType::Height, + CssProperty::MinWidth(_) => CssPropertyType::MinWidth, + CssProperty::MinHeight(_) => CssPropertyType::MinHeight, + CssProperty::MaxWidth(_) => CssPropertyType::MaxWidth, + CssProperty::MaxHeight(_) => CssPropertyType::MaxHeight, + CssProperty::Position(_) => CssPropertyType::Position, + CssProperty::Top(_) => CssPropertyType::Top, + CssProperty::Right(_) => CssPropertyType::Right, + CssProperty::Left(_) => CssPropertyType::Left, + CssProperty::Bottom(_) => CssPropertyType::Bottom, + CssProperty::Padding(_) => CssPropertyType::Padding, + CssProperty::Margin(_) => CssPropertyType::Margin, + CssProperty::FlexWrap(_) => CssPropertyType::FlexWrap, + CssProperty::FlexDirection(_) => CssPropertyType::FlexDirection, + CssProperty::FlexGrow(_) => CssPropertyType::FlexGrow, + CssProperty::FlexShrink(_) => CssPropertyType::FlexShrink, + CssProperty::JustifyContent(_) => CssPropertyType::JustifyContent, + CssProperty::AlignItems(_) => CssPropertyType::AlignItems, + CssProperty::AlignContent(_) => CssPropertyType::AlignContent, + CssProperty::Overflow(_) => CssPropertyType::Overflow, + } + } +} + +impl_from!(StyleBorderRadius, CssProperty::BorderRadius); +impl_from!(StyleBackground, CssProperty::Background); +impl_from!(StyleBoxShadow, CssProperty::BoxShadow); +impl_from!(StyleBorder, CssProperty::Border); +impl_from!(StyleFontSize, CssProperty::FontSize); +impl_from!(StyleFontFamily, CssProperty::FontFamily); +impl_from!(StyleTextAlignmentHorz, CssProperty::TextAlign); +impl_from!(StyleLineHeight, CssProperty::LineHeight); +impl_from!(StyleTabWidth, CssProperty::TabWidth); +impl_from!(StyleWordSpacing, CssProperty::WordSpacing); +impl_from!(StyleLetterSpacing, CssProperty::LetterSpacing); +impl_from!(StyleBackgroundSize, CssProperty::BackgroundSize); +impl_from!(StyleBackgroundRepeat, CssProperty::BackgroundRepeat); +impl_from!(StyleTextColor, CssProperty::TextColor); +impl_from!(StyleCursor, CssProperty::Cursor); + +impl_from!(LayoutOverflow, CssProperty::Overflow); +impl_from!(LayoutWidth, CssProperty::Width); +impl_from!(LayoutHeight, CssProperty::Height); +impl_from!(LayoutMinWidth, CssProperty::MinWidth); +impl_from!(LayoutMinHeight, CssProperty::MinHeight); +impl_from!(LayoutMaxWidth, CssProperty::MaxWidth); +impl_from!(LayoutMaxHeight, CssProperty::MaxHeight); + +impl_from!(LayoutPosition, CssProperty::Position); +impl_from!(LayoutTop, CssProperty::Top); +impl_from!(LayoutBottom, CssProperty::Bottom); +impl_from!(LayoutRight, CssProperty::Right); +impl_from!(LayoutLeft, CssProperty::Left); + +impl_from!(LayoutPadding, CssProperty::Padding); +impl_from!(LayoutMargin, CssProperty::Margin); + +impl_from!(LayoutWrap, CssProperty::FlexWrap); +impl_from!(LayoutDirection, CssProperty::FlexDirection); +impl_from!(LayoutFlexGrow, CssProperty::FlexGrow); +impl_from!(LayoutFlexShrink, CssProperty::FlexShrink); +impl_from!(LayoutJustifyContent, CssProperty::JustifyContent); +impl_from!(LayoutAlignItems, CssProperty::AlignItems); +impl_from!(LayoutAlignContent, CssProperty::AlignContent); + +/// Multiplier for floating point accuracy. Elements such as px or % +/// are only accurate until a certain number of decimal points, therefore +/// they have to be casted to isizes in order to make the f32 values +/// hash-able: Css has a relatively low precision here, roughly 5 digits, i.e +/// `1.00001 == 1.0` +const FP_PRECISION_MULTIPLIER: f32 = 1000.0; +const FP_PRECISION_MULTIPLIER_CONST: isize = FP_PRECISION_MULTIPLIER as isize; + +/// FloatValue, but associated with a certain metric (i.e. px, em, etc.) +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct PixelValue { + pub metric: SizeMetric, + pub number: FloatValue, +} + +// Manual Debug implementation, because the auto-generated one is nearly unreadable +impl fmt::Debug for PixelValue { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}{:?}", self.number, self.metric) + } +} + +impl fmt::Debug for SizeMetric { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use self::SizeMetric::*; + match self { + Px => write!(f, "px"), + Pt => write!(f, "pt"), + Em => write!(f, "pt"), + } + } +} + +impl fmt::Debug for FloatValue { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.get()) + } +} + +impl PixelValue { + + /// Same as `PixelValue::px()`, but only accepts whole numbers, + /// since using `f32` in const fn is not yet stabilized. + #[inline] + pub const fn const_px(value: isize) -> Self { + Self::const_from_metric(SizeMetric::Px, value) + } + + /// Same as `PixelValue::em()`, but only accepts whole numbers, + /// since using `f32` in const fn is not yet stabilized. + #[inline] + pub const fn const_em(value: isize) -> Self { + Self::const_from_metric(SizeMetric::Em, value) + } + + /// Same as `PixelValue::pt()`, but only accepts whole numbers, + /// since using `f32` in const fn is not yet stabilized. + #[inline] + pub const fn const_pt(value: isize) -> Self { + Self::const_from_metric(SizeMetric::Pt, value) + } + + #[inline] + pub const fn const_from_metric(metric: SizeMetric, value: isize) -> Self { + Self { + metric: metric, + number: FloatValue::const_new(value), + } + } + + #[inline] + pub fn px(value: f32) -> Self { + Self::from_metric(SizeMetric::Px, value) + } + + #[inline] + pub fn em(value: f32) -> Self { + Self::from_metric(SizeMetric::Em, value) + } + + #[inline] + pub fn pt(value: f32) -> Self { + Self::from_metric(SizeMetric::Pt, value) + } + + #[inline] + pub fn from_metric(metric: SizeMetric, value: f32) -> Self { + Self { + metric: metric, + number: FloatValue::new(value), + } + } + + /// Returns the value of the SizeMetric in pixels + #[inline] + pub fn to_pixels(&self) -> f32 { + match self.metric { + SizeMetric::Px => { self.number.get() }, + SizeMetric::Pt => { (self.number.get()) * PT_TO_PX }, + SizeMetric::Em => { (self.number.get()) * EM_HEIGHT }, + } + } +} + +/// Wrapper around FloatValue, represents a percentage instead +/// of just being a regular floating-point value, i.e `5` = `5%` +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct PercentageValue { + number: FloatValue, +} + +impl fmt::Debug for PercentageValue { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}%", self.get()) + } +} + +impl PercentageValue { + + /// Same as `PercentageValue::new()`, but only accepts whole numbers, + /// since using `f32` in const fn is not yet stabilized. + pub const fn const_new(value: isize) -> Self { + Self { number: FloatValue::const_new(value) } + } + + pub fn new(value: f32) -> Self { + Self { number: value.into() } + } + + pub fn get(&self) -> f32 { + self.number.get() + } +} + +/// Wrapper around an f32 value that is internally casted to an isize, +/// in order to provide hash-ability (to avoid numerical instability). +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct FloatValue { + pub number: isize, +} + +impl FloatValue { + + /// Same as `FloatValue::new()`, but only accepts whole numbers, + /// since using `f32` in const fn is not yet stabilized. + pub const fn const_new(value: isize) -> Self { + Self { number: value * FP_PRECISION_MULTIPLIER_CONST } + } + + pub fn new(value: f32) -> Self { + Self { number: (value * FP_PRECISION_MULTIPLIER) as isize } + } + + pub fn get(&self) -> f32 { + self.number as f32 / FP_PRECISION_MULTIPLIER + } +} + +impl From for FloatValue { + fn from(val: f32) -> Self { + Self::new(val) + } +} + +/// Enum representing the metric associated with a number (px, pt, em, etc.) +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum SizeMetric { + Px, + Pt, + Em, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct StyleBorderRadius(pub BorderRadius); + +impl StyleBorderRadius { + pub const fn zero() -> Self { + StyleBorderRadius(BorderRadius::zero()) + } +} + +/// Represents a `background-size` attribute +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum StyleBackgroundSize { + Contain, + Cover, +} + +/// Represents a `background-repeat` attribute +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum StyleBackgroundRepeat { + NoRepeat, + Repeat, + RepeatX, + RepeatY, +} + +impl Default for StyleBackgroundRepeat { + fn default() -> Self { + StyleBackgroundRepeat::Repeat + } +} + +/// Represents a `color` attribute +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct StyleTextColor(pub ColorU); + +/// Represents a `padding` attribute +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct LayoutPadding { + pub top: Option, + pub bottom: Option, + pub left: Option, + pub right: Option, +} + +// $struct_name has to have top, left, right, bottom properties +macro_rules! merge_struct {($struct_name:ident) => ( +impl $struct_name { + pub fn merge(a: &mut Option<$struct_name>, b: &$struct_name) { + if let Some(ref mut existing) = a { + if b.top.is_some() { existing.top = b.top; } + if b.bottom.is_some() { existing.bottom = b.bottom; } + if b.left.is_some() { existing.left = b.left; } + if b.right.is_some() { existing.right = b.right; } + } else { + *a = Some(*b); + } + } +})} + +macro_rules! struct_all {($struct_name:ident, $field_type:ty) => ( +impl $struct_name { + /// Sets all of the fields (top, left, right, bottom) to `Some(field)` + pub fn all(field: $field_type) -> Self { + Self { + top: Some(field), + right: Some(field), + left: Some(field), + bottom: Some(field), + } + } +})} + +merge_struct!(LayoutPadding); +merge_struct!(LayoutMargin); +struct_all!(LayoutPadding, PixelValue); +struct_all!(LayoutMargin, PixelValue); + +/// Represents a parsed `padding` attribute +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct LayoutMargin { + pub top: Option, + pub bottom: Option, + pub left: Option, + pub right: Option, +} + +/// Wrapper for the `overflow-{x,y}` + `overflow` property +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct LayoutOverflow { + pub horizontal: Option, + pub vertical: Option, +} + +impl LayoutOverflow { + + // "merges" two LayoutOverflow properties + pub fn merge(a: &mut Option, b: &Self) { + + fn merge_property(p: &mut Option, other: &Option) { + if *other == None { + return; + } + *p = *other; + } + + if let Some(ref mut existing_overflow) = a { + merge_property(&mut existing_overflow.horizontal, &b.horizontal); + merge_property(&mut existing_overflow.vertical, &b.vertical); + } else { + *a = Some(*b) + } + } + + pub fn needs_horizontal_scrollbar(&self, currently_overflowing_horz: bool) -> bool { + self.horizontal.unwrap_or_default().needs_scrollbar(currently_overflowing_horz) + } + + pub fn needs_vertical_scrollbar(&self, currently_overflowing_vert: bool) -> bool { + self.vertical.unwrap_or_default().needs_scrollbar(currently_overflowing_vert) + } + + pub fn is_horizontal_overflow_visible(&self) -> bool { + self.horizontal.unwrap_or_default().is_overflow_visible() + } + + pub fn is_vertical_overflow_visible(&self) -> bool { + self.vertical.unwrap_or_default().is_overflow_visible() + } +} + +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct StyleBorder { + pub top: Option, + pub left: Option, + pub bottom: Option, + pub right: Option, +} + +merge_struct!(StyleBorder); +struct_all!(StyleBorder, StyleBorderSide); + +impl StyleBorder { + + /// Returns the merged offsets and details for the top, left, + /// right and bottom styles - necessary, so we can combine `border-top`, + /// `border-left`, etc. into one border + pub fn get_webrender_border(&self, border_radius: Option) -> Option<(LayoutSideOffsets, BorderDetails)> { + match (self.top, self.left, self.bottom, self.right) { + (None, None, None, None) => None, + (top, left, bottom, right) => { + + // Widths + let border_width_top = top.and_then(|top| Some(top.border_width.to_pixels())).unwrap_or(0.0); + let border_width_bottom = bottom.and_then(|bottom| Some(bottom.border_width.to_pixels())).unwrap_or(0.0); + let border_width_left = left.and_then(|left| Some(left.border_width.to_pixels())).unwrap_or(0.0); + let border_width_right = right.and_then(|right| Some(right.border_width.to_pixels())).unwrap_or(0.0); + + // Color + let border_color_top = top.and_then(|top| Some(top.border_color.into())).unwrap_or(DEFAULT_BORDER_COLOR); + let border_color_bottom = bottom.and_then(|bottom| Some(bottom.border_color.into())).unwrap_or(DEFAULT_BORDER_COLOR); + let border_color_left = left.and_then(|left| Some(left.border_color.into())).unwrap_or(DEFAULT_BORDER_COLOR); + let border_color_right = right.and_then(|right| Some(right.border_color.into())).unwrap_or(DEFAULT_BORDER_COLOR); + + // Styles + let border_style_top = top.and_then(|top| Some(top.border_style)).unwrap_or(DEFAULT_BORDER_STYLE); + let border_style_bottom = bottom.and_then(|bottom| Some(bottom.border_style)).unwrap_or(DEFAULT_BORDER_STYLE); + let border_style_left = left.and_then(|left| Some(left.border_style)).unwrap_or(DEFAULT_BORDER_STYLE); + let border_style_right = right.and_then(|right| Some(right.border_style)).unwrap_or(DEFAULT_BORDER_STYLE); + + let border_widths = LayoutSideOffsets { + top: FloatValue::new(border_width_top), + right: FloatValue::new(border_width_right), + bottom: FloatValue::new(border_width_bottom), + left: FloatValue::new(border_width_left), + }; + let border_details = BorderDetails::Normal(NormalBorder { + top: BorderSide { color: border_color_top.into(), style: border_style_top }, + left: BorderSide { color: border_color_left.into(), style: border_style_left }, + right: BorderSide { color: border_color_right.into(), style: border_style_right }, + bottom: BorderSide { color: border_color_bottom.into(), style: border_style_bottom }, + radius: border_radius.and_then(|r| Some(r.0)), + }); + + Some((border_widths, border_details)) + } + } + } +} + +const DEFAULT_BORDER_STYLE: BorderStyle = BorderStyle::Solid; +const DEFAULT_BORDER_COLOR: ColorU = ColorU { r: 0, g: 0, b: 0, a: 255 }; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct StyleBorderSide { + pub border_width: PixelValue, + pub border_style: BorderStyle, + pub border_color: ColorU, +} + +/// Represents a `box-shadow` attribute. +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct StyleBoxShadow { + pub top: Option>, + pub left: Option>, + pub bottom: Option>, + pub right: Option>, +} + +merge_struct!(StyleBoxShadow); +struct_all!(StyleBoxShadow, Option); + +// missing StyleBorderRadius & LayoutRect +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct BoxShadowPreDisplayItem { + pub offset: [PixelValue;2], + pub color: ColorU, + pub blur_radius: PixelValue, + pub spread_radius: PixelValue, + pub clip_mode: BoxShadowClipMode, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum StyleBackground { + LinearGradient(LinearGradient), + RadialGradient(RadialGradient), + Image(CssImageId), + Color(ColorU), + NoBackground, +} + +impl StyleBackground { + pub fn get_css_image_id(&self) -> Option<&CssImageId> { + use self::StyleBackground::*; + match self { + Image(i) => Some(i), + _ => None, + } + } +} + +impl<'a> From for StyleBackground { + fn from(id: CssImageId) -> Self { + StyleBackground::Image(id) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct LinearGradient { + pub direction: Direction, + pub extend_mode: ExtendMode, + pub stops: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct RadialGradient { + pub shape: Shape, + pub extend_mode: ExtendMode, + pub stops: Vec, +} + +/// CSS direction (necessary for gradients). Can either be a fixed angle or +/// a direction ("to right" / "to left", etc.). +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Direction { + Angle(FloatValue), + FromTo(DirectionCorner, DirectionCorner), +} + +impl Direction { + /// Calculates the points of the gradient stops for angled linear gradients + pub fn to_points(&self, rect: &LayoutRect) + -> (LayoutPoint, LayoutPoint) + { + match self { + Direction::Angle(deg) => { + // note: assumes that the LayoutRect has positive sides + + // see: https://hugogiraudel.com/2013/02/04/css-gradients/ + + let deg = deg.get(); // FloatValue -> f32 + + let deg = -deg; // negate winding direction + + let width_half = rect.size.width as usize / 2; + let height_half = rect.size.height as usize / 2; + + // hypotenuse_len is the length of the center of the rect to the corners + let hypotenuse_len = (((width_half * width_half) + (height_half * height_half)) as f64).sqrt(); + + // The corner also serves to determine what quadrant we're in + // Get the quadrant (corner) the angle is in and get the degree associated + // with that corner. + + let angle_to_top_left = (height_half as f64 / width_half as f64).atan().to_degrees(); + + // We need to calculate the angle from the center to the corner! + let ending_point_degrees = if deg < 90.0 { + // top left corner + 90.0 - angle_to_top_left + } else if deg < 180.0 { + // bottom left corner + 90.0 + angle_to_top_left + } else if deg < 270.0 { + // bottom right corner + 270.0 - angle_to_top_left + } else /* deg > 270.0 && deg < 360.0 */ { + // top right corner + 270.0 + angle_to_top_left + }; + + // assuming deg = 36deg, then degree_diff_to_corner = 9deg + let degree_diff_to_corner = ending_point_degrees - deg as f64; + + // Searched_len is the distance between the center of the rect and the + // ending point of the gradient + let searched_len = (hypotenuse_len * degree_diff_to_corner.to_radians().cos()).abs(); + + // TODO: This searched_len is incorrect... + + // Once we have the length, we can simply rotate the length by the angle, + // then translate it to the center of the rect + let dx = deg.to_radians().sin() * searched_len as f32; + let dy = deg.to_radians().cos() * searched_len as f32; + + let start_point_location = LayoutPoint { x: width_half as f32 + dx, y: height_half as f32 + dy }; + let end_point_location = LayoutPoint { x: width_half as f32 - dx, y: height_half as f32 - dy }; + + (start_point_location, end_point_location) + }, + Direction::FromTo(from, to) => { + (from.to_point(rect), to.to_point(rect)) + } + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Shape { + Ellipse, + Circle, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum StyleCursor { + /// `alias` + Alias, + /// `all-scroll` + AllScroll, + /// `cell` + Cell, + /// `col-resize` + ColResize, + /// `context-menu` + ContextMenu, + /// `copy` + Copy, + /// `crosshair` + Crosshair, + /// `default` - note: called "arrow" in winit + Default, + /// `e-resize` + EResize, + /// `ew-resize` + EwResize, + /// `grab` + Grab, + /// `grabbing` + Grabbing, + /// `help` + Help, + /// `move` + Move, + /// `n-resize` + NResize, + /// `ns-resize` + NsResize, + /// `nesw-resize` + NeswResize, + /// `nwse-resize` + NwseResize, + /// `pointer` - note: called "hand" in winit + Pointer, + /// `progress` + Progress, + /// `row-resize` + RowResize, + /// `s-resize` + SResize, + /// `se-resize` + SeResize, + /// `text` + Text, + /// `unset` + Unset, + /// `vertical-text` + VerticalText, + /// `w-resize` + WResize, + /// `wait` + Wait, + /// `zoom-in` + ZoomIn, + /// `zoom-out` + ZoomOut, +} + +impl Default for StyleCursor { + fn default() -> StyleCursor { + StyleCursor::Default + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum DirectionCorner { + Right, + Left, + Top, + Bottom, + TopRight, + TopLeft, + BottomRight, + BottomLeft, +} + +impl DirectionCorner { + + pub fn opposite(&self) -> Self { + use self::DirectionCorner::*; + match *self { + Right => Left, + Left => Right, + Top => Bottom, + Bottom => Top, + TopRight => BottomLeft, + BottomLeft => TopRight, + TopLeft => BottomRight, + BottomRight => TopLeft, + } + } + + pub fn combine(&self, other: &Self) -> Option { + use self::DirectionCorner::*; + match (*self, *other) { + (Right, Top) | (Top, Right) => Some(TopRight), + (Left, Top) | (Top, Left) => Some(TopLeft), + (Right, Bottom) | (Bottom, Right) => Some(BottomRight), + (Left, Bottom) | (Bottom, Left) => Some(BottomLeft), + _ => { None } + } + } + + pub fn to_point(&self, rect: &LayoutRect) -> LayoutPoint + { + use self::DirectionCorner::*; + match *self { + Right => LayoutPoint { x: rect.size.width, y: rect.size.height / 2.0 }, + Left => LayoutPoint { x: 0.0, y: rect.size.height / 2.0 }, + Top => LayoutPoint { x: rect.size.width / 2.0, y: 0.0 }, + Bottom => LayoutPoint { x: rect.size.width / 2.0, y: rect.size.height }, + TopRight => LayoutPoint { x: rect.size.width, y: 0.0 }, + TopLeft => LayoutPoint { x: 0.0, y: 0.0 }, + BottomRight => LayoutPoint { x: rect.size.width, y: rect.size.height }, + BottomLeft => LayoutPoint { x: 0.0, y: rect.size.height }, + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum BackgroundType { + Color, + LinearGradient, + RepeatingLinearGradient, + RadialGradient, + RepeatingRadialGradient, + Image, +} + +/// Note: In theory, we could take a reference here, +/// but this leads to horrible lifetime issues. +/// +/// Ownership allows the `Css` struct to be independent +/// of the original source text. For example, when parsing a style +/// from CSS, the original string can be deallocated afterwards. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct CssImageId(pub String); + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct GradientStopPre { + // this is set to None if there was no offset that could be parsed + pub offset: Option, + pub color: ColorU, +} + +/// Represents a `width` attribute +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct LayoutWidth(pub PixelValue); +/// Represents a `min-width` attribute +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct LayoutMinWidth(pub PixelValue); +/// Represents a `max-width` attribute +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct LayoutMaxWidth(pub PixelValue); +/// Represents a `height` attribute +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct LayoutHeight(pub PixelValue); +/// Represents a `min-height` attribute +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct LayoutMinHeight(pub PixelValue); +/// Represents a `max-height` attribute +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct LayoutMaxHeight(pub PixelValue); + +/// Represents a `top` attribute +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct LayoutTop(pub PixelValue); +/// Represents a `left` attribute +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct LayoutLeft(pub PixelValue); +/// Represents a `right` attribute +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct LayoutRight(pub PixelValue); +/// Represents a `bottom` attribute +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct LayoutBottom(pub PixelValue); + +/// Represents a `flex-grow` attribute +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct LayoutFlexGrow(pub FloatValue); +/// Represents a `flex-shrink` attribute +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct LayoutFlexShrink(pub FloatValue); + +impl_float_value!(LayoutFlexGrow); +impl_float_value!(LayoutFlexShrink); + +/// Represents a `flex-direction` attribute - default: `Column` +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum LayoutDirection { + Row, + RowReverse, + Column, + ColumnReverse, +} + +impl Default for LayoutDirection { + fn default() -> Self { + LayoutDirection::Column + } +} + +impl LayoutDirection { + pub fn get_axis(&self) -> LayoutAxis { + use self::{LayoutAxis::*, LayoutDirection::*}; + match self { + Row | RowReverse => Horizontal, + Column | ColumnReverse => Vertical, + } + } + + /// Returns true, if this direction is a `column-reverse` or `row-reverse` direction + pub fn is_reverse(&self) -> bool { + *self == LayoutDirection::RowReverse || *self == LayoutDirection::ColumnReverse + } +} + +/// Represents a `line-height` attribute +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct StyleLineHeight(pub PercentageValue); +/// Represents a `tab-width` attribute +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct StyleTabWidth(pub PercentageValue); +/// Represents a `letter-spacing` attribute +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct StyleLetterSpacing(pub PixelValue); +/// Represents a `word-spacing` attribute +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct StyleWordSpacing(pub PixelValue); + +impl_percentage_value!(StyleTabWidth); +impl_percentage_value!(StyleLineHeight); + +/// Same as the `LayoutDirection`, but without the `-reverse` properties, used in the layout solver, +/// makes decisions based on horizontal / vertical direction easier to write. +/// Use `LayoutDirection::get_axis()` to get the axis for a given `LayoutDirection`. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum LayoutAxis { + Horizontal, + Vertical, +} + +/// Represents a `position` attribute - default: `Static` +/// +/// NOTE: No inline positioning is supported. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum LayoutPosition { + Static, + Relative, + Absolute, +} + +impl Default for LayoutPosition { + fn default() -> Self { + LayoutPosition::Static + } +} + +/// Represents a `flex-wrap` attribute - default: `Wrap` +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum LayoutWrap { + Wrap, + NoWrap, +} + +impl Default for LayoutWrap { + fn default() -> Self { + LayoutWrap::Wrap + } +} + +/// Represents a `justify-content` attribute +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum LayoutJustifyContent { + /// Default value. Items are positioned at the beginning of the container + Start, + /// Items are positioned at the end of the container + End, + /// Items are positioned at the center of the container + Center, + /// Items are positioned with space between the lines + SpaceBetween, + /// Items are positioned with space before, between, and after the lines + SpaceAround, +} + +impl Default for LayoutJustifyContent { + fn default() -> Self { + LayoutJustifyContent::Start + } +} + +/// Represents a `align-items` attribute +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum LayoutAlignItems { + /// Items are stretched to fit the container + Stretch, + /// Items are positioned at the center of the container + Center, + /// Items are positioned at the beginning of the container + Start, + /// Items are positioned at the end of the container + End, +} + +impl Default for LayoutAlignItems { + fn default() -> Self { + LayoutAlignItems::Stretch + } +} + +/// Represents a `align-content` attribute +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum LayoutAlignContent { + /// Default value. Lines stretch to take up the remaining space + Stretch, + /// Lines are packed toward the center of the flex container + Center, + /// Lines are packed toward the start of the flex container + Start, + /// Lines are packed toward the end of the flex container + End, + /// Lines are evenly distributed in the flex container + SpaceBetween, + /// Lines are evenly distributed in the flex container, with half-size spaces on either end + SpaceAround, +} + +/// Represents a `overflow-x` or `overflow-y` property, see +/// [`TextOverflowBehaviour`](./struct.TextOverflowBehaviour.html) - default: `Auto` +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Overflow { + /// Always shows a scroll bar, overflows on scroll + Scroll, + /// Does not show a scroll bar by default, only when text is overflowing + Auto, + /// Never shows a scroll bar, simply clips text + Hidden, + /// Doesn't show a scroll bar, simply overflows the text + Visible, +} + +impl Default for Overflow { + fn default() -> Self { + Overflow::Auto + } +} + +impl Overflow { + + /// Returns whether this overflow value needs to display the scrollbars. + /// + /// - `overflow:scroll` always shows the scrollbar + /// - `overflow:auto` only shows the scrollbar when the content is currently overflowing + /// - `overflow:hidden` and `overflow:visible` do not show any scrollbars + pub fn needs_scrollbar(&self, currently_overflowing: bool) -> bool { + use self::Overflow::*; + match self { + Scroll => true, + Auto => currently_overflowing, + Hidden | Visible => false, + } + } + + /// Returns whether this is an `overflow:visible` node + /// (the only overflow type that doesn't clip its children) + pub fn is_overflow_visible(&self) -> bool { + *self == Overflow::Visible + } +} + +/// Horizontal text alignment enum (left, center, right) - default: `Center` +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum StyleTextAlignmentHorz { + Left, + Center, + Right, +} + +impl Default for StyleTextAlignmentHorz { + fn default() -> Self { + StyleTextAlignmentHorz::Center + } +} + +/// Vertical text alignment enum (top, center, bottom) - default: `Center` +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum StyleTextAlignmentVert { + Top, + Center, + Bottom, +} + +impl Default for StyleTextAlignmentVert { + fn default() -> Self { + StyleTextAlignmentVert::Center + } +} + +/// Options of a cascaded (styled) DOM node that are only relevant +/// for styling and don't affect the layout of the rectangle +#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct RectStyle { + /// Background size of this rectangle + pub background_size: Option, + /// Background repetition + pub background_repeat: Option, + /// Shadow color + pub box_shadow: Option, + /// Gradient (location) + stops + pub background: Option, + /// Border + pub border: Option, + /// Border radius + pub border_radius: Option, + /// Font size + pub font_size: Option, + /// Font name / family + pub font_family: Option, + /// Text color + pub font_color: Option, + /// Text alignment + pub text_align: Option, + /// `line-height` property + pub line_height: Option, + /// `letter-spacing` property + pub letter_spacing: Option, + /// `word-spacing` property + pub word_spacing: Option, + /// `tab-width` property + pub tab_width: Option, +} + +impl_pixel_value!(StyleLetterSpacing); +impl_pixel_value!(StyleWordSpacing); + +impl RectStyle { + + pub fn get_horizontal_scrollbar_style(&self) -> ScrollbarInfo { + ScrollbarInfo::default() + } + + pub fn get_vertical_scrollbar_style(&self) -> ScrollbarInfo { + ScrollbarInfo::default() + } +} + +/// Holds info necessary for layouting / styling scrollbars (-webkit-scrollbar) +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ScrollbarInfo { + /// Total width (or height for vertical scrollbars) of the scrollbar in pixels + pub width: LayoutWidth, + /// Padding of the scrollbar tracker, in pixels. The inner bar is `width - padding` pixels wide. + pub padding: LayoutPadding, + /// Style of the scrollbar background + /// (`-webkit-scrollbar` / `-webkit-scrollbar-track` / `-webkit-scrollbar-track-piece` combined) + pub track: RectStyle, + /// Style of the scrollbar thumbs (the "up" / "down" arrows), (`-webkit-scrollbar-thumb`) + pub thumb: RectStyle, + /// Styles the directional buttons on the scrollbar (`-webkit-scrollbar-button`) + pub button: RectStyle, + /// If two scrollbars are present, addresses the (usually) bottom corner + /// of the scrollable element, where two scrollbars might meet (`-webkit-scrollbar-corner`) + pub corner: RectStyle, + /// Addresses the draggable resizing handle that appears above the + /// `corner` at the bottom corner of some elements (`-webkit-resizer`) + pub resizer: RectStyle, +} + +impl Default for ScrollbarInfo { + fn default() -> Self { + ScrollbarInfo { + width: LayoutWidth(PixelValue::px(17.0)), + padding: LayoutPadding { + left: Some(PixelValue::px(2.0)), + right: Some(PixelValue::px(2.0)), + .. Default::default() + }, + track: RectStyle { + background: Some(StyleBackground::Color(ColorU { + r: 241, g: 241, b: 241, a: 255 + })), + .. Default::default() + }, + thumb: RectStyle { + background: Some(StyleBackground::Color(ColorU { + r: 193, g: 193, b: 193, a: 255 + })), + .. Default::default() + }, + button: RectStyle { + background: Some(StyleBackground::Color(ColorU { + r: 163, g: 163, b: 163, a: 255 + })), + .. Default::default() + }, + corner: RectStyle::default(), + resizer: RectStyle::default(), + } + } +} + +/// Options of a cascaded (styled) DOM node that are relevant for constructing the layout of a div +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct RectLayout { + + pub width: Option, + pub height: Option, + pub min_width: Option, + pub min_height: Option, + pub max_width: Option, + pub max_height: Option, + + pub position: Option, + pub top: Option, + pub bottom: Option, + pub right: Option, + pub left: Option, + + pub padding: Option, + pub margin: Option, + pub overflow: Option, + + pub direction: Option, + pub wrap: Option, + pub flex_grow: Option, + pub flex_shrink: Option, + pub justify_content: Option, + pub align_items: Option, + pub align_content: Option, +} + +impl RectLayout { + + pub fn get_horizontal_padding(&self) -> f32 { + let padding = self.padding.unwrap_or_default(); + padding.left.map(|l| l.to_pixels()).unwrap_or(0.0) + + padding.right.map(|r| r.to_pixels()).unwrap_or(0.0) + } + + pub fn get_vertical_padding(&self) -> f32 { + let padding = self.padding.unwrap_or_default(); + padding.bottom.map(|l| l.to_pixels()).unwrap_or(0.0) + + padding.top.map(|r| r.to_pixels()).unwrap_or(0.0) + } + + pub fn get_horizontal_margin(&self) -> f32 { + let margin = self.margin.unwrap_or_default(); + margin.left.map(|l| l.to_pixels()).unwrap_or(0.0) + + margin.right.map(|r| r.to_pixels()).unwrap_or(0.0) + } + + pub fn get_vertical_margin(&self) -> f32 { + let margin = self.margin.unwrap_or_default(); + margin.bottom.map(|r| r.to_pixels()).unwrap_or(0.0) + + margin.top.map(|l| l.to_pixels()).unwrap_or(0.0) + } + + pub fn is_horizontal_overflow_visible(&self) -> bool { + self.overflow.unwrap_or_default().is_horizontal_overflow_visible() + } + + pub fn is_vertical_overflow_visible(&self) -> bool { + self.overflow.unwrap_or_default().is_vertical_overflow_visible() + } +} + +impl_pixel_value!(LayoutWidth); +impl_pixel_value!(LayoutHeight); +impl_pixel_value!(LayoutMinHeight); +impl_pixel_value!(LayoutMinWidth); +impl_pixel_value!(LayoutMaxWidth); +impl_pixel_value!(LayoutMaxHeight); +impl_pixel_value!(LayoutTop); +impl_pixel_value!(LayoutBottom); +impl_pixel_value!(LayoutRight); +impl_pixel_value!(LayoutLeft); + +/// Represents a `font-size` attribute +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct StyleFontSize(pub PixelValue); + +impl_pixel_value!(StyleFontSize); + +impl StyleFontSize { + pub fn to_pixels(&self) -> f32 { + self.0.to_pixels() + } +} + +/// Represents a `font-family` attribute +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct StyleFontFamily { + // fonts in order of precedence, i.e. "Webly Sleeky UI", "monospace", etc. + pub fonts: Vec +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct FontId(pub String); + +impl FontId { + pub fn get_str(&self) -> &str { + &self.0 + } +} diff --git a/azul-css/src/hot_reload.rs b/azul-css/src/hot_reload.rs new file mode 100644 index 000000000..3f19b3463 --- /dev/null +++ b/azul-css/src/hot_reload.rs @@ -0,0 +1,56 @@ +//! Traits and datatypes associated with reloading styles at runtime. + +use crate::css::Css; +use std::time::Duration; + +/// Interface that can be used to reload a stylesheet while an application is running. +/// Initialize the `Window::new` with a `Box` - this allows the hot-reloading +/// to be independent of the source format, making CSS only one frontend format. +/// +/// You can, for example, parse and load styles directly from a SASS, LESS or JSON parser. +/// The default parser is `azul-css-parser`. +pub trait HotReloadHandler { + /// Reloads the style from the source format. Should return Ok() when the CSS has be correctly + /// reloaded, and an human-readable error string otherwise (since the error needs to be printed + /// to stdout when hot-reloading). + fn reload_style(&mut self) -> Result; + /// Returns how quickly the hot-reloader should reload the source format. + fn get_reload_interval(&self) -> Duration; +} + +/// Custom hot-reloader combinator that can be used to merge hot-reloaded styles onto a base style. +/// Can be useful when working from a base configuration, such as the OS-native styles. +pub struct HotReloadOverrideHandler { + /// The base style, usually provided by `azul-native-style`. + pub base_style: Css, + /// The style that will be added on top of the `base_style`. + pub hot_reloader: Box, +} + +impl HotReloadOverrideHandler { + /// Creates a new `HotReloadHandler` that merges styles onto the given base style + /// (usually the system-native style, in order to let the user override properties). + pub fn new(base_style: Css, hot_reloader: Box) -> Self { + Self { + base_style, + hot_reloader, + } + } +} + +impl HotReloadHandler for HotReloadOverrideHandler { + fn reload_style(&mut self) -> Result { + let mut css = Css::new(); + for stylesheet in self.base_style.clone().stylesheets { + css.append_stylesheet(stylesheet); + } + for stylesheet in self.hot_reloader.reload_style()?.stylesheets { + css.append_stylesheet(stylesheet); + } + Ok(css) + } + + fn get_reload_interval(&self) -> Duration { + self.hot_reloader.get_reload_interval() + } +} diff --git a/azul-css/src/lib.rs b/azul-css/src/lib.rs new file mode 100644 index 000000000..702a6b0ae --- /dev/null +++ b/azul-css/src/lib.rs @@ -0,0 +1,11 @@ +//! Provides datatypes used to describe an application's style using the Azul GUI framework. + +#[macro_use] +mod macros; +mod css; +mod css_properties; +mod hot_reload; + +pub use crate::css::*; +pub use crate::css_properties::*; +pub use crate::hot_reload::*; diff --git a/azul-css/src/macros.rs b/azul-css/src/macros.rs new file mode 100644 index 000000000..b1d295ba6 --- /dev/null +++ b/azul-css/src/macros.rs @@ -0,0 +1,31 @@ +/// Implement the `From` trait for any type. +/// Example usage: +/// ``` +/// enum MyError<'a> { +/// Bar(BarError<'a>) +/// Foo(FooError<'a>) +/// } +/// +/// impl_from!(BarError<'a>, Error::Bar); +/// impl_from!(BarError<'a>, Error::Bar); +/// +/// ``` +macro_rules! impl_from { + // From a type with a lifetime to a type which also has a lifetime + ($a:ident<$c:lifetime>, $b:ident::$enum_type:ident) => { + impl<$c> From<$a<$c>> for $b<$c> { + fn from(e: $a<$c>) -> Self { + $b::$enum_type(e) + } + } + }; + + // From a type without a lifetime to a type which also does not have a lifetime + ($a:ident, $b:ident::$enum_type:ident) => { + impl From<$a> for $b { + fn from(e: $a) -> Self { + $b::$enum_type(e) + } + } + }; +} diff --git a/azul-native-style/Cargo.toml b/azul-native-style/Cargo.toml new file mode 100644 index 000000000..fb16c5a27 --- /dev/null +++ b/azul-native-style/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "azul-native-style" +version = "0.1.0" +authors = ["Felix Schütt "] +license = "MIT" +description = ''' + OS-native styles for use with the Azul desktop GUI framework +''' +documentation = "https://docs.rs/azul" +homepage = "https://azul.rs/" +keywords = ["gui", "GUI", "user interface", "svg", "graphics", "native" ] +categories = ["gui"] +repository = "https://github.com/maps4print/azul" +readme = "README.md" +exclude = ["assets/*", "doc/*", "examples/*"] +autoexamples = false + +[dependencies] +azul-css = { path = "../azul-css", default-features = false } +azul-css-parser = { path = "../azul-css-parser", default-features = false } diff --git a/azul-native-style/src/lib.rs b/azul-native-style/src/lib.rs new file mode 100644 index 000000000..91c1efa58 --- /dev/null +++ b/azul-native-style/src/lib.rs @@ -0,0 +1,32 @@ +//! Provides azul-compatible approximations of OS-native styles. + +extern crate azul_css; +use azul_css::Css; + +extern crate azul_css_parser; + +/// CSS mimicking the OS-native look - Windows: `styles/native_windows.css` +#[cfg(target_os="windows")] +pub const NATIVE_CSS: &str = concat!( + include_str!("styles/native_windows.css"), + include_str!("styles/shared/table.css"), +); + +/// CSS mimicking the OS-native look - Linux: `styles/native_linux.css` +#[cfg(target_os="linux")] +pub const NATIVE_CSS: &str = concat!( + include_str!("styles/native_linux.css"), + include_str!("styles/shared/table.css"), +); + +/// CSS mimicking the OS-native look - Mac: `styles/native_macos.css` +#[cfg(target_os="macos")] +pub const NATIVE_CSS: &str = concat!( + include_str!("styles/native_macos.css"), + include_str!("styles/shared/table.css"), +); + +/// Returns the native style for the OS +pub fn native() -> Css { + azul_css_parser::new_from_str(NATIVE_CSS).unwrap() +} diff --git a/azul-native-style/src/styles/native_linux.css b/azul-native-style/src/styles/native_linux.css new file mode 100644 index 000000000..b1efc7357 --- /dev/null +++ b/azul-native-style/src/styles/native_linux.css @@ -0,0 +1,41 @@ +* { + font-size: 16px; + font-family: sans-serif; + color: #4c4c4c; +} + +.__azul-native-button { + border: 1px solid #b7b7b7; + border-radius: 4px; + box-shadow: 0px 0px 3px #c5c5c5ad; + background: linear-gradient(#fcfcfc, #efefef); + text-align: center; + flex-direction: column; + justify-content: center; +} + +.__azul-native-label { + text-align: center; + flex-direction: column; + justify-content: center; +} + +.__azul-native-input-text { + background-color: white; + height: 14px; + border: 1px solid #9b9b9b; + padding: 1px; + overflow: hidden; + text-align: left; + flex-direction: row; + align-content: flex-end; + justify-content: flex-end; +} + +.__azul-native-input-text:hover { + border: 1px solid #4286f4; +} + +.__azul-native-input-text-label { + +} diff --git a/azul-native-style/src/styles/native_macos.css b/azul-native-style/src/styles/native_macos.css new file mode 100644 index 000000000..8d50e286b --- /dev/null +++ b/azul-native-style/src/styles/native_macos.css @@ -0,0 +1,42 @@ +* { + font-size: 12px; + font-family: Helvetica; + color: #4c4c4c; +} + +.__azul-native-button { + background-color: #e7e7e7; + border: 1px solid #b7b7b7; + border-radius: 4px; + box-shadow: 0px 0px 3px #c5c5c5ad; + background: linear-gradient(#fcfcfc, #efefef); + text-align: center; + flex-direction: column; + justify-content: center; +} + +.__azul-native-label { + text-align: center; + flex-direction: column; + justify-content: center; +} + +.__azul-native-input-text { + background-color: white; + height: 14px; + border: 1px solid #9b9b9b; + padding: 1px; + overflow: hidden; + text-align: left; + flex-direction: row; + align-content: flex-end; + justify-content: flex-end; +} + +.__azul-native-input-text:hover { + border: 1px solid #4286f4; +} + +.__azul-native-input-text-label { + +} \ No newline at end of file diff --git a/azul-native-style/src/styles/native_windows.css b/azul-native-style/src/styles/native_windows.css new file mode 100644 index 000000000..040add01e --- /dev/null +++ b/azul-native-style/src/styles/native_windows.css @@ -0,0 +1,53 @@ +* { + font-size: 14.66px; +} + +.__azul-native-button { + border: 1px solid rgb(172, 172, 172); + background: linear-gradient(to bottom, rgb(239, 239, 239), rgb(229, 229, 229)); + text-align: center; + flex-direction: column; + justify-content: center; + cursor: pointer; + color: green; +} + +.__azul-native-button:hover { + background: linear-gradient(to bottom, rgb(234, 243, 252), rgb(126, 180, 234)); + border: 1px solid rgb(126, 180, 234); +} + +.__azul-native-button:active { + background: linear-gradient(to bottom, rgb(217, 235, 252), rgb(86, 157, 229)); + border: 1px solid rgb(86, 157, 229); +} + +.__azul-native-button:focus { + border: 1px solid rgb(51, 153, 255); +} + +.__azul-native-label { + text-align: center; + flex-direction: column; + justify-content: center; +} + +.__azul-native-input-text { + background-color: white; + height: 24px; + border: 1px solid #9b9b9b; + padding: 1px; + overflow: hidden; + text-align: left; + flex-direction: row; + align-content: flex-end; + justify-content: flex-end; +} + +.__azul-native-input-text:hover { + border: 1px solid #4286f4; +} + +.__azul-native-input-text-label { + +} \ No newline at end of file diff --git a/azul-native-style/src/styles/shared/table.css b/azul-native-style/src/styles/shared/table.css new file mode 100644 index 000000000..21d321345 --- /dev/null +++ b/azul-native-style/src/styles/shared/table.css @@ -0,0 +1,86 @@ +.__azul-native-table-container { + flex-direction: row; +} + +.__azul-native-table-column-container { + flex-direction: row; + position: relative; +} + +.__azul-native-table-column { + flex-direction: column; + min-width: 100px; + border-right: 1px solid #d1d1d1; +} + +.__azul-native-table-row-number-wrapper { + flex-direction: column; + max-width: 30px; +} + +.__azul-native-table-column-name { + height: 20px; +} + +.__azul-native-table-top-left-rect { + height: 20px; + background-color: #e6e6e6; + border-bottom: 1px solid #b5b5b5; + border-right: 1px solid #b5b5b5; +} + +.__azul-native-table-cell { + font-family: sans-serif; + color: black; + text-align: left; + align-items: flex-start; + font-size: 14px; + border-bottom: 1px solid #d1d1d1; + height: 20px; +} + +.__azul-native-table-row { + font-size: 14px; + flex-direction: row; + justify-content: center; + align-items: center; + min-height: 20px; + border-bottom: 0.6px solid #b5b5b5; +} + +.__azul-native-table-column-name { + font-family: sans-serif; + color: #2d2d2d; + font-size: 14px; + background-color: #e6e6e6; + flex-direction: row; + align-items: center; + border-right: 0.6px solid #b5b5b5; + box-shadow-bottom: 0px 0px 3px black; +} + +.__azul-native-table-row-numbers { + font-family: sans-serif; + color: #2d2d2d; + background-color: #e6e6e6; + flex-direction: column; + box-shadow-right: 0px 0px 3px black; +} + +.__azul-native-table-selection { + width: 100px; + height: 20px; + margin-top: 500px; + margin-left: 100px; + position: absolute; + border: 2px solid #407c40; +} + +.__azul-native-table-selection-handle { + position: absolute; + width: 10px; + height: 10px; + background-color: #407c40; + bottom: -5px; + right: -5px; +} \ No newline at end of file diff --git a/azul/Cargo.toml b/azul/Cargo.toml new file mode 100644 index 000000000..cfe2b1f25 --- /dev/null +++ b/azul/Cargo.toml @@ -0,0 +1,143 @@ +[package] +name = "azul" +version = "0.1.0" +authors = ["Felix Schütt "] +license = "MIT" +description = ''' + Azul GUI is a free, functional, immediate-mode GUI framework + for rapid development of desktop applications written in Rust, + supported by the Mozilla WebRender rendering engine +''' +documentation = "https://docs.rs/azul" +homepage = "https://azul.rs/" +keywords = ["gui", "GUI", "user interface", "svg", "graphics" ] +categories = ["gui"] +repository = "https://github.com/maps4print/azul" +readme = "../README.md" +exclude = ["../assets/*", "../doc/*", "../examples/*"] +autoexamples = false + +[dependencies] +azul-css = { version = "0.1.0", path = "../azul-css" } +azul-native-style = { version = "0.1.0", path = "../azul-native-style", optional = true } +azul-css-parser = { version = "0.1.0", path = "../azul-css-parser", optional = true } +azul-dependencies = { version = "0.1.0", git = "https://github.com/maps4print/azul-dependencies", rev = "380b7e7cba8b728a3fc89fe28952e3b07aa624e7" } +serde_derive = { version = "1", optional = true } +serde = { version = "1", optional = true } + +[features] +# The "SVG" feature only enables the creation of shapes / polygons, etc. not the actual parsing +# (which needs the `svg_parsing` feature). +default = [ + "logging", + "native_style", + "css_parser", +] +# Enable this feature to enable crash logging & reporting. +# Azul will insert custom panic handlers to pop up a message and log +# crashes to an "error.log" file, see AppConfig for more details +logging = ["azul-dependencies/logging"] +# The SVG rendering module is pretty huge since it needs lyon - if you don't use +# SVG rendering in your app, you can turn this off to increase compilation +# speed and decrease your binary size +svg = ["azul-dependencies/svg"] +# This is for activating **parsing** of SVG files. If you, for example, just +# want to draw shapes on the screen, you do not need to activate this feature, +# this is just for parsing the SVG from a file. +svg_parsing = ["azul-dependencies/svg_parsing", "svg"] +# If you want an application icon, you can either load it via the raw +# RGBA bytes or use the icon_loading feature to decode it from a PNG / JPG / +# whatever image format on startup. Note that this will import the image +# dependency and use a bit of extra runtime. +icon_loading = ["azul-dependencies/icon_loading"] +# For serializing / deserializing CSS colors using serde +serde_serialization = ["azul-dependencies/serde_serialization", "serde_derive", "serde"] +serde_serialization_css = ["css_parser", "azul-css-parser/serde_serialization", "serde_serialization"] +# On some applications you might not want to load any images. For these purposes +# the image crate can be disabled, to speed up compile times +image_loading = ["azul-dependencies/image_loading"] +# Features to load extra image formats +ico = ["azul-dependencies/ico"] +tga = ["azul-dependencies/tga"] +hdr = ["azul-dependencies/hdr"] +jpeg_rayon = ["azul-dependencies/jpeg_rayon"] +dxt = ["azul-dependencies/dxt"] +webp = ["azul-dependencies/webp"] +css_parser = ["azul-css-parser"] +native_style = ["azul-native-style"] + +[[example]] +name = "async" +path = "../examples/async/async.rs" +required-features = [] + +[[example]] +name = "calculator" +path = "../examples/calculator/calculator.rs" +required-features = [] + +[[example]] +name = "game_of_life" +path = "../examples/game_of_life/game_of_life.rs" +required-features = [] + +[[example]] +name = "hello_world" +path = "../examples/hello_world/hello_world.rs" +required-features = [] + + +[[example]] +name = "hot_reload" +path = "../examples/hot_reload/hot_reload.rs" +required-features = ["image_loading"] + +[[example]] +name = "list" +path = "../examples/list/list.rs" +required-features = [] + +[[example]] +name = "opengl" +path = "../examples/opengl/opengl.rs" +required-features = [] + +[[example]] +name = "slider" +path = "../examples/slider/slider.rs" +required-features = [] + +[[example]] +name = "svg" +path = "../examples/svg/svg.rs" +required-features = ["svg_parsing"] + +[[example]] +name = "table" +path = "../examples/table/table.rs" +required-features = [] + +# [[example]] +# name = "text_editor" +# path = "../examples/text_editor/text_editor.rs" +# required-features = [] + +[[example]] +name = "text_input" +path = "../examples/text_input/text_input.rs" +required-features = [] + +[[example]] +name = "text_shaping" +path = "../examples/text_shaping/text_shaping.rs" +required-features = [] + +[[example]] +name = "transparent_window" +path = "../examples/transparent_window/transparent_window.rs" +required-features = [] + +[[example]] +name = "xml" +path = "../examples/xml/xml.rs" +required-features = [] diff --git a/azul/src/app.rs b/azul/src/app.rs new file mode 100644 index 000000000..9415f7710 --- /dev/null +++ b/azul/src/app.rs @@ -0,0 +1,1505 @@ +use std::{ + mem, + fmt, + time::Instant, + collections::BTreeMap, + sync::{Arc, Mutex, PoisonError}, +}; +#[cfg(debug_assertions)] +use azul_css::HotReloadHandler; +use glium::{ + SwapBuffersError, + glutin::{ + WindowEvent, WindowId as GliumWindowId, + dpi::{LogicalPosition, LogicalSize} + }, +}; +use gleam::gl::{self, Gl, GLuint}; +use webrender::{ + PipelineInfo, Renderer, + api::{ + HitTestResult, HitTestFlags, DevicePixel, + WorldPoint, LayoutSize, LayoutPoint, + Epoch, Transaction, ImageData, ImageDescriptor, + }, +}; +#[cfg(feature = "image_loading")] +use app_resources::ImageSource; +#[cfg(feature = "logging")] +use log::LevelFilter; +use azul_css::{Css, ColorU}; +use { + FastHashMap, + error::ClipboardError, + window::{ + Window, FakeWindow, ScrollStates, + WindowCreateError, WindowCreateOptions, RendererType, + }, + window_state::{WindowSize, DebugState}, + app_resources::TextId, + dom::{Dom, ScrollTagId}, + app_resources::{ + ImageId, FontSource, FontId, ImageReloadError, + FontReloadError, CssImageId, + }, + traits::Layout, + ui_state::UiState, + ui_description::UiDescription, + async::{Task, Timer, TimerId, TerminateTimer}, + callbacks::{FocusTarget, UpdateScreen, Redraw, DontRedraw, LayoutInfo}, +}; +pub use app_resources::AppResources; + +type DeviceIntSize = ::euclid::TypedSize2D; + +// Default clear color is white, to signify that there is rendering going on +// (otherwise, "transparent") backgrounds would be painted black. +const COLOR_WHITE: ColorU = ColorU { r: 255, g: 255, b: 255, a: 0 }; + +/// Graphical application that maintains some kind of application state +pub struct App { + /// The graphical windows, indexed by their system ID / handle + windows: BTreeMap>, + /// The global application state + pub app_state: AppState, + /// Application configuration, whether to enable logging, etc. + pub config: AppConfig, + /// The `Layout::layout()` callback, stored as a function pointer, + /// There are multiple reasons for doing this (instead of requiring `T: Layout` everywhere): + /// + /// - It seperates the `Dom` from the `Layout` trait, making it possible to split the UI solving and styling into reusable crates + /// - It's less typing work (prevents having to type `` everywhere) + /// - It's potentially more efficient to compile (less type-checking required) + /// - It's a preparation for the C ABI, in which traits don't exist (for language bindings). + /// In the C ABI "traits" are simply structs with function pointers (and void* instead of T) + layout_callback: fn(&T, layout_info: LayoutInfo) -> Dom, +} + +/// Configuration for optional features, such as whether to enable logging or panic hooks +#[derive(Debug, Clone)] +#[cfg_attr(not(feature = "logging"), derive(Copy))] +pub struct AppConfig { + /// If enabled, logs error and info messages. + /// + /// Default is `Some(LevelFilter::Error)` to log all errors by default + #[cfg(feature = "logging")] + pub enable_logging: Option, + /// Path to the output log if the logger is enabled + #[cfg(feature = "logging")] + pub log_file_path: Option, + /// If the app crashes / panics, a window with a message box pops up. + /// Setting this to `false` disables the popup box. + #[cfg(feature = "logging")] + pub enable_visual_panic_hook: bool, + /// If this is set to `true` (the default), a backtrace + error information + /// gets logged to stdout and the logging file (only if logging is enabled). + #[cfg(feature = "logging")] + pub enable_logging_on_panic: bool, + /// (STUB) Whether keyboard navigation should be enabled (default: true). + /// Currently not implemented. + pub enable_tab_navigation: bool, + /// Whether to force a hardware or software renderer + pub renderer_type: RendererType, + /// Debug state for all windows + pub debug_state: DebugState, + /// Background color for all windows + pub background_color: ColorU, +} + +impl Default for AppConfig { + fn default() -> Self { + Self { + #[cfg(feature = "logging")] + enable_logging: Some(LevelFilter::Error), + #[cfg(feature = "logging")] + log_file_path: None, + #[cfg(feature = "logging")] + enable_visual_panic_hook: true, + #[cfg(feature = "logging")] + enable_logging_on_panic: true, + enable_tab_navigation: true, + renderer_type: RendererType::default(), + debug_state: DebugState::default(), + background_color: COLOR_WHITE, + } + } +} + +/// Wrapper for your application data, stores the data, windows and resources, as +/// well as running timers and asynchronous tasks. +/// +/// In order to be layout-able, your data model needs to satisfy the `Layout` trait, +/// which maps the state of your application to a DOM (how the application data should be laid out) +pub struct AppState { + /// Your data (the global struct which all callbacks will have access to) + pub data: Arc>, + /// This field represents the state of the windows, public to the user. You can + /// mess around with the state as you like, however, the actual window won't update + /// until the next frame. This is done to "decouple" the frameworks internal + /// state updating logic from the user code (and to make the API future-proof + /// in case extra functions are introduced). + /// + /// Another reason this is needed is to (later) introduce testing for the window + /// state - if the API would directly modify the window itself, these changes + /// wouldn't be recorded anywhere, so there wouldn't be a way to unit-test certain APIs. + /// + /// The state of these `FakeWindow`s gets deleted and recreated on each frame, especially + /// the app's style. This should force a user to design his code in a functional way, + /// without relying on state-based conditions. Example: + /// + /// ```no_run,ignore + /// let window_state = &mut app_state.windows[event.window]; + /// // Update the title + /// window_state.state.title = "Hello"; + /// ``` + pub windows: BTreeMap>, + /// Fonts, images and cached text that is currently loaded inside the app (window-independent). + /// + /// Accessing this field is often required to load new fonts or images, so instead of + /// requiring the `FontHashMap`, a lot of functions just require the whole `AppResources` field. + pub resources: AppResources, + /// Currently running timers (polling functions, run on the main thread) + pub(crate) timers: FastHashMap>, + /// Currently running tasks (asynchronous functions running each on a different thread) + pub(crate) tasks: Vec>, +} + +/// Same as the [AppState](./struct.AppState.html) but without the +/// `self.data` field - used for default callbacks, so that callbacks can +/// load and unload fonts or images + access the system clipboard +/// +/// Default callbacks don't have access to the `AppState.data` field, +/// since they use a `StackCheckedPointer` instead. +pub struct AppStateNoData<'a, T> { + /// See [`AppState.windows`](./struct.AppState.html#structfield.windows) + pub windows: &'a BTreeMap>, + /// See [`AppState.resources`](./struct.AppState.html#structfield.resources) + pub resources : &'a mut AppResources, + /// Currently running timers (polling functions, run on the main thread) + pub(crate) timers: FastHashMap>, + /// Currently running tasks (asynchronous functions running each on a different thread) + pub(crate) tasks: Vec>, +} + +/// Error returned by the `.run()` function +/// +/// If the `.run()` function would panic, that would need `T` to +/// implement `Debug`, which is not necessary if we just return an error. +pub enum RuntimeError { + // Could not swap the display (drawing error) + GlSwapError(SwapBuffersError), + ArcUnlockError, + MutexPoisonError(PoisonError), + MutexLockError, + WindowIndexError, +} + +pub(crate) struct FrameEventInfo { + pub(crate) should_redraw_window: bool, + pub(crate) should_hittest: bool, + pub(crate) cur_cursor_pos: LogicalPosition, + pub(crate) new_window_size: Option, + pub(crate) new_dpi_factor: Option, + pub(crate) is_resize_event: bool, +} + +impl Default for FrameEventInfo { + fn default() -> Self { + Self { + should_redraw_window: false, + should_hittest: false, + cur_cursor_pos: LogicalPosition::new(0.0, 0.0), + new_window_size: None, + new_dpi_factor: None, + is_resize_event: false, + } + } +} + +impl From> for RuntimeError { + fn from(e: PoisonError) -> Self { + RuntimeError::MutexPoisonError(e) + } +} + +impl From for RuntimeError { + fn from(e: SwapBuffersError) -> Self { + RuntimeError::GlSwapError(e) + } +} + +impl fmt::Debug for RuntimeError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use self::RuntimeError::*; + match self { + GlSwapError(e) => write!(f, "Failed to swap GL display: {}", e), + ArcUnlockError => write!(f, "Failed to unlock arc on application shutdown"), + MutexPoisonError(e) => write!(f, "Mutex poisoned (thread panicked unexpectedly): {}", e), + MutexLockError => write!(f, "Failed to lock application state mutex"), + WindowIndexError => write!(f, "Invalid window index"), + } + } +} + +impl fmt::Display for RuntimeError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", format!("{:?}", self)) + } +} + +impl<'a, T: 'a> AppStateNoData<'a, T> { + impl_deamon_api!(); +} + +impl App { + + #[cfg(not(test))] + #[allow(unused_variables)] + /// Create a new, empty application. This does not open any windows. + pub fn new(initial_data: T, config: AppConfig) -> Result { + + #[cfg(feature = "logging")] { + if let Some(log_level) = config.enable_logging { + ::logging::set_up_logging(config.log_file_path.as_ref().map(|s| s.as_str()), log_level); + + if config.enable_logging_on_panic { + ::logging::set_up_panic_hooks(); + } + + if config.enable_visual_panic_hook { + use std::sync::atomic::Ordering; + ::logging::SHOULD_ENABLE_PANIC_HOOK.store(true, Ordering::SeqCst); + } + } + } + + let mut app_state = AppState::new(initial_data, &config)?; + + if let Some(r) = &mut app_state.resources.fake_display.renderer { + set_webrender_debug_flags(r, &DebugState::default(), &config.debug_state); + } + + Ok(Self { + windows: BTreeMap::new(), + app_state, + config, + layout_callback: T::layout, + }) + } +} + +impl App { + + /// Creates a new window + #[cfg(not(test))] + pub fn create_window(&mut self, options: WindowCreateOptions, css: Css) + -> Result, WindowCreateError> + { + Window::new( + &mut self.app_state.resources.fake_display.render_api, + &mut self.app_state.resources.fake_display.hidden_display.gl_window().context(), + &mut self.app_state.resources.fake_display.hidden_events_loop, + options, + css, + self.config.background_color, + ) + } + + #[cfg(debug_assertions)] + #[cfg(not(test))] + pub fn create_hot_reload_window(&mut self, options: WindowCreateOptions, css_loader: Box) + -> Result, WindowCreateError> + { + Window::new_hot_reload( + &mut self.app_state.resources.fake_display.render_api, + &mut self.app_state.resources.fake_display.hidden_display.gl_window().context(), + &mut self.app_state.resources.fake_display.hidden_events_loop, + options, + css_loader, + self.config.background_color, + ) + } + + /// Spawn a new window on the screen. Note that this should only be used to + /// create extra windows, the default window will be the window submitted to + /// the `.run` method. + pub fn add_window(&mut self, window: Window) { + use callbacks::DefaultCallbackSystem; + + let window_id = window.id; + let fake_window = FakeWindow { + state: window.state.clone(), + default_callbacks: DefaultCallbackSystem::new(), + read_only_window: window.display.clone(), + }; + + self.app_state.windows.insert(window_id, fake_window); + self.windows.insert(window_id, window); + } + + /// Start the rendering loop for the currently open windows + /// This is the "main app loop", "main game loop" or whatever you want to call it. + /// Usually this is the last function you call in your `main()` function, since exiting + /// it means that the user has closed all windows and wants to close the app. + /// + /// When all windows are closed, this function returns the internal data again. + /// This is useful for ex. CLI application that run procedurally, but then want to + /// open a window temporarily, to ask for user input in a "nicer" way than a pure + /// CLI-way. + /// + /// This way you can do this: + /// + /// ```no_run,ignore + /// let app = App::new(MyData { username: None, password: None }); + /// app.create_window(WindowCreateOptions::default(), azul_native_style::native()); + /// + /// // pop open a window that asks the user for his username and password... + /// let MyData { username, password } = app.run(); + /// + /// // continue the rest of the program here... + /// println!("username: {:?}, password: {:?}", username, password); + /// ``` + #[cfg(not(test))] + pub fn run(mut self, window: Window) -> Result> { + + // Apps need to have at least one window open + self.add_window(window); + self.run_inner()?; + + // NOTE: This is necessary because otherwise, the Arc::try_unwrap would fail, + // since one Arc is still owned by the app_state.tasks structure + // + // See https://github.com/maps4print/azul/issues/24#issuecomment-429737273 + mem::drop(self.app_state.tasks); + + let unique_arc = Arc::try_unwrap(self.app_state.data).map_err(|_| RuntimeError::ArcUnlockError)?; + unique_arc.into_inner().map_err(|e| e.into()) + } + + #[cfg(not(test))] + fn run_inner(&mut self) -> Result<(), RuntimeError> { + + use std::{thread, time::Duration}; + use glium::glutin::Event; + + let mut ui_state_cache = { + let app_state = &mut self.app_state; + let mut ui_state_map = BTreeMap::new(); + for window_id in self.windows.keys() { + ui_state_map.insert(*window_id, UiState::from_app_state(app_state, window_id, self.layout_callback)?); + } + ui_state_map + }; + let mut ui_description_cache = self.windows.keys().map(|window_id| (*window_id, UiDescription::default())).collect::>(); + let mut force_redraw_cache = self.windows.keys().map(|window_id| (*window_id, 2)).collect(); + let mut awakened_tasks = self.windows.keys().map(|window_id| (*window_id, false)).collect(); + + #[cfg(debug_assertions)] + let mut last_style_reload = Instant::now(); + #[cfg(debug_assertions)] + let mut should_print_css_error = true; + + while !self.windows.is_empty() { + + let time_start = Instant::now(); + + let mut closed_windows = Vec::::new(); + let mut frame_was_resize = false; + let mut events = BTreeMap::new(); + + self.app_state.resources.fake_display.hidden_events_loop.poll_events(|e| match e { + // Filter out all events that are uninteresting or unnecessary + Event::WindowEvent { event: WindowEvent::Refresh, .. } => { }, + Event::WindowEvent { window_id, event } => { + events.entry(window_id).or_insert_with(|| Vec::new()).push(event); + }, + _ => { }, + }); + + let mut single_window_results = Vec::with_capacity(self.windows.len()); + + for (current_window_id, mut window) in self.windows.iter_mut() { + + // Only process the events belong to this window ID... + let window_events: Vec = events.get(current_window_id).cloned().unwrap_or_default(); + + let single_window_result = + hit_test_single_window( + &window_events, + ¤t_window_id, + &mut window, + &mut self.app_state, + &mut ui_state_cache, + &mut force_redraw_cache, + &mut awakened_tasks, + )?; + + if single_window_result.needs_relayout_resize { + frame_was_resize = true; + } + + if single_window_result.window_should_close { + closed_windows.push(*current_window_id); + + // TODO: Currently there is no way to return from the main event loop + // i.e. the windows aren't actually getting closed + // This is a hack, so that windows currently close properly + return Ok(()); + } + + single_window_results.push(single_window_result); + } + + #[cfg(debug_assertions)] { + hot_reload_css( + &mut self.windows, + &mut last_style_reload, + &mut should_print_css_error, + &mut awakened_tasks, + )?; + } + + // Close windows if necessary + closed_windows.into_iter().for_each(|closed_window_id| { + ui_state_cache.remove(&closed_window_id); + ui_description_cache.remove(&closed_window_id); + force_redraw_cache.remove(&closed_window_id); + self.windows.remove(&closed_window_id); + }); + + let should_relayout_all_windows = single_window_results.iter().any(|res| res.should_relayout()); + let should_rerender_all_windows = single_window_results.iter().any(|res| res.should_rerender()); + + let should_redraw_timers = self.app_state.run_all_timers(); + let should_redraw_tasks = self.app_state.clean_up_finished_tasks(); + let should_redraw_timers_or_tasks = [should_redraw_timers, should_redraw_tasks].into_iter().any(|e| *e == Redraw); + + // If there is a relayout necessary, re-layout *all* windows! + if should_relayout_all_windows || should_redraw_timers_or_tasks{ + for (current_window_id, mut window) in self.windows.iter_mut() { + relayout_single_window( + self.layout_callback, + ¤t_window_id, + &mut window, + &mut self.app_state, + &mut ui_state_cache, + &mut ui_description_cache, + &mut force_redraw_cache, + &mut awakened_tasks, + )?; + } + } + + // If there is a re-render necessary, re-render *all* windows + if should_rerender_all_windows || should_redraw_timers_or_tasks { + for window in self.windows.values_mut() { + // TODO: For some reason this function has to be called twice in order + // to actually update the screen. For some reason the first swap_buffers() has + // no effect (winit bug?) + rerender_single_window( + &self.config, + window, + &mut self.app_state.resources, + ); + rerender_single_window( + &self.config, + window, + &mut self.app_state.resources, + ); + } + // Automatically remove unused fonts and images from webrender + // Tell the font + image GC to start a new frame + self.app_state.resources.garbage_collect_fonts_and_images(); + } + + if !frame_was_resize { + // Wait until 16ms have passed, but not during a resize event + let diff = time_start.elapsed(); + const FRAME_TIME: Duration = Duration::from_millis(16); + if diff < FRAME_TIME { + thread::sleep(FRAME_TIME - diff); + } + } + } + + Ok(()) + } + + /// See `AppState::add_task`. + pub fn add_task(&mut self, task: Task) { + self.app_state.add_task(task); + } + + /// Toggles debugging flags in webrender, updates `self.config.debug_state` + #[cfg(not(test))] + pub fn toggle_debug_flags(&mut self, new_state: DebugState) { + if let Some(r) = &mut self.app_state.resources.fake_display.renderer { + set_webrender_debug_flags(r, &self.config.debug_state, &new_state); + } + self.config.debug_state = new_state; + } +} + +image_api!(App::app_state); +font_api!(App::app_state); +text_api!(App::app_state); +clipboard_api!(App::app_state); +timer_api!(App::app_state); + +impl AppState { + + /// Creates a new `AppState` + fn new(initial_data: T, config: &AppConfig) -> Result { + Ok(Self { + data: Arc::new(Mutex::new(initial_data)), + windows: BTreeMap::new(), + resources: AppResources::new(config)?, + timers: FastHashMap::default(), + tasks: Vec::new(), + }) + } + + impl_deamon_api!(); + + /// Run all currently registered timers + #[must_use] + fn run_all_timers(&mut self) -> UpdateScreen { + let mut should_update_screen = DontRedraw; + let mut lock = self.data.lock().unwrap(); + let mut timers_to_terminate = Vec::new(); + + for (key, timer) in self.timers.iter_mut() { + let (should_update, should_terminate) = timer.invoke_callback_with_data(&mut lock, &mut self.resources); + + if should_update == Redraw && + should_update_screen == DontRedraw { + should_update_screen = Redraw; + } + + if should_terminate == TerminateTimer::Terminate { + timers_to_terminate.push(key.clone()); + } + } + + for key in timers_to_terminate { + self.timers.remove(&key); + } + + should_update_screen + } + + /// Remove all tasks that have finished executing + #[must_use] fn clean_up_finished_tasks(&mut self) -> UpdateScreen { + let old_count = self.tasks.len(); + let mut timers_to_add = Vec::new(); + self.tasks.retain(|task| { + if task.is_finished() { + if let Some(timer) = task.after_completion_timer { + timers_to_add.push((TimerId::new(), timer)); + } + false + } else { + true + } + }); + + let timers_is_empty = timers_to_add.is_empty(); + let new_count = self.tasks.len(); + + // Start all the timers that should run after the completion of the task + for (timer_id, timer) in timers_to_add { + self.add_timer(timer_id, timer); + } + + if old_count == new_count && timers_is_empty { + DontRedraw + } else { + Redraw + } + } +} + +image_api!(AppState::resources); +font_api!(AppState::resources); +text_api!(AppState::resources); +clipboard_api!(AppState::resources); + +struct SingleWindowContentResult { + needs_rerender_hover_active: bool, + needs_relayout_hover_active: bool, + needs_relayout_resize: bool, + window_should_close: bool, + should_scroll_render: bool, + needs_relayout_tasks: bool, + needs_relayout_refresh: bool, + callbacks_update_screen: UpdateScreen, + hit_test_results: Option, + new_focus_target: Option, +} + +impl SingleWindowContentResult { + + pub fn should_relayout(&self) -> bool { + self.needs_relayout_hover_active || + self.needs_relayout_resize || + self.needs_relayout_tasks || + self.needs_relayout_refresh || + self.callbacks_update_screen == Redraw + } + + pub fn should_rerender(&self) -> bool { + self.should_relayout() || self.should_scroll_render || self.needs_rerender_hover_active + } +} + +/// Call the callbacks / do the hit test +/// Returns (if the event was a resize event, if the window was closed) +#[cfg(not(test))] +fn hit_test_single_window( + events: &[WindowEvent], + window_id: &GliumWindowId, + window: &mut Window, + app_state: &mut AppState, + ui_state_cache: &mut BTreeMap>, + force_redraw_cache: &mut BTreeMap, + awakened_tasks: &mut BTreeMap, +) -> Result> { + + use self::RuntimeError::*; + + let (mut frame_event_info, window_should_close) = window.state.update_window_state(&events); + let mut ret = SingleWindowContentResult { + needs_rerender_hover_active: false, + needs_relayout_hover_active: false, + needs_relayout_resize: frame_event_info.is_resize_event, + window_should_close, + should_scroll_render: false, + needs_relayout_tasks: *(awakened_tasks.get(window_id).ok_or(WindowIndexError)?), + needs_relayout_refresh: *(force_redraw_cache.get(window_id).ok_or(WindowIndexError)?) > 0, + callbacks_update_screen: DontRedraw, + hit_test_results: None, + new_focus_target: None, + }; + + if events.is_empty() && !ret.should_relayout() && !ret.should_rerender() { + // Event was not a resize event, window should **not** close + ret.window_should_close = window_should_close; + return Ok(ret); + } + + if frame_event_info.should_hittest { + + ret.hit_test_results = do_hit_test(&window, &app_state.resources); + + for event in events.iter() { + + let callback_result = call_callbacks( + ret.hit_test_results.as_ref(), + event, + window, + &window_id, + ui_state_cache.get_mut(window_id).ok_or(WindowIndexError)?, + app_state + )?; + + if callback_result.should_update_screen == Redraw { + ret.callbacks_update_screen = Redraw; + } + if callback_result.needs_redraw_anyways { + ret.needs_rerender_hover_active = true; + } + + if callback_result.needs_relayout_anyways { + ret.needs_relayout_hover_active = true; + } + + // Note: Don't set `pending_focus_target` directly here, because otherwise + // callbacks that return `Some()` would get immediately overwritten again + // by callbacks that return `None`. + if let Some(overwrites_focus) = callback_result.callbacks_overwrites_focus { + window.state.internal.pending_focus_target = Some(overwrites_focus.clone()); + ret.new_focus_target = Some(overwrites_focus); + } + } + } + + ret.hit_test_results = ret.hit_test_results.or_else(|| do_hit_test(window, &app_state.resources)); + + // Scroll for the scrolled amount for each node that registered a scroll state. + let should_scroll_render = match &ret.hit_test_results { + Some(hit_test_results) => update_scroll_state(window, hit_test_results), + None => false, + }; + + ret.should_scroll_render = should_scroll_render; + + if frame_event_info.is_resize_event { + // This is a hack because during a resize event, winit eats the "awakened" + // event. So what we do is that we call the layout-and-render again, to + // trigger a second "awakened" event. So when the window is resized, the + // layout function is called twice (the first event will be eaten by winit) + // + // This is a reported bug and should be fixed somewhere in July + *force_redraw_cache.get_mut(window_id).ok_or(WindowIndexError)? = 2; + } + + // See: https://docs.rs/glutin/0.19.0/glutin/struct.CombinedContext.html#method.resize + // + // Some platforms (macOS, Wayland) require being manually updated when their window + // or surface is resized. + #[cfg(not(target_os = "windows"))] { + if frame_event_info.is_resize_event { + // Resize gl window + let gl_window = window.display.gl_window(); + let size = gl_window.get_inner_size().unwrap().to_physical(gl_window.get_hidpi_factor()); + gl_window.resize(size); + } + } + + // Update the window state that we got from the frame event (updates window dimensions and DPI) + // Sets frame_event_info.needs redraw if the event was a + window.update_from_external_window_state(&mut frame_event_info, &app_state.resources.fake_display.hidden_events_loop); + // Update the window state every frame that was set by the user + window.update_from_user_window_state(app_state.windows[&window_id].state.clone()); + // Reset the scroll amount to 0 (for the next frame) + window.clear_scroll_state(); + + Ok(ret) +} + +#[cfg(not(test))] +fn relayout_single_window( + layout_callback: fn(&T, LayoutInfo) -> Dom, + window_id: &GliumWindowId, + window: &mut Window, + app_state: &mut AppState, + ui_state_cache: &mut BTreeMap>, + ui_description_cache: &mut BTreeMap>, + force_redraw_cache: &mut BTreeMap, + awakened_tasks: &mut BTreeMap, +) -> Result<(), RuntimeError> { + + use self::RuntimeError::*; + + // Call the Layout::layout() fn, get the DOM + *ui_state_cache.get_mut(window_id).ok_or(WindowIndexError)? = + UiState::from_app_state(app_state, window_id, layout_callback)?; + + // Style the DOM (is_mouse_down is necessary for styling :hover, :active + :focus nodes) + let is_mouse_down = window.state.internal.mouse_state.mouse_down(); + + *ui_description_cache.get_mut(window_id).ok_or(WindowIndexError)? = + UiDescription::match_css_to_dom( + ui_state_cache.get_mut(window_id).ok_or(WindowIndexError)?, + &window.css, + &mut window.state.internal.focused_node, + &mut window.state.internal.pending_focus_target, + &window.state.internal.hovered_nodes, + is_mouse_down, + ); + + let mut fake_window = app_state.windows.get_mut(window_id).ok_or(WindowIndexError)?; + update_display_list( + &mut app_state.data, + &ui_description_cache[window_id], + &ui_state_cache[window_id], + &mut *window, + &mut fake_window, + &mut app_state.resources, + ); + *awakened_tasks.get_mut(window_id).ok_or(WindowIndexError)? = false; + + if let Some(i) = force_redraw_cache.get_mut(window_id) { + if *i > 0 { *i -= 1 }; + if *i == 1 { + clean_up_unused_opengl_textures(app_state.resources.fake_display.renderer.as_mut().unwrap().flush_pipeline_info()); + } + } + + Ok(()) +} + +#[cfg(not(test))] +fn rerender_single_window( + config: &AppConfig, + window: &mut Window, + resources: &mut AppResources, +) { + render_inner(window, resources, Transaction::new(), config.background_color); +} + +/// Returns if there was an error with the CSS reloading, necessary so that the error message is only printed once +#[cfg(debug_assertions)] +fn hot_reload_css( + windows: &mut BTreeMap>, + last_style_reload: &mut Instant, + should_print_error: &mut bool, + awakened_tasks: &mut BTreeMap, +) -> Result<(), RuntimeError> { + + use self::RuntimeError::*; + + for (window_id, window) in windows.iter_mut() { + // Hot-reload a style if necessary + let hot_reloader = match window.css_loader.as_mut() { + None => continue, + Some(s) => s, + }; + + let should_reload = Instant::now() - *last_style_reload > hot_reloader.get_reload_interval(); + + if !should_reload { + continue; + } + + match hot_reloader.reload_style() { + Ok(mut new_css) => { + new_css.sort_by_specificity(); + window.css = new_css; + if !(*should_print_error) { + println!("--- OK: CSS parsed without errors, continuing hot-reload."); + } + *last_style_reload = Instant::now(); + // window.events_loop.create_proxy().wakeup().unwrap_or(()); + *awakened_tasks.get_mut(window_id).ok_or(WindowIndexError)? = true; + + *should_print_error = true; + }, + Err(why) => { + if *should_print_error { + println!("{}", why); + } + *should_print_error = false; + }, + }; + } + + Ok(()) +} + +/// Returns the currently hit-tested results, in back-to-front order +#[cfg(not(test))] +fn do_hit_test(window: &Window, app_resources: &AppResources) -> Option { + + let cursor_location = window.state.internal.mouse_state.cursor_pos + .map(|pos| WorldPoint::new(pos.x as f32, pos.y as f32))?; + + let mut hit_test_results = app_resources.fake_display.render_api.hit_test( + window.internal.document_id, + Some(window.internal.pipeline_id), + cursor_location, + HitTestFlags::FIND_ALL + ); + + // Execute callbacks back-to-front, not front-to-back + hit_test_results.items.reverse(); + + Some(hit_test_results) +} + +/// Struct returned from the `call_callbacks()` function - +/// returns important information from the callbacks +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +struct CallCallbackReturn { + /// Whether one or more callbacks say to redraw the screen or not + pub should_update_screen: UpdateScreen, + /// Whether one or more callbacks have messed with the current + /// focused element i.e. via `.clear_focus()` or similar. + pub callbacks_overwrites_focus: Option, + /// Whether the screen should be redrawn even if no Callback returns an `UpdateScreen::Redraw`. + /// This is necessary for `:hover` and `:active` mouseovers - otherwise the screen would + /// only update on the next resize. + pub needs_redraw_anyways: bool, + /// Same as `needs_redraw_anyways`, but for reusing the layout from the previous frame. + /// Each `:hover` and `:active` group stores whether it modifies the layout, as + /// a performance optimization. + pub needs_relayout_anyways: bool, +} + +/// Returns an bool whether the window should be redrawn or not (true - redraw the screen, false: don't redraw). +fn call_callbacks( + hit_test_results: Option<&HitTestResult>, + event: &WindowEvent, + window: &mut Window, + window_id: &GliumWindowId, + ui_state: &UiState, + app_state: &mut AppState) +-> Result> { + + use { + callbacks::CallbackInfo, + window_state::{KeyboardState, MouseState}, + self::RuntimeError::*, + }; + + let mut should_update_screen = DontRedraw; + + let hit_test_items = hit_test_results.map(|h| h.items.clone()).unwrap_or_default(); + + let callbacks_filter_list = window.state.determine_callbacks(&hit_test_items, event, ui_state); + + // TODO: this should be refactored - currently very stateful and error-prone! + app_state.windows.get_mut(window_id).ok_or(WindowIndexError)? + .set_keyboard_state(&window.state.internal.keyboard_state); + app_state.windows.get_mut(window_id).ok_or(WindowIndexError)? + .set_mouse_state(&window.state.internal.mouse_state); + + let mut callbacks_overwrites_focus = None; + + let mut default_timers = FastHashMap::default(); + let mut default_tasks = Vec::new(); + + // Run all default callbacks - **before** the user-defined callbacks are run! + { + let mut lock = app_state.data.lock().map_err(|_| RuntimeError::MutexLockError)?; + + for (node_id, callback_results) in callbacks_filter_list.nodes_with_callbacks.iter() { + let hit_item = &callback_results.hit_test_item; + for default_callback_id in callback_results.default_callbacks.values() { + + let mut callback_info = CallbackInfo { + focus: None, + window_id, + hit_dom_node: *node_id, + ui_state, + hit_test_items: &hit_test_items, + cursor_relative_to_item: hit_item.as_ref().map(|hi| (hi.point_relative_to_item.x, hi.point_relative_to_item.y)), + cursor_in_viewport: hit_item.as_ref().map(|hi| (hi.point_in_viewport.x, hi.point_in_viewport.y)), + }; + + let mut app_state_no_data = AppStateNoData { + windows: &app_state.windows, + resources: &mut app_state.resources, + timers: FastHashMap::default(), + tasks: Vec::new(), + }; + + if app_state.windows[window_id].default_callbacks.run_callback( + &mut *lock, + default_callback_id, + &mut app_state_no_data, + &mut callback_info + ) == Redraw { + should_update_screen = Redraw; + } + + default_timers.extend(app_state_no_data.timers.into_iter()); + default_tasks.extend(app_state_no_data.tasks.into_iter()); + + // Overwrite the focus from the callback info + if let Some(new_focus) = callback_info.focus { + callbacks_overwrites_focus = Some(new_focus); + } + } + } + } + + // If the default callbacks have started timers or tasks, add them to the main app state + for (timer_id, timer) in default_timers { + app_state.add_timer(timer_id, timer); + } + + for task in default_tasks { + app_state.add_task(task); + } + + for (node_id, callback_results) in callbacks_filter_list.nodes_with_callbacks.iter() { + let hit_item = &callback_results.hit_test_item; + for callback in callback_results.normal_callbacks.values() { + + let mut callback_info = CallbackInfo { + focus: None, + window_id, + hit_dom_node: *node_id, + ui_state: &ui_state, + hit_test_items: &hit_test_items, + cursor_relative_to_item: hit_item.as_ref().map(|hi| (hi.point_relative_to_item.x, hi.point_relative_to_item.y)), + cursor_in_viewport: hit_item.as_ref().map(|hi| (hi.point_in_viewport.x, hi.point_in_viewport.y)), + }; + + if (callback.0)(app_state, &mut callback_info) == Redraw { + should_update_screen = Redraw; + } + + if let Some(new_focus) = callback_info.focus { + callbacks_overwrites_focus = Some(new_focus); + } + } + } + + app_state.windows.get_mut(window_id).ok_or(WindowIndexError)? + .set_keyboard_state(&KeyboardState::default()); + app_state.windows.get_mut(window_id).ok_or(WindowIndexError)? + .set_mouse_state(&MouseState::default()); + + Ok(CallCallbackReturn { + should_update_screen, + callbacks_overwrites_focus, + needs_redraw_anyways: callbacks_filter_list.needs_redraw_anyways, + needs_relayout_anyways: callbacks_filter_list.needs_relayout_anyways, + }) +} + +/// Build the display list and send it to webrender +#[cfg(not(test))] +fn update_display_list( + app_data: &mut Arc>, + ui_description: &UiDescription, + ui_state: &UiState, + window: &mut Window, + fake_window: &mut FakeWindow, + app_resources: &mut AppResources, +) { + use display_list::DisplayList; + + let display_list = DisplayList::new_from_ui_description(ui_description, ui_state); + + // NOTE: layout_result contains all words, text information, etc. + // - very important for selection! + let (builder, scrolled_nodes, _layout_result) = display_list.into_display_list_builder( + app_data, + window, + fake_window, + app_resources, + ); + + // NOTE: Display list has to be rebuilt every frame, otherwise, the epochs get out of sync + let display_list_builder = builder.finalize().2; + window.internal.last_scrolled_nodes = scrolled_nodes; + + let (logical_size, _) = convert_window_size(&window.state.size); + + let mut txn = Transaction::new(); + txn.set_display_list( + window.internal.epoch, + None, + logical_size.clone(), + (window.internal.pipeline_id, logical_size, display_list_builder), + true, + ); + + app_resources.fake_display.render_api.send_transaction(window.internal.document_id, txn); +} + +/// Scroll all nodes in the ScrollStates to their correct position and insert +/// the positions into the transaction +/// +/// NOTE: scroll_states has to be mutable, since every key has a "visited" field, to +/// indicate whether it was used during the current frame or not. +fn scroll_all_nodes(scroll_states: &mut ScrollStates, txn: &mut Transaction) { + use webrender::api::ScrollClamping; + for (key, value) in scroll_states.0.iter_mut() { + let (x, y) = value.get(); + txn.scroll_node_with_id(LayoutPoint::new(x, y), *key, ScrollClamping::ToContentBounds); + } +} + +/// Returns the (logical_size, physical_size) as LayoutSizes, which can then be passed to webrender +fn convert_window_size(size: &WindowSize) -> (LayoutSize, DeviceIntSize) { + let logical_size = LayoutSize::new( + (size.dimensions.width * size.winit_hidpi_factor) as f32, + (size.dimensions.height * size.winit_hidpi_factor) as f32 + ); + let physical_size = size.dimensions.to_physical(size.winit_hidpi_factor); + let physical_size = DeviceIntSize::new(physical_size.width as i32, physical_size.height as i32); + (logical_size, physical_size) +} + +/// Special rendering function that skips building a layout and only does +/// hit-testing and rendering - called on pure scroll events, since it's +/// significantly less CPU-intensive to just render the last display list instead of +/// re-layouting on every single scroll event. +#[must_use] +fn update_scroll_state( + window: &mut Window, + hit_test_results: &HitTestResult, +) -> bool { + + const SCROLL_THRESHOLD: f64 = 0.5; // px + + let scroll_x = window.state.internal.mouse_state.scroll_x; + let scroll_y = window.state.internal.mouse_state.scroll_y; + + if scroll_x.abs() < SCROLL_THRESHOLD && scroll_y.abs() < SCROLL_THRESHOLD { + return false; + } + + let mut should_scroll_render = false; + + let scrolled_nodes = &window.internal.last_scrolled_nodes; + let scroll_states = &mut window.scroll_states; + + for scroll_node in hit_test_results.items.iter() + .filter_map(|item| scrolled_nodes.tags_to_node_ids.get(&ScrollTagId(item.tag.0))) + .filter_map(|node_id| scrolled_nodes.overflowing_nodes.get(&node_id)) { + + // The external scroll ID is constructed from the DOM hash + let scroll_id = scroll_node.parent_external_scroll_id; + + if scroll_states.0.contains_key(&scroll_id) { + // TODO: make scroll speed configurable (system setting?) + scroll_states.scroll_node(&scroll_id, scroll_x as f32, scroll_y as f32); + should_scroll_render = true; + } + } + + should_scroll_render +} + +fn clean_up_unused_opengl_textures(pipeline_info: PipelineInfo) { + + use compositor::ACTIVE_GL_TEXTURES; + + // TODO: currently active epochs can be empty, why? + // + // I mean, while the renderer is rendering, there can never be "no epochs" active, + // at least one epoch must always be active. + if pipeline_info.epochs.is_empty() { + return; + } + + // TODO: pipeline_info.epochs does not contain all active epochs, + // at best it contains the lowest in-use epoch. I.e. if `Epoch(43)` + // is listed, you can remove all textures from Epochs **lower than 43** + // BUT NOT EPOCHS HIGHER THAN 43. + // + // This means that "all active epochs" (in the documentation) is misleading + // since it doesn't actually list all active epochs, otherwise it'd list Epoch(43), + // Epoch(44), Epoch(45), which are currently active. + let oldest_to_remove_epoch = pipeline_info.epochs.values().min().unwrap(); + + let mut active_textures_lock = ACTIVE_GL_TEXTURES.lock().unwrap(); + + // Retain all OpenGL textures from epochs higher than the lowest epoch + // + // TODO: Handle overflow of Epochs correctly (low priority) + active_textures_lock.retain(|key, _| key > oldest_to_remove_epoch); +} + +// We don't want the epoch to increase to u32::MAX, since +// u32::MAX represents an invalid epoch, which could confuse webrender +fn increase_epoch(old: Epoch) -> Epoch { + use std::u32; + const MAX_ID: u32 = u32::MAX - 1; + match old.0 { + MAX_ID => Epoch(0), + other => Epoch(other + 1), + } +} + +// Function wrapper that is invoked on scrolling and normal rendering - only renders the +// window contents and updates the screen, assumes that all transactions via the RenderApi +// have been committed before this function is called. +// +// WebRender doesn't reset the active shader back to what it was, but rather sets it +// to zero, which glium doesn't know about, so on the next frame it tries to draw with shader 0. +// This leads to problems when invoking GlTextureCallbacks, because those don't expect +// the OpenGL state to change between calls. Also see: https://github.com/servo/webrender/pull/2880 +// +// NOTE: For some reason, webrender allows rendering to a framebuffer with a +// negative width / height, although that doesn't make sense +#[cfg(not(test))] +fn render_inner( + window: &mut Window, + app_resources: &mut AppResources, + mut txn: Transaction, + background_color: ColorU, +) { + + use window::get_gl_context; + use glium::glutin::ContextTrait; + use webrender::api::{DeviceIntRect, DeviceIntPoint}; + use azul_css::ColorF; + + let (_, framebuffer_size) = convert_window_size(&window.state.size); + + // Especially during minimization / maximization of a window, it can happen that the window + // width or height is zero. In that case, no rendering is necessary (doing so would crash + // the application, since glTexImage2D may never have a 0 as the width or height. + if framebuffer_size.width == 0 || framebuffer_size.height == 0 { + return; + } + + window.internal.epoch = increase_epoch(window.internal.epoch); + + txn.set_window_parameters( + framebuffer_size.clone(), + DeviceIntRect::new(DeviceIntPoint::new(0, 0), framebuffer_size), + window.state.size.hidpi_factor as f32 + ); + txn.set_root_pipeline(window.internal.pipeline_id); + scroll_all_nodes(&mut window.scroll_states, &mut txn); + txn.generate_frame(); + + app_resources.fake_display.render_api.send_transaction(window.internal.document_id, txn); + + // Update WR texture cache + app_resources.fake_display.renderer.as_mut().unwrap().update(); + + let background_color_f: ColorF = background_color.into(); + + unsafe { + + // NOTE: GlContext is the context of the app-global, hidden window + // (that shares the renderer), not the context of the window itself. + let gl_context = get_gl_context(&app_resources.fake_display.hidden_display).unwrap(); + + // NOTE: The `hidden_display` must share the OpenGL context with the `window`, + // otherwise this will segfault! Use `ContextBuilder::with_shared_lists` to share the + // OpenGL context across different windows. + // + // The context **must** be made current before calling `.bind_framebuffer()`, + // otherwise EGL will panic with EGL_BAD_MATCH. The current context has to be the + // hidden_display context, otherwise this will segfault on Windows. + app_resources.fake_display.hidden_display.gl_window().make_current().unwrap(); + + let mut current_program = [0_i32]; + gl_context.get_integer_v(gl::CURRENT_PROGRAM, &mut current_program); + + // Generate a framebuffer (that will contain the final, rendered screen output). + let framebuffers = gl_context.gen_framebuffers(1); + gl_context.bind_framebuffer(gl::FRAMEBUFFER, framebuffers[0]); + + // Create the texture to render to + let textures = gl_context.gen_textures(1); + + gl_context.bind_texture(gl::TEXTURE_2D, textures[0]); + gl_context.tex_image_2d(gl::TEXTURE_2D, 0, gl::RGB as i32, framebuffer_size.width, framebuffer_size.height, 0, gl::RGB, gl::UNSIGNED_BYTE, None); + + gl_context.tex_parameter_i(gl::TEXTURE_2D, gl::TEXTURE_MAG_FILTER, gl::NEAREST as i32); + gl_context.tex_parameter_i(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, gl::NEAREST as i32); + gl_context.tex_parameter_i(gl::TEXTURE_2D, gl::TEXTURE_WRAP_S, gl::CLAMP_TO_EDGE as i32); + gl_context.tex_parameter_i(gl::TEXTURE_2D, gl::TEXTURE_WRAP_T, gl::CLAMP_TO_EDGE as i32); + + let depthbuffers = gl_context.gen_renderbuffers(1); + gl_context.bind_renderbuffer(gl::RENDERBUFFER, depthbuffers[0]); + gl_context.renderbuffer_storage(gl::RENDERBUFFER, gl::DEPTH_COMPONENT, framebuffer_size.width, framebuffer_size.height); + gl_context.framebuffer_renderbuffer(gl::FRAMEBUFFER, gl::DEPTH_ATTACHMENT, gl::RENDERBUFFER, depthbuffers[0]); + + // Set "textures[0]" as the color attachement #0 + gl_context.framebuffer_texture_2d(gl::FRAMEBUFFER, gl::COLOR_ATTACHMENT0, gl::TEXTURE_2D, textures[0], 0); + + gl_context.draw_buffers(&[gl::COLOR_ATTACHMENT0]); + + // Check that the framebuffer is complete + debug_assert!(gl_context.check_frame_buffer_status(gl::FRAMEBUFFER) == gl::FRAMEBUFFER_COMPLETE); + + // Invoke WebRender to render the frame - renders to the currently bound FB + gl_context.clear_color(background_color_f.r, background_color_f.g, background_color_f.b, background_color_f.a); + gl_context.clear_depth(0.0); + + // Disable SRGB and multisample, otherwise, WebRender will crash + gl_context.disable(gl::FRAMEBUFFER_SRGB); + gl_context.disable(gl::MULTISAMPLE); + gl_context.disable(gl::POLYGON_SMOOTH); + + app_resources.fake_display.renderer.as_mut().unwrap().render(framebuffer_size).unwrap(); + + gl_context.delete_framebuffers(&framebuffers); + gl_context.delete_renderbuffers(&depthbuffers); + + // FBOs can't be shared between windows, but textures can. + // In order to draw on the windows backbuffer, first make the window current, then draw to FB 0 + window.display.gl_window().make_current().unwrap(); + draw_texture_to_screen(&*gl_context, textures[0], framebuffer_size); + window.display.swap_buffers().unwrap(); + + app_resources.fake_display.hidden_display.gl_window().make_current().unwrap(); + + // Only delete the texture here... + gl_context.delete_textures(&textures); + + gl_context.bind_framebuffer(gl::FRAMEBUFFER, 0); + gl_context.bind_texture(gl::TEXTURE_2D, 0); + gl_context.use_program(current_program[0] as u32); + + }; + + // The initial setup can lead to flickering during startup, by default + // the window is hidden until the first frame has been rendered. + if window.create_options.state.is_visible && window.state.is_visible { + window.display.gl_window().window().show(); + window.state.is_visible = true; + window.create_options.state.is_visible = false; + } +} + +/// When called with glDrawArrays(0, 3), generates a simple triangle that +/// spans the whole screen. +const DISPLAY_VERTEX_SHADER: &[u8] = b" + #version 140 + out vec2 vTexCoords; + void main() { + float x = -1.0 + float((gl_VertexID & 1) << 2); + float y = -1.0 + float((gl_VertexID & 2) << 1); + vTexCoords = vec2((x+1.0)*0.5, (y+1.0)*0.5); + gl_Position = vec4(x, y, 0, 1); + } +\0"; + +/// Shader that samples an input texture (`fScreenTex`) to the output FB. +const DISPLAY_FRAGMENT_SHADER: &[u8] = b" + #version 140 + in vec2 vTexCoords; + uniform sampler2D fScreenTex; + out vec4 fColorOut; + + void main() { + fColorOut = texture(fScreenTex, vTexCoords); + } +\0"; + +// NOTE: Compilation is thread-unsafe, should only be compiled on the main thread +static mut DISPLAY_SHADER: Option = None; + +/// Compiles the display vertex / fragment shader, returns the compiled shaders. +fn compile_screen_shader(context: &Gl) -> GLuint { + + unsafe { + match DISPLAY_SHADER { + Some(s) => return s, + None => { }, + } + } + + let vertex_shader_object = context.create_shader(gl::VERTEX_SHADER); + context.shader_source(vertex_shader_object, &[DISPLAY_VERTEX_SHADER]); + context.compile_shader(vertex_shader_object); + + #[cfg(debug_assertions)] { + if get_gl_shader_error(context, vertex_shader_object) { + let err = context.get_shader_info_log(vertex_shader_object); + context.delete_shader(vertex_shader_object); + panic!("VS compile error: {}", err); + } + } + + let fragment_shader_object = context.create_shader(gl::FRAGMENT_SHADER); + context.shader_source(fragment_shader_object, &[DISPLAY_FRAGMENT_SHADER]); + context.compile_shader(fragment_shader_object); + + #[cfg(debug_assertions)] { + if get_gl_shader_error(context, fragment_shader_object) { + let err = context.get_shader_info_log(fragment_shader_object); + context.delete_shader(vertex_shader_object); + context.delete_shader(fragment_shader_object); + panic!("FS compile error: {}", err); + } + } + + let program = context.create_program(); + context.attach_shader(program, vertex_shader_object); + context.attach_shader(program, fragment_shader_object); + context.link_program(program); + + #[cfg(debug_assertions)] { + if get_gl_program_error(context, program) { + let err = context.get_program_info_log(program); + context.delete_shader(vertex_shader_object); + context.delete_shader(fragment_shader_object); + context.delete_program(program); + panic!("Program link error: {}", err); + } + } + + context.delete_shader(vertex_shader_object); + context.delete_shader(fragment_shader_object); + + unsafe { DISPLAY_SHADER = Some(program) }; + + program +} + +// Returns true on error, false otherwise +#[cfg(debug_assertions)] +fn get_gl_shader_error(context: &Gl, shader_object: GLuint) -> bool { + let mut err = [0]; + unsafe { context.get_shader_iv(shader_object, gl::COMPILE_STATUS, &mut err) }; + err[0] == 0 +} + +#[cfg(debug_assertions)] +fn get_gl_program_error(context: &Gl, shader_object: GLuint) -> bool { + let mut err = [0]; + unsafe { context.get_program_iv(shader_object, gl::LINK_STATUS, &mut err) }; + err[0] == 0 +} + +// Draws a texture to the currently bound framebuffer. Texture has to be cleaned up by the caller. +fn draw_texture_to_screen(context: &Gl, texture: GLuint, framebuffer_size: DeviceIntSize) { + + context.bind_framebuffer(gl::FRAMEBUFFER, 0); + + // Compile or get the cached shader + let shader = compile_screen_shader(context); + let texture_location = context.get_uniform_location(shader, "fScreenTex"); + + // The uniform value for a sampler refers to the texture unit, not the texture id, i.e.: + // + // TEXTURE0 = uniform_1i(location, 0); + // TEXTURE1 = uniform_1i(location, 1); + + context.active_texture(gl::TEXTURE0); + context.bind_texture(gl::TEXTURE_2D, texture); + context.use_program(shader); + context.uniform_1i(texture_location, 0); + + // The vertices are generated in the vertex shader using gl_VertexID, however, + // drawing without a VAO is not allowed (except for glDrawArraysInstanced, + // which is only available in OGL 3.3) + + let vao = context.gen_vertex_arrays(1); + context.bind_vertex_array(vao[0]); + context.viewport(0, 0, framebuffer_size.width, framebuffer_size.height); + context.draw_arrays(gl::TRIANGLE_STRIP, 0, 3); + context.delete_vertex_arrays(&vao); + + context.bind_vertex_array(0); + context.use_program(0); + context.bind_texture(gl::TEXTURE_2D, 0); +} + +fn set_webrender_debug_flags(r: &mut Renderer, old_flags: &DebugState, new_flags: &DebugState) { + + use webrender::DebugFlags; + + if old_flags.profiler_dbg != new_flags.profiler_dbg { + r.set_debug_flag(DebugFlags::PROFILER_DBG, new_flags.profiler_dbg); + } + if old_flags.render_target_dbg != new_flags.render_target_dbg { + r.set_debug_flag(DebugFlags::RENDER_TARGET_DBG, new_flags.render_target_dbg); + } + if old_flags.texture_cache_dbg != new_flags.texture_cache_dbg { + r.set_debug_flag(DebugFlags::TEXTURE_CACHE_DBG, new_flags.texture_cache_dbg); + } + if old_flags.gpu_time_queries != new_flags.gpu_time_queries { + r.set_debug_flag(DebugFlags::GPU_TIME_QUERIES, new_flags.gpu_time_queries); + } + if old_flags.gpu_sample_queries != new_flags.gpu_sample_queries { + r.set_debug_flag(DebugFlags::GPU_SAMPLE_QUERIES, new_flags.gpu_sample_queries); + } + if old_flags.disable_batching != new_flags.disable_batching { + r.set_debug_flag(DebugFlags::DISABLE_BATCHING, new_flags.disable_batching); + } + if old_flags.epochs != new_flags.epochs { + r.set_debug_flag(DebugFlags::EPOCHS, new_flags.epochs); + } + if old_flags.compact_profiler != new_flags.compact_profiler { + r.set_debug_flag(DebugFlags::COMPACT_PROFILER, new_flags.compact_profiler); + } + if old_flags.echo_driver_messages != new_flags.echo_driver_messages { + r.set_debug_flag(DebugFlags::ECHO_DRIVER_MESSAGES, new_flags.echo_driver_messages); + } + if old_flags.new_frame_indicator != new_flags.new_frame_indicator { + r.set_debug_flag(DebugFlags::NEW_FRAME_INDICATOR, new_flags.new_frame_indicator); + } + if old_flags.new_scene_indicator != new_flags.new_scene_indicator { + r.set_debug_flag(DebugFlags::NEW_SCENE_INDICATOR, new_flags.new_scene_indicator); + } + if old_flags.show_overdraw != new_flags.show_overdraw { + r.set_debug_flag(DebugFlags::SHOW_OVERDRAW, new_flags.show_overdraw); + } + if old_flags.gpu_cache_dbg != new_flags.gpu_cache_dbg { + r.set_debug_flag(DebugFlags::GPU_CACHE_DBG, new_flags.gpu_cache_dbg); + } +} diff --git a/azul/src/app_resources.rs b/azul/src/app_resources.rs new file mode 100644 index 000000000..d4420abc0 --- /dev/null +++ b/azul/src/app_resources.rs @@ -0,0 +1,1292 @@ +use std::{ + fmt, + path::PathBuf, + io::Error as IoError, + sync::atomic::{AtomicUsize, Ordering}, +}; +use webrender::api::{ + FontKey, FontInstanceKey, ImageKey, AddImage, + ResourceUpdate, AddFont, AddFontInstance, RenderApi, +}; +use app_units::Au; +use clipboard2::{Clipboard, ClipboardError, SystemClipboard}; +use { + FastHashMap, FastHashSet, + window::{FakeDisplay, WindowCreateError}, + app::AppConfig, + display_list::DisplayList, + text_layout::Words, +}; +pub use webrender::api::{ImageFormat as RawImageFormat, ImageData, ImageDescriptor}; +#[cfg(feature = "image_loading")] +pub use image::{ImageError, DynamicImage, GenericImageView}; + +pub type CssImageId = String; +pub type CssFontId = String; + +/// Stores the resources for the application, souch as fonts, images and cached +/// texts, also clipboard strings +/// +/// Images and fonts can be references across window contexts (not yet tested, +/// but should work). +pub struct AppResources { + /// In order to properly load / unload fonts and images as well as share resources + /// between windows, this field stores the (application-global) Renderer. + #[cfg(not(test))] + pub(crate) fake_display: FakeDisplay, + /// Necessary to unit-test module-internal font GC without creating a visual display + #[cfg(test)] + fake_render_api: FakeRenderApi, + /// The CssImageId is the string used in the CSS, i.e. "my_image" -> ImageId(4) + css_ids_to_image_ids: FastHashMap, + /// Same as CssImageId -> ImageId, but for fonts, i.e. "Roboto" -> FontId(9) + css_ids_to_font_ids: FastHashMap, + /// Stores where the images were loaded from + image_sources: FastHashMap, + /// Stores where the fonts were loaded from + font_sources: FastHashMap, + /// All image keys currently active in the RenderApi + currently_registered_images: FastHashMap, + /// All font keys currently active in the RenderApi + currently_registered_fonts: FastHashMap, + /// If an image isn't displayed, it is deleted from memory, only + /// the `ImageSource` (i.e. the path / source where the image was loaded from) remains. + /// + /// This way the image can be re-loaded if necessary but doesn't have to reside in memory at all times. + last_frame_image_keys: FastHashSet, + /// If a font does not get used for one frame, the corresponding instance key gets + /// deleted. If a FontId has no FontInstanceKeys anymore, the font key gets deleted. + /// + /// The only thing remaining in memory permanently is the FontSource (which is only + /// the string of the file path where the font was loaded from, so no huge memory pressure). + /// The reason for this agressive strategy is that the + last_frame_font_keys: FastHashMap>, + /// Stores long texts across frames + text_cache: TextCache, + /// Keyboard clipboard storage and retrieval functionality + clipboard: SystemClipboard, +} + +static TEXT_ID_COUNTER: AtomicUsize = AtomicUsize::new(0); + +impl TextId { + fn new() -> Self { + Self { inner: TEXT_ID_COUNTER.fetch_add(1, Ordering::SeqCst) } + } +} + +/// A unique ID by which a large block of text can be uniquely identified +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] +pub struct TextId { + inner: usize, +} + +static IMAGE_ID_COUNTER: AtomicUsize = AtomicUsize::new(0); + +/// A unique ID by which an image can be uniquely identified +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ImageId { id: usize } + +impl ImageId { + pub(crate) fn new() -> Self { + let unique_id = IMAGE_ID_COUNTER.fetch_add(1, Ordering::SeqCst); + Self { + id: unique_id, + } + } +} + +static FONT_ID_COUNTER: AtomicUsize = AtomicUsize::new(0); + +/// A unique ID by which a font can be uniquely identified +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct FontId { + id: usize, +} + +impl FontId { + pub(crate) fn new() -> Self { + let unique_id = FONT_ID_COUNTER.fetch_add(1, Ordering::SeqCst); + Self { + id: unique_id, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ImageSource { + /// The image is embedded inside the binary file + Embedded(&'static [u8]), + /// The image is already decoded and loaded from a set of bytes + Raw(RawImage), + /// The image is loaded from a file + File(PathBuf), +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum FontSource { + /// The font is embedded inside the binary file + Embedded(&'static [u8]), + /// The font is loaded from a file + File(PathBuf), + /// The font is a system built-in font + System(String), +} + +#[derive(Debug)] +pub enum ImageReloadError { + Io(IoError, PathBuf), + #[cfg(feature = "image_loading")] + DecodingError(ImageError), + #[cfg(not(feature = "image_loading"))] + DecodingModuleNotActive, +} + +impl Clone for ImageReloadError { + fn clone(&self) -> Self { + use self::ImageReloadError::*; + match self { + Io(err, path) => Io(IoError::new(err.kind(), "Io Error"), path.clone()), + #[cfg(feature = "image_loading")] + DecodingError(e) => DecodingError(e.clone()), + #[cfg(not(feature = "image_loading"))] + DecodingModuleNotActive => DecodingModuleNotActive, + } + } +} + +impl fmt::Display for ImageReloadError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use self::ImageReloadError::*; + match &self { + Io(err, path_buf) => write!(f, "Could not load \"{}\" - IO error: {}", path_buf.as_path().to_string_lossy(), err), + #[cfg(feature = "image_loading")] + DecodingError(err) => write!(f, "Image decoding error: \"{}\"", err), + #[cfg(not(feature = "image_loading"))] + DecodingModuleNotActive => write!(f, "Found decoded image, but crate was not compiled with --features=\"image_loading\""), + } + } +} + +#[derive(Debug)] +pub enum FontReloadError { + Io(IoError, PathBuf), + FontNotFound(String), +} + +impl Clone for FontReloadError { + fn clone(&self) -> Self { + use self::FontReloadError::*; + match self { + Io(err, path) => Io(IoError::new(err.kind(), "Io Error"), path.clone()), + FontNotFound(id) => FontNotFound(id.clone()), + } + } +} + +impl_display!(FontReloadError, { + Io(err, path_buf) => format!("Could not load \"{}\" - IO error: {}", path_buf.as_path().to_string_lossy(), err), + FontNotFound(id) => format!("Could not locate system font: \"{}\" found", id), +}); + +impl ImageSource { + + /// Returns the **decoded** bytes of the image + the descriptor (contains width / height). + /// Returns an error if the data is encoded, but the crate wasn't built with `--features="image_loading"` + #[allow(unused_variables)] + pub fn get_bytes(&self) -> Result<(ImageData, ImageDescriptor), ImageReloadError> { + + use self::ImageSource::*; + + match self { + Embedded(bytes) => { + #[cfg(feature = "image_loading")] { + decode_image_data(bytes.to_vec()).map_err(|e| ImageReloadError::DecodingError(e)) + } + #[cfg(not(feature = "image_loading"))] { + Err(ImageReloadError::DecodingModuleNotActive) + } + }, + Raw(raw_image) => { + let opaque = is_image_opaque(raw_image.data_format, &raw_image.pixels[..]); + let allow_mipmaps = true; + let descriptor = ImageDescriptor::new( + raw_image.image_dimensions.0 as i32, + raw_image.image_dimensions.1 as i32, + raw_image.data_format, + opaque, + allow_mipmaps + ); + let data = ImageData::new(raw_image.pixels.clone()); + Ok((data, descriptor)) + }, + File(file_path) => { + #[cfg(feature = "image_loading")] { + use std::fs; + let bytes = fs::read(file_path).map_err(|e| ImageReloadError::Io(e, file_path.clone()))?; + decode_image_data(bytes).map_err(|e| ImageReloadError::DecodingError(e)) + } + #[cfg(not(feature = "image_loading"))] { + Err(ImageReloadError::DecodingModuleNotActive) + } + }, + } + } +} + +impl FontSource { + + /// Returns the bytes of the font (loads the font from the system in case it is a `FontSource::System` font). + /// Also returns the index into the font (in case the font is a font collection). + pub fn get_bytes(&self) -> Result<(Vec, i32), FontReloadError> { + use std::fs; + use self::FontSource::*; + match self { + Embedded(bytes) => Ok((bytes.to_vec(), 0)), + File(file_path) => { + fs::read(file_path) + .map_err(|e| FontReloadError::Io(e, file_path.clone())) + .map(|f| (f, 0)) + }, + System(id) => load_system_font(id).ok_or(FontReloadError::FontNotFound(id.clone())), + } + } +} + +/// Raw image made up of raw pixels (either BRGA8 or A8) +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RawImage { + pub pixels: Vec, + pub image_dimensions: (u32, u32), + pub data_format: RawImageFormat, +} + +#[derive(Debug, Clone)] +pub struct LoadedFont { + pub font_key: FontKey, + pub font_bytes: Vec, + /// Index of the font in case the bytes indicate a font collection + pub font_index: i32, + pub font_instances: FastHashMap, +} + +impl LoadedFont { + + /// Creates a new loaded font with 0 font instances + pub fn new(font_key: FontKey, font_bytes: Vec, font_index: i32) -> Self { + Self { + font_key, + font_bytes, + font_index, + font_instances: FastHashMap::default(), + } + } + + fn delete_font_instance(&mut self, size: &Au) { + self.font_instances.remove(size); + } +} + +/// Cache for accessing large amounts of text +#[derive(Debug, Default, Clone)] +pub struct TextCache { + /// Mapping from the TextID to the actual, UTF-8 String + /// + /// This is stored outside of the actual glyph calculation, because usually you don't + /// need the string, except for rebuilding a cached string (for example, when the font is changed) + pub(crate) string_cache: FastHashMap, + + // -- for now, don't cache ScaledWords, it's too complicated... + + // /// Caches the layout of the strings / words. + // /// + // /// TextId -> FontId (to look up by font) + // /// FontId -> PixelValue (to categorize by size within a font) + // /// PixelValue -> layouted words (to cache the glyph widths on a per-font-size basis) + // pub(crate) layouted_strings_cache: FastHashMap>, +} + +impl TextCache { + + /// Add a new, large text to the resources + pub fn add_text(&mut self, text: &str) -> TextId { + use text_layout::split_text_into_words; + let id = TextId::new(); + self.string_cache.insert(id, split_text_into_words(text)); + id + } + + pub fn get_text(&self, text_id: &TextId) -> Option<&Words> { + self.string_cache.get(text_id) + } + + /// Removes a string from the string cache, but not the layouted text cache + pub fn delete_text(&mut self, id: TextId) { + self.string_cache.remove(&id); + } + + pub fn clear_all_texts(&mut self) { + self.string_cache.clear(); + } +} + +/// Used only for debugging, so that the AppResource garbage +/// collection tests can run without a real RenderApi +#[cfg(test)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +struct FakeRenderApi { } + +#[cfg(test)] +impl FakeRenderApi { fn new() -> Self { Self { } } } + +pub(crate) trait FontImageApi { + fn new_image_key(&self) -> ImageKey; + fn new_font_key(&self) -> FontKey; + fn new_font_instance_key(&self) -> FontInstanceKey; + fn update_resources(&self, Vec); + fn flush_scene_builder(&self); +} + +impl FontImageApi for RenderApi { + fn new_image_key(&self) -> ImageKey { self.generate_image_key() } + fn new_font_key(&self) -> FontKey { self.generate_font_key() } + fn new_font_instance_key(&self) -> FontInstanceKey { self.generate_font_instance_key() } + fn update_resources(&self, updates: Vec) { self.update_resources(updates); } + fn flush_scene_builder(&self) { self.flush_scene_builder(); } +} + +#[cfg(test)] +use webrender::api::IdNamespace; + +// Fake RenderApi for unit testing +#[cfg(test)] +impl FontImageApi for FakeRenderApi { + fn new_image_key(&self) -> ImageKey { ImageKey::DUMMY } + fn new_font_key(&self) -> FontKey { FontKey::new(IdNamespace(0), 0) } + fn new_font_instance_key(&self) -> FontInstanceKey { FontInstanceKey::new(IdNamespace(0), 0) } + fn update_resources(&self, _: Vec) { } + fn flush_scene_builder(&self) { } +} + +impl AppResources { + + /// Creates a new renderer (the renderer manages the resources and is therefore tied to the resources). + #[must_use] pub(crate) fn new(app_config: &AppConfig) -> Result { + Ok(Self { + #[cfg(not(test))] + fake_display: FakeDisplay::new(app_config.renderer_type)?, + #[cfg(test)] + fake_render_api: FakeRenderApi::new(), + css_ids_to_font_ids: FastHashMap::default(), + css_ids_to_image_ids: FastHashMap::default(), + font_sources: FastHashMap::default(), + image_sources: FastHashMap::default(), + currently_registered_fonts: FastHashMap::default(), + currently_registered_images: FastHashMap::default(), + last_frame_font_keys: FastHashMap::default(), + last_frame_image_keys: FastHashSet::default(), + text_cache: TextCache::default(), + clipboard: SystemClipboard::new().unwrap(), + }) + } + + pub(crate) fn get_render_api(&self) -> &impl FontImageApi { + #[cfg(not(test))] { + &self.fake_display.render_api + } + #[cfg(test)] { + &self.fake_render_api + } + } + + /// Returns the IDs of all currently loaded fonts in `self.font_data` + pub fn get_loaded_font_ids(&self) -> Vec { + self.font_sources.keys().cloned().collect() + } + + pub fn get_loaded_image_ids(&self) -> Vec { + self.image_sources.keys().cloned().collect() + } + + pub fn get_loaded_css_image_ids(&self) -> Vec { + self.css_ids_to_image_ids.keys().cloned().collect() + } + + pub fn get_loaded_css_font_ids(&self) -> Vec { + self.css_ids_to_font_ids.keys().cloned().collect() + } + + pub fn get_loaded_text_ids(&self) -> Vec { + self.text_cache.string_cache.keys().cloned().collect() + } + + // -- ImageId cache + + /// Add an image from a PNG, JPEG or other - note that for specialized image formats, + /// you have to enable them as features in the Cargo.toml file. + #[cfg(feature = "image_loading")] + pub fn add_image(&mut self, image_id: ImageId, image_source: ImageSource) { + self.image_sources.insert(image_id, image_source); + } + + /// Returns whether the AppResources has currently a certain image ID registered + pub fn has_image(&self, image_id: &ImageId) -> bool { + self.image_sources.get(image_id).is_some() + } + + /// Given an `ImageId`, returns the decoded bytes of that image or `None`, if the `ImageId` is invalid. + /// Returns an error on IO failure / image decoding failure or image + pub fn get_image_bytes(&self, image_id: &ImageId) -> Option> { + self.image_sources.get(image_id).map(|image_source| image_source.get_bytes()) + } + + pub fn delete_image(&mut self, image_id: &ImageId) { + self.image_sources.remove(image_id); + } + + pub fn add_css_image_id>(&mut self, css_id: S) -> ImageId { + *self.css_ids_to_image_ids.entry(css_id.into()).or_insert_with(|| ImageId::new()) + } + + pub fn has_css_image_id(&self, css_id: &str) -> bool { + self.get_css_image_id(css_id).is_some() + } + + pub fn get_css_image_id(&self, css_id: &str) -> Option<&ImageId> { + self.css_ids_to_image_ids.get(css_id) + } + + pub fn delete_css_image_id(&mut self, css_id: &str) -> Option { + self.css_ids_to_image_ids.remove(css_id) + } + + pub fn get_image_info(&self, key: &ImageId) -> Option<&ImageInfo> { + self.currently_registered_images.get(key) + } + + // -- FontId cache + + pub fn add_css_font_id>(&mut self, css_id: S) -> FontId { + *self.css_ids_to_font_ids.entry(css_id.into()).or_insert_with(|| FontId::new()) + } + + pub fn has_css_font_id(&self, css_id: &str) -> bool { + self.get_css_font_id(css_id).is_some() + } + + pub fn get_css_font_id(&self, css_id: &str) -> Option<&FontId> { + self.css_ids_to_font_ids.get(css_id) + } + + pub fn delete_css_font_id(&mut self, css_id: &str) -> Option { + self.css_ids_to_font_ids.remove(css_id) + } + + pub fn add_font(&mut self, font_id: FontId, font_source: FontSource) { + self.font_sources.insert(font_id, font_source); + } + + /// Given a `FontId`, returns the bytes for that font or `None`, if the `FontId` is invalid. + pub fn get_font_bytes(&self, font_id: &FontId) -> Option, i32), FontReloadError>> { + let font_source = self.font_sources.get(font_id)?; + Some(font_source.get_bytes()) + } + + /// Checks if a `FontId` is valid, i.e. if a font is currently ready-to-use + pub fn has_font(&self, id: &FontId) -> bool { + self.font_sources.get(id).is_some() + } + + pub fn delete_font(&mut self, id: &FontId) { + self.font_sources.remove(id); + } + + // -- TextId cache + + /// Adds a string to the internal text cache, but only store it as a string, + /// without caching the layout of the string. + pub fn add_text(&mut self, text: &str) -> TextId { + self.text_cache.add_text(text) + } + + pub fn get_text(&self, id: &TextId) -> Option<&Words> { + self.text_cache.get_text(id) + } + + /// Removes a string from both the string cache and the layouted text cache + pub fn delete_text(&mut self, id: TextId) { + self.text_cache.delete_text(id); + } + + /// Empties the entire internal text cache, invalidating all `TextId`s. Use with care. + pub fn clear_all_texts(&mut self) { + self.text_cache.clear_all_texts(); + } + + // -- Clipboard + + /// Returns the contents of the system clipboard + pub fn get_clipboard_string(&self) -> Result { + self.clipboard.get_string_contents() + } + + /// Sets the contents of the system clipboard - currently only strings are supported + pub fn set_clipboard_string>(&mut self, contents: S) -> Result<(), ClipboardError> { + self.clipboard.set_string_contents(contents.into()) + } + + pub(crate) fn get_loaded_font(&self, font_id: &ImmediateFontId) -> Option<&LoadedFont> { + self.currently_registered_fonts.get(font_id) + } + + /// Scans the DisplayList for new images and fonts. After this call, the RenderApi is + /// guaranteed to know about all FontKeys and FontInstanceKey + pub(crate) fn add_fonts_and_images(&mut self, display_list: &DisplayList) { + let font_keys = scan_ui_description_for_font_keys(&self, display_list); + let image_keys = scan_ui_description_for_image_keys(&self, display_list); + + self.last_frame_font_keys.extend(font_keys.clone().into_iter()); + self.last_frame_image_keys.extend(image_keys.clone().into_iter()); + + let add_font_resource_updates = build_add_font_resource_updates(self, &font_keys); + let add_image_resource_updates = build_add_image_resource_updates(self, &image_keys); + + add_resources(self, add_font_resource_updates, add_image_resource_updates); + } + + /// To be called at the end of a frame (after the UI has rendered): + /// Deletes all FontKeys and FontImageKeys that weren't used in + /// the last frame, to save on memory. If the font needs to be recreated, it + /// needs to be reloaded from the `FontSource`. + pub(crate) fn garbage_collect_fonts_and_images(&mut self) { + + let delete_font_resource_updates = build_delete_font_resource_updates(self); + let delete_image_resource_updates = build_delete_image_resource_updates(self); + + delete_resources(self, delete_font_resource_updates, delete_image_resource_updates); + + self.last_frame_font_keys.clear(); + self.last_frame_image_keys.clear(); + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum ImmediateFontId { + Resolved(FontId), + Unresolved(CssFontId), +} + +/// Scans the display list for all font IDs + their font size +fn scan_ui_description_for_font_keys<'a, T>( + app_resources: &AppResources, + display_list: &DisplayList<'a, T> +) -> FastHashMap> { + + use dom::NodeType::*; + use ui_solver; + + let mut font_keys = FastHashMap::default(); + + for node_id in display_list.rectangles.linear_iter() { + + let node_data = &display_list.ui_descr.ui_descr_arena.node_data[node_id]; + let display_rect = &display_list.rectangles[node_id]; + + match node_data.node_type { + Text(_) | Label(_) => { + let css_font_id = ui_solver::get_font_id(&display_rect.style); + let font_id = match app_resources.css_ids_to_font_ids.get(css_font_id) { + Some(s) => ImmediateFontId::Resolved(*s), + None => ImmediateFontId::Unresolved(css_font_id.to_string()), + }; + let font_size = ui_solver::get_font_size(&display_rect.style); + font_keys + .entry(font_id) + .or_insert_with(|| FastHashSet::default()) + .insert(ui_solver::font_size_to_au(font_size)); + }, + _ => { } + } + } + + font_keys +} + +/// Scans the display list for all image keys +fn scan_ui_description_for_image_keys<'a, T>( + app_resources: &AppResources, + display_list: &DisplayList<'a, T> +) -> FastHashSet { + + use dom::NodeType::*; + + display_list.rectangles + .iter() + .zip(display_list.ui_descr.ui_descr_arena.node_data.iter()) + .filter_map(|(display_rect, node_data)| { + match node_data.node_type { + Image(id) => Some(id), + _ => { + let background = display_rect.style.background.as_ref()?; + let css_image_id = background.get_css_image_id()?; + let image_id = app_resources.get_css_image_id(&css_image_id.0)?; + Some(*image_id) + } + } + }).collect() +} + +// Debug, PartialEq, Eq, PartialOrd, Ord +#[derive(Clone)] +enum AddFontMsg { + Font(LoadedFont), + Instance(AddFontInstance, Au), +} + +// Debug, PartialEq, Eq, PartialOrd, Ord +#[derive(Clone)] +enum DeleteFontMsg { + Font(FontKey), + Instance(FontInstanceKey, Au), +} +// Debug, PartialEq, Eq, PartialOrd, Ord +#[derive(Clone)] +struct AddImageMsg(AddImage, ImageInfo); + +// Debug, PartialEq, Eq, PartialOrd, Ord +#[derive(Clone)] +struct DeleteImageMsg(ImageKey, ImageInfo); + +impl AddFontMsg { + fn into_resource_update(&self) -> ResourceUpdate { + use self::AddFontMsg::*; + match self { + Font(f) => ResourceUpdate::AddFont(AddFont::Raw(f.font_key, f.font_bytes.clone(), f.font_index as u32)), + Instance(fi, _) => ResourceUpdate::AddFontInstance(fi.clone()), + } + } +} + +impl DeleteFontMsg { + fn into_resource_update(&self) -> ResourceUpdate { + use self::DeleteFontMsg::*; + match self { + Font(f) => ResourceUpdate::DeleteFont(*f), + Instance(fi, _) => ResourceUpdate::DeleteFontInstance(*fi), + } + } +} + +impl AddImageMsg { + fn into_resource_update(&self) -> ResourceUpdate { + ResourceUpdate::AddImage(self.0.clone()) + } +} + +impl DeleteImageMsg { + fn into_resource_update(&self) -> ResourceUpdate { + ResourceUpdate::DeleteImage(self.0.clone()) + + } +} + +/// Given the fonts of the current frame, returns `AddFont` and `AddFontInstance`s of +/// which fonts / instances are currently not in the `current_registered_fonts` and +/// need to be added. +/// +/// Deleting fonts can only be done after the entire frame has finished drawing, +/// otherwise (if removing fonts would happen after every DOM) we'd constantly +/// add-and-remove fonts after every IFrameCallback, which would cause a lot of +/// I/O waiting. +fn build_add_font_resource_updates( + app_resources: &AppResources, + fonts_in_dom: &FastHashMap>, +) -> Vec<(ImmediateFontId, AddFontMsg)> { + + use webrender::api::{FontInstancePlatformOptions, FontInstanceOptions, FontRenderMode, FontInstanceFlags}; + + let mut resource_updates = Vec::new(); + + for (im_font_id, font_sizes) in fonts_in_dom { + + macro_rules! insert_font_instances {($font_id:expr, $font_key:expr, $font_index:expr, $font_size:expr) => ({ + + let font_instance_key_exists = app_resources.currently_registered_fonts + .get(&$font_id) + .and_then(|loaded_font| loaded_font.font_instances.get(&$font_size)) + .is_some(); + + if !font_instance_key_exists { + + let font_instance_key = app_resources.get_render_api().new_font_instance_key(); + + // For some reason the gamma is way to low on Windows + #[cfg(target_os = "windows")] + let platform_options = FontInstancePlatformOptions { + gamma: 300, + contrast: 100, + }; + + #[cfg(target_os = "linux")] + use webrender::api::{FontLCDFilter, FontHinting}; + + #[cfg(target_os = "linux")] + let platform_options = FontInstancePlatformOptions { + lcd_filter: FontLCDFilter::Default, + hinting: FontHinting::LCD, + }; + + #[cfg(target_os = "macos")] + let platform_options = FontInstancePlatformOptions::default(); + + let mut font_instance_flags = FontInstanceFlags::empty(); + + font_instance_flags.set(FontInstanceFlags::SUBPIXEL_BGR, false); + font_instance_flags.set(FontInstanceFlags::NO_AUTOHINT, true); + font_instance_flags.set(FontInstanceFlags::LCD_VERTICAL, false); + + let options = FontInstanceOptions { + render_mode: FontRenderMode::Subpixel, + flags: font_instance_flags, + .. Default::default() + }; + + resource_updates.push(($font_id, AddFontMsg::Instance(AddFontInstance { + key: font_instance_key, + font_key: $font_key, + glyph_size: $font_size, + options: Some(options), + platform_options: Some(platform_options), + variations: Vec::new(), + }, $font_size))); + } + })} + + match app_resources.currently_registered_fonts.get(im_font_id) { + Some(loaded_font) => { + for font_size in font_sizes.iter() { + insert_font_instances!(im_font_id.clone(), loaded_font.font_key, loaded_font.font_index, *font_size); + } + }, + None => { + use self::ImmediateFontId::*; + + // If there is no font key, that means there's also no font instances + let font_source = match im_font_id { + Resolved(font_id) => { + match app_resources.font_sources.get(font_id) { + Some(s) => s.clone(), + None => continue, + } + }, + Unresolved(css_font_id) => FontSource::System(css_font_id.clone()), + }; + + let (font_bytes, font_index) = match font_source.get_bytes() { + Ok(o) => o, + Err(e) => { + #[cfg(feature = "logging")] { + warn!("Could not load font with ID: {:?} - error: {}", im_font_id, e); + } + continue; + } + }; + + if !font_sizes.is_empty() { + let font_key = app_resources.get_render_api().new_font_key(); + + resource_updates.push((im_font_id.clone(), AddFontMsg::Font(LoadedFont::new(font_key, font_bytes, font_index)))); + + for font_size in font_sizes { + insert_font_instances!(im_font_id.clone(), font_key, font_index, *font_size); + } + } + } + } + } + + resource_updates +} + +/// Given the images of the current frame, returns `AddImage`s of +/// which image keys are currently not in the `current_registered_fonts` and +/// need to be added. Modifies `last_frame_image_keys` to contain the added image keys +/// +/// Deleting images can only be done after the entire frame has finished drawing, +/// otherwise (if removing images would happen after every DOM) we'd constantly +/// add-and-remove images after every IFrameCallback, which would cause a lot of +/// I/O waiting. +#[allow(unused_variables)] +fn build_add_image_resource_updates( + app_resources: &AppResources, + images_in_dom: &FastHashSet, +) -> Vec<(ImageId, AddImageMsg)> { + + images_in_dom.iter() + .filter(|image_id| !app_resources.currently_registered_images.contains_key(*image_id)) + .filter_map(|image_id| { + let (data, descriptor) = match app_resources.image_sources.get(image_id)?.get_bytes() { + Ok(o) => o, + Err(e) => { + #[cfg(feature = "logging")] { + warn!("Could not load image with ID: {:?} - error: {}", image_id, e); + } + return None; + } + }; + + let key = app_resources.get_render_api().new_image_key(); + let add_image = AddImage { key, data, descriptor, tiling: None }; + Some((*image_id, AddImageMsg(add_image, ImageInfo { key, descriptor }))) + + }).collect() +} + +/// Submits the `AddFont`, `AddFontInstance` and `AddImage` resources to the RenderApi. +/// Extends `currently_registered_images` and `currently_registered_fonts` by the +/// `last_frame_image_keys` and `last_frame_font_keys`, so that we don't lose track of +/// what font and image keys are currently in the API. +fn add_resources( + app_resources: &mut AppResources, + add_font_resources: Vec<(ImmediateFontId, AddFontMsg)>, + add_image_resources: Vec<(ImageId, AddImageMsg)>, +) { + let mut merged_resource_updates = Vec::new(); + + merged_resource_updates.extend(add_font_resources.iter().map(|(_, f)| f.into_resource_update())); + merged_resource_updates.extend(add_image_resources.iter().map(|(_, i)| i.into_resource_update())); + + if !merged_resource_updates.is_empty() { + app_resources.get_render_api().update_resources(merged_resource_updates); + // Assure that the AddFont / AddImage updates get processed immediately + app_resources.get_render_api().flush_scene_builder(); + } + + for (image_id, add_image_msg) in add_image_resources.iter() { + app_resources.currently_registered_images.insert(*image_id, add_image_msg.1); + } + + for (font_id, add_font_msg) in add_font_resources { + use self::AddFontMsg::*; + match add_font_msg { + Font(f) => { app_resources.currently_registered_fonts.insert(font_id, LoadedFont::new(f.font_key, f.font_bytes, f.font_index)); }, + Instance(fi, size) => { app_resources.currently_registered_fonts.get_mut(&font_id).unwrap().font_instances.insert(size, fi.key); }, + } + } +} + +fn build_delete_font_resource_updates( + app_resources: &AppResources +) -> Vec<(ImmediateFontId, DeleteFontMsg)> { + + let mut resource_updates = Vec::new(); + + // Delete fonts that were not used in the last frame or have zero font instances + for (font_id, loaded_font) in app_resources.currently_registered_fonts.iter() { + resource_updates.extend( + loaded_font.font_instances.iter() + .filter(|(au, _)| app_resources.last_frame_font_keys[font_id].contains(au)) + .map(|(au, font_instance_key)| (font_id.clone(), DeleteFontMsg::Instance(*font_instance_key, *au))) + ); + if !app_resources.last_frame_font_keys.contains_key(font_id) || loaded_font.font_instances.is_empty() { + // Delete the font and all instances if there are no more instances of the font + resource_updates.push((font_id.clone(), DeleteFontMsg::Font(loaded_font.font_key))); + } + } + + resource_updates +} + +/// At the end of the frame, all images that are registered, but weren't used in the last frame +fn build_delete_image_resource_updates( + app_resources: &AppResources +) -> Vec<(ImageId, DeleteImageMsg)> { + app_resources.currently_registered_images.iter() + .filter(|(id, _info)| !app_resources.last_frame_image_keys.contains(id)) + .map(|(id, info)| (*id, DeleteImageMsg(info.key, *info))) + .collect() +} + +fn delete_resources( + app_resources: &mut AppResources, + delete_font_resources: Vec<(ImmediateFontId, DeleteFontMsg)>, + delete_image_resources: Vec<(ImageId, DeleteImageMsg)>, +) { + let mut merged_resource_updates = Vec::new(); + + merged_resource_updates.extend(delete_font_resources.iter().map(|(_, f)| f.into_resource_update())); + merged_resource_updates.extend(delete_image_resources.iter().map(|(_, i)| i.into_resource_update())); + + if !merged_resource_updates.is_empty() { + app_resources.get_render_api().update_resources(merged_resource_updates); + } + + for (removed_id, _removed_info) in delete_image_resources { + app_resources.currently_registered_images.remove(&removed_id); + } + + for (font_id, delete_font_msg) in delete_font_resources { + use self::DeleteFontMsg::*; + match delete_font_msg { + Font(_) => { app_resources.currently_registered_fonts.remove(&font_id); }, + Instance(_, size) => { app_resources.currently_registered_fonts.get_mut(&font_id).unwrap().delete_font_instance(&size); }, + } + } +} + +#[cfg(feature = "image_loading")] +fn decode_image_data(image_data: Vec) -> Result<(ImageData, ImageDescriptor), ImageError> { + use image; // the crate + + let image_format = image::guess_format(&image_data)?; + let decoded = image::load_from_memory_with_format(&image_data, image_format)?; + Ok(prepare_image(decoded)?) +} + +/// Returns the font + the index of the font (in case the font is a collection) +fn load_system_font(id: &str) -> Option<(Vec, i32)> { + use font_loader::system_fonts::{self, FontPropertyBuilder}; + + let font_builder = match id { + "monospace" => { + #[cfg(target_os = "linux")] { + let native_monospace_font = linux_get_native_font(LinuxNativeFontType::Monospace); + FontPropertyBuilder::new().family(&native_monospace_font) + } + #[cfg(not(target_os = "linux"))] { + FontPropertyBuilder::new().monospace() + } + }, + "fantasy" => FontPropertyBuilder::new().oblique(), + "sans-serif" => { + #[cfg(target_os = "mac_os")] { + FontPropertyBuilder::new().family("Helvetica") + } + #[cfg(target_os = "linux")] { + let native_sans_serif_font = linux_get_native_font(LinuxNativeFontType::SansSerif); + FontPropertyBuilder::new().family(&native_sans_serif_font) + } + #[cfg(all(not(target_os = "linux"), not(target_os = "mac_os")))] { + FontPropertyBuilder::new().family("Segoe UI") + } + }, + "serif" => { + FontPropertyBuilder::new().family("Times New Roman") + }, + other => FontPropertyBuilder::new().family(other) + }; + + system_fonts::get(&font_builder.build()) +} + +/// Return the native fonts +#[cfg(target_os = "linux")] +enum LinuxNativeFontType { SansSerif, Monospace } + +#[cfg(target_os = "linux")] +fn linux_get_native_font(font_type: LinuxNativeFontType) -> String { + + use std::process::Command; + use self::LinuxNativeFontType::*; + + let font_name = match font_type { + SansSerif => "font-name", + Monospace => "monospace-font-name", + }; + + let fallback_font_name = match font_type { + SansSerif => "Ubuntu", + Monospace => "Ubuntu Mono", + }; + + // Execute "gsettings get org.gnome.desktop.interface font-name" and parse the output + let gsetting_cmd_result = + Command::new("gsettings") + .arg("get") + .arg("org.gnome.desktop.interface") + .arg(font_name) + .output() + .ok().map(|output| output.stdout) + .and_then(|stdout_bytes| String::from_utf8(stdout_bytes).ok()) + .map(|stdout_string| stdout_string.lines().collect::()); + + match &gsetting_cmd_result { + Some(s) => parse_gsettings_font(s).to_string(), + None => fallback_font_name.to_string(), + } +} + +// 'Ubuntu Mono 13' => Ubuntu Mono +#[cfg(target_os = "linux")] +fn parse_gsettings_font(input: &str) -> &str { + use std::char; + let input = input.trim(); + let input = input.trim_matches('\''); + let input = input.trim_end_matches(char::is_numeric); + let input = input.trim(); + input +} + +#[test] +#[cfg(target_os = "linux")] +fn test_parse_gsettings_font() { + assert_eq!(parse_gsettings_font("'Ubuntu 11'"), "Ubuntu"); + assert_eq!(parse_gsettings_font("'Ubuntu Mono 13'"), "Ubuntu Mono"); +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct ImageInfo { + pub(crate) key: ImageKey, + pub descriptor: ImageDescriptor, +} + +impl ImageInfo { + /// Returns the (width, height) of this image. + pub fn get_dimensions(&self) -> (usize, usize) { + let width = self.descriptor.size.width; + let height = self.descriptor.size.height; + (width as usize, height as usize) + } +} + +// The next three functions are taken from: +// https://github.com/christolliday/limn/blob/master/core/src/resources/image.rs + +#[cfg(feature = "image_loading")] +fn prepare_image(image_decoded: DynamicImage) + -> Result<(ImageData, ImageDescriptor), ImageError> +{ + use image; + let image_dims = image_decoded.dimensions(); + + // see: https://github.com/servo/webrender/blob/80c614ab660bf6cca52594d0e33a0be262a7ac12/wrench/src/yaml_frame_reader.rs#L401-L427 + let (format, bytes) = match image_decoded { + image::ImageLuma8(bytes) => { + let pixels = bytes.into_raw(); + (RawImageFormat::R8, pixels) + }, + image::ImageLumaA8(bytes) => { + let mut pixels = Vec::with_capacity(image_dims.0 as usize * image_dims.1 as usize * 4); + for greyscale_alpha in bytes.chunks(2) { + let grey = greyscale_alpha[0]; + let alpha = greyscale_alpha[1]; + pixels.extend_from_slice(&[ + grey, + grey, + grey, + alpha, + ]); + } + // TODO: necessary for greyscale? + premultiply(pixels.as_mut_slice()); + (RawImageFormat::BGRA8, pixels) + }, + image::ImageRgba8(mut bytes) => { + let mut pixels = bytes.into_raw(); + // no extra allocation necessary, but swizzling + for rgba in pixels.chunks_mut(4) { + let r = rgba[0]; + let g = rgba[1]; + let b = rgba[2]; + let a = rgba[3]; + rgba[0] = b; + rgba[1] = r; + rgba[2] = g; + rgba[3] = a; + } + premultiply(pixels.as_mut_slice()); + (RawImageFormat::BGRA8, pixels) + }, + image::ImageRgb8(bytes) => { + let mut pixels = Vec::with_capacity(image_dims.0 as usize * image_dims.1 as usize * 4); + for rgb in bytes.chunks(3) { + pixels.extend_from_slice(&[ + rgb[2], // b + rgb[1], // g + rgb[0], // r + 0xff // a + ]); + } + (RawImageFormat::BGRA8, pixels) + }, + image::ImageBgr8(bytes) => { + let mut pixels = Vec::with_capacity(image_dims.0 as usize * image_dims.1 as usize * 4); + for bgr in bytes.chunks(3) { + pixels.extend_from_slice(&[ + bgr[0], // b + bgr[1], // g + bgr[2], // r + 0xff // a + ]); + } + (RawImageFormat::BGRA8, pixels) + }, + image::ImageBgra8(bytes) => { + // Already in the correct format + let mut pixels = bytes.into_raw(); + premultiply(pixels.as_mut_slice()); + (RawImageFormat::BGRA8, pixels) + }, + }; + + let opaque = is_image_opaque(format, &bytes[..]); + let allow_mipmaps = true; + let descriptor = ImageDescriptor::new(image_dims.0 as i32, image_dims.1 as i32, format, opaque, allow_mipmaps); + let data = ImageData::new(bytes); + + Ok((data, descriptor)) +} + +fn is_image_opaque(format: RawImageFormat, bytes: &[u8]) -> bool { + match format { + RawImageFormat::BGRA8 => { + let mut is_opaque = true; + for i in 0..(bytes.len() / 4) { + if bytes[i * 4 + 3] != 255 { + is_opaque = false; + break; + } + } + is_opaque + } + RawImageFormat::R8 => true, + _ => unreachable!(), + } +} + +// From webrender/wrench +// These are slow. Gecko's gfx/2d/Swizzle.cpp has better versions +fn premultiply(data: &mut [u8]) { + for pixel in data.chunks_mut(4) { + let a = u32::from(pixel[3]); + pixel[0] = (((pixel[0] as u32 * a) + 128) / 255) as u8; + pixel[1] = (((pixel[1] as u32 * a) + 128) / 255) as u8; + pixel[2] = (((pixel[2] as u32 * a) + 128) / 255) as u8; + } +} + +#[test] +fn test_premultiply() { + let mut color = [255, 0, 0, 127]; + premultiply(&mut color); + assert_eq!(color, [127, 0, 0, 127]); +} + +#[test] +fn test_font_gc() { + + use std::collections::BTreeMap; + use prelude::*; + use ui_description::UiDescription; + use ui_state::UiState; + use ui_solver::px_to_au; + use {FastHashMap, FastHashSet}; + use std::hash::Hash; + + struct Mock { } + + let mut app_resources = AppResources::new(&AppConfig::default()).unwrap(); + let mut focused_node = None; + let mut pending_focus_target = None; + let is_mouse_down = false; + let hovered_nodes = BTreeMap::new(); + let css = css::from_str(r#" + #one { font-family: Helvetica; } + #two { font-family: Arial; } + #three { font-family: Times New Roman; } + "#).unwrap(); + + let mut ui_state_frame_1: UiState = Dom::mock_from_xml(r#" +

Hello

+

Hello

+

Hello

+ "#).into_ui_state(); + let ui_description_frame_1 = UiDescription::match_css_to_dom(&mut ui_state_frame_1, &css, &mut focused_node, &mut pending_focus_target, &hovered_nodes, is_mouse_down); + let display_list_frame_1 = DisplayList::new_from_ui_description(&ui_description_frame_1, &ui_state_frame_1); + + + let mut ui_state_frame_2: UiState = Dom::mock_from_xml(r#" +

Hello

+ "#).into_ui_state(); + let ui_description_frame_2 = UiDescription::match_css_to_dom(&mut ui_state_frame_2, &css, &mut focused_node, &mut pending_focus_target, &hovered_nodes, is_mouse_down); + let display_list_frame_2 = DisplayList::new_from_ui_description(&ui_description_frame_2, &ui_state_frame_2); + + + let mut ui_state_frame_3: UiState = Dom::mock_from_xml(r#" +

Hello

+

Hello

+

Hello

+ "#).into_ui_state(); + let ui_description_frame_3 = UiDescription::match_css_to_dom(&mut ui_state_frame_3, &css, &mut focused_node, &mut pending_focus_target, &hovered_nodes, is_mouse_down); + let display_list_frame_3 = DisplayList::new_from_ui_description(&ui_description_frame_3, &ui_state_frame_3); + + + // Assert that all fonts got added and detected correctly + let mut expected_fonts = FastHashMap::new(); + expected_fonts.insert(FontId::new(), FontSource::System(String::from("Helvetica"))); + expected_fonts.insert(FontId::new(), FontSource::System(String::from("Arial"))); + expected_fonts.insert(FontId::new(), FontSource::System(String::from("Times New Roman"))); + + fn build_map(i: Vec<(T, U)>) -> FastHashMap { + let mut map = FastHashMap::default(); + for (k, v) in i { map.insert(k, v); } + map + } + + fn build_set(i: Vec) -> FastHashSet { + let mut set = FastHashSet::default(); + for x in i { set.insert(x); } + set + } + + assert_eq!(scan_ui_description_for_image_keys(&app_resources, &display_list_frame_1), FastHashSet::default()); + assert_eq!(scan_ui_description_for_image_keys(&app_resources, &display_list_frame_2), FastHashSet::default()); + assert_eq!(scan_ui_description_for_image_keys(&app_resources, &display_list_frame_3), FastHashSet::default()); + + assert_eq!(scan_ui_description_for_font_keys(&app_resources, &display_list_frame_1), build_map(vec![ + (ImmediateFontId::Unresolved("Arial".to_string()), build_set(vec![px_to_au(10.0)])), + (ImmediateFontId::Unresolved("Helvetica".to_string()), build_set(vec![px_to_au(10.0)])), + (ImmediateFontId::Unresolved("Times New Roman".to_string()), build_set(vec![px_to_au(10.0)])), + ])); + assert_eq!(scan_ui_description_for_font_keys(&app_resources, &display_list_frame_2), build_map(vec![ + (ImmediateFontId::Unresolved("sans-serif".to_string()), build_set(vec![px_to_au(10.0)])), + ])); + assert_eq!(scan_ui_description_for_font_keys(&app_resources, &display_list_frame_3), build_map(vec![ + (ImmediateFontId::Unresolved("Arial".to_string()), build_set(vec![px_to_au(10.0)])), + (ImmediateFontId::Unresolved("Helvetica".to_string()), build_set(vec![px_to_au(10.0)])), + (ImmediateFontId::Unresolved("Times New Roman".to_string()), build_set(vec![px_to_au(10.0)])), + ])); + + + + app_resources.add_fonts_and_images(&display_list_frame_1); + assert_eq!(app_resources.currently_registered_fonts.len(), 3); + assert_eq!(app_resources.last_frame_font_keys.len(), 3); + + // Assert that the first frame doesn't delete the fonts again + app_resources.garbage_collect_fonts_and_images(); + assert_eq!(app_resources.currently_registered_fonts.len(), 3); // fails + + // Assert that fonts don't get double-inserted, still the same font sources as previously + app_resources.add_fonts_and_images(&display_list_frame_3); + app_resources.garbage_collect_fonts_and_images(); + assert_eq!(app_resources.currently_registered_fonts.len(), 3); + + // Assert that no new fonts get added on subsequent frames + app_resources.add_fonts_and_images(&display_list_frame_3); + app_resources.add_fonts_and_images(&display_list_frame_3); + app_resources.add_fonts_and_images(&display_list_frame_3); + app_resources.add_fonts_and_images(&display_list_frame_3); + app_resources.add_fonts_and_images(&display_list_frame_3); + app_resources.garbage_collect_fonts_and_images(); + assert_eq!(app_resources.currently_registered_fonts.len(), 3); + + // If the DOM changes, the fonts should get deleted, the only font still present is "sans-serif" + app_resources.add_fonts_and_images(&display_list_frame_2); + app_resources.garbage_collect_fonts_and_images(); + assert_eq!(app_resources.currently_registered_fonts.len(), 1); + + app_resources.add_fonts_and_images(&display_list_frame_1); + app_resources.garbage_collect_fonts_and_images(); + assert_eq!(app_resources.currently_registered_fonts.len(), 3); +} \ No newline at end of file diff --git a/azul/src/async.rs b/azul/src/async.rs new file mode 100644 index 000000000..1b9c17089 --- /dev/null +++ b/azul/src/async.rs @@ -0,0 +1,357 @@ +use std::{ + sync::{Arc, Mutex, Weak, atomic::{AtomicUsize, Ordering}}, + thread::{self, JoinHandle}, + time::{Duration, Instant}, + fmt, + hash::{Hash, Hasher}, +}; +use { + callbacks::{UpdateScreen, DontRedraw, TimerCallback, TimerCallbackType}, + app_resources::AppResources, +}; + +/// Should a timer terminate or not - used to remove active timers +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum TerminateTimer { + /// Remove the timer from the list of active timers + Terminate, + /// Do nothing and let the timers continue to run + Continue, +} + +static MAX_DAEMON_ID: AtomicUsize = AtomicUsize::new(0); + +/// Generate a new, unique TimerId +fn new_timer_id() -> TimerId { + TimerId(MAX_DAEMON_ID.fetch_add(1, Ordering::SeqCst)) +} + +/// ID for uniquely identifying a timer +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct TimerId(usize); + +impl TimerId { + /// Generates a new, unique `TimerId`. + pub fn new() -> Self { + new_timer_id() + } +} + +/// A `Timer` is a function that is run on every frame. +/// +/// There are often a lot of visual tasks such as animations or fetching the +/// next frame for a GIF or video, etc. - that need to run every frame or every X milliseconds, +/// but they aren't heavy enough to warrant creating a thread - otherwise the framework +/// would create too many threads, which leads to a lot of context switching and bad performance. +/// +/// The callback of a `Timer` should be fast enough to run under 16ms, +/// otherwise running timers will block the main UI thread. +pub struct Timer { + /// Stores when the timer was created (usually acquired by `Instant::now()`) + pub created: Instant, + /// When the timer was last called (`None` only when the timer hasn't been called yet). + pub last_run: Option, + /// If the timer shouldn't start instantly, but rather be delayed by a certain timeframe + pub delay: Option, + /// How frequently the timer should run, i.e. set this to `Some(Duration::from_millis(16))` + /// to run the timer every 16ms. If this value is set to `None`, (the default), the timer + /// will execute the timer as-fast-as-possible (i.e. at a faster framerate + /// than the framework itself) - which might be performance intensive. + pub interval: Option, + /// When to stop the timer (for example, you can stop the + /// execution after 5s using `Some(Duration::from_secs(5))`). + pub timeout: Option, + /// Callback to be called for this timer + pub callback: TimerCallback, +} + +impl Timer { + + /// Create a new timer + pub fn new(callback: TimerCallbackType,) -> Self { + Timer { + created: Instant::now(), + last_run: None, + delay: None, + interval: None, + timeout: None, + callback: TimerCallback(callback), + } + } + + /// Delays the timer to not start immediately but rather + /// start after a certain time frame has elapsed. + #[inline] + pub fn with_delay(mut self, delay: Duration) -> Self { + self.delay = Some(delay); + self + } + + /// Converts the timer into a timer, running the function only + /// if the given `Duration` has elapsed since the last run + #[inline] + pub fn with_interval(mut self, interval: Duration) -> Self { + self.interval = Some(interval); + self + } + + /// Converts the timer into a countdown, by giving it a maximum duration + /// (counted from the creation of the Timer, not the first use). + #[inline] + pub fn with_timeout(mut self, timeout: Duration) -> Self { + self.timeout = Some(timeout); + self + } + + /// Crate-internal: Invokes the timer if the timer and + /// the `self.timeout` allow it to + pub(crate) fn invoke_callback_with_data( + &mut self, + data: &mut T, + app_resources: &mut AppResources) + -> (UpdateScreen, TerminateTimer) + { + let instant_now = Instant::now(); + let delay = self.delay.unwrap_or_else(|| Duration::from_millis(0)); + + // Check if the timers timeout is reached + if let Some(timeout) = self.timeout { + if instant_now - self.created > timeout { + return (DontRedraw, TerminateTimer::Terminate); + } + } + + match self.last_run { + Some(last_run) => { + if let Some(interval) = self.interval { + if instant_now - last_run < interval { + return (DontRedraw, TerminateTimer::Continue); + } + } + + } + None => { + if instant_now < self.created + delay { + return (DontRedraw, TerminateTimer::Continue); + } + } + } + + let res = (self.callback.0)(data, app_resources); + + self.last_run = Some(instant_now); + + res + } +} + +// #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] for Timer + +impl fmt::Debug for Timer { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, + "Timer {{ \ + created: {:?}, \ + last_run: {:?}, \ + delay: {:?}, \ + interval: {:?}, \ + timeout: {:?}, \ + callback: {:?}, \ + }}", + self.created, + self.last_run, + self.delay, + self.interval, + self.timeout, + self.callback, + ) + } +} + +impl Clone for Timer { + fn clone(&self) -> Self { + Timer { .. *self } + } +} + +impl Hash for Timer { + fn hash(&self, state: &mut H) where H: Hasher { + self.created.hash(state); + self.last_run.hash(state); + self.delay.hash(state); + self.interval.hash(state); + self.timeout.hash(state); + self.callback.hash(state); + } +} + +impl PartialEq for Timer { + fn eq(&self, rhs: &Self) -> bool { + self.created == rhs.created && + self.last_run == rhs.last_run && + self.delay == rhs.delay && + self.interval == rhs.interval && + self.timeout == rhs.timeout && + self.callback == rhs.callback + } +} + +impl Eq for Timer { } + +impl Copy for Timer { } + +/// Simple struct that is used by Azul internally to determine when the thread has finished executing. +/// When this struct goes out of scope, Azul will call `.join()` on the thread (so in order to not +/// block the main thread, simply let it go out of scope naturally. +pub struct DropCheck(Arc<()>); + +/// A `Task` is a seperate thread that is owned by the framework. +/// +/// In difference to a `Thread`, you don't have to `await()` the result of a `Task`, +/// you can just hand the task to the framework (via `AppResources::add_task`) and +/// the framework will automatically update the UI when the task is finished. +/// This is useful to offload actions such as loading long files, etc. to a background thread. +/// +/// Azul will join the thread automatically after it is finished (joining won't block the UI). +pub struct Task { + // Task is in progress + join_handle: Option>, + dropcheck: Weak<()>, + /// Timer that will run directly after this task is completed. + pub(crate) after_completion_timer: Option>, +} + +impl Task { + + /// Creates a new task from a callback and a set of input data - which has to be wrapped in an `Arc>>`. + pub fn new(data: &Arc>, callback: fn(Arc>, DropCheck)) -> Self where U: Send + 'static { + + let thread_check = Arc::new(()); + let thread_weak = Arc::downgrade(&thread_check); + let app_state_clone = data.clone(); + + let thread_handle = thread::spawn(move || { + callback(app_state_clone, DropCheck(thread_check)) + }); + + Self { + join_handle: Some(thread_handle), + dropcheck: thread_weak, + after_completion_timer: None, + } + } + + /// Stores a `Timer` that will run after the task has finished. + /// + /// Often necessary to "clean up" or copy data from the background task into the UI. + #[inline] + pub fn then(mut self, timer: Timer) -> Self { + self.after_completion_timer = Some(timer); + self + } + + /// Returns true if the task has been finished, false otherwise + pub(crate) fn is_finished(&self) -> bool { + self.dropcheck.upgrade().is_none() + } +} + +impl Drop for Task { + fn drop(&mut self) { + if let Some(thread_handle) = self.join_handle.take() { + let _ = thread_handle.join().unwrap(); + } + } +} + +/// A `Thread` is a simple abstraction over `std::thread` that allows to offload a pure +/// function to a different thread (essentially emulating async / await for older compilers) +pub struct Thread { + data: Option>>, + join_handle: Option>, +} + +/// Error that can happen while calling `.await()` +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum AwaitError { + /// Arc::into_inner() failed + ArcUnlockError, + /// The background thread panicked + ThreadJoinError, + /// Mutex::into_inner() failed + MutexIntoInnerError, +} + +impl Thread { + + /// Creates a new thread that spawns a certain (pure) function on a separate thread. + /// This is a workaround until `await` is implemented. Note that invoking this function + /// will create an OS-level thread. + /// + /// **Warning**: You *must* call `.await()`, otherwise the `Thread` will panic when it is dropped! + /// + /// # Example + /// + /// ```rust + /// # use azul::async::Thread; + /// # + /// fn pure_function(input: usize) -> usize { input + 1 } + /// + /// let thread_1 = Thread::new(5, pure_function); + /// let thread_2 = Thread::new(10, pure_function); + /// let thread_3 = Thread::new(20, pure_function); + /// + /// // thread_1, thread_2 and thread_3 run in parallel here... + /// + /// let result_1 = thread_1.await(); + /// let result_2 = thread_2.await(); + /// let result_3 = thread_3.await(); + /// + /// assert_eq!(result_1, Ok(6)); + /// assert_eq!(result_2, Ok(11)); + /// assert_eq!(result_3, Ok(21)); + /// ``` + pub fn new(initial_data: U, callback: fn(U) -> T) -> Self where T: Send + 'static, U: Send + 'static { + + use std::mem; + + // Reserve memory for T and zero it + let data = Arc::new(Mutex::new(unsafe { mem::zeroed() })); + let data_arc = data.clone(); + + // For some reason, Rust doesn't realize that we're *moving* the data into the + // child thread, which is why the 'static is unnecessary - that would only be necessary + // if we'd reference the data from the main thread + let thread_handle = thread::spawn(move || { + *data_arc.lock().unwrap() = callback(initial_data); + }); + + Self { + data: Some(data), + join_handle: Some(thread_handle), + } + } + + /// Block until the internal thread has finished and return T + pub fn await(mut self) -> Result { + + // .await() can only be called once, so these .unwrap()s are safe + let handle = self.join_handle.take().unwrap(); + let data = self.data.take().unwrap(); + + handle.join().map_err(|_| AwaitError::ThreadJoinError)?; + + let data_arc = Arc::try_unwrap(data).map_err(|_| AwaitError::ArcUnlockError)?; + let data = data_arc.into_inner().map_err(|_| AwaitError::MutexIntoInnerError)?; + + Ok(data) + } +} + +impl Drop for Thread { + fn drop(&mut self) { + if self.join_handle.take().is_some() { + panic!("Thread has not been await()-ed correctly!"); + } + } +} diff --git a/azul/src/callbacks.rs b/azul/src/callbacks.rs new file mode 100644 index 000000000..615c0f349 --- /dev/null +++ b/azul/src/callbacks.rs @@ -0,0 +1,489 @@ +use std::{ + fmt, + rc::Rc, + hash::{Hash, Hasher}, + collections::BTreeMap, + sync::atomic::{AtomicUsize, Ordering}, +}; +use azul_css::CssPath; +#[cfg(feature = "css_parser")] +use azul_css_parser::CssPathParseError; +use webrender::api::{HitTestItem, LayoutRect}; +use { + app::AppState, + async::TerminateTimer, + dom::{Dom, NodeType, NodeData}, + app::AppStateNoData, + ui_state::UiState, + id_tree::{NodeId, Node, NodeHierarchy}, + app_resources::AppResources, + window::FakeWindow, +}; +pub use stack_checked_pointer::StackCheckedPointer; +pub use glium::texture::Texture2d; +pub use glium::framebuffer::SimpleFrameBuffer; +pub use glium::glutin::WindowId as GliumWindowId; +pub use glium::glutin::dpi::{LogicalSize, PhysicalSize}; + +pub type DefaultCallbackType = fn(&mut U, &mut AppStateNoData, &mut CallbackInfo) -> UpdateScreen; +pub type DefaultCallbackTypeUnchecked = fn(&StackCheckedPointer, &mut AppStateNoData, &mut CallbackInfo) -> UpdateScreen; + +static LAST_DEFAULT_CALLBACK_ID: AtomicUsize = AtomicUsize::new(0); + +/// Each default callback is identified by its ID (not by it's function pointer), +/// since multiple IDs could point to the same function. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] +pub struct DefaultCallbackId(usize); + +pub(crate) fn get_new_unique_default_callback_id() -> DefaultCallbackId { + DefaultCallbackId(LAST_DEFAULT_CALLBACK_ID.fetch_add(1, Ordering::SeqCst)) +} + +/// Callback that is invoked "by default", for example a text field that always +/// has a default "ontextinput" handler +pub struct DefaultCallback(pub DefaultCallbackTypeUnchecked); + +impl_callback!(DefaultCallback); + +/// A callback function has to return if the screen should be updated after the +/// function has run. +/// +/// NOTE: This is currently a typedef for `Option<()>`, so that you can use +/// the `?` operator in callbacks (to simply not redraw if there is an error). +/// This was an enum previously, but since Rust doesn't have a "custom try" operator, +/// this led to a lot of usability problems. In the future, this might change back +/// to an enum therefore the constants "Redraw" and "DontRedraw" are not capitalized, +/// to minimize breakage. +pub type UpdateScreen = Option<()>; +/// After the callback is called, the screen needs to redraw +/// (layout() function being called again). +#[allow(non_upper_case_globals)] +pub const Redraw: Option<()> = Some(()); +/// The screen does not need to redraw after the callback has been called. +#[allow(non_upper_case_globals)] +pub const DontRedraw: Option<()> = None; + +pub type CallbackType = fn(&mut AppState, &mut CallbackInfo) -> UpdateScreen; +/// Stores a function pointer that is executed when the given UI element is hit +/// +/// Must return an `UpdateScreen` that denotes if the screen should be redrawn. +/// The style is not affected by this, so if you make changes to the window's style +/// inside the function, the screen will not be automatically redrawn, unless you return +/// an `UpdateScreen::Redraw` from the function +pub struct Callback(pub CallbackType); +impl_callback!(Callback); + +pub type GlTextureCallbackType = fn(&StackCheckedPointer, LayoutInfo, HidpiAdjustedBounds) -> Option; +/// Callbacks that returns a rendered OpenGL texture +pub struct GlTextureCallback(pub GlTextureCallbackType); +impl_callback!(GlTextureCallback); + +pub type IFrameCallbackType = fn(&StackCheckedPointer, LayoutInfo, HidpiAdjustedBounds) -> Dom; +/// Callback that, given a rectangle area on the screen, returns the DOM appropriate for that bounds (useful for infinite lists) +pub struct IFrameCallback(pub IFrameCallbackType); +impl_callback!(IFrameCallback); + +pub type TimerCallbackType = fn(&mut T, app_resources: &mut AppResources) -> (UpdateScreen, TerminateTimer); +/// Callback that can runs on every frame on the main thread - can modify the app data model +pub struct TimerCallback(pub TimerCallbackType); +impl_callback!(TimerCallback); + +/// Wrapper for storing, inserting and registering default callbacks +pub(crate) struct DefaultCallbackSystem { + callbacks: BTreeMap, DefaultCallback)>, +} + +impl DefaultCallbackSystem { + + /// Creates a new, empty list of callbacks + pub(crate) fn new() -> Self { + Self { + callbacks: BTreeMap::new(), + } + } + + /// Registers a new callback + pub fn add_callback(&mut self, id: DefaultCallbackId, ptr: StackCheckedPointer, func: DefaultCallback) { + self.callbacks.insert(id, (ptr, func)); + } + + /// Invokes a certain default callback and returns its result + /// + /// NOTE: `app_data` is required so we know that we don't + /// accidentally alias the data in `self.internal` (which could lead to UB). + /// + /// What we know is that the pointer (`self.internal`) points to somewhere + /// in `T`, so we know that `self.internal` isn't aliased + pub(crate) fn run_callback( + &self, + _app_data: &mut T, + callback_id: &DefaultCallbackId, + app_state_no_data: &mut AppStateNoData, + window_event: &mut CallbackInfo) + -> UpdateScreen + { + if let Some((callback_ptr, callback_fn)) = self.callbacks.get(callback_id) { + (callback_fn.0)(callback_ptr, app_state_no_data, window_event) + } else { + #[cfg(feature = "logging")] { + warn!("Calling default callback with invalid ID {:?}", callback_id); + } + DontRedraw + } + } +} + +impl Clone for DefaultCallbackSystem { + fn clone(&self) -> Self { + Self { + callbacks: self.callbacks.clone(), + } + } +} + +/// Gives the `layout()` function access to the `AppResources` and the `Window` +/// (for querying images and fonts, as well as width / height) +pub struct LayoutInfo<'a, 'b, T: 'b> { + /// Gives _mutable_ access to the window + pub window: &'b mut FakeWindow, + /// Allows the layout() function to reference app resources + pub resources: &'a AppResources, +} + +/// Information about the callback that is passed to the callback whenever a callback is invoked +pub struct CallbackInfo<'a, T: 'a> { + /// The callback can change the focus - note that the focus is set before the + /// next frames' layout() function is invoked, but the current frames callbacks are not affected. + pub focus: Option, + /// The ID of the window that the event was clicked on (for indexing into + /// `app_state.windows`). `app_state.windows[event.window]` should never panic. + pub window_id: &'a GliumWindowId, + /// The ID of the node that was hit. You can use this to query information about + /// the node, but please don't hard-code any if / else statements based on the `NodeId` + pub hit_dom_node: NodeId, + /// UiState containing the necessary data for testing what + pub(crate) ui_state: &'a UiState, + /// What items are currently being hit + pub(crate) hit_test_items: &'a [HitTestItem], + /// The (x, y) position of the mouse cursor, **relative to top left of the element that was hit**. + pub cursor_relative_to_item: Option<(f32, f32)>, + /// The (x, y) position of the mouse cursor, **relative to top left of the window**. + pub cursor_in_viewport: Option<(f32, f32)>, +} + +impl<'a, T: 'a> Clone for CallbackInfo<'a, T> { + fn clone(&self) -> Self { + Self { + focus: self.focus.clone(), + window_id: self.window_id, + hit_dom_node: self.hit_dom_node, + ui_state: self.ui_state, + hit_test_items: self.hit_test_items, + cursor_relative_to_item: self.cursor_relative_to_item, + cursor_in_viewport: self.cursor_in_viewport, + } + } +} + +impl<'a, T: 'a> fmt::Debug for CallbackInfo<'a, T> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "CallbackInfo {{ \ + focus: {:?}, \ + window_id: {:?}, \ + hit_dom_node: {:?}, \ + ui_state: {:?}, \ + hit_test_items: {:?}, \ + cursor_relative_to_item: {:?}, \ + cursor_in_viewport: {:?}, \ + }}", + self.focus, + self.window_id, + self.hit_dom_node, + self.ui_state, + self.hit_test_items, + self.cursor_relative_to_item, + self.cursor_in_viewport, + ) + } +} + +/// Information about the bounds of a laid-out div rectangle. +/// +/// Necessary when invoking `IFrameCallbacks` and `GlTextureCallbacks`, so +/// that they can change what their content is based on their size. +#[derive(Debug, Copy, Clone)] +pub struct HidpiAdjustedBounds { + logical_size: LogicalSize, + hidpi_factor: f64, + winit_hidpi_factor: f64, + // TODO: Scroll state / focus state of this div! +} + +impl HidpiAdjustedBounds { + pub(crate) fn from_bounds(bounds: LayoutRect, hidpi_factor: f64, winit_hidpi_factor: f64) -> Self { + let logical_size = LogicalSize::new(bounds.size.width as f64, bounds.size.height as f64); + Self { + logical_size, + hidpi_factor, + winit_hidpi_factor, + } + } + + pub fn get_physical_size(&self) -> PhysicalSize { + self.get_logical_size().to_physical(self.winit_hidpi_factor) + } + + pub fn get_logical_size(&self) -> LogicalSize { + // NOTE: hidpi factor, not winit_hidpi_factor! + LogicalSize::new( + self.logical_size.width * self.hidpi_factor, + self.logical_size.height * self.hidpi_factor + ) + } + + pub fn get_hidpi_factor(&self) -> f64 { + self.hidpi_factor + } +} + +/// OpenGL texture, use `ReadOnlyWindow::create_texture` to create a texture +/// +/// **WARNING**: Don't forget to call `ReadOnlyWindow::unbind_framebuffer()` +/// when you are done with your OpenGL drawing, otherwise WebRender will render +/// to the texture, not the window, so your texture will actually never show up. +/// If you use a `Texture` and you get a blank screen, this is probably why. +#[derive(Debug, Clone)] +pub struct Texture { + pub(crate) inner: Rc, +} + +impl Texture { + /// Note: You can initialize this texture from an existing (external texture). + pub fn new(tex: Texture2d) -> Self { + Self { + inner: Rc::new(tex), + } + } + + /// Prepares the texture for drawing - you can only draw + /// on a framebuffer, the texture itself is readonly from the + /// OpenGL drivers point of view. + /// + /// **WARNING**: Don't forget to call `ReadOnlyWindow::unbind_framebuffer()` + /// when you are done with your OpenGL drawing, otherwise WebRender will render + /// to the texture instead of the window, so your texture will actually + /// never show up on the screen, since it is never rendered. + /// If you use a `Texture` and you get a blank screen, this is probably why. + pub fn as_surface<'a>(&'a self) -> SimpleFrameBuffer<'a> { + self.inner.as_surface() + } +} + +impl Hash for Texture { + fn hash(&self, state: &mut H) { + use glium::GlObject; + self.inner.get_id().hash(state); + } +} + +impl PartialEq for Texture { + /// Note: Comparison uses only the OpenGL ID, it doesn't compare the + /// actual contents of the texture. + fn eq(&self, other: &Texture) -> bool { + use glium::GlObject; + self.inner.get_id() == other.inner.get_id() + } +} + +impl Eq for Texture { } + +/// Iterator that, starting from a certain starting point, returns the +/// parent node until it gets to the root node. +pub struct ParentNodesIterator<'a> { + current_item: NodeId, + node_hierarchy: &'a NodeHierarchy, +} + +impl<'a> ParentNodesIterator<'a> { + + /// Returns what node ID the iterator is currently processing + pub fn current_node(&self) -> NodeId { + self.current_item + } + + /// Returns the offset into the parent of the current node or None if the item has no parent + pub fn current_index_in_parent(&self) -> Option { + if self.node_hierarchy[self.current_item].has_parent() { + Some(self.node_hierarchy.get_index_in_parent(self.current_item)) + } else { + None + } + } +} + +impl<'a> Iterator for ParentNodesIterator<'a> { + type Item = NodeId; + + /// For each item in the current item path, returns the index of the item in the parent + fn next(&mut self) -> Option { + let new_parent = self.node_hierarchy[self.current_item].parent?; + self.current_item = new_parent; + Some(new_parent) + } +} + +impl<'a, T: 'a> CallbackInfo<'a, T> { + + /// Creates an iterator that starts at the current DOM node and continouusly + /// returns the parent NodeId, until it gets to the root component. + pub fn parent_nodes<'b>(&'b self) -> ParentNodesIterator<'b> { + ParentNodesIterator { + current_item: self.hit_dom_node, + node_hierarchy: &self.ui_state.dom.arena.node_layout, + } + } + + /// For any node ID, returns what the position in its parent it is, plus the parent itself. + /// Returns `None` on the root ID (because the root has no parent, therefore it's the 1st item) + /// + /// Note: Index is 0-based (first item has the index of 0) + pub fn get_index_in_parent(&self, node_id: NodeId) -> Option<(usize, NodeId)> { + let node_layout = &self.ui_state.dom.arena.node_layout; + + if node_id.index() > node_layout.len() { + return None; // node_id out of range + } + + let parent = node_layout[node_id].parent?; + Some((node_layout.get_index_in_parent(node_id), parent)) + } + + // Functions that are may be called from the user callback + // - the `CallbackInfo` contains a `&mut UiState`, which can be + // used to query DOM information when the callbacks are run + + /// Returns the hierarchy of the given node ID + pub fn get_node<'b>(&'b self, node_id: NodeId) -> Option<&'b Node> { + self.ui_state.dom.arena.node_layout.internal.get(node_id.index()) + } + + /// Returns the node hierarchy (DOM tree order) + pub fn get_node_hierarchy<'b>(&'b self) -> &'b NodeHierarchy { + &self.ui_state.dom.arena.node_layout + } + + /// Returns the node content of a specific node + pub fn get_node_content<'b>(&'b self, node_id: NodeId) -> Option<&'b NodeData> { + self.ui_state.dom.arena.node_data.internal.get(node_id.index()) + } + + /// Returns the index of the target NodeId (the target that received the event) + /// in the targets parent or None if the target is the root node + pub fn target_index_in_parent(&self) -> Option { + if self.get_node(self.hit_dom_node)?.parent.is_some() { + Some(self.ui_state.dom.arena.node_layout.get_index_in_parent(self.hit_dom_node)) + } else { + None + } + } + + /// Returns the parent of the given `NodeId` or None if the target is the root node. + pub fn parent(&self, node_id: NodeId) -> Option { + self.get_node(node_id)?.parent + } + + /// Returns the parent of the current target or None if the target is the root node. + pub fn target_parent(&self) -> Option { + self.parent(self.hit_dom_node) + } + + /// Checks whether the target of the CallbackInfo has a certain node type + pub fn target_is_node_type(&self, node_type: NodeType) -> bool { + if let Some(self_node) = self.get_node_content(self.hit_dom_node) { + self_node.is_node_type(node_type) + } else { + false + } + } + + /// Checks whether the target of the CallbackInfo has a certain ID + pub fn target_has_id(&self, id: &str) -> bool { + if let Some(self_node) = self.get_node_content(self.hit_dom_node) { + self_node.has_id(id) + } else { + false + } + } + + /// Checks whether the target of the CallbackInfo has a certain class + pub fn target_has_class(&self, class: &str) -> bool { + if let Some(self_node) = self.get_node_content(self.hit_dom_node) { + self_node.has_class(class) + } else { + false + } + } + + /// Traverses up the hierarchy, checks whether any parent has a certain ID, + /// the returns that parent + pub fn any_parent_has_id(&self, id: &str) -> Option { + self.parent_nodes().find(|parent_id| { + if let Some(self_node) = self.get_node_content(*parent_id) { + self_node.has_id(id) + } else { + false + } + }) + } + + /// Traverses up the hierarchy, checks whether any parent has a certain class + pub fn any_parent_has_class(&self, class: &str) -> Option { + self.parent_nodes().find(|parent_id| { + if let Some(self_node) = self.get_node_content(*parent_id) { + self_node.has_class(class) + } else { + false + } + }) + } +} + +/// Defines the focused node ID for the next frame +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum FocusTarget { + Id(NodeId), + Path(CssPath), + NoFocus, +} + +impl<'a, T: 'a> CallbackInfo<'a, T> { + + /// Set the focus to a certain div by parsing a string. + /// Note that the parsing of the string can fail, therefore the Result + #[cfg(feature = "css_parser")] + pub fn set_focus<'b>(&mut self, input: &'b str) -> Result<(), CssPathParseError<'b>> { + use azul_css_parser::parse_css_path; + let path = parse_css_path(input)?; + self.focus = Some(FocusTarget::Path(path)); + Ok(()) + } + + /// Sets the focus by using an already-parsed `CssPath`. + pub fn set_focus_by_path(&mut self, path: CssPath) { + self.focus = Some(FocusTarget::Path(path)) + } + + /// Set the focus of the window to a specific div using a `NodeId`. + /// + /// Note that this ID will be dependent on the position in the DOM and therefore + /// the next frames UI must be the exact same as the current one, otherwise + /// the focus will be cleared or shifted (depending on apps setting). + pub fn set_focus_by_node_id(&mut self, id: NodeId) { + self.focus = Some(FocusTarget::Id(id)); + } + + /// Clears the focus for the next frame. + pub fn clear_focus(&mut self) { + self.focus = Some(FocusTarget::NoFocus); + } +} \ No newline at end of file diff --git a/azul/src/compositor.rs b/azul/src/compositor.rs new file mode 100644 index 000000000..9b82bdf61 --- /dev/null +++ b/azul/src/compositor.rs @@ -0,0 +1,102 @@ +use std::sync::{Mutex, atomic::{Ordering, AtomicUsize}}; +use webrender::{ + ExternalImageHandler, ExternalImage, ExternalImageSource, + api::{ExternalImageId, TexelRect, DevicePixel, Epoch, ImageRendering}, +}; +use euclid::TypedPoint2D; +use { + FastHashMap, + callbacks::Texture, +}; + +static LAST_OPENGL_ID: AtomicUsize = AtomicUsize::new(0); + +pub fn new_opengl_texture_id() -> usize { + LAST_OPENGL_ID.fetch_add(1, Ordering::SeqCst) +} + +lazy_static! { + + /// Non-cleaned up textures. When a GlTexture is registered, it has to stay active as long + /// as WebRender needs it for drawing. To transparently do this, we store the epoch that the + /// texture was originally created with, and check, **after we have drawn the frame**, + /// if there are any textures that need cleanup. + /// + /// Because the Texture2d is wrapped in an Rc, the destructor (which cleans up the OpenGL + /// texture) does not run until we remove the textures + /// + /// Note: Because textures could be used after the current draw call (ex. for scrolling), + /// the ACTIVE_GL_TEXTURES are indexed by their epoch. Use `renderer.flush_pipeline_info()` + /// to see which textures are still active and which ones can be safely removed. + /// + /// See: https://github.com/servo/webrender/issues/2940 + pub(crate) static ref ACTIVE_GL_TEXTURES: Mutex>> = Mutex::new(FastHashMap::default()); +} + +/// The Texture struct is public to the user +/// +/// With this wrapper struct we can implement Send + Sync, but we don't want to do that +/// on the Texture itself +#[derive(Debug)] +pub(crate) struct ActiveTexture { + pub(crate) texture: Texture, +} + +// necessary because of lazy_static rules - theoretically unsafe, +// but we do addition / removal of textures on the main thread +unsafe impl Send for ActiveTexture { } +unsafe impl Sync for ActiveTexture { } + +#[derive(Debug, Copy, Clone)] +pub(crate) struct Compositor { } + +impl Default for Compositor { + fn default() -> Self { + Self { } + } +} + +impl ExternalImageHandler for Compositor { + fn lock(&mut self, key: ExternalImageId, _channel_index: u8, _rendering: ImageRendering) -> ExternalImage { + use glium::GlObject; + + let gl_tex_lock = ACTIVE_GL_TEXTURES.lock().unwrap(); + + // Search all epoch hash maps for the given key + // There does not seem to be a way to get the epoch for the key, + // so we simply have to search all active epochs + // + // NOTE: Invalid textures can be generated on minimize / maximize + // Luckily, webrender simply ignores an invalid texture, so we don't + // need to check whether a window is maximized or minimized - if + // we encounter an invalid ID, webrender simply won't draw anything, + // but at least it won't crash. Usually invalid textures are also 0x0 + // pixels large - so it's not like we had anything to draw anyway. + let (tex, wh) = gl_tex_lock + .values() + .filter_map(|epoch_map| epoch_map.get(&key)) + .next() + .and_then(|tex| { + Some(( + ExternalImageSource::NativeTexture(tex.texture.inner.get_id()), + TypedPoint2D::::new( + tex.texture.inner.width() as f32, + tex.texture.inner.height() as f32, + ) + )) + }) + .unwrap_or((ExternalImageSource::Invalid, TypedPoint2D::zero())); + + ExternalImage { + uv: TexelRect { + uv0: TypedPoint2D::zero(), + uv1: wh, + }, + source: tex, + } + } + + fn unlock(&mut self, _key: ExternalImageId, _channel_index: u8) { + // Since the renderer is currently single-threaded, there is nothing to do here + } +} diff --git a/azul/src/css.rs b/azul/src/css.rs new file mode 100644 index 000000000..23c13dff1 --- /dev/null +++ b/azul/src/css.rs @@ -0,0 +1,298 @@ +//! +//! # Supported CSS properties +//! +//! | CSS Key | Value syntax | Description | Example(s) | Parsing function | +//! |----------------------------------------------------|--------------|-------------|------------|------------------| +//! | `border-radius` | | | | | +//! | `background` | | | | | +//! | `background-color` | | | | | +//! | `background-size` | | | | | +//! | `background-repeat` | | | | | +//! | `color` | | | | | +//! | `font-size` | | | | | +//! | `font-family` | | | | | +//! | `text-align` | | | | | +//! | `letter-spacing` | | | | | +//! | `line-height` | | | | | +//! | `word-spacing` | | | | | +//! | `tab-width` | | | | | +//! | `cursor` | | | | | +//! | `width`, `min-width`, `max-width` | | | | | +//! | `height`, `min-height`, `max-height` | | | | | +//! | `position` | | | | | +//! | `top`, `right`, `left`, `bottom` | | | | | +//! | `flex-wrap` | | | | | +//! | `flex-direction` | | | | | +//! | `flex-grow` | | | | | +//! | `flex-shrink` | | | | | +//! | `justify-content` | | | | | +//! | `align-items` | | | | | +//! | `align-content` | | | | | +//! | `overflow`, `overflow-x`, `overflow-y` | | | | | +//! | `padding`, `-top`, `-left`, `-right`, `-bottom` | | | | | +//! | `margin`, `-top`, `-left`, `-right`, `-bottom` | | | | | +//! | `border`, `-top`, `-left`, `-right`, `-bottom` | | | | | +//! | `box-shadow`, `-top`, `-left`, `-right`, `-bottom` | | | | | + +#[cfg(debug_assertions)] +use std::time::Duration; +#[cfg(debug_assertions)] +use std::path::PathBuf; + +pub use azul_css::*; +#[cfg(feature = "css_parser")] +pub mod css_parser { + pub use azul_css_parser::*; +} + +#[cfg(feature = "css_parser")] +pub use azul_css_parser::CssColor; + +#[cfg(feature = "native_style")] +pub mod native_style { + pub use azul_native_style::*; +} + +#[cfg(feature = "css_parser")] +use azul_css_parser::{self, CssParseError}; + +/// Returns a style with the native appearance for the operating system. Convenience wrapper +/// for functionality from the the `azul-native-style` crate. +#[cfg(feature = "native_style")] +pub fn native() -> Css { + azul_native_style::native() +} + +/// Parses CSS stylesheet from a string. Convenience wrapper for `azul-css-parser::new_from_str`. +#[cfg(feature = "css_parser")] +pub fn from_str(input: &str) -> Result { + azul_css_parser::new_from_str(input) +} + +/// Appends a custom stylesheet to `css::native()`. +#[cfg(all(feature = "css_parser", feature = "native_style"))] +pub fn override_native(input: &str) -> Result { + let mut css = native(); + css.append(from_str(input)?); + Ok(css) +} + +/// Allows dynamic reloading of a CSS file during an applications runtime, useful for +/// changing the look & feel while the application is running. +#[cfg(all(debug_assertions, feature = "css_parser"))] +pub fn hot_reload>(file_path: P, reload_interval: Duration) -> Box { + Box::new(azul_css_parser::HotReloader::new(file_path).with_reload_interval(reload_interval)) +} + +/// Same as `Self::hot_reload`, but appends the given file to the +/// `Self::native()` style before the hot-reloaded styles, similar to `override_native`. +#[cfg(all(debug_assertions, feature = "css_parser", feature = "native_style"))] +pub fn hot_reload_override_native>(file_path: P, reload_interval: Duration) -> Box { + Box::new(HotReloadOverrideHandler::new(native(), hot_reload(file_path, reload_interval))) +} + +/// Type translation functions (from azul-css to webrender types) +/// +/// The reason for doing this is so that azul-css doesn't depend on webrender or euclid +/// (since webrender is a huge dependency) just to use the types. Only if you depend on +/// azul, you have to depend on webrender. +pub(crate) mod webrender_translate { + + // NOTE: In rustc 1.31, most or all of these functions can be const + + use webrender::api::BoxShadowClipMode as WrBoxShadowClipMode; + use azul_css::BoxShadowClipMode as CssBoxShadowClipMode; + + #[inline(always)] + pub fn wr_translate_box_shadow_clip_mode(input: CssBoxShadowClipMode) -> WrBoxShadowClipMode { + match input { + CssBoxShadowClipMode::Outset => WrBoxShadowClipMode::Outset, + CssBoxShadowClipMode::Inset => WrBoxShadowClipMode::Inset, + } + } + + use webrender::api::ExtendMode as WrExtendMode; + use azul_css::ExtendMode as CssExtendMode; + + #[inline(always)] + pub fn wr_translate_extend_mode(input: CssExtendMode) -> WrExtendMode { + match input { + CssExtendMode::Clamp => WrExtendMode::Clamp, + CssExtendMode::Repeat => WrExtendMode::Repeat, + } + } + + use webrender::api::BorderStyle as WrBorderStyle; + use azul_css::BorderStyle as CssBorderStyle; + + #[inline(always)] + pub fn wr_translate_border_style(input: CssBorderStyle) -> WrBorderStyle { + match input { + CssBorderStyle::None => WrBorderStyle::None, + CssBorderStyle::Solid => WrBorderStyle::Solid, + CssBorderStyle::Double => WrBorderStyle::Double, + CssBorderStyle::Dotted => WrBorderStyle::Dotted, + CssBorderStyle::Dashed => WrBorderStyle::Dashed, + CssBorderStyle::Hidden => WrBorderStyle::Hidden, + CssBorderStyle::Groove => WrBorderStyle::Groove, + CssBorderStyle::Ridge => WrBorderStyle::Ridge, + CssBorderStyle::Inset => WrBorderStyle::Inset, + CssBorderStyle::Outset => WrBorderStyle::Outset, + } + } + + use webrender::api::LayoutSideOffsets as WrLayoutSideOffsets; + use azul_css::LayoutSideOffsets as CssLayoutSideOffsets; + + #[inline(always)] + pub fn wr_translate_layout_side_offsets(input: CssLayoutSideOffsets) -> WrLayoutSideOffsets { + WrLayoutSideOffsets::new( + input.top.get(), + input.right.get(), + input.bottom.get(), + input.left.get(), + ) + } + + use webrender::api::ColorU as WrColorU; + use azul_css::ColorU as CssColorU; + + #[inline(always)] + pub fn wr_translate_color_u(input: CssColorU) -> WrColorU { + WrColorU { r: input.r, g: input.g, b: input.b, a: input.a } + } + + use azul_css::BorderRadius as CssBorderRadius; + use webrender::api::BorderRadius as WrBorderRadius; + + #[inline(always)] + pub fn wr_translate_border_radius(input: CssBorderRadius) -> WrBorderRadius { + use webrender::api::LayoutSize; + let CssBorderRadius { top_left, top_right, bottom_left, bottom_right } = input; + WrBorderRadius { + top_left: LayoutSize::new(top_left.width.to_pixels(), top_left.height.to_pixels()), + top_right: LayoutSize::new(top_right.width.to_pixels(), top_right.height.to_pixels()), + bottom_left: LayoutSize::new(bottom_left.width.to_pixels(), bottom_left.height.to_pixels()), + bottom_right: LayoutSize::new(bottom_right.width.to_pixels(), bottom_right.height.to_pixels()), + } + } + + use azul_css::BorderSide as CssBorderSide; + use webrender::api::BorderSide as WrBorderSide; + + #[inline(always)] + pub fn wr_translate_border_side(input: CssBorderSide) -> WrBorderSide { + WrBorderSide { + color: wr_translate_color_u(input.color).into(), + style: wr_translate_border_style(input.style), + } + } + + use azul_css::NormalBorder as CssNormalBorder; + use webrender::api::NormalBorder as WrNormalBorder; + + #[inline(always)] + pub fn wr_translate_normal_border(input: CssNormalBorder) -> WrNormalBorder { + + // Webrender crashes if anti-aliasing is disabled and the border isn't pure-solid + let is_not_solid = [input.top.style, input.bottom.style, input.left.style, input.right.style].iter().any(|style| { + *style != CssBorderStyle::Solid + }); + let do_aa = input.radius.is_some() || is_not_solid; + + WrNormalBorder { + left: wr_translate_border_side(input.left), + right: wr_translate_border_side(input.right), + top: wr_translate_border_side(input.top), + bottom: wr_translate_border_side(input.bottom), + radius: wr_translate_border_radius(input.radius.unwrap_or_default()), + do_aa, + } + } + + use azul_css::LayoutPoint as CssLayoutPoint; + use webrender::api::LayoutPoint as WrLayoutPoint; + + #[inline(always)] + pub fn wr_translate_layout_point(input: CssLayoutPoint) -> WrLayoutPoint { + WrLayoutPoint::new(input.x, input.y) + } + + use azul_css::LayoutRect as CssLayoutRect; + use azul_css::LayoutSize as CssLayoutSize; + use webrender::api::LayoutRect as WrLayoutRect; + + // NOTE: Reverse direction: Translate from webrender::LayoutRect to css::LayoutRect + #[inline(always)] + pub fn wr_translate_layout_rect(input: WrLayoutRect) -> CssLayoutRect { + CssLayoutRect { + origin: CssLayoutPoint { x: input.origin.x, y: input.origin.y }, + size: CssLayoutSize { width: input.size.width, height: input.size.height }, + } + } + + use azul_css::BorderDetails as CssBorderDetails; + use webrender::api::BorderDetails as WrBorderDetails; + + // NOTE: Reverse direction: Translate from webrender::LayoutRect to css::LayoutRect + #[inline(always)] + pub fn wr_translate_border_details(input: CssBorderDetails) -> WrBorderDetails { + let zero_border_side = WrBorderSide { + color: WrColorU { r: 0, g: 0, b: 0, a: 0 }.into(), + style: WrBorderStyle::None + }; + + match input { + CssBorderDetails::Normal(normal) => WrBorderDetails::Normal(wr_translate_normal_border(normal)), + // TODO: Do 9patch border properly - currently this can't be reached since there + // is no parsing for 9patch border yet! + CssBorderDetails::NinePatch(_) => WrBorderDetails::Normal(WrNormalBorder { + left: zero_border_side, + right: zero_border_side, + bottom: zero_border_side, + top: zero_border_side, + radius: WrBorderRadius::zero(), + do_aa: false, + }) + } + } + + use azul_css::StyleCursor as CssCursor; + use glium::glutin::MouseCursor as WinitCursor; + + #[inline(always)] + pub fn winit_translate_cursor(input: CssCursor) -> WinitCursor { + match input { + CssCursor::Alias => WinitCursor::Alias, + CssCursor::AllScroll => WinitCursor::AllScroll, + CssCursor::Cell => WinitCursor::Cell, + CssCursor::ColResize => WinitCursor::ColResize, + CssCursor::ContextMenu => WinitCursor::ContextMenu, + CssCursor::Copy => WinitCursor::Copy, + CssCursor::Crosshair => WinitCursor::Crosshair, + CssCursor::Default => WinitCursor::Arrow, /* note: default -> arrow */ + CssCursor::EResize => WinitCursor::EResize, + CssCursor::EwResize => WinitCursor::EwResize, + CssCursor::Grab => WinitCursor::Grab, + CssCursor::Grabbing => WinitCursor::Grabbing, + CssCursor::Help => WinitCursor::Help, + CssCursor::Move => WinitCursor::Move, + CssCursor::NResize => WinitCursor::NResize, + CssCursor::NsResize => WinitCursor::NsResize, + CssCursor::NeswResize => WinitCursor::NeswResize, + CssCursor::NwseResize => WinitCursor::NwseResize, + CssCursor::Pointer => WinitCursor::Hand, /* note: pointer -> hand */ + CssCursor::Progress => WinitCursor::Progress, + CssCursor::RowResize => WinitCursor::RowResize, + CssCursor::SResize => WinitCursor::SResize, + CssCursor::SeResize => WinitCursor::SeResize, + CssCursor::Text => WinitCursor::Text, + CssCursor::Unset => WinitCursor::Arrow, /* note: pointer -> hand */ + CssCursor::VerticalText => WinitCursor::VerticalText, + CssCursor::WResize => WinitCursor::WResize, + CssCursor::Wait => WinitCursor::Wait, + CssCursor::ZoomIn => WinitCursor::ZoomIn, + CssCursor::ZoomOut => WinitCursor::ZoomOut, + } + } +} diff --git a/azul/src/dialogs.rs b/azul/src/dialogs.rs new file mode 100644 index 000000000..f7262aed1 --- /dev/null +++ b/azul/src/dialogs.rs @@ -0,0 +1,181 @@ +pub use tinyfiledialogs::{MessageBoxIcon, DefaultColorValue}; + +/// Ok or cancel result, returned from the `msg_box_ok_cancel` function +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum OkCancel { + Ok, + Cancel, +} + +impl From<::tinyfiledialogs::OkCancel> for OkCancel { + #[inline] + fn from(e: ::tinyfiledialogs::OkCancel) -> OkCancel { + match e { + ::tinyfiledialogs::OkCancel::Ok => OkCancel::Ok, + ::tinyfiledialogs::OkCancel::Cancel => OkCancel::Cancel, + } + } +} + +impl From for ::tinyfiledialogs::OkCancel { + #[inline] + fn from(e: OkCancel) -> ::tinyfiledialogs::OkCancel { + match e { + OkCancel::Ok => ::tinyfiledialogs::OkCancel::Ok, + OkCancel::Cancel => ::tinyfiledialogs::OkCancel::Cancel, + } + } +} + +/// "Ok / Cancel" MsgBox (title, message, icon, default) +pub fn msg_box_ok_cancel(title: &str, message: &str, icon: MessageBoxIcon, default: OkCancel) -> OkCancel { + ::tinyfiledialogs::message_box_ok_cancel(title, message, icon, default.into()).into() +} + +/// Yes or No result, returned from the `msg_box_yes_no` function +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum YesNo { + Yes, + No, +} + +impl From for ::tinyfiledialogs::YesNo { + #[inline] + fn from(e: YesNo) -> ::tinyfiledialogs::YesNo { + match e { + YesNo::Yes => ::tinyfiledialogs::YesNo::Yes, + YesNo::No => ::tinyfiledialogs::YesNo::No, + } + } +} + +impl From<::tinyfiledialogs::YesNo> for YesNo { + #[inline] + fn from(e: ::tinyfiledialogs::YesNo) -> YesNo { + match e { + ::tinyfiledialogs::YesNo::Yes => YesNo::Yes, + ::tinyfiledialogs::YesNo::No => YesNo::No, + } + } +} + +/// MsgBox icon to use in the `msg_box_*` functions +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum MsgBoxIcon { + Info, + Warning, + Error, + Question, +} + +impl From for MessageBoxIcon { + #[inline] + fn from(e: MsgBoxIcon) -> MessageBoxIcon { + match e { + MsgBoxIcon::Info => MessageBoxIcon::Info, + MsgBoxIcon::Warning => MessageBoxIcon::Warning, + MsgBoxIcon::Error => MessageBoxIcon::Error, + MsgBoxIcon::Question => MessageBoxIcon::Question, + } + } +} + +// Note: password_box, input_box and list_dialog do not work, so they're not included here. + +/// "Y/N" MsgBox (title, message, icon, default) +pub fn msg_box_yes_no(title: &str, message: &str, icon: MessageBoxIcon, default: YesNo) -> YesNo { + ::tinyfiledialogs::message_box_yes_no(title, message, icon, default.into()).into() +} + +/// "Ok" MsgBox (title, message, icon) +pub fn msg_box_ok(title: &str, message: &str, icon: MessageBoxIcon) { + ::tinyfiledialogs::message_box_ok(title, message, icon) +} + +/// Wrapper around `message_box_ok` with the default title "Info" + an info icon. +/// +/// Note: If you are too young to remember Visual Basics glorious `MsgBox` +/// then I pity you. Those were the days. +pub fn msg_box(content: &str) { + msg_box_ok("Info", content, MessageBoxIcon::Info); +} + +/// Color value (hex or rgb) to open the `color_chooser_dialog` with +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum ColorValue<'a> { + Hex(&'a str), + RGB(&'a [u8; 3]), +} + +/// Default color in the color picker +const DEFAULT_COLOR: [u8; 3] = [0, 0, 0]; + +impl<'a> Default for ColorValue<'a> { + fn default() -> Self { + ColorValue::RGB(&DEFAULT_COLOR) + } +} + +impl<'a> Into> for ColorValue<'a> { + fn into(self) -> DefaultColorValue<'a> { + match self { + ColorValue::Hex(s) => DefaultColorValue::Hex(s), + ColorValue::RGB(r) => DefaultColorValue::RGB(r), + } + } +} + +/// Opens the default color picker dialog +pub fn color_picker_dialog(title: &str, default_value: Option) +-> Option<(String, [u8; 3])> +{ + let default = default_value.unwrap_or_default().into(); + ::tinyfiledialogs::color_chooser_dialog(title, default) +} + +/// Open a single file, returns `None` if the user canceled the dialog. +/// +/// Filters are the file extensions, i.e. `Some(&["doc", "docx"])` to only allow +/// "doc" and "docx" files +pub fn open_file_dialog(default_path: Option<&str>, filter_list: Option<&[&str]>) +-> Option +{ + let filter_list = filter_list.map(|f| (f, "")); + let path = default_path.unwrap_or(""); + ::tinyfiledialogs::open_file_dialog("Open File", path, filter_list) +} + +/// Open a directory, returns `None` if the user canceled the dialog +pub fn open_directory_dialog(default_path: Option<&str>) +-> Option +{ + ::tinyfiledialogs::select_folder_dialog("Open Folder", default_path.unwrap_or("")) +} + +/// Open multiple files at once, returns `None` if the user canceled the dialog, +/// otherwise returns the `Vec` with the given file paths +/// +/// Filters are the file extensions, i.e. `Some(&["doc", "docx"])` to only allow +/// "doc" and "docx" files +pub fn open_multiple_files_dialog(default_path: Option<&str>, filter_list: Option<&[&str]>) +-> Option> +{ + let filter_list = filter_list.map(|f| (f, "")); + let path = default_path.unwrap_or(""); + ::tinyfiledialogs::open_file_dialog_multi("Open Folder", path, filter_list) +} + +/// Opens a save file dialog, returns `None` if the user canceled the dialog +pub fn save_file_dialog(default_path: Option<&str>) +-> Option +{ + let path = default_path.unwrap_or(""); + ::tinyfiledialogs::save_file_dialog("Save File", path) +} + +// TODO (at least on Windows): +// - Find and replace dialog +// - Font picker dialog +// - Page setup dialog +// - Print dialog +// - Print property dialog diff --git a/azul/src/diff.rs b/azul/src/diff.rs new file mode 100644 index 000000000..2062d06f1 --- /dev/null +++ b/azul/src/diff.rs @@ -0,0 +1,147 @@ +#![allow(unused_variables)] +#![allow(dead_code)] + +use std::{collections::BTreeMap, marker::PhantomData}; +use { + id_tree::{NodeId, NodeHierarchy}, + dom::{Dom, NodeData}, +}; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct DomRange { + pub start: DomNode, + pub end: DomNode, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct OldState { } + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct NewState { } + +pub(crate) trait FrameMarker { } + +impl FrameMarker for OldState { } +impl FrameMarker for NewState { } + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct DomNode { + pub(crate) id: NodeId, + pub(crate) marker: PhantomData, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum DomChange { + Added(DomRange), + Removed(DomRange), +} + +#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DomDiff { + /// What the actual changes nodes (not trees / subtrees) were in this diff, in order of appearance + pub(crate) changed_nodes: Vec, + /// Which items simply need updating in terms of image source? + pub(crate) only_replace_images: Vec, + /// Which nodes / subtrees need re-styling? + pub(crate) need_restyling: Vec>, + /// Which nodes need a re-layout? + /// For example A would be: + pub(crate) need_relayout: Vec>, +} + +type TreeDepth = usize; +type ParentNodeId = NodeId; +type LeafNodeId = NodeId; + +impl DomRange { + + /// Is `other` a subtree of `self`? - Assumes that the DOM was + /// constructed in a linear order, i.e. the child being within + /// the parents start / end bounds + pub fn contains(&self, other: &Self) -> bool { + other.start.id.index() >= self.start.id.index() && + other.end.id.index() <= self.end.id.index() + } + + /// Compares two DOM ranges *without* looking at the DOM hashes (not equivalent to `==`) + pub fn equals_range(&self, other: &Self) -> bool { + other.start == self.start && + other.end == self.end + } +} + +// In order to test two DOM nodes for "equality", you'd need to +// test if the node type, the classes and the ids are the same. +// The rest of the attributes can be ignored, since they are not +// used by the CSS engine. +// +// Right now the CSS doesn't support adjacent modifiers ("+" selectors), +// which will make this algorithm a bit more complex (but should be +// solvable with adjacency lists). +// +// for each leaf node (sorted by depth): +// - if the node has only changed its position: +// - node doesn't need restyle (but may need re-layout when "+" selectors are implemented) +// - else insert it it +// - add the end added / removed nodes +// +// for each parent node (sorted by depth, bubble up): +// - if the parent has changed: +// +// - ask if that child has affected that parents layout +// - if yes, set the parent to be restyled +// - ask if that child affects its siblings layout +// - if yes, add the siblings to the changeset +// + +const NODE_CHANGED_NOTHING: u8 = 0x01; +const NODE_CHANGED_TYPE: u8 = 0x02; +const NODE_CHANGED_CLASSES: u8 = 0x04; +const NODE_CHANGED_IDS: u8 = 0x08; + +fn node_needs_restyle(old: &NodeData, new: &NodeData) -> u8 { + let mut result = NODE_CHANGED_NOTHING; + + if old.node_type != new.node_type { + result &= NODE_CHANGED_TYPE; + } + + if old.classes != new.classes { + result &= NODE_CHANGED_CLASSES; + } + + if old.ids != new.ids { + result &= NODE_CHANGED_CLASSES; + } + + result +} + +fn get_leaf_nodes_by_depth(hierarchy: &NodeHierarchy) +-> BTreeMap>>> +{ + let mut map = BTreeMap::new(); + let parent_nodes = hierarchy.get_parents_sorted_by_depth(); + + for (parent_depth, parent_id) in parent_nodes { + for child_id in parent_id.children(hierarchy).filter(|child| hierarchy[*child].first_child.is_some()) { + map.entry(parent_depth + 1).or_insert_with(|| BTreeMap::new()) + .entry(parent_id).or_insert_with(|| Vec::new()) + .push(DomNode { id: child_id, marker: PhantomData }); + } + } + + map +} + +pub(crate) fn diff_dom_tree(old: &Dom, new:Dom) -> DomDiff { + + // TODO! + + // let old_leaf_nodes = get_leaf_nodes_by_depth(&old.arena.node_layout, OldState { }); + // let new_leaf_nodes = get_leaf_nodes_by_depth(&new.arena.node_layout, NewState { }); + + // depth -> parents (in order) -> [leaf children] + + DomDiff::default() +} \ No newline at end of file diff --git a/azul/src/display_list.rs b/azul/src/display_list.rs new file mode 100644 index 000000000..b963759e9 --- /dev/null +++ b/azul/src/display_list.rs @@ -0,0 +1,1592 @@ +#![allow(unused_variables)] + +use std::{ + fmt, + sync::{Arc, Mutex}, + collections::BTreeMap, +}; +use euclid::{TypedRect, TypedSize2D}; +use webrender::api::{ + LayoutPixel, DisplayListBuilder, PrimitiveInfo, GradientStop, + ColorF, PipelineId, Epoch, ImageData, ImageDescriptor, + ResourceUpdate, AddImage, BorderRadius, ClipMode, + LayoutPoint, LayoutSize, GlyphOptions, LayoutRect, ExternalScrollId, + ComplexClipRegion, LayoutPrimitiveInfo, ExternalImageId, + ExternalImageData, ImageFormat, ExternalImageType, TextureTarget, + ImageRendering, AlphaType, FontInstanceFlags, FontRenderMode, +}; +use azul_css::{ + Css, LayoutPosition,CssProperty, LayoutOverflow, + StyleBorderRadius, LayoutMargin, LayoutPadding, BoxShadowClipMode, + StyleTextColor, StyleBackground, StyleBoxShadow, + StyleBackgroundSize, StyleBackgroundRepeat, StyleBorder, BoxShadowPreDisplayItem, + RectStyle, RectLayout, ColorU as StyleColorU, DynamicCssPropertyDefault, +}; +use { + FastHashMap, + app_resources::AppResources, + callbacks::{IFrameCallback, GlTextureCallback, HidpiAdjustedBounds, StackCheckedPointer}, + ui_state::UiState, + ui_description::{UiDescription, StyledNode}, + id_tree::{NodeDataContainer, NodeId, NodeHierarchy}, + dom::{ + NodeData, ScrollTagId, DomHash, DomString, new_scroll_tag_id, + NodeType::{self, Div, Text, Image, GlTexture, IFrame, Label}, + }, + ui_solver::{do_the_layout, LayoutResult, PositionedRectangle}, + app_resources::ImageId, + compositor::new_opengl_texture_id, + window::{Window, FakeWindow, ScrollStates}, + callbacks::LayoutInfo, + window_state::WindowSize, +}; + +const DEFAULT_FONT_COLOR: StyleTextColor = StyleTextColor(StyleColorU { r: 0, b: 0, g: 0, a: 255 }); + +pub(crate) struct DisplayList<'a, T: 'a> { + pub(crate) ui_descr: &'a UiDescription, + pub(crate) rectangles: NodeDataContainer> +} + +impl<'a, T: 'a> fmt::Debug for DisplayList<'a, T> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, + "DisplayList {{ ui_descr: {:?}, rectangles: {:?} }}", + self.ui_descr, self.rectangles + ) + } +} + +/// DisplayRectangle is the main type which the layout parsing step gets operated on. +#[derive(Debug)] +pub(crate) struct DisplayRectangle<'a> { + /// `Some(id)` if this rectangle has a callback attached to it + /// Note: this is not the same as the `NodeId`! + /// These two are completely separate numbers! + pub tag: Option, + /// The original styled node + pub(crate) styled_node: &'a StyledNode, + /// The style properties of the node, parsed + pub(crate) style: RectStyle, + /// The layout properties of the node, parsed + pub(crate) layout: RectLayout, +} + +impl<'a> DisplayRectangle<'a> { + #[inline] + pub fn new(tag: Option, styled_node: &'a StyledNode) -> Self { + Self { tag, styled_node, style: RectStyle::default(), layout: RectLayout::default() } + } +} + +impl<'a, T: 'a> DisplayList<'a, T> { + + /// NOTE: This function assumes that the UiDescription has an initialized arena + /// + /// This only looks at the user-facing styles of the `UiDescription`, not the actual + /// layout. The layout is done only in the `into_display_list_builder` step. + pub(crate) fn new_from_ui_description(ui_description: &'a UiDescription, ui_state: &UiState) -> Self { + let arena = &ui_description.ui_descr_arena; + + let display_rect_arena = arena.node_data.transform(|node, node_id| { + let style = &ui_description.styled_nodes[node_id]; + let tag = ui_state.node_ids_to_tag_ids.get(&node_id).map(|tag| *tag); + let mut rect = DisplayRectangle::new(tag, style); + populate_css_properties(&mut rect, node_id, &ui_description.dynamic_css_overrides); + rect + }); + + Self { + ui_descr: ui_description, + rectangles: display_rect_arena, + } + } + + /// Inserts and solves the top-level DOM (i.e. the DOM with the ID 0) + pub(crate) fn into_display_list_builder( + &self, + app_data_access: &mut Arc>, + window: &mut Window, + fake_window: &mut FakeWindow, + app_resources: &mut AppResources + ) -> (DisplayListBuilder, ScrolledNodes, LayoutResult) { + + use glium::glutin::dpi::LogicalSize; + + let mut resource_updates = Vec::::new(); + + let arena = &self.ui_descr.ui_descr_arena; + let node_hierarchy = &arena.node_layout; + let node_data = &arena.node_data; + + // Scan the styled DOM for image and font keys. + // + // The problem is that we need to scan all DOMs for image and font keys and insert them + // before the layout() step - however, can't call IFrameCallbacks upfront, because each + // IFrameCallback needs to know its size (so it has to be invoked after the layout() step). + // So, this process needs to follow an order like: + // + // - For each DOM to render: + // - Create a DOM ID + // - Style the DOM according to the stylesheet + // - Scan all the font keys and image keys + // - Insert the new font keys and image keys into the render API + // - Scan all IFrameCallbacks, generate the DomID for each callback + // - Repeat while number_of_iframe_callbacks != 0 + app_resources.add_fonts_and_images(&self); + + let window_size = window.state.size.get_reverse_logical_size(); + let layout_result = do_the_layout( + node_hierarchy, + node_data, + &self.rectangles, + &*app_resources, + LayoutSize::new(window_size.width as f32, window_size.height as f32), + LayoutPoint::new(0.0, 0.0), + ); + + // TODO: After the layout has been done, call all IFrameCallbacks and get and insert + // their font keys / image keys + + let mut scrollable_nodes = get_nodes_that_need_scroll_clip( + node_hierarchy, &self.rectangles, node_data, &layout_result.rects, + &layout_result.node_depths, window.internal.pipeline_id + ); + + // Make sure unused scroll states are garbage collected. + window.scroll_states.remove_unused_scroll_states(); + + let LogicalSize { width, height } = window.state.size.dimensions; + let mut builder = DisplayListBuilder::with_capacity(window.internal.pipeline_id, TypedSize2D::new(width as f32, height as f32), self.rectangles.len()); + + let rects_in_rendering_order = determine_rendering_order(node_hierarchy, &self.rectangles, &layout_result.rects); + + push_rectangles_into_displaylist( + window.internal.epoch, + window.state.size, + rects_in_rendering_order, + &mut scrollable_nodes, + &mut window.scroll_states, + &DisplayListParametersRef { + pipeline_id: window.internal.pipeline_id, + node_hierarchy, + node_data, + display_rectangle_arena: &self.rectangles, + css: &window.css, + layout_result: &layout_result, + }, + &mut DisplayListParametersMut { + app_data: app_data_access, + app_resources, + fake_window, + builder: &mut builder, + resource_updates: &mut resource_updates, + pipeline_id: window.internal.pipeline_id, + }, + ); + + (builder, scrollable_nodes, layout_result) + } +} + +/// In order to render rectangles in the correct order, we have to group them together: +/// As long as there are no position:absolute items, items are inserted in a parents-then-child order +/// +/// ```no_run,ignore +/// a +/// |- b +/// |- c +/// | |- d +/// e +/// |- f +/// g +/// ``` +/// is rendered in the order `a, b, c, d, e, f, g`. This is necessary for clipping and scrolling, +/// meaning that if there is an overflow:scroll element, all children of that element are clipped +/// within that group. This means, that the z-order is completely determined by the DOM hierarchy. +/// +/// Trees with elements with `position:absolute` are more complex: The absolute items need +/// to be rendered completely on top of all other items, however, they still need to clip +/// and scroll properly. +/// +/// ```no_run,ignore +/// a:relative +/// |- b +/// |- c:absolute +/// | |- d +/// e +/// |- f +/// g +/// ``` +/// +/// will be rendered as: `a,b,e,f,g,c,d`, so that the `c,d` sub-DOM is on top of the rest +/// of the content. To support this, the content needs to be grouped: Whenever there is a +/// `position:absolute` encountered, the children are grouped into a new `ContentGroup`: +/// +/// ```no_run,ignore +/// Group 1: [a, b, c, e, f, g] +/// Group 2: [c, d] +/// ``` +/// Then the groups are simply rendered in-order: if there are multiple position:absolute +/// groups, this has the side effect of later groups drawing on top of earlier groups. +#[derive(Debug, Clone, PartialEq)] +struct ContentGroup { + /// The parent of the current node group, i.e. either the root node (0) + /// or the last positioned node () + root: RenderableNodeId, + /// Depth of the root node in the DOM hierarchy + root_depth: usize, + /// Node ids in order of drawing + node_ids: Vec, +} + +#[derive(Debug, Copy, Clone, PartialEq)] +struct RenderableNodeId { + /// Whether the (hierarchical) children of this group need to be clipped (usually + /// because the parent has an `overflow:hidden` property set). + clip_children: bool, + /// Whether the children overflow the parent (see `O`) + scrolls_children: bool, + /// The actual node ID of the content + node_id: NodeId, +} + +#[derive(Debug, Clone, PartialEq)] +struct ContentGroupOrder { + groups: Vec, +} + +fn determine_rendering_order<'a>( + node_hierarchy: &NodeHierarchy, + rectangles: &NodeDataContainer>, + layouted_rects: &NodeDataContainer, +) -> ContentGroupOrder +{ + let mut content_groups = Vec::new(); + + determine_rendering_order_inner( + node_hierarchy, + rectangles, + layouted_rects, + 0, // depth of this node + NodeId::new(0), + &mut content_groups + ); + + ContentGroupOrder { groups: content_groups } +} + +fn determine_rendering_order_inner<'a>( + node_hierarchy: &NodeHierarchy, + rectangles: &NodeDataContainer>, + layouted_rects: &NodeDataContainer, + // recursive parameters + root_depth: usize, + root_id: NodeId, + content_groups: &mut Vec, +) +{ + use id_tree::NodeEdge; + + let mut root_group = ContentGroup { + root: RenderableNodeId { + node_id: root_id, + clip_children: node_needs_to_clip_children(&rectangles[root_id].layout), + scrolls_children: false, // TODO + }, + root_depth, + node_ids: Vec::new(), + }; + + let mut absolute_node_ids = Vec::new(); + let mut depth = root_depth + 1; + + // Same as the traverse function, but allows us to skip items, returns the next element + fn traverse_simple(root_id: NodeId, current_node: NodeEdge, node_hierarchy: &NodeHierarchy) -> Option> { + // returns the next item + match current_node { + NodeEdge::Start(current_node) => { + match node_hierarchy[current_node].first_child { + Some(first_child) => Some(NodeEdge::Start(first_child)), + None => Some(NodeEdge::End(current_node.clone())) + } + } + NodeEdge::End(current_node) => { + if current_node == root_id { + None + } else { + match node_hierarchy[current_node].next_sibling { + Some(next_sibling) => Some(NodeEdge::Start(next_sibling)), + None => node_hierarchy[current_node].parent.and_then(|parent| Some(NodeEdge::End(parent))), + } + } + } + } + } + + let mut current_node_edge = NodeEdge::Start(root_id); + while let Some(next_node_id) = traverse_simple(root_id, current_node_edge.clone(), node_hierarchy) { + let mut should_continue_loop = true; + + if next_node_id.clone().inner_value() != root_id { + match next_node_id { + NodeEdge::Start(node_id) => { + let rect_node = &rectangles[node_id]; + let position = rect_node.layout.position.unwrap_or_default(); + if position == LayoutPosition::Absolute { + // For now, ignore the node and put it aside for later + absolute_node_ids.push((depth, node_id)); + // Skip this sub-tree and go straight to the next sibling + // Since the tree is positioned absolute, we'll worry about it later + current_node_edge = NodeEdge::End(node_id); + should_continue_loop = false; + } else { + // TODO: Overflow hidden in horizontal / vertical direction + let node_is_overflow_hidden = node_needs_to_clip_children(&rect_node.layout); + let node_needs_to_scroll_children = false; // TODO + root_group.node_ids.push(RenderableNodeId { + node_id, + clip_children: node_is_overflow_hidden, + scrolls_children: node_needs_to_scroll_children, + }); + } + + depth += 1; + }, + NodeEdge::End(node_id) => { + depth -= 1; + }, + } + } + + if should_continue_loop { + current_node_edge = next_node_id; + } + } + + content_groups.push(root_group); + + // Note: Currently reversed order, so that earlier absolute + // items are drawn on top of later absolute items + for (absolute_depth, absolute_node_id) in absolute_node_ids.into_iter().rev() { + determine_rendering_order_inner(node_hierarchy, rectangles, layouted_rects, absolute_depth, absolute_node_id, content_groups); + } +} + +#[derive(Default, Debug, Clone)] +pub(crate) struct ScrolledNodes { + pub(crate) overflowing_nodes: BTreeMap, + pub(crate) tags_to_node_ids: BTreeMap, +} + +#[derive(Debug, Clone)] +pub(crate) struct OverflowingScrollNode { + pub(crate) parent_rect: PositionedRectangle, + pub(crate) child_rect: LayoutRect, + pub(crate) parent_external_scroll_id: ExternalScrollId, + pub(crate) parent_dom_hash: DomHash, + pub(crate) scroll_tag_id: ScrollTagId, +} + +/// Returns all node IDs where the children overflow the parent, together with the +/// `(parent_rect, child_rect)` - the child rect is the sum of the children. +/// +/// TODO: The performance of this function can be theoretically improved: +/// +/// - Unioning the rectangles is heavier than just looping through the children and +/// summing up their width / height / padding + margin. +/// - Scroll nodes only need to be inserted if the parent doesn't have `overflow: hidden` +/// activated +/// - Overflow for X and Y needs to be tracked seperately (for overflow-x / overflow-y separation), +/// so there we'd need to track in which direction the inner_rect is overflowing. +fn get_nodes_that_need_scroll_clip<'a, T: 'a>( + node_hierarchy: &NodeHierarchy, + display_list_rects: &NodeDataContainer>, + dom_rects: &NodeDataContainer>, + layouted_rects: &NodeDataContainer, + parents: &[(usize, NodeId)], + pipeline_id: PipelineId, +) -> ScrolledNodes { + + let mut nodes = BTreeMap::new(); + let mut tags_to_node_ids = BTreeMap::new(); + + for (_, parent) in parents { + + let mut children_sum_rect = None; + + for child in parent.children(&node_hierarchy) { + let old = children_sum_rect.unwrap_or(LayoutRect::zero()); + children_sum_rect = Some(old.union(&layouted_rects[child].bounds)); + } + + let children_sum_rect = match children_sum_rect { + None => continue, + Some(sum) => sum, + }; + + let parent_rect = layouted_rects.get(*parent).unwrap(); + + if children_sum_rect.contains_rect(&parent_rect.bounds) { + continue; + } + + let parent_dom_hash = dom_rects[*parent].calculate_node_data_hash(); + + // Create an external scroll id. This id is required to preserve its + // scroll state accross multiple frames. + let parent_external_scroll_id = ExternalScrollId(parent_dom_hash.0, pipeline_id); + + // Create a unique scroll tag for hit-testing + let scroll_tag_id = match display_list_rects.get(*parent).and_then(|node| node.tag) { + Some(existing_tag) => ScrollTagId(existing_tag), + None => new_scroll_tag_id(), + }; + + tags_to_node_ids.insert(scroll_tag_id, *parent); + nodes.insert(*parent, OverflowingScrollNode { + parent_rect: parent_rect.clone(), + child_rect: children_sum_rect, + parent_external_scroll_id, + parent_dom_hash, + scroll_tag_id, + }); + } + + ScrolledNodes { overflowing_nodes: nodes, tags_to_node_ids } +} + +fn node_needs_to_clip_children(layout: &RectLayout) -> bool { + let overflow = layout.overflow.unwrap_or_default(); + !overflow.is_horizontal_overflow_visible() || + !overflow.is_vertical_overflow_visible() +} + +#[test] +fn test_overflow_parsing() { + + use azul_css::Overflow; + + let layout1 = RectLayout::default(); + + // The default for overflowing is overflow: auto, which clips + // children, so this should evaluate to true by default + assert_eq!(node_needs_to_clip_children(&layout1), true); + + let layout2 = RectLayout { + overflow: Some(LayoutOverflow { + horizontal: Some(Overflow::Visible), + vertical: Some(Overflow::Visible), + }), + .. Default::default() + }; + assert_eq!(node_needs_to_clip_children(&layout2), false); + + let layout3 = RectLayout { + overflow: Some(LayoutOverflow { + horizontal: Some(Overflow::Hidden), + vertical: Some(Overflow::Visible), + }), + .. Default::default() + }; + assert_eq!(node_needs_to_clip_children(&layout3), true); +} + +fn push_rectangles_into_displaylist<'a, 'b, 'c, 'd, 'e, 'f, T>( + epoch: Epoch, + window_size: WindowSize, + content_grouped_rectangles: ContentGroupOrder, + scrollable_nodes: &mut ScrolledNodes, + scroll_states: &mut ScrollStates, + referenced_content: &DisplayListParametersRef<'a,'b,'c,'d,'e, T>, + referenced_mutable_content: &mut DisplayListParametersMut<'f, T>) +{ + let mut clip_stack = Vec::new(); + + for content_group in content_grouped_rectangles.groups { + let rectangle = DisplayListRectParams { + epoch, + rect_idx: content_group.root.node_id, + html_node: &referenced_content.node_data[content_group.root.node_id].node_type, + window_size, + }; + + // Push the root of the node + push_rectangles_into_displaylist_inner( + content_group.root, + scrollable_nodes, + &rectangle, + referenced_content, + referenced_mutable_content, + &mut clip_stack + ); + + for item in content_group.node_ids { + + let rectangle = DisplayListRectParams { + epoch, + rect_idx: item.node_id, + html_node: &referenced_content.node_data[item.node_id].node_type, + window_size, + }; + + push_rectangles_into_displaylist_inner( + item, + scrollable_nodes, + &rectangle, + referenced_content, + referenced_mutable_content, + &mut clip_stack + ); + } + } +} + +fn push_rectangles_into_displaylist_inner<'a,'b,'c,'d,'e,'f, T>( + item: RenderableNodeId, + scrollable_nodes: &mut ScrolledNodes, + rectangle: &DisplayListRectParams<'a, T>, + referenced_content: &DisplayListParametersRef<'a,'b,'c,'d,'e, T>, + referenced_mutable_content: &mut DisplayListParametersMut<'f, T>, + clip_stack: &mut Vec, +) { + displaylist_handle_rect( + scrollable_nodes, + rectangle, + referenced_content, + referenced_mutable_content + ); +/* + + // NOTE: table demo has problems with clipping + + if item.clip_children { + if let Some(last_child) = referenced_content.node_hierarchy[rectangle.rect_idx].last_child { + let styled_node = &referenced_content.display_rectangle_arena[rectangle.rect_idx]; + let solved_rect = &referenced_content.layout_result.rects[rectangle.rect_idx]; + let clip = get_clip_region(solved_rect.bounds, &styled_node) + .unwrap_or(ComplexClipRegion::new(solved_rect.bounds, BorderRadius::zero(), ClipMode::Clip)); + let clip_id = referenced_mutable_content.builder.define_clip(solved_rect.bounds, vec![clip], /* image_mask: */ None); + referenced_mutable_content.builder.push_clip_id(clip_id); + clip_stack.push(last_child); + } + } + + if clip_stack.last().cloned() == Some(rectangle.rect_idx) { + referenced_mutable_content.builder.pop_clip_id(); + clip_stack.pop(); + } +*/ +} + +/// Parameters that apply to a single rectangle / div node +#[derive(Copy, Clone)] +pub(crate) struct DisplayListRectParams<'a, T: 'a> { + pub epoch: Epoch, + pub rect_idx: NodeId, + pub html_node: &'a NodeType, + window_size: WindowSize, +} + +fn get_clip_region<'a>(bounds: LayoutRect, rect: &DisplayRectangle<'a>) -> Option { + use css::webrender_translate::wr_translate_border_radius; + rect.style.border_radius.and_then(|border_radius| { + Some(ComplexClipRegion { + rect: bounds, + radii: wr_translate_border_radius(border_radius.0).into(), + mode: ClipMode::Clip, + }) + }) +} + +/// Push a single rectangle into the display list builder +#[inline] +fn displaylist_handle_rect<'a,'b,'c,'d,'e,'f,'g, T>( + scrollable_nodes: &mut ScrolledNodes, + rectangle: &DisplayListRectParams<'a, T>, + referenced_content: &DisplayListParametersRef<'b,'c,'d,'e,'f, T>, + referenced_mutable_content: &mut DisplayListParametersMut<'g, T>) +{ + let DisplayListParametersRef { + css, display_rectangle_arena, + pipeline_id, node_hierarchy, node_data, + layout_result, + } = referenced_content; + + let DisplayListRectParams { + epoch, rect_idx, html_node, window_size, + } = rectangle; + + let rect = &display_rectangle_arena[*rect_idx]; + let bounds = layout_result.rects[*rect_idx].bounds; + + let info = LayoutPrimitiveInfo { + rect: bounds, + clip_rect: bounds, + is_backface_visible: false, + tag: rect.tag.map(|tag| (tag, 0)).or({ + scrollable_nodes.overflowing_nodes + .get(&rect_idx) + .map(|scrolled| (scrolled.scroll_tag_id.0, 0)) + }), + }; + + let clip_region_id = get_clip_region(bounds, &rect).map(|clip| + referenced_mutable_content.builder.define_clip(bounds, vec![clip], None) + ); + + // Push the "outset" box shadow, before the clip is active + push_box_shadow( + referenced_mutable_content.builder, + &rect.style, + &bounds, + BoxShadowClipMode::Outset, + ); + + if let Some(id) = clip_region_id { + referenced_mutable_content.builder.push_clip_id(id); + } + + // If the rect is hit-testing relevant, we need to push a rect anyway. + // Otherwise the hit-testing gets confused + if let Some(bg) = &rect.style.background { + push_background( + &info, + &bounds, + referenced_mutable_content.builder, + bg, + &rect.style.background_size, + &rect.style.background_repeat, + &referenced_mutable_content.app_resources, + ); + } else if info.tag.is_some() { + const TRANSPARENT_BG: StyleColorU = StyleColorU { r: 0, g: 0, b: 0, a: 0 }; + push_rect( + &info, + referenced_mutable_content.builder, + &TRANSPARENT_BG, + ); + } + + if let Some(ref border) = rect.style.border { + push_border( + &info, + referenced_mutable_content.builder, + &border, + &rect.style.border_radius, + ); + } + + match html_node { + Div => { }, + Text(_) | Label(_) => { + // Text is laid out and positioned during the layout pass, + // so this should succeed - if there were problems + // + // TODO: In the table demo, the numbers don't show - empty glyphs (why?)! + push_text( + &info, + referenced_mutable_content.builder, + layout_result, + rect_idx, + &rect.style, + &rect.layout, + ) + }, + Image(image_id) => push_image( + &info, + referenced_mutable_content.builder, + referenced_mutable_content.app_resources, + image_id, + LayoutSize::new(info.rect.size.width, info.rect.size.height) + ), + GlTexture(callback) => push_opengl_texture(callback, &info, rectangle, referenced_content, referenced_mutable_content), + IFrame(callback) => push_iframe(callback, &info, scrollable_nodes, rectangle, referenced_content, referenced_mutable_content), + }; + + // Push the inset shadow (if any) + push_box_shadow( + referenced_mutable_content.builder, + &rect.style, + &bounds, + BoxShadowClipMode::Inset + ); + + if clip_region_id.is_some() { + referenced_mutable_content.builder.pop_clip_id(); + } +} + +fn push_opengl_texture<'a,'b,'c,'d,'e,'f, T>( + (texture_callback, texture_stack_ptr): &(GlTextureCallback, StackCheckedPointer), + info: &LayoutPrimitiveInfo, + rectangle: &DisplayListRectParams<'a, T>, + referenced_content: &DisplayListParametersRef<'a,'b,'c,'d,'e, T>, + referenced_mutable_content: &mut DisplayListParametersMut<'f, T>, +) { + use compositor::{ActiveTexture, ACTIVE_GL_TEXTURES}; + use gleam::gl; + use app_resources::FontImageApi; + + let bounds = HidpiAdjustedBounds::from_bounds( + info.rect, + rectangle.window_size.hidpi_factor, + rectangle.window_size.winit_hidpi_factor + ); + + let texture; + + { + // Make sure that the app data is locked before invoking the callback + let _lock = referenced_mutable_content.app_data.lock().unwrap(); + texture = (texture_callback.0)(&texture_stack_ptr, LayoutInfo { + window: &mut *referenced_mutable_content.fake_window, + resources: &referenced_mutable_content.app_resources, + }, bounds); + + // Reset the framebuffer and SRGB color target to 0 + let gl_context = referenced_mutable_content.fake_window.read_only_window().get_gl_context(); + + gl_context.bind_framebuffer(gl::FRAMEBUFFER, 0); + gl_context.disable(gl::FRAMEBUFFER_SRGB); + gl_context.disable(gl::MULTISAMPLE); + gl_context.viewport(0, 0, info.rect.size.width as i32, info.rect.size.height as i32); + } + + let texture = match texture { + Some(s) => s, + None => return, + }; + + let texture_width = texture.inner.width() as f32; + let texture_height = texture.inner.height() as f32; + + let opaque = false; + + // The texture gets mapped 1:1 onto the display, so there is no need for mipmaps + let allow_mipmaps = false; + + // Note: The ImageDescriptor has no effect on how large the image appears on-screen + let descriptor = ImageDescriptor::new(texture_width as i32, texture_height as i32, ImageFormat::BGRA8, opaque, allow_mipmaps); + let key = referenced_mutable_content.app_resources.get_render_api().new_image_key(); + let external_image_id = ExternalImageId(new_opengl_texture_id() as u64); + + let data = ImageData::External(ExternalImageData { + id: external_image_id, + channel_index: 0, + image_type: ExternalImageType::TextureHandle(TextureTarget::Default), + }); + + ACTIVE_GL_TEXTURES.lock().unwrap() + .entry(rectangle.epoch).or_insert_with(|| FastHashMap::default()) + .insert(external_image_id, ActiveTexture { texture: texture.clone() }); + + referenced_mutable_content.resource_updates.push(ResourceUpdate::AddImage( + AddImage { key, descriptor, data, tiling: None } + )); + + referenced_mutable_content.builder.push_image( + &info, + LayoutSize::new(texture_width, texture_height), + LayoutSize::zero(), + ImageRendering::Auto, + AlphaType::Alpha, + key, + ColorF::WHITE + ); +} + +fn push_iframe<'a,'b,'c,'d,'e,'f, T>( + (iframe_callback, iframe_pointer): &(IFrameCallback, StackCheckedPointer), + info: &LayoutPrimitiveInfo, + parent_scrollable_nodes: &mut ScrolledNodes, + rectangle: &DisplayListRectParams<'a, T>, + referenced_content: &DisplayListParametersRef<'a,'b,'c,'d,'e, T>, + referenced_mutable_content: &mut DisplayListParametersMut<'f, T>, +) { + let bounds = HidpiAdjustedBounds::from_bounds( + info.rect, + rectangle.window_size.hidpi_factor, + rectangle.window_size.hidpi_factor + ); + + let new_dom = { + // Make sure that the app data is locked before invoking the callback + let _lock = referenced_mutable_content.app_data.lock().unwrap(); + + let window_info = LayoutInfo { + window: referenced_mutable_content.fake_window, + resources: &referenced_mutable_content.app_resources, + }; + + (iframe_callback.0)(&iframe_pointer, window_info, bounds) + }; + + // TODO: Right now, no focusing, hovering or :active allowed in iframes! + let is_mouse_down = false; + let mut focused_node = None; + let mut focus_target = None; + let hovered_nodes = BTreeMap::new(); + + let mut ui_state = new_dom.into_ui_state(); + let ui_description = UiDescription::::match_css_to_dom( + &mut ui_state, + &referenced_content.css, + &mut focused_node, + &mut focus_target, + &hovered_nodes, + is_mouse_down + ); + + let display_list = DisplayList::new_from_ui_description(&ui_description, &ui_state); + referenced_mutable_content.app_resources.add_fonts_and_images(&display_list); + + let arena = &ui_description.ui_descr_arena; + let node_hierarchy = &arena.node_layout; + let node_data = &arena.node_data; + + // Insert the DOM into the solver so we can solve the layout of the rectangles + let rect_size = LayoutSize::new( + info.rect.size.width / rectangle.window_size.hidpi_factor as f32 * rectangle.window_size.winit_hidpi_factor as f32, + info.rect.size.height / rectangle.window_size.hidpi_factor as f32 * rectangle.window_size.winit_hidpi_factor as f32, + ); + let rect_origin = LayoutPoint::new(info.rect.origin.x, info.rect.origin.y); + let layout_result = do_the_layout( + &node_hierarchy, + &node_data, + &display_list.rectangles, + &*referenced_mutable_content.app_resources, + rect_size, + rect_origin, + ); + + let mut scrollable_nodes = get_nodes_that_need_scroll_clip( + node_hierarchy, &display_list.rectangles, node_data, &layout_result.rects, + &layout_result.node_depths, referenced_content.pipeline_id + ); + + let rects_in_rendering_order = determine_rendering_order( + node_hierarchy, &display_list.rectangles, &layout_result.rects + ); + + let referenced_content = DisplayListParametersRef { + // Important: Need to update the ui description, otherwise this function would be endlessly recursive + node_hierarchy, + node_data, + display_rectangle_arena: &display_list.rectangles, + layout_result: &layout_result, + .. *referenced_content + }; + + push_rectangles_into_displaylist( + rectangle.epoch, + rectangle.window_size, + rects_in_rendering_order, + &mut scrollable_nodes, + &mut ScrollStates::new(), + &referenced_content, + referenced_mutable_content + ); + + parent_scrollable_nodes.overflowing_nodes.extend(scrollable_nodes.overflowing_nodes.into_iter()); + parent_scrollable_nodes.tags_to_node_ids.extend(scrollable_nodes.tags_to_node_ids.into_iter()); +} + +/// Since the display list can take a lot of parameters, we don't want to +/// continually pass them as parameters of the function and rather use a +/// struct to pass them around. This is purely for ergonomic reasons. +/// +/// `DisplayListParametersRef` has only members that are +/// **immutable references** to other things that need to be passed down the display list +#[derive(Copy, Clone)] +struct DisplayListParametersRef<'a, 'b, 'c, 'd, 'e, T: 'a> { + pub node_data: &'a NodeDataContainer>, + /// The CSS that should be applied to the DOM + pub css: &'b Css, + /// Laid out words and rectangles (contains info about content bounds and text layout) + pub layout_result: &'c LayoutResult, + /// Reference to the arena that contains all the styled rectangles + pub display_rectangle_arena: &'d NodeDataContainer>, + pub node_hierarchy: &'e NodeHierarchy, + pub pipeline_id: PipelineId, +} + +/// Same as `DisplayListParametersRef`, but for `&mut Something` +/// +/// Note: The `'a` in the `'a + Layout` is technically not required. +/// Only rustc 1.28 requires this, more modern compiler versions insert it automatically. +struct DisplayListParametersMut<'a, T: 'a> { + /// Needs to be present, because the dom_to_displaylist_builder + /// could call (recursively) a sub-DOM function again, for example an OpenGL callback + pub app_data: &'a mut Arc>, + /// The original, top-level display list builder that we need to push stuff into + pub builder: &'a mut DisplayListBuilder, + /// The app resources, so that a sub-DOM / iframe can register fonts and images + /// TODO: How to handle cleanup ??? + pub app_resources: &'a mut AppResources, + /// If new fonts or other stuff are created, we need to tell WebRender about this + pub resource_updates: &'a mut Vec, + /// Window access, so that sub-items can register OpenGL textures + pub fake_window: &'a mut FakeWindow, + pub pipeline_id: PipelineId, +} + +fn push_rect( + info: &PrimitiveInfo, + builder: &mut DisplayListBuilder, + color: &StyleColorU +) { + use css::webrender_translate::wr_translate_color_u; + builder.push_rect(&info, wr_translate_color_u(*color).into()); +} + +fn push_text( + info: &PrimitiveInfo, + builder: &mut DisplayListBuilder, + layout_result: &LayoutResult, + node_id: &NodeId, + rect_style: &RectStyle, + rect_layout: &RectLayout, +) { + use text_layout::get_layouted_glyphs; + use css::webrender_translate::wr_translate_color_u; + use ui_solver::determine_text_alignment; + + let (scaled_words, _font_instance_key) = match layout_result.scaled_words.get(node_id) { + Some(s) => s, + None => return, + }; + + let (word_positions, font_instance_key) = match layout_result.positioned_word_cache.get(node_id) { + Some(s) => s, + None => return, + }; + + let (horz_alignment, vert_alignment) = determine_text_alignment(rect_style, rect_layout); + + let rect_padding_top = rect_layout.padding.unwrap_or_default().top.map(|top| top.to_pixels()).unwrap_or(0.0); + let rect_padding_left = rect_layout.padding.unwrap_or_default().left.map(|left| left.to_pixels()).unwrap_or(0.0); + let rect_offset = LayoutPoint::new(info.rect.origin.x + rect_padding_left, info.rect.origin.y + rect_padding_top); + let bounding_size_height_px = info.rect.size.height - rect_layout.get_vertical_padding(); + + let layouted_glyphs = get_layouted_glyphs( + word_positions, + scaled_words, + horz_alignment, + vert_alignment, + rect_offset.clone(), + bounding_size_height_px + ); + + let font_color = rect_style.font_color.unwrap_or(DEFAULT_FONT_COLOR).0; + let font_color = wr_translate_color_u(font_color); + + // WARNING: Do not enable FontInstanceFlags::FONT_SMOOTHING or FontInstanceFlags::FORCE_AUTOHINT - + // they seem to interfere with the text layout thereby messing with the actual text layout. + let mut flags = FontInstanceFlags::empty(); + flags.set(FontInstanceFlags::SUBPIXEL_BGR, true); + flags.set(FontInstanceFlags::NO_AUTOHINT, true); + flags.set(FontInstanceFlags::LCD_VERTICAL, true); + + let overflow_horizontal_visible = rect_layout.is_horizontal_overflow_visible(); + let overflow_vertical_visible = rect_layout.is_horizontal_overflow_visible(); + + let max_bounds = builder.content_size(); + let current_bounds = info.rect; + let original_text_bounds = rect_layout.padding + .as_ref() + .map(|padding| subtract_padding(¤t_bounds, padding)) + .unwrap_or(current_bounds); + + // Adjust the bounds by the padding, depending on the overflow:visible parameter + let mut text_bounds = match (overflow_horizontal_visible, overflow_vertical_visible) { + (true, true) => None, + (false, false) => Some(original_text_bounds), + (true, false) => { + // Horizontally visible, vertically cut + Some(LayoutRect::new(rect_offset, LayoutSize::new(max_bounds.width, original_text_bounds.size.height))) + }, + (false, true) => { + // Vertically visible, horizontally cut + Some(LayoutRect::new(rect_offset, LayoutSize::new(original_text_bounds.size.width, max_bounds.height))) + }, + }; + + if let Some(text_bounds) = &mut text_bounds { + text_bounds.size.width = text_bounds.size.width.max(0.0); + text_bounds.size.height = text_bounds.size.height.max(0.0); + let clip_id = builder.define_clip(*text_bounds, vec![ComplexClipRegion { + rect: *text_bounds, + radii: BorderRadius::zero(), + mode: ClipMode::Clip, + }], None); + builder.push_clip_id(clip_id); + } + + builder.push_text( + &info, + &layouted_glyphs.glyphs, + *font_instance_key, + font_color.into(), + Some(GlyphOptions { + render_mode: FontRenderMode::Subpixel, + flags: flags, + }) + ); + + if text_bounds.is_some() { + builder.pop_clip_id(); + } +} + +enum ShouldPushShadow { + OneShadow, + TwoShadows, + AllShadows, +} + +/// WARNING: For "inset" shadows, you must push a clip ID first, otherwise the +/// shadow will not show up. +/// +/// To prevent a shadow from being pushed twice, you have to annotate the clip +/// mode for this - outset or inset. +#[inline] +fn push_box_shadow( + builder: &mut DisplayListBuilder, + style: &RectStyle, + bounds: &LayoutRect, + shadow_type: BoxShadowClipMode) +{ + use self::ShouldPushShadow::*; + + // Box-shadow can be applied to each corner separately. This means, in practice + // that we simply overlay multiple shadows with shifted clipping rectangles + let StyleBoxShadow { top, left, bottom, right } = match &style.box_shadow { + Some(s) => s, + None => return, + }; + + let border_radius = style.border_radius.unwrap_or(StyleBorderRadius::zero()); + + let what_shadow_to_push = match [top, left, bottom, right].iter().filter(|x| x.is_some()).count() { + 1 => OneShadow, + 2 => TwoShadows, + 4 => AllShadows, + _ => return, + }; + + match what_shadow_to_push { + OneShadow => { + let current_shadow = match (top, left, bottom, right) { + | (Some(Some(shadow)), None, None, None) + | (None, Some(Some(shadow)), None, None) + | (None, None, Some(Some(shadow)), None) + | (None, None, None, Some(Some(shadow))) + => shadow, + _ => return, // reachable, but invalid box-shadow + }; + + push_single_box_shadow_edge( + builder, current_shadow, bounds, border_radius, shadow_type, + top, bottom, left, right + ); + }, + // Two shadows in opposite directions: + // + // box-shadow-top: 0px 0px 5px red; + // box-shadow-bottom: 0px 0px 5px blue; + TwoShadows => { + match (top, left, bottom, right) { + + // top + bottom box-shadow pair + (Some(Some(t)), None, Some(Some(b)), right) => { + push_single_box_shadow_edge( + builder, t, bounds, border_radius, shadow_type, + top, &None, &None, &None + ); + push_single_box_shadow_edge( + builder, b, bounds, border_radius, shadow_type, + &None, bottom, &None, &None + ); + }, + // left + right box-shadow pair + (None, Some(Some(l)), None, Some(Some(r))) => { + push_single_box_shadow_edge( + builder, l, bounds, border_radius, shadow_type, + &None, &None, left, &None + ); + push_single_box_shadow_edge( + builder, r, bounds, border_radius, shadow_type, + &None, &None, &None, right + ); + } + _ => return, // reachable, but invalid + } + }, + AllShadows => { + + // Assumes that all box shadows are the same, so just use the top shadow + let top_shadow = top.unwrap(); + let clip_rect = top_shadow + .as_ref() + .map(|top_shadow| get_clip_rect(top_shadow, bounds)) + .unwrap_or(*bounds); + + push_box_shadow_inner( + builder, + &top_shadow, + border_radius, + bounds, + clip_rect, + shadow_type + ); + } + } +} + +fn push_box_shadow_inner( + builder: &mut DisplayListBuilder, + pre_shadow: &Option, + border_radius: StyleBorderRadius, + bounds: &LayoutRect, + clip_rect: LayoutRect, + shadow_type: BoxShadowClipMode) +{ + use webrender::api::LayoutVector2D; + use css::webrender_translate::{ + wr_translate_color_u, wr_translate_border_radius, + wr_translate_box_shadow_clip_mode + }; + + let pre_shadow = match pre_shadow { + None => return, + Some(ref s) => s, + }; + + // The pre_shadow is missing the StyleBorderRadius & LayoutRect + if pre_shadow.clip_mode != shadow_type { + return; + } + + let full_screen_rect = LayoutRect::new(LayoutPoint::zero(), builder.content_size());; + + // prevent shadows that are larger than the full screen + let clip_rect = clip_rect.intersection(&full_screen_rect).unwrap_or(clip_rect); + + // Apply a gamma of 2.2 to the original value + // + // NOTE: strangely box-shadow is the only thing that needs to be gamma-corrected... + fn apply_gamma(color: ColorF) -> ColorF { + + const GAMMA: f32 = 2.2; + const GAMMA_F: f32 = 1.0 / GAMMA; + + ColorF { + r: color.r.powf(GAMMA_F), + g: color.g.powf(GAMMA_F), + b: color.b.powf(GAMMA_F), + a: color.a, + } + } + + let info = LayoutPrimitiveInfo::with_clip_rect(LayoutRect::zero(), clip_rect); + builder.push_box_shadow( + &info, + *bounds, + LayoutVector2D::new(pre_shadow.offset[0].to_pixels(), pre_shadow.offset[1].to_pixels()), + apply_gamma(wr_translate_color_u(pre_shadow.color).into()), + pre_shadow.blur_radius.to_pixels(), + pre_shadow.spread_radius.to_pixels(), + wr_translate_border_radius(border_radius.0).into(), + wr_translate_box_shadow_clip_mode(pre_shadow.clip_mode) + ); +} + +fn get_clip_rect(pre_shadow: &BoxShadowPreDisplayItem, bounds: &LayoutRect) -> LayoutRect { + if pre_shadow.clip_mode == BoxShadowClipMode::Inset { + // inset shadows do not work like outset shadows + // for inset shadows, you have to push a clip ID first, so that they are + // clipped to the bounds -we trust that the calling function knows to do this + *bounds + } else { + // calculate the maximum extent of the outset shadow + let mut clip_rect = *bounds; + + let origin_displace = (pre_shadow.spread_radius.to_pixels() + pre_shadow.blur_radius.to_pixels()) * 2.0; + clip_rect.origin.x = clip_rect.origin.x - pre_shadow.offset[0].to_pixels() - origin_displace; + clip_rect.origin.y = clip_rect.origin.y - pre_shadow.offset[1].to_pixels() - origin_displace; + + clip_rect.size.height = clip_rect.size.height + (origin_displace * 2.0); + clip_rect.size.width = clip_rect.size.width + (origin_displace * 2.0); + clip_rect + } +} + +#[allow(clippy::collapsible_if)] +fn push_single_box_shadow_edge( + builder: &mut DisplayListBuilder, + current_shadow: &BoxShadowPreDisplayItem, + bounds: &LayoutRect, + border_radius: StyleBorderRadius, + shadow_type: BoxShadowClipMode, + top: &Option>, + bottom: &Option>, + left: &Option>, + right: &Option>, +) { + let is_inset_shadow = current_shadow.clip_mode == BoxShadowClipMode::Inset; + let origin_displace = (current_shadow.spread_radius.to_pixels() + current_shadow.blur_radius.to_pixels()) * 2.0; + + let mut shadow_bounds = *bounds; + let mut clip_rect = *bounds; + + if is_inset_shadow { + // If the shadow is inset, we adjust the clip rect to be + // exactly the amount of the shadow + if let Some(Some(top)) = top { + clip_rect.size.height = origin_displace; + shadow_bounds.size.width += origin_displace; + shadow_bounds.origin.x -= origin_displace / 2.0; + } else if let Some(Some(bottom)) = bottom { + clip_rect.size.height = origin_displace; + clip_rect.origin.y += bounds.size.height - origin_displace; + shadow_bounds.size.width += origin_displace; + shadow_bounds.origin.x -= origin_displace / 2.0; + } else if let Some(Some(left)) = left { + clip_rect.size.width = origin_displace; + shadow_bounds.size.height += origin_displace; + shadow_bounds.origin.y -= origin_displace / 2.0; + } else if let Some(Some(right)) = right { + clip_rect.size.width = origin_displace; + clip_rect.origin.x += bounds.size.width - origin_displace; + shadow_bounds.size.height += origin_displace; + shadow_bounds.origin.y -= origin_displace / 2.0; + } + } else { + if let Some(Some(top)) = top { + clip_rect.size.height = origin_displace; + clip_rect.origin.y -= origin_displace; + shadow_bounds.size.width += origin_displace; + shadow_bounds.origin.x -= origin_displace / 2.0; + } else if let Some(Some(bottom)) = bottom { + clip_rect.size.height = origin_displace; + clip_rect.origin.y += bounds.size.height; + shadow_bounds.size.width += origin_displace; + shadow_bounds.origin.x -= origin_displace / 2.0; + } else if let Some(Some(left)) = left { + clip_rect.size.width = origin_displace; + clip_rect.origin.x -= origin_displace; + shadow_bounds.size.height += origin_displace; + shadow_bounds.origin.y -= origin_displace / 2.0; + } else if let Some(Some(right)) = right { + clip_rect.size.width = origin_displace; + clip_rect.origin.x += bounds.size.width; + shadow_bounds.size.height += origin_displace; + shadow_bounds.origin.y -= origin_displace / 2.0; + } + } + + push_box_shadow_inner( + builder, + &Some(*current_shadow), + border_radius, + &shadow_bounds, + clip_rect, + shadow_type + ); +} + +#[inline] +fn push_background( + info: &PrimitiveInfo, + bounds: &TypedRect, + builder: &mut DisplayListBuilder, + background: &StyleBackground, + background_size: &Option, + background_repeat: &Option, + app_resources: &AppResources) +{ + use azul_css::{Shape, StyleBackground::*}; + use css::webrender_translate::{ + wr_translate_color_u, wr_translate_extend_mode, wr_translate_layout_point, + wr_translate_layout_rect, + }; + + match background { + RadialGradient(gradient) => { + let stops: Vec = gradient.stops.iter().map(|gradient_pre| + GradientStop { + offset: gradient_pre.offset.unwrap().get(), + color: wr_translate_color_u(gradient_pre.color).into(), + }).collect(); + + let center = bounds.center(); + + // Note: division by 2.0 because it's the radius, not the diameter + let radius = match gradient.shape { + Shape::Ellipse => TypedSize2D::new(bounds.size.width / 2.0, bounds.size.height / 2.0), + Shape::Circle => { + let largest_bound_size = bounds.size.width.max(bounds.size.height); + TypedSize2D::new(largest_bound_size / 2.0, largest_bound_size / 2.0) + }, + }; + + let gradient = builder.create_radial_gradient(center, radius, stops, wr_translate_extend_mode(gradient.extend_mode)); + builder.push_radial_gradient(&info, gradient, bounds.size, LayoutSize::zero()); + }, + LinearGradient(gradient) => { + + let stops: Vec = gradient.stops.iter().map(|gradient_pre| + GradientStop { + offset: gradient_pre.offset.unwrap().get() / 100.0, + color: wr_translate_color_u(gradient_pre.color).into(), + }).collect(); + + let (begin_pt, end_pt) = gradient.direction.to_points(&wr_translate_layout_rect(*bounds)); + let gradient = builder.create_gradient( + wr_translate_layout_point(begin_pt), + wr_translate_layout_point(end_pt), + stops, + wr_translate_extend_mode(gradient.extend_mode), + ); + + builder.push_gradient(&info, gradient, bounds.size, LayoutSize::zero()); + }, + Image(style_image_id) => { + // TODO: background-origin, background-position, background-repeat + if let Some(image_id) = app_resources.get_css_image_id(&style_image_id.0) { + + let bounds = info.rect; + let image_dimensions = app_resources.get_image_info(image_id) + .map(|info| (info.descriptor.size.width, info.descriptor.size.height)) + .unwrap_or((bounds.size.width as i32, bounds.size.height as i32)); // better than crashing... + + let size = match background_size { + Some(bg_size) => calculate_background_size(bg_size, &info, &image_dimensions), + None => TypedSize2D::new(image_dimensions.0 as f32, image_dimensions.1 as f32), + }; + + let background_repeat = background_repeat.unwrap_or_default(); + let background_repeat_info = get_background_repeat_info(&info, background_repeat, size); + + push_image(&background_repeat_info, builder, app_resources, image_id, size); + } + }, + Color(c) => { + push_rect(&info, builder, c); + }, + NoBackground => { }, + } +} + +fn get_background_repeat_info( + info: &LayoutPrimitiveInfo, + background_repeat: StyleBackgroundRepeat, + background_size: TypedSize2D, +) -> LayoutPrimitiveInfo { + use azul_css::StyleBackgroundRepeat::*; + match background_repeat { + NoRepeat => LayoutPrimitiveInfo::with_clip_rect( + info.rect, + TypedRect::new( + info.rect.origin, + TypedSize2D::new(background_size.width, background_size.height), + ), + ), + Repeat => *info, + RepeatX => LayoutPrimitiveInfo::with_clip_rect( + info.rect, + TypedRect::new( + info.rect.origin, + TypedSize2D::new(info.rect.size.width, background_size.height), + ), + ), + RepeatY => LayoutPrimitiveInfo::with_clip_rect( + info.rect, + TypedRect::new( + info.rect.origin, + TypedSize2D::new(background_size.width, info.rect.size.height), + ), + ), + } +} + +struct Ratio { + width: f32, + height: f32 +} + +fn calculate_background_size( + bg_size: &StyleBackgroundSize, + info: &PrimitiveInfo, + image_dimensions: &(i32, i32) +) -> TypedSize2D { + + let original_ratios = Ratio { + width: info.rect.size.width / image_dimensions.0 as f32, + height: info.rect.size.height / image_dimensions.1 as f32, + }; + + let ratio = match bg_size { + StyleBackgroundSize::Contain => original_ratios.width.min(original_ratios.height), + StyleBackgroundSize::Cover => original_ratios.width.max(original_ratios.height) + }; + + TypedSize2D::new(image_dimensions.0 as f32 * ratio, image_dimensions.1 as f32 * ratio) +} + +#[inline] +fn push_image( + info: &PrimitiveInfo, + builder: &mut DisplayListBuilder, + app_resources: &AppResources, + image_id: &ImageId, + size: TypedSize2D +) { + if let Some(image_info) = app_resources.get_image_info(image_id) { + builder.push_image( + info, + size, + LayoutSize::zero(), + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + image_info.key, + ColorF::WHITE, + ); + } +} + +#[inline] +fn push_border( + info: &PrimitiveInfo, + builder: &mut DisplayListBuilder, + border: &StyleBorder, + border_radius: &Option) +{ + use css::webrender_translate::{ + wr_translate_layout_side_offsets, wr_translate_border_details + }; + + if let Some((border_widths, border_details)) = border.get_webrender_border(*border_radius) { + builder.push_border( + info, + wr_translate_layout_side_offsets(border_widths), + wr_translate_border_details(border_details)); + } +} + +/// Subtracts the padding from the bounds, returning the new bounds +/// +/// Warning: The resulting rectangle may have negative width or height +fn subtract_padding(bounds: &TypedRect, padding: &LayoutPadding) +-> TypedRect +{ + let top = padding.top.map(|top| top.to_pixels()).unwrap_or(0.0); + let bottom = padding.bottom.map(|bottom| bottom.to_pixels()).unwrap_or(0.0); + let left = padding.left.map(|left| left.to_pixels()).unwrap_or(0.0); + let right = padding.right.map(|right| right.to_pixels()).unwrap_or(0.0); + + let mut new_bounds = *bounds; + + new_bounds.origin.x += left; + new_bounds.size.width -= right + left; + new_bounds.origin.y += top; + new_bounds.size.height -= top + bottom; + + new_bounds +} + +/// Populate the style properties of the `DisplayRectangle`, apply static / dynamic properties +fn populate_css_properties( + rect: &mut DisplayRectangle, + node_id: NodeId, + css_overrides: &BTreeMap> +) { + use azul_css::CssDeclaration::*; + + for constraint in rect.styled_node.css_constraints.values() { + match &constraint { + Static(static_property) => apply_style_property(rect, static_property), + Dynamic(dynamic_property) => { + let is_dynamic_prop = css_overrides.get(&node_id).and_then(|overrides| { + overrides.get(&DomString::Heap(dynamic_property.dynamic_id.clone())) + }); + + if let Some(overridden_property) = is_dynamic_prop { + // Only apply the dynamic style property default, if it isn't set to auto + if property_type_matches(overridden_property, &dynamic_property.default) { + apply_style_property(rect, overridden_property); + } else { + #[cfg(feature = "logging")] { + error!( + "Dynamic style property on rect {:?} don't have the same discriminant type,\r\n + cannot override {:?} with {:?} - enum discriminant mismatch", + rect, dynamic_property.default, overridden_property + ) + } + } + } else if let DynamicCssPropertyDefault::Exact(default) = &dynamic_property.default { + apply_style_property(rect, default); + } + } + } + } +} + +// Assert that the types of two properties matches +fn property_type_matches(a: &CssProperty, b: &DynamicCssPropertyDefault) -> bool { + use std::mem::discriminant; + use azul_css::DynamicCssPropertyDefault::*; + match b { + Exact(e) => discriminant(a) == discriminant(e), + Auto => true, // "auto" always matches + } +} + +fn apply_style_property(rect: &mut DisplayRectangle, property: &CssProperty) { + + use azul_css::CssProperty::*; + + match property { + BorderRadius(b) => { rect.style.border_radius = Some(*b); }, + BackgroundSize(s) => { rect.style.background_size = Some(*s); }, + BackgroundRepeat(r) => { rect.style.background_repeat = Some(*r); }, + TextColor(t) => { rect.style.font_color = Some(*t); }, + Border(b) => { StyleBorder::merge(&mut rect.style.border, &b); }, + Background(b) => { rect.style.background = Some(b.clone()); }, + FontSize(f) => { rect.style.font_size = Some(*f); }, + FontFamily(f) => { rect.style.font_family = Some(f.clone()); }, + LetterSpacing(l) => { rect.style.letter_spacing = Some(*l); }, + TextAlign(ta) => { rect.style.text_align = Some(*ta); }, + BoxShadow(b) => { StyleBoxShadow::merge(&mut rect.style.box_shadow, b); }, + LineHeight(lh) => { rect.style.line_height = Some(*lh); }, + + Width(w) => { rect.layout.width = Some(*w); }, + Height(h) => { rect.layout.height = Some(*h); }, + MinWidth(mw) => { rect.layout.min_width = Some(*mw); }, + MinHeight(mh) => { rect.layout.min_height = Some(*mh); }, + MaxWidth(mw) => { rect.layout.max_width = Some(*mw); }, + MaxHeight(mh) => { rect.layout.max_height = Some(*mh); }, + + Position(p) => { rect.layout.position = Some(*p); }, + Top(t) => { rect.layout.top = Some(*t); }, + Bottom(b) => { rect.layout.bottom = Some(*b); }, + Right(r) => { rect.layout.right = Some(*r); }, + Left(l) => { rect.layout.left = Some(*l); }, + + Padding(p) => { LayoutPadding::merge(&mut rect.layout.padding, &p); }, + Margin(m) => { LayoutMargin::merge(&mut rect.layout.margin, &m); }, + Overflow(o) => { LayoutOverflow::merge(&mut rect.layout.overflow, &o); }, + WordSpacing(ws) => { rect.style.word_spacing = Some(*ws); }, + TabWidth(tw) => { rect.style.tab_width = Some(*tw); }, + + FlexGrow(g) => { rect.layout.flex_grow = Some(*g) }, + FlexShrink(s) => { rect.layout.flex_shrink = Some(*s) }, + FlexWrap(w) => { rect.layout.wrap = Some(*w); }, + FlexDirection(d) => { rect.layout.direction = Some(*d); }, + JustifyContent(j) => { rect.layout.justify_content = Some(*j); }, + AlignItems(a) => { rect.layout.align_items = Some(*a); }, + AlignContent(a) => { rect.layout.align_content = Some(*a); }, + Cursor(_) => { /* cursor neither affects layout nor styling */ }, + } +} diff --git a/azul/src/dom.rs b/azul/src/dom.rs new file mode 100644 index 000000000..a3a09c5f4 --- /dev/null +++ b/azul/src/dom.rs @@ -0,0 +1,1594 @@ +use std::{ + fmt, + path::Path, + hash::{Hash, Hasher}, + sync::atomic::{AtomicUsize, Ordering}, + cmp::Ordering as CmpOrdering, + collections::BTreeMap, + iter::FromIterator, +}; +use azul_css::{ NodeTypePath, CssProperty }; +use { + ui_state::UiState, + callbacks::{ + DefaultCallbackId, StackCheckedPointer, + Callback, GlTextureCallback, IFrameCallback, + }, + app_resources::{ImageId, TextId}, + id_tree::{Arena, NodeDataContainer}, + xml::{self, XmlParseError, XmlComponentMap}, +}; + +pub use id_tree::{NodeHierarchy, Node, NodeId}; + +static TAG_ID: AtomicUsize = AtomicUsize::new(1); + +pub(crate) type TagId = u64; + +/// Same as the `TagId`, but only for scrollable nodes +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] +pub(crate) struct ScrollTagId(pub TagId); + +pub(crate) fn new_tag_id() -> TagId { + TAG_ID.fetch_add(1, Ordering::SeqCst) as TagId +} + +pub(crate) fn new_scroll_tag_id() -> ScrollTagId { + ScrollTagId(new_tag_id()) +} + +static DOM_ID: AtomicUsize = AtomicUsize::new(1); + +/// DomID - used for identifying different DOMs (for example IFrameCallbacks) +/// have a different DomId than the root DOM +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DomId { + /// Unique ID for this DOM + id: usize, + /// If this DOM was generated from an IFrameCallback, stores the parents + /// DomId + the NodeId (from the parent DOM) which the IFrameCallback + /// was attached to. + parent: Option<(Box, NodeId)>, +} + +pub(crate) fn new_dom_id(parent: Option<(DomId, NodeId)>) -> DomId { + let new_dom_id = DOM_ID.fetch_add(1, Ordering::SeqCst); + DomId { + id: new_dom_id, + parent: parent.map(|(p, node_id)| (Box::new(p), node_id)), + } +} + +/// Reset the DOM ID to 1, usually done once-per-frame for the root DOM +pub(crate) fn reset_dom_id() { + DOM_ID.swap(1, Ordering::SeqCst); +} + +impl DomId { + + /// Creates an ID for the root node + #[inline] + pub(crate) const fn create_root_dom_id() -> Self { + Self { + id: 0, + parent: None, + } + } + + /// Returns if this is the root node + pub fn is_root(&self) -> bool { + *self == Self::create_root_dom_id() + } +} + +/// Calculated hash of a DOM node, used for querying attributes of the DOM node +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, Ord, PartialOrd)] +pub struct DomHash(pub u64); + +/// List of core DOM node types built-into by `azul`. +pub enum NodeType { + /// Regular div with no particular type of data attached + Div, + /// A small label that can be (optionally) be selectable with the mouse + Label(DomString), + /// Larger amount of text, that has to be cached + Text(TextId), + /// An image that is rendered by WebRender. The id is acquired by the + /// `AppState::add_image()` function + Image(ImageId), + /// OpenGL texture. The `Svg` widget deserizalizes itself into a texture + /// Equality and Hash values are only checked by the OpenGl texture ID, + /// Azul does not check that the contents of two textures are the same + GlTexture((GlTextureCallback, StackCheckedPointer)), + /// DOM that gets passed its width / height during the layout + IFrame((IFrameCallback, StackCheckedPointer)), +} + +impl NodeType { + fn get_text_content(&self) -> Option { + use self::NodeType::*; + match self { + Div => None, + Label(s) => Some(format!("{}", s)), + Image(id) => Some(format!("image({:?})", id)), + Text(t) => Some(format!("textid({:?})", t)), + GlTexture(g) => Some(format!("gltexture({:?})", g)), + IFrame(i) => Some(format!("iframe({:?})", i)), + } + } +} + +// #[derive(Debug, Clone, PartialEq, Hash, Eq)] for NodeType + +impl fmt::Debug for NodeType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use self::NodeType::*; + match self { + Div => write!(f, "NodeType::Div"), + Label(a) => write!(f, "NodeType::Label {{ {:?} }}", a), + Text(a) => write!(f, "NodeType::Text {{ {:?} }}", a), + Image(a) => write!(f, "NodeType::Image {{ {:?} }}", a), + GlTexture((ptr, cb)) => write!(f, "NodeType::GlTexture {{ ptr: {:?}, callback: {:?} }}", ptr, cb), + IFrame((ptr, cb)) => write!(f, "NodeType::IFrame {{ ptr: {:?}, callback: {:?} }}", ptr, cb), + } + } +} + +impl Clone for NodeType { + fn clone(&self) -> Self { + use self::NodeType::*; + match self { + Div => Div, + Label(a) => Label(a.clone()), + Text(a) => Text(a.clone()), + Image(a) => Image(a.clone()), + GlTexture((ptr, a)) => GlTexture((ptr.clone(), a.clone())), + IFrame((ptr, a)) => IFrame((ptr.clone(), a.clone())), + } + } +} + +impl Hash for NodeType { + fn hash(&self, state: &mut H) where H: Hasher { + use self::NodeType::*; + use std::mem; + mem::discriminant(&self).hash(state); + match self { + Div => { }, + Label(a) => a.hash(state), + Text(a) => a.hash(state), + Image(a) => a.hash(state), + GlTexture((ptr, a)) => { + ptr.hash(state); + a.hash(state); + }, + IFrame((ptr, a)) => { + ptr.hash(state); + a.hash(state); + }, + } + } +} + +impl PartialEq for NodeType { + fn eq(&self, rhs: &Self) -> bool { + use self::NodeType::*; + match (self, rhs) { + (Div, Div) => true, + (Label(a), Label(b)) => a == b, + (Text(a), Text(b)) => a == b, + (Image(a), Image(b)) => a == b, + (GlTexture((ptr_a, a)), GlTexture((ptr_b, b))) => { + a == b && ptr_a == ptr_b + }, + (IFrame((ptr_a, a)), IFrame((ptr_b, b))) => { + a == b && ptr_a == ptr_b + }, + _ => false, + } + } +} + +impl Eq for NodeType { } + +impl NodeType { + #[inline] + pub(crate) fn get_path(&self) -> NodeTypePath { + use self::NodeType::*; + match self { + Div => NodeTypePath::Div, + Label(_) | Text(_) => NodeTypePath::P, + Image(_) => NodeTypePath::Img, + GlTexture(_) => NodeTypePath::Texture, + IFrame(_) => NodeTypePath::IFrame, + } + } +} + +/// When to call a callback action - `On::MouseOver`, `On::MouseOut`, etc. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum On { + /// Mouse cursor is hovering over the element + MouseOver, + /// Mouse cursor has is over element and is pressed + /// (not good for "click" events - use `MouseUp` instead) + MouseDown, + /// (Specialization of `MouseDown`). Fires only if the left mouse button + /// has been pressed while cursor was over the element + LeftMouseDown, + /// (Specialization of `MouseDown`). Fires only if the middle mouse button + /// has been pressed while cursor was over the element + MiddleMouseDown, + /// (Specialization of `MouseDown`). Fires only if the right mouse button + /// has been pressed while cursor was over the element + RightMouseDown, + /// Mouse button has been released while cursor was over the element + MouseUp, + /// (Specialization of `MouseUp`). Fires only if the left mouse button has + /// been released while cursor was over the element + LeftMouseUp, + /// (Specialization of `MouseUp`). Fires only if the middle mouse button has + /// been released while cursor was over the element + MiddleMouseUp, + /// (Specialization of `MouseUp`). Fires only if the right mouse button has + /// been released while cursor was over the element + RightMouseUp, + /// Mouse cursor has entered the element + MouseEnter, + /// Mouse cursor has left the element + MouseLeave, + /// Mousewheel / touchpad scrolling + Scroll, + /// The window received a unicode character (also respects the system locale). + /// Check `keyboard_state.current_char` to get the current pressed character. + TextInput, + /// A **virtual keycode** was pressed. Note: This is only the virtual keycode, + /// not the actual char. If you want to get the character, use `TextInput` instead. + /// A virtual key does not have to map to a printable character. + /// + /// You can get all currently pressed virtual keycodes in the `keyboard_state.current_virtual_keycodes` + /// and / or just the last keycode in the `keyboard_state.latest_virtual_keycode`. + VirtualKeyDown, + /// A **virtual keycode** was release. See `VirtualKeyDown` for more info. + VirtualKeyUp, + /// A file has been dropped on the element + HoveredFile, + /// A file is being hovered on the element + DroppedFile, + /// A file was hovered, but has exited the window + HoveredFileCancelled, + /// Equivalent to `onfocus` + FocusReceived, + /// Equivalent to `onblur` + FocusLost, +} + +/// Sets the target for what events can reach the callbacks specifically. +/// +/// Filtering events can happen on several layers, depending on +/// if a DOM node is hovered over or actively focused. For example, +/// for text input, you wouldn't want to use hovering, because that +/// would mean that the user needs to hold the mouse over the text input +/// in order to enter text. To solve this, the DOM needs to fire events +/// for elements that are currently not part of the hit-test. +/// `EventFilter` implements `From` as a shorthand (so that you can opt-in +/// to a more specific event) and use +/// +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum EventFilter { + /// Calls the attached callback when the mouse is actively over the + /// given element. + Hover(HoverEventFilter), + /// Inverse of `Hover` - calls the attached callback if the mouse is **not** + /// over the given element. This is particularly useful for popover menus + /// where you want to close the menu when the user clicks anywhere else but + /// the menu itself. + Not(NotEventFilter), + /// Calls the attached callback when the element is currently focused. + Focus(FocusEventFilter), + /// Calls the callback when anything related to the window is happening. + /// The "hit item" will be the root item of the DOM. + /// For example, this can be useful for tracking the mouse position + /// (in relation to the window). In difference to `Desktop`, this only + /// fires when the window is focused. + /// + /// This can also be good for capturing controller input, touch input + /// (i.e. global gestures that aren't attached to any component, but rather + /// the "window" itself). + Window(WindowEventFilter), +} + +/// Creates a function inside an impl block that returns a single +/// variant if the enum is that variant. +/// +/// ```rust +/// enum A { +/// Abc(AbcType), +/// } +/// +/// struct AbcType { } +/// +/// impl A { +/// // fn as_abc_type(&self) -> Option +/// get_single_enum_type!(as_abc_type, A::Abc(AbcType)); +/// } +/// ``` +macro_rules! get_single_enum_type { + ($fn_name:ident, $enum_name:ident::$variant:ident($return_type:ty)) => ( + fn $fn_name(&self) -> Option<$return_type> { + use self::$enum_name::*; + match self { + $variant(e) => Some(*e), + _ => None, + } + } + ) +} + +impl EventFilter { + get_single_enum_type!(as_hover_event_filter, EventFilter::Hover(HoverEventFilter)); + get_single_enum_type!(as_focus_event_filter, EventFilter::Focus(FocusEventFilter)); + get_single_enum_type!(as_not_event_filter, EventFilter::Not(NotEventFilter)); + get_single_enum_type!(as_window_event_filter, EventFilter::Window(WindowEventFilter)); +} + +impl From for EventFilter { + fn from(input: On) -> EventFilter { + use self::On::*; + match input { + MouseOver => EventFilter::Hover(HoverEventFilter::MouseOver), + MouseDown => EventFilter::Hover(HoverEventFilter::MouseDown), + LeftMouseDown => EventFilter::Hover(HoverEventFilter::LeftMouseDown), + MiddleMouseDown => EventFilter::Hover(HoverEventFilter::MiddleMouseDown), + RightMouseDown => EventFilter::Hover(HoverEventFilter::RightMouseDown), + MouseUp => EventFilter::Hover(HoverEventFilter::MouseUp), + LeftMouseUp => EventFilter::Hover(HoverEventFilter::LeftMouseUp), + MiddleMouseUp => EventFilter::Hover(HoverEventFilter::MiddleMouseUp), + RightMouseUp => EventFilter::Hover(HoverEventFilter::RightMouseUp), + + MouseEnter => EventFilter::Hover(HoverEventFilter::MouseEnter), + MouseLeave => EventFilter::Hover(HoverEventFilter::MouseLeave), + Scroll => EventFilter::Hover(HoverEventFilter::Scroll), + TextInput => EventFilter::Focus(FocusEventFilter::TextInput), // focus! + VirtualKeyDown => EventFilter::Window(WindowEventFilter::VirtualKeyDown), // window! + VirtualKeyUp => EventFilter::Window(WindowEventFilter::VirtualKeyUp), // window! + HoveredFile => EventFilter::Hover(HoverEventFilter::HoveredFile), + DroppedFile => EventFilter::Hover(HoverEventFilter::DroppedFile), + HoveredFileCancelled => EventFilter::Hover(HoverEventFilter::HoveredFileCancelled), + FocusReceived => EventFilter::Focus(FocusEventFilter::FocusReceived), // focus! + FocusLost => EventFilter::Focus(FocusEventFilter::FocusLost), // focus! + } + } +} + +/// Event filter that only fires when an element is hovered over +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum HoverEventFilter { + MouseOver, + MouseDown, + LeftMouseDown, + RightMouseDown, + MiddleMouseDown, + MouseUp, + LeftMouseUp, + RightMouseUp, + MiddleMouseUp, + MouseEnter, + MouseLeave, + Scroll, + TextInput, + VirtualKeyDown, + VirtualKeyUp, + HoveredFile, + DroppedFile, + HoveredFileCancelled, +} + +impl HoverEventFilter { + pub(crate) fn to_focus_event_filter(&self) -> Option { + use self::HoverEventFilter::*; + match self { + MouseOver => Some(FocusEventFilter::MouseOver), + MouseDown => Some(FocusEventFilter::MouseDown), + LeftMouseDown => Some(FocusEventFilter::LeftMouseDown), + RightMouseDown => Some(FocusEventFilter::RightMouseDown), + MiddleMouseDown => Some(FocusEventFilter::MiddleMouseDown), + MouseUp => Some(FocusEventFilter::MouseUp), + LeftMouseUp => Some(FocusEventFilter::LeftMouseUp), + RightMouseUp => Some(FocusEventFilter::RightMouseUp), + MiddleMouseUp => Some(FocusEventFilter::MiddleMouseUp), + MouseEnter => Some(FocusEventFilter::MouseEnter), + MouseLeave => Some(FocusEventFilter::MouseLeave), + Scroll => Some(FocusEventFilter::Scroll), + TextInput => Some(FocusEventFilter::TextInput), + VirtualKeyDown => Some(FocusEventFilter::VirtualKeyDown), + VirtualKeyUp => Some(FocusEventFilter::VirtualKeyDown), + HoveredFile => None, + DroppedFile => None, + HoveredFileCancelled => None, + } + } +} + +/// The inverse of an `onclick` event filter, fires when an item is *not* hovered / focused. +/// This is useful for cleanly implementing things like popover dialogs or dropdown boxes that +/// want to close when the user clicks any where *but* the item itself. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum NotEventFilter { + Hover(HoverEventFilter), + Focus(FocusEventFilter), +} + +/// Event filter similar to `HoverEventFilter` that only fires when the element is focused +/// +/// **Important**: In order for this to fire, the item must have a `tabindex` attribute +/// (to indicate that the item is focus-able). +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum FocusEventFilter { + MouseOver, + MouseDown, + LeftMouseDown, + RightMouseDown, + MiddleMouseDown, + MouseUp, + LeftMouseUp, + RightMouseUp, + MiddleMouseUp, + MouseEnter, + MouseLeave, + Scroll, + TextInput, + VirtualKeyDown, + VirtualKeyUp, + FocusReceived, + FocusLost, +} + +/// Event filter that fires when any action fires on the entire window +/// (regardless of whether any element is hovered or focused over). +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum WindowEventFilter { + MouseOver, + MouseDown, + LeftMouseDown, + RightMouseDown, + MiddleMouseDown, + MouseUp, + LeftMouseUp, + RightMouseUp, + MiddleMouseUp, + MouseEnter, + MouseLeave, + Scroll, + TextInput, + VirtualKeyDown, + VirtualKeyUp, + HoveredFile, + DroppedFile, + HoveredFileCancelled, +} + +impl WindowEventFilter { + pub(crate) fn to_hover_event_filter(&self) -> Option { + use self::WindowEventFilter::*; + match self { + MouseOver => Some(HoverEventFilter::MouseOver), + MouseDown => Some(HoverEventFilter::MouseDown), + LeftMouseDown => Some(HoverEventFilter::LeftMouseDown), + RightMouseDown => Some(HoverEventFilter::RightMouseDown), + MiddleMouseDown => Some(HoverEventFilter::MiddleMouseDown), + MouseUp => Some(HoverEventFilter::MouseUp), + LeftMouseUp => Some(HoverEventFilter::LeftMouseUp), + RightMouseUp => Some(HoverEventFilter::RightMouseUp), + MiddleMouseUp => Some(HoverEventFilter::MiddleMouseUp), + Scroll => Some(HoverEventFilter::Scroll), + TextInput => Some(HoverEventFilter::TextInput), + VirtualKeyDown => Some(HoverEventFilter::VirtualKeyDown), + VirtualKeyUp => Some(HoverEventFilter::VirtualKeyDown), + HoveredFile => Some(HoverEventFilter::HoveredFile), + DroppedFile => Some(HoverEventFilter::DroppedFile), + HoveredFileCancelled => Some(HoverEventFilter::HoveredFileCancelled), + // MouseEnter and MouseLeave on the **window** - does not mean a mouseenter + // and a mouseleave on the hovered element + MouseEnter => None, + MouseLeave => None, + } + } +} + +/// Represents one single DOM node (node type, classes, ids and callbacks are stored here) +pub struct NodeData { + /// `div` + pub node_type: NodeType, + /// `#main #something` + pub ids: Vec, + /// `.myclass .otherclass` + pub classes: Vec, + /// `On::MouseUp` -> `Callback(my_button_click_handler)` + pub callbacks: Vec<(EventFilter, Callback)>, + /// Usually not set by the user directly - `FakeWindow::add_default_callback` + /// returns a callback ID, so that we know which default callback(s) are attached + /// to this node. + /// + /// This is only important if this node has any default callbacks. + pub default_callback_ids: Vec<(EventFilter, DefaultCallbackId)>, + /// Override certain dynamic styling properties in this frame. For this, + /// these properties have to have a name (the ID). + /// + /// For example, in the CSS stylesheet: + /// + /// ```css,ignore + /// #my_item { width: [[ my_custom_width | 200px ]] } + /// ``` + /// + /// ```rust,ignore + /// let node = NodeData { + /// id: Some("my_item".into()), + /// dynamic_css_overrides: vec![("my_custom_width".into(), CssProperty::Width(LayoutWidth::px(500.0)))] + /// } + /// ``` + pub dynamic_css_overrides: Vec<(DomString, CssProperty)>, + /// Whether this div can be dragged or not, similar to `draggable = "true"` in HTML, . + /// + /// **TODO**: Currently doesn't do anything, since the drag & drop implementation is missing, API stub. + pub is_draggable: bool, + /// Whether this div can be focused, and if yes, in what default to `None` (not focusable). + /// Note that without this, there can be no `On::FocusReceived` (equivalent to onfocus), + /// `On::FocusLost` (equivalent to onblur), etc. events. + pub tab_index: Option, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)] +pub enum TabIndex { + /// Automatic tab index, similar to simply setting `focusable = "true"` or `tabindex = 0` + /// (both have the effect of making the element focusable). + /// + /// Sidenote: See https://www.w3.org/TR/html5/editing.html#sequential-focus-navigation-and-the-tabindex-attribute + /// for interesting notes on tabindex and accessibility + Auto, + /// Set the tab index in relation to its parent element. I.e. if you have a list of elements, + /// the focusing order is restricted to the current parent. + /// + /// Ex. a div might have: + /// + /// ```no_run,ignore + /// div (Auto) + /// |- element1 (OverrideInParent 0) <- current focus + /// |- element2 (OverrideInParent 5) + /// |- element3 (OverrideInParent 2) + /// |- element4 (Global 5) + /// ``` + /// + /// When pressing tab repeatedly, the focusing order will be + /// "element3, element2, element4, div", since OverrideInParent elements + /// take precedence among global order. + OverrideInParent(usize), + /// Elements can be focused in callbacks, but are not accessible via + /// keyboard / tab navigation (-1) + NoKeyboardFocus, +} + +impl TabIndex { + /// Returns the HTML-compatible number of the `tabindex` element + pub fn get_index(&self) -> isize { + use self::TabIndex::*; + match self { + Auto => 0, + OverrideInParent(x) => *x as isize, + NoKeyboardFocus => -1, + } + } +} +impl Default for TabIndex { + fn default() -> Self { + TabIndex::Auto + } +} + +impl PartialEq for NodeData { + fn eq(&self, other: &Self) -> bool { + self.node_type == other.node_type && + self.ids == other.ids && + self.classes == other.classes && + self.callbacks == other.callbacks && + self.default_callback_ids == other.default_callback_ids && + self.dynamic_css_overrides == other.dynamic_css_overrides && + self.is_draggable == other.is_draggable && + self.tab_index == other.tab_index + } +} + +impl Eq for NodeData { } + +impl Default for NodeData { + fn default() -> Self { + NodeData::new(NodeType::Div) + } +} + +impl Hash for NodeData { + fn hash(&self, state: &mut H) { + self.node_type.hash(state); + for id in &self.ids { + id.hash(state); + } + for class in &self.classes { + class.hash(state); + } + for callback in &self.callbacks { + callback.hash(state); + } + for default_callback_id in &self.default_callback_ids { + default_callback_id.hash(state); + } + for dynamic_css_override in &self.dynamic_css_overrides { + dynamic_css_override.hash(state); + } + self.is_draggable.hash(state); + self.tab_index.hash(state); + } +} + +impl Clone for NodeData { + fn clone(&self) -> Self { + Self { + node_type: self.node_type.clone(), + ids: self.ids.clone(), + classes: self.classes.clone(), + callbacks: self.callbacks.clone(), + default_callback_ids: self.default_callback_ids.clone(), + dynamic_css_overrides: self.dynamic_css_overrides.clone(), + is_draggable: self.is_draggable.clone(), + tab_index: self.tab_index.clone(), + } + } +} + +impl fmt::Display for NodeData { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + + let html_type = self.node_type.get_path(); + let text_content = self.node_type.get_text_content(); + + let id_string = if self.ids.is_empty() { + String::new() + } else { + format!(" id=\"{}\"", self.ids.iter().map(|s| s.as_str().to_string()).collect::>().join(" ")) + }; + + let class_string = if self.classes.is_empty() { + String::new() + } else { + format!(" class=\"{}\"", self.classes.iter().map(|s| s.as_str().to_string()).collect::>().join(" ")) + }; + + let draggable = if self.is_draggable { + format!(" draggable=\"true\"") + } else { + String::new() + }; + + let tabindex = if let Some(tab_index) = self.tab_index { + format!(" tabindex=\"{}\"", tab_index.get_index()) + } else { + String::new() + }; + + let callbacks = if self.callbacks.is_empty() { + String::new() + } else { + format!(" callbacks=\"{}\"", self.callbacks.iter().map(|(evt, cb)| format!("({:?}={:?})", evt, cb)).collect::>().join(" ")) + }; + + let default_callbacks = if self.default_callback_ids.is_empty() { + String::new() + } else { + format!(" default-callbacks=\"{}\"", self.default_callback_ids.iter().map(|(evt, cb)| format!("({:?}={:?})", evt, cb)).collect::>().join(" ")) + }; + + let css_overrides = if self.dynamic_css_overrides.is_empty() { + String::new() + } else { + format!(" css-overrides=\"{}\"", self.dynamic_css_overrides.iter().map(|(id, prop)| format!("{}={:?};", id, prop)).collect::>().join(" ")) + }; + + if let Some(content) = text_content { + write!(f, "<{}{}{}{}{}{}{}{}>{}", + html_type, id_string, class_string, tabindex, draggable, callbacks, default_callbacks, css_overrides, content, html_type + ) + } else { + write!(f, "<{}{}{}{}{}{}{}{}/>", + html_type, id_string, class_string, tabindex, draggable, callbacks, default_callbacks, css_overrides, + ) + } + } +} + +impl fmt::Debug for NodeData { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, + "NodeData {{ \ + \tnode_type: {:?}, \ + \tids: {:?}, \ + \tclasses: {:?}, \ + \tcallbacks: {:?}, \ + \tdefault_callback_ids: {:?}, \ + \tdynamic_css_overrides: {:?}, \ + \tis_draggable: {:?}, \ + \ttab_index: {:?}, \ + }}", + self.node_type, + self.ids, + self.classes, + self.callbacks, + self.default_callback_ids, + self.dynamic_css_overrides, + self.is_draggable, + self.tab_index, + ) + } +} + +impl NodeData { + + /// Creates a new `NodeData` instance from a given `NodeType` + /// + /// TODO: promote to const fn once `const_vec_new` is stable! + #[inline] + pub fn new(node_type: NodeType) -> Self { + Self { + node_type, + ids: Vec::new(), + classes: Vec::new(), + callbacks: Vec::new(), + default_callback_ids: Vec::new(), + dynamic_css_overrides: Vec::new(), + is_draggable: false, + tab_index: None, + } + } + + /// Checks whether this node is of the given node type (div, image, text) + #[inline] + pub fn is_node_type(&self, searched_type: NodeType) -> bool { + self.node_type == searched_type + } + + /// Checks whether this node has the searched ID attached + pub fn has_id(&self, id: &str) -> bool { + self.ids.iter().any(|self_id| self_id.equals_str(id)) + } + + /// Checks whether this node has the searched class attached + pub fn has_class(&self, class: &str) -> bool { + self.classes.iter().any(|self_class| self_class.equals_str(class)) + } + + pub(crate) fn calculate_node_data_hash(&self) -> DomHash { + + // Pick hash algorithm based on features + #[cfg(feature = "faster-hashing")] + use twox_hash::XxHash as HashAlgorithm; + #[cfg(not(feature = "faster-hashing"))] + use std::collections::hash_map::DefaultHasher as HashAlgorithm; + + let mut hasher = HashAlgorithm::default(); + self.hash(&mut hasher); + + DomHash(hasher.finish()) + } +} + +/// Most strings are known at compile time, spares a bit of +/// heap allocations - for `&'static str`, simply stores the pointer, +/// instead of converting it into a String. This is good for class names +/// or IDs, whose content rarely changes. +#[derive(Debug, Clone)] +pub enum DomString { + Static(&'static str), + Heap(String), +} + +impl Eq for DomString { } + +impl PartialEq for DomString { + fn eq(&self, other: &Self) -> bool { + self.as_str() == other.as_str() + } +} + +impl PartialOrd for DomString { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.as_str().cmp(other.as_str())) + } +} + +impl Ord for DomString { + fn cmp(&self, other: &Self) -> CmpOrdering { + self.as_str().cmp(other.as_str()) + } +} + +impl Hash for DomString { + fn hash(&self, state: &mut H) { + self.as_str().hash(state); + } +} + +impl DomString { + + pub fn equals_str(&self, target: &str) -> bool { + use self::DomString::*; + match &self { + Static(s) => *s == target, + Heap(h) => h == target, + } + } + + pub fn as_str(&self) -> &str { + use self::DomString::*; + match &self { + Static(s) => s, + Heap(h) => h, + } + } +} + +impl_display!{ DomString, { + Static(s) => format!("{}", s), + Heap(h) => h, +}} + +type StaticString = &'static str; +impl_from!(String, DomString::Heap); +impl_from!(StaticString, DomString::Static); + +/// The document model, similar to HTML. This is a create-only structure, you don't actually read anything back +pub struct Dom { + pub(crate) arena: Arena>, + pub(crate) root: NodeId, + pub(crate) head: NodeId, +} + +impl Clone for Dom { + fn clone(&self) -> Self { + Dom { + arena: self.arena.clone(), + root: self.root.clone(), + head: self.head.clone(), + } + } +} + +impl PartialEq for Dom { + fn eq(&self, rhs: &Self) -> bool { + self.arena == rhs.arena && + self.root == rhs.root && + self.head == rhs.head + } +} + +impl Eq for Dom { } + +impl fmt::Debug for Dom { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, + "Dom {{ arena: {:?}, root: {:?}, head: {:?} }}", + self.arena, + self.root, + self.head) + } +} + +impl FromIterator> for Dom { + fn from_iter>>(iter: I) -> Self { + let mut c = Dom::new(NodeType::Div); + for i in iter { + c.add_child(i); + } + c + } +} + +impl FromIterator> for Dom { + fn from_iter>>(iter: I) -> Self { + + // We have to use a "root" node, otherwise we run into problems if + // the iterator executes 0 times (and therefore pushes 0 nodes) + + // "Root" node of this DOM + let mut node_data = vec![NodeData::new(NodeType::Div)]; + let mut node_layout = vec![Node { + parent: None, + previous_sibling: None, + next_sibling: None, + last_child: None, + first_child: None, + }]; + + let mut idx = 0; + + for item in iter { + let node = Node { + parent: Some(NodeId::new(0)), + previous_sibling: if idx == 0 { None } else { Some(NodeId::new(idx)) }, + next_sibling: Some(NodeId::new(idx + 2)), + last_child: None, + first_child: None, + }; + node_layout.push(node); + node_data.push(item); + + idx += 1; + } + + let nodes_len = node_layout.len(); + + // nodes_len is always at least 1, since we pushed the original root node + // Check if there is a child DOM + if nodes_len > 1 { + if let Some(last) = node_layout.get_mut(nodes_len - 1) { + last.next_sibling = None; + } + node_layout[0].last_child = Some(NodeId::new(nodes_len - 1)); + node_layout[0].first_child = Some(NodeId::new(1)); + } + + Dom { + head: NodeId::new(0), + root: NodeId::new(0), + arena: Arena { + node_data: NodeDataContainer::new(node_data), + node_layout: NodeHierarchy::new(node_layout), + }, + } + } +} + +impl FromIterator> for Dom { + fn from_iter>>(iter: I) -> Self { + iter.into_iter().map(|i| NodeData { node_type: i, .. Default::default() }).collect() + } +} + +impl Arena> { + /// TODO: promote to const fn once `const_vec_new` is stable + fn init_with_node_data(node_data: NodeData) -> Self { + use id_tree::ROOT_NODE; + Arena { + node_layout: NodeHierarchy { internal: vec![ROOT_NODE] }, + node_data: NodeDataContainer { internal: vec![node_data] }, + } + } +} + +impl Dom { + + /// Creates an empty DOM with a give `NodeType`. Note: This is a `const fn` and + /// doesn't allocate, it only allocates once you add at least one child node. + /// + /// TODO: promote to const fn once `const_vec_new` is stable + #[inline] + pub fn new(node_type: NodeType) -> Self { + use id_tree::ROOT_NODE_ID; + let node_data = NodeData::new(node_type); // not const fn yet + let arena = Arena::init_with_node_data(node_data); // not const fn yet + Self { + arena, + root: ROOT_NODE_ID, + head: ROOT_NODE_ID, + } + } + + /// Creates an empty DOM with space reserved for `cap` nodes + #[inline] + pub fn with_capacity(node_type: NodeType, cap: usize) -> Self { + let mut arena = Arena::with_capacity(cap.saturating_add(1)); + let root = arena.new_node(NodeData::new(node_type)); + Self { + arena: arena, + root: root, + head: root, + } + } + + /// Shorthand for `Dom::new(NodeType::Div)`. + #[inline] + pub fn div() -> Self { + Self::new(NodeType::Div) + } + + /// Shorthand for `Dom::new(NodeType::Label(value.into()))` + #[inline] + pub fn label>(value: S) -> Self { + Self::new(NodeType::Label(value.into())) + } + + /// Shorthand for `Dom::new(NodeType::Text(text_id))` + #[inline] + pub fn text_id(text_id: TextId) -> Self { + Self::new(NodeType::Text(text_id)) + } + + /// Shorthand for `Dom::new(NodeType::Image(image_id))` + #[inline] + pub fn image(image: ImageId) -> Self { + Self::new(NodeType::Image(image)) + } + + /// Shorthand for `Dom::new(NodeType::GlTexture((callback, ptr)))` + #[inline] + pub fn gl_texture(callback: GlTextureCallback, ptr: StackCheckedPointer) -> Self { + Self::new(NodeType::GlTexture((callback, ptr))) + } + + /// Shorthand for `Dom::new(NodeType::IFrame((callback, ptr)))` + #[inline] + pub fn iframe(callback: IFrameCallback, ptr: StackCheckedPointer) -> Self { + Self::new(NodeType::IFrame((callback, ptr))) + } + + /// Parses and loads a DOM from an XML string + #[inline] + pub fn from_xml(xml: &str, component_map: &mut XmlComponentMap) -> Result { + xml::str_to_dom(xml, component_map) + } + + #[cfg(test)] + pub fn mock_from_xml(xml: &str) -> Self { + let actual_xml = format!("{}", xml); + Self::from_xml(&actual_xml, &mut XmlComponentMap::default()).unwrap() + } + + /// Convenience function, only available in tests, useful for quickly writing UI tests. + /// Wraps the XML string in the required `` braces, panics if the XML couldn't be parsed. + /// + /// ## Example + /// + /// ```rust + /// # use azul::dom::Dom; + /// let dom = Dom::div().with_id("test"); + /// dom.assert_eq("
"); + /// ``` + #[cfg(test)] + pub fn assert_eq(self, xml: &str) { + let fixed = Self::div().with_child(self); + let expected = Self::mock_from_xml(xml); + if expected != fixed { + panic!("\r\nExpected DOM did not match:\r\n\r\nexpected: ----------\r\n{}\r\ngot: ----------\r\n{}\r\n", expected.debug_dump(), fixed.debug_dump()); + } + } + + /// Loads, parses and builds a DOM from an XML file - warning: Disk I/O on every + /// function call - do not use this in release builds! This function deliberately + /// never fails: In an error case, the error gets rendered as a `NodeType::Label`. + pub fn from_file>(file_path: I, component_map: &mut XmlComponentMap) -> Self { + use std::fs; + + let xml = match fs::read_to_string(file_path) { + Ok(xml) => xml, + Err(e) => return Dom::label(format!("{}", e)), + }; + + match Self::from_xml(&xml, component_map) { + Ok(o) => o, + Err(e) => Dom::label(format!("{}", e)), + } + } + + /// Returns the number of nodes in this DOM + #[inline] + pub fn len(&self) -> usize { + self.arena.len() + } + + /// Returns an immutable reference to the current HEAD of the DOM structure (the last inserted element) + #[inline] + pub fn get_head_node(&self) -> &NodeData { + &self.arena.node_data[self.head] + } + + /// Returns a mutable reference to the current HEAD of the DOM structure (the last inserted element) + #[inline] + pub fn get_head_node_mut(&mut self) -> &mut NodeData { + &mut self.arena.node_data[self.head] + } + + /// Adds a child DOM to the current DOM + pub fn add_child(&mut self, mut child: Self) { + + // Note: for a more readable Python version of this algorithm, + // see: https://gist.github.com/fschutt/4b3bd9a2654b548a6eb0b6a8623bdc8a#file-dow_new_2-py-L65-L107 + + let self_len = self.arena.len(); + let child_len = child.arena.len(); + + if child_len == 0 { + // No nodes to append, nothing to do + return; + } + + if self_len == 0 { + // Self has no nodes, therefore all child nodes will + // replace the self nodes, so + *self = child; + return; + } + + let self_arena = &mut self.arena; + let child_arena = &mut child.arena; + + let mut last_sibling = None; + + for node_id in 0..child_len { + let node_id = NodeId::new(node_id); + let node_id_child: &mut Node = &mut child_arena.node_layout[node_id]; + + // WARNING: Order of these blocks is important! + + if node_id_child.previous_sibling.as_mut().and_then(|previous_sibling| { + // Some(previous_sibling) - increase the parent ID by the current arena length + *previous_sibling += self_len; + Some(previous_sibling) + }).is_none() { + // None - set the current heads' last child as the new previous sibling + let last_child = self_arena.node_layout[self.head].last_child; + if last_child.is_some() && node_id_child.parent.is_none() { + node_id_child.previous_sibling = last_child; + self_arena.node_layout[last_child.unwrap()].next_sibling = Some(node_id + self_len); + } + } + + if node_id_child.parent.as_mut().map(|parent| { *parent += self_len; parent }).is_none() { + // Have we encountered the last root item? + if node_id_child.next_sibling.is_none() { + last_sibling = Some(node_id); + } + node_id_child.parent = Some(self.head); + } + + if let Some(next_sibling) = node_id_child.next_sibling.as_mut() { + *next_sibling += self_len; + } + + if let Some(first_child) = node_id_child.first_child.as_mut() { + *first_child += self_len; + } + + if let Some(last_child) = node_id_child.last_child.as_mut() { + *last_child += self_len; + } + } + + self_arena.node_layout[self.head].first_child.get_or_insert(NodeId::new(self_len)); + self_arena.node_layout[self.head].last_child = Some(last_sibling.unwrap() + self_len); + + (&mut *self_arena).append_arena(child_arena); + } + + /// Same as `id`, but easier to use for method chaining in a builder-style pattern + #[inline] + pub fn with_id>(mut self, id: S) -> Self { + self.add_id(id); + self + } + + /// Same as `id`, but easier to use for method chaining in a builder-style pattern + #[inline] + pub fn with_class>(mut self, class: S) -> Self { + self.add_class(class); + self + } + + /// Same as `event`, but easier to use for method chaining in a builder-style pattern + #[inline] + pub fn with_callback>(mut self, on: O, callback: Callback) -> Self { + self.add_callback(on, callback); + self + } + + #[inline] + pub fn with_child(mut self, child: Self) -> Self { + self.add_child(child); + self + } + + #[inline] + pub fn with_css_override>(mut self, id: S, property: CssProperty) -> Self { + self.add_css_override(id, property); + self + } + + #[inline] + pub fn with_tab_index(mut self, tab_index: TabIndex) -> Self { + self.set_tab_index(tab_index); + self + } + + #[inline] + pub fn is_draggable(mut self, draggable: bool) -> Self { + self.set_draggable(draggable); + self + } + + #[inline] + pub fn add_id>(&mut self, id: S) { + self.arena.node_data[self.head].ids.push(id.into()); + } + + #[inline] + pub fn add_class>(&mut self, class: S) { + self.arena.node_data[self.head].classes.push(class.into()); + } + + #[inline] + pub fn add_callback>(&mut self, on: O, callback: Callback) { + self.arena.node_data[self.head].callbacks.push((on.into(), callback)); + } + + #[inline] + pub fn add_default_callback_id>(&mut self, on: O, id: DefaultCallbackId) { + self.arena.node_data[self.head].default_callback_ids.push((on.into(), id)); + } + + #[inline] + pub fn add_css_override>(&mut self, override_id: S, property: CssProperty) { + self.arena.node_data[self.head].dynamic_css_overrides.push((override_id.into(), property)); + } + + #[inline] + pub fn set_tab_index(&mut self, tab_index: TabIndex) { + self.arena.node_data[self.head].tab_index = Some(tab_index); + } + + #[inline] + pub fn set_draggable(&mut self, draggable: bool) { + self.arena.node_data[self.head].is_draggable = draggable; + } + + /// Returns a debug formatted version of the DOM for easier debugging + pub fn debug_dump(&self) -> String { + format!("{}", self.arena.print_tree(|t| format!("{}", t))) + } + + /// The UiState contains all the tags (for hit-testing) as well as the mapping + /// from Hit-testing tags to NodeIds (which are important for filtering input events + /// and routing input events to the callbacks). + pub(crate) fn into_ui_state(self) -> UiState { + + // NOTE: Originally it was allowed to create a DOM with + // multiple root elements using `add_sibling()` and `with_sibling()`. + // + // However, it was decided to remove these functions (in commit #586933), + // as they aren't practical (you can achieve the same thing with one + // wrapper div and multiple add_child() calls) and they create problems + // when layouting elements since add_sibling() essentially modifies the + // space that the parent can distribute, which in code, simply looks weird + // and led to bugs. + // + // It is assumed that the DOM returned by the user has exactly one root node + // with no further siblings and that the root node is the Node with the ID 0. + + // All tags that have can be focused (necessary for hit-testing) + let mut tab_index_tags = BTreeMap::new(); + // All tags that have can be dragged & dropped (necessary for hit-testing) + let mut draggable_tags = BTreeMap::new(); + + // Mapping from tags to nodes (necessary so that the hit-testing can resolve the NodeId from any given tag) + let mut tag_ids_to_node_ids = BTreeMap::new(); + // Mapping from nodes to tags, reverse mapping (not used right now, may be useful in the future) + let mut node_ids_to_tag_ids = BTreeMap::new(); + // Which nodes have extra dynamic CSS overrides? + let mut dynamic_css_overrides = BTreeMap::new(); + + let mut hover_callbacks = BTreeMap::new(); + let mut hover_default_callbacks = BTreeMap::new(); + let mut focus_callbacks = BTreeMap::new(); + let mut focus_default_callbacks = BTreeMap::new(); + let mut not_callbacks = BTreeMap::new(); + let mut not_default_callbacks = BTreeMap::new(); + let mut window_callbacks = BTreeMap::new(); + let mut window_default_callbacks = BTreeMap::new(); + + // data.callbacks, HoverEventFilter, Callback, as_hover_event_filter, hover_callbacks, (optional) + macro_rules! filter_and_insert_callbacks { + ( + $node_id:ident, + $data_source:expr, + $event_filter:ident, + $callback_type:ty, + $filter_func:ident, + $final_callback_list:ident, + ) => { + let node_hover_callbacks: BTreeMap<$event_filter, $callback_type> = $data_source.iter() + .filter_map(|(event_filter, cb)| event_filter.$filter_func().map(|not_evt| (not_evt, *cb))) + .collect(); + + if !node_hover_callbacks.is_empty() { + $final_callback_list.insert($node_id, node_hover_callbacks); + } + }; + ( + $node_id:ident, + $data_source:expr, + $event_filter:ident, + $callback_type:ty, + $filter_func:ident, + $final_callback_list:ident, + $node_tag_id:ident, + ) => { + let node_hover_callbacks: BTreeMap<$event_filter, $callback_type> = $data_source.iter() + .filter_map(|(event_filter, cb)| event_filter.$filter_func().map(|not_evt| (not_evt, *cb))) + .collect(); + + if !node_hover_callbacks.is_empty() { + $final_callback_list.insert($node_id, node_hover_callbacks); + let tag_id = $node_tag_id.unwrap_or_else(|| new_tag_id()); + $node_tag_id = Some(tag_id); + } + }; + } + + // Reset the tag + TAG_ID.swap(1, Ordering::SeqCst); + + { + let arena = &self.arena; + + debug_assert!(arena.node_layout[NodeId::new(0)].next_sibling.is_none()); + + for node_id in arena.linear_iter() { + + let node = &arena.node_data[node_id]; + + let mut node_tag_id = None; + + // Optimization since on most nodes, the callbacks will be empty + if !node.callbacks.is_empty() { + + // Filter and insert HoverEventFilter callbacks + filter_and_insert_callbacks!( + node_id, + node.callbacks, + HoverEventFilter, + Callback, + as_hover_event_filter, + hover_callbacks, + node_tag_id, + ); + + // Filter and insert FocusEventFilter callbacks + filter_and_insert_callbacks!( + node_id, + node.callbacks, + FocusEventFilter, + Callback, + as_focus_event_filter, + focus_callbacks, + node_tag_id, + ); + + filter_and_insert_callbacks!( + node_id, + node.callbacks, + NotEventFilter, + Callback, + as_not_event_filter, + not_callbacks, + node_tag_id, + ); + + filter_and_insert_callbacks!( + node_id, + node.callbacks, + WindowEventFilter, + Callback, + as_window_event_filter, + window_callbacks, + ); + } + + if !node.default_callback_ids.is_empty() { + + // Filter and insert HoverEventFilter callbacks + filter_and_insert_callbacks!( + node_id, + node.default_callback_ids, + HoverEventFilter, + DefaultCallbackId, + as_hover_event_filter, + hover_default_callbacks, + node_tag_id, + ); + + // Filter and insert FocusEventFilter callbacks + filter_and_insert_callbacks!( + node_id, + node.default_callback_ids, + FocusEventFilter, + DefaultCallbackId, + as_focus_event_filter, + focus_default_callbacks, + node_tag_id, + ); + + filter_and_insert_callbacks!( + node_id, + node.default_callback_ids, + NotEventFilter, + DefaultCallbackId, + as_not_event_filter, + not_default_callbacks, + node_tag_id, + ); + + filter_and_insert_callbacks!( + node_id, + node.default_callback_ids, + WindowEventFilter, + DefaultCallbackId, + as_window_event_filter, + window_default_callbacks, + ); + } + + if node.is_draggable { + let tag_id = node_tag_id.unwrap_or_else(|| new_tag_id()); + draggable_tags.insert(tag_id, node_id); + node_tag_id = Some(tag_id); + } + + if let Some(tab_index) = node.tab_index { + let tag_id = node_tag_id.unwrap_or_else(|| new_tag_id()); + tab_index_tags.insert(tag_id, (node_id, tab_index)); + node_tag_id = Some(tag_id); + } + + if let Some(tag_id) = node_tag_id { + tag_ids_to_node_ids.insert(tag_id, node_id); + node_ids_to_tag_ids.insert(node_id, tag_id); + } + + // Collect all the styling overrides into one hash map + if !node.dynamic_css_overrides.is_empty() { + dynamic_css_overrides.insert(node_id, node.dynamic_css_overrides.iter().cloned().collect()); + } + } + } + + UiState { + + dom: self, + dynamic_css_overrides, + tag_ids_to_hover_active_states: BTreeMap::new(), + + tab_index_tags, + draggable_tags, + node_ids_to_tag_ids, + tag_ids_to_node_ids, + + hover_callbacks, + hover_default_callbacks, + focus_callbacks, + focus_default_callbacks, + not_callbacks, + not_default_callbacks, + window_callbacks, + window_default_callbacks, + + } + } +} + +#[test] +fn test_dom_sibling_1() { + + struct TestLayout; + + let dom: Dom = + Dom::new(NodeType::Div) + .with_child( + Dom::new(NodeType::Div) + .with_id("sibling-1") + .with_child(Dom::new(NodeType::Div) + .with_id("sibling-1-child-1"))) + .with_child(Dom::new(NodeType::Div) + .with_id("sibling-2") + .with_child(Dom::new(NodeType::Div) + .with_id("sibling-2-child-1"))); + + let arena = &dom.arena; + + assert_eq!(NodeId::new(0), dom.root); + + assert_eq!(vec![DomString::Static("sibling-1")], + arena.node_data[ + arena.node_layout[dom.root] + .first_child.expect("root has no first child") + ].ids); + + assert_eq!(vec![DomString::Static("sibling-2")], + arena.node_data[ + arena.node_layout[ + arena.node_layout[dom.root] + .first_child.expect("root has no first child") + ].next_sibling.expect("root has no second sibling") + ].ids); + + assert_eq!(vec![DomString::Static("sibling-1-child-1")], + arena.node_data[ + arena.node_layout[ + arena.node_layout[dom.root] + .first_child.expect("root has no first child") + ].first_child.expect("first child has no first child") + ].ids); + + assert_eq!(vec![DomString::Static("sibling-2-child-1")], + arena.node_data[ + arena.node_layout[ + arena.node_layout[ + arena.node_layout[dom.root] + .first_child.expect("root has no first child") + ].next_sibling.expect("first child has no second sibling") + ].first_child.expect("second sibling has no first child") + ].ids); +} + +#[test] +fn test_dom_from_iter_1() { + + use id_tree::Node; + + struct TestLayout; + + let dom: Dom = (0..5).map(|e| NodeData::new(NodeType::Label(format!("{}", e + 1).into()))).collect(); + let arena = &dom.arena; + + // We need to have 6 nodes: + // + // root NodeId(0) + // |-> 1 NodeId(1) + // |-> 2 NodeId(2) + // |-> 3 NodeId(3) + // |-> 4 NodeId(4) + // '-> 5 NodeId(5) + + assert_eq!(arena.len(), 6); + + // Check root node + assert_eq!(arena.node_layout.get(NodeId::new(0)), Some(&Node { + parent: None, + previous_sibling: None, + next_sibling: None, + first_child: Some(NodeId::new(1)), + last_child: Some(NodeId::new(5)), + })); + assert_eq!(arena.node_data.get(NodeId::new(0)), Some(&NodeData::new(NodeType::Div))); + + assert_eq!(arena.node_layout.get(NodeId::new(arena.node_layout.len() - 1)), Some(&Node { + parent: Some(NodeId::new(0)), + previous_sibling: Some(NodeId::new(4)), + next_sibling: None, + first_child: None, + last_child: None, + })); + + assert_eq!(arena.node_data.get(NodeId::new(arena.node_data.len() - 1)), Some(&NodeData { + node_type: NodeType::Label(DomString::Heap(String::from("5"))), + .. Default::default() + })); +} + +/// Test that there shouldn't be a DOM that has 0 nodes +#[test] +fn test_zero_size_dom() { + + struct TestLayout; + + let mut null_dom: Dom = (0..0).map(|_| NodeData::default()).collect(); + + assert!(null_dom.arena.len() == 1); + + null_dom.add_class("hello"); // should not panic + null_dom.add_id("id-hello"); // should not panic +} diff --git a/azul/src/error.rs b/azul/src/error.rs new file mode 100644 index 000000000..ebd610812 --- /dev/null +++ b/azul/src/error.rs @@ -0,0 +1,39 @@ +pub use { + app::RuntimeError, + app_resources::{ImageReloadError, FontReloadError}, + widgets::errors::*, + window::WindowCreateError, +}; +// TODO: re-export the sub-types of ClipboardError! +pub use clipboard2::ClipboardError; + +#[derive(Debug)] +pub enum Error { + Resource(ResourceReloadError), + Clipboard(ClipboardError), + WindowCreate(WindowCreateError), +} + +impl_from!(ResourceReloadError, Error::Resource); +impl_from!(ClipboardError, Error::Clipboard); +impl_from!(WindowCreateError, Error::WindowCreate); + +#[derive(Debug)] +pub enum ResourceReloadError { + Image(ImageReloadError), + Font(FontReloadError), +} + +impl_from!(ImageReloadError, ResourceReloadError::Image); +impl_from!(FontReloadError, ResourceReloadError::Font); + +impl_display!(ResourceReloadError, { + Image(e) => format!("Failed to load image: {}", e), + Font(e) => format!("Failed to load font: {}", e), +}); + +impl_display!(Error, { + Resource(e) => format!("{}", e), + Clipboard(e) => format!("Clipboard error: {}", e), + WindowCreate(e) => format!("Window creation error: {}", e), +}); diff --git a/azul/src/id_tree.rs b/azul/src/id_tree.rs new file mode 100644 index 000000000..09ccc165d --- /dev/null +++ b/azul/src/id_tree.rs @@ -0,0 +1,667 @@ +use std::{ + ops::{Index, IndexMut}, + slice::{Iter, IterMut}, +}; +use dom::NodeData; + +pub use self::node_id::NodeId; + +// Since private fields are module-based, this prevents any module from accessing +// `NodeId.index` directly. To get the correct node index is by using `NodeId::index()`, +// which subtracts 1 from the ID (because of Option optimizations) +mod node_id { + + use std::{ + fmt, + num::NonZeroUsize, + ops::{Add, AddAssign}, + }; + + pub(crate) const ROOT_NODE_ID: NodeId = NodeId { index: unsafe { NonZeroUsize::new_unchecked(1) } }; + + /// A node identifier within a particular `Arena`. + #[derive(Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)] + pub struct NodeId { + index: NonZeroUsize, + } + + impl NodeId { + /// **NOTE**: In debug mode, it panics on overflow, since having a + /// pointer that is zero is undefined behaviour (it would basically be + /// cast to a `None`), which is incorrect, so we rather panic on overflow + /// to prevent that. + /// + /// To trigger an overflow however, you'd need more that 4 billion DOM nodes - + /// it is more likely that you run out of RAM before you do that. The only thing + /// that could lead to an overflow would be a bug. Therefore, overflow-checking is + /// disabled in release mode. + #[inline(always)] + pub(crate) fn new(value: usize) -> Self { + NodeId { index: unsafe { NonZeroUsize::new_unchecked(value + 1) } } + } + + #[inline(always)] + pub fn index(&self) -> usize { + self.index.get() - 1 + } + } + + impl Add for NodeId { + type Output = NodeId; + #[inline(always)] + fn add(self, other: usize) -> NodeId { + NodeId::new(self.index() + other) + } + } + + impl AddAssign for NodeId { + #[inline(always)] + fn add_assign(&mut self, other: usize) { + *self = *self + other; + } + } + + impl fmt::Display for NodeId { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.index()) + } + } + + impl fmt::Debug for NodeId { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "NodeId({})", self.index()) + } + } +} + +/// Hierarchical information about a node (stores the indicies of the parent / child nodes). +#[derive(Debug, Default, Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)] +pub struct Node { + pub parent: Option, + pub previous_sibling: Option, + pub next_sibling: Option, + pub first_child: Option, + pub last_child: Option, +} + +pub(crate) use self::node_id::ROOT_NODE_ID; + +// Node that initializes a Dom +pub(crate) const ROOT_NODE: Node = Node { + parent: None, + previous_sibling: None, + next_sibling: None, + first_child: None, + last_child: None, +}; + +impl Node { + #[inline] + pub fn has_parent(&self) -> bool { self.parent.is_some() } + #[inline] + pub fn has_previous_sibling(&self) -> bool { self.previous_sibling.is_some() } + #[inline] + pub fn has_next_sibling(&self) -> bool { self.next_sibling.is_some() } + #[inline] + pub fn has_first_child(&self) -> bool { self.first_child.is_some() } + #[inline] + pub fn has_last_child(&self) -> bool { self.last_child.is_some() } +} + +#[derive(Debug, Default, Clone, PartialEq, Hash, Eq)] +pub struct Arena { + pub(crate) node_layout: NodeHierarchy, + pub(crate) node_data: NodeDataContainer, +} + +/// The hierarchy of nodes is stored separately from the actual node content in order +/// to save on memory, since the hierarchy can be re-used across several DOM trees even +/// if the content changes. +#[derive(Debug, Default, Clone, PartialEq, Hash, Eq)] +pub struct NodeHierarchy { + pub(crate) internal: Vec, +} + +impl NodeHierarchy { + + #[inline] + pub const fn new(data: Vec) -> Self { + Self { + internal: data, + } + } + + #[inline] + pub fn len(&self) -> usize { + self.internal.len() + } + + #[inline] + pub fn get(&self, id: NodeId) -> Option<&Node> { + self.internal.get(id.index()) + } + + #[inline] + pub fn linear_iter(&self) -> LinearIterator { + LinearIterator { + arena_len: self.len(), + position: 0, + } + } + + /// Returns the `(depth, NodeId)` of all parent nodes (i.e. nodes that have a + /// `first_child`), in depth sorted order, (i.e. `NodeId(0)` with a depth of 0) is + /// the first element. + /// + /// Runtime: O(n) max + pub fn get_parents_sorted_by_depth(&self) -> Vec<(usize, NodeId)> { + + let mut non_leaf_nodes = Vec::new(); + let mut current_children = vec![(0, NodeId::new(0))]; + let mut next_children = Vec::new(); + let mut depth = 1; + + loop { + + for id in ¤t_children { + for child_id in id.1.children(self).filter(|id| self[*id].first_child.is_some()) { + next_children.push((depth, child_id)); + } + } + + non_leaf_nodes.extend(&mut current_children.drain(..)); + + if next_children.is_empty() { + break; + } else { + current_children.extend(&mut next_children.drain(..)); + depth += 1; + } + } + + non_leaf_nodes + } + + /// Returns the index in the parent node of a certain NodeId + /// (starts at 0, i.e. the first node has the index of 0). + pub fn get_index_in_parent(&self, node_id: NodeId) -> usize { + node_id.preceding_siblings(&self).count() - 1 + } +} + +#[derive(Debug, Default, Clone, PartialEq, Hash, Eq)] +pub struct NodeDataContainer { + pub(crate) internal: Vec, +} + +impl Index for NodeHierarchy { + type Output = Node; + + #[inline] + fn index(&self, node_id: NodeId) -> &Node { + unsafe { self.internal.get_unchecked(node_id.index()) } + } +} + +impl IndexMut for NodeHierarchy { + + #[inline] + fn index_mut(&mut self, node_id: NodeId) -> &mut Node { + unsafe { self.internal.get_unchecked_mut(node_id.index()) } + } +} + +impl NodeDataContainer { + + #[inline] + pub const fn new(data: Vec) -> Self { + Self { internal: data } + } + + pub fn len(&self) -> usize { self.internal.len() } + + pub fn transform(&self, closure: F) -> NodeDataContainer where F: Fn(&T, NodeId) -> U { + // TODO if T: Send (which is usually the case), then we could use rayon here! + NodeDataContainer { + internal: self.internal.iter().enumerate().map(|(node_id, node)| closure(node, NodeId::new(node_id))).collect(), + } + } + + pub fn get(&self, id: NodeId) -> Option<&T> { + self.internal.get(id.index()) + } + + pub fn iter(&self) -> Iter { + self.internal.iter() + } + + pub fn iter_mut(&mut self) -> IterMut { + self.internal.iter_mut() + } + + pub fn linear_iter(&self) -> LinearIterator { + LinearIterator { + arena_len: self.len(), + position: 0, + } + } +} + +impl Index for NodeDataContainer { + type Output = T; + + #[inline] + fn index(&self, node_id: NodeId) -> &T { + unsafe { self.internal.get_unchecked(node_id.index()) } + } +} + +impl IndexMut for NodeDataContainer { + + #[inline] + fn index_mut(&mut self, node_id: NodeId) -> &mut T { + unsafe { self.internal.get_unchecked_mut(node_id.index()) } + } +} + +impl Arena { + + #[inline] + pub fn new() -> Arena { + // NOTE: This is a separate function, since Vec::new() is a const fn (so this function doesn't allocate) + Arena { + node_layout: NodeHierarchy { internal: Vec::new() }, + node_data: NodeDataContainer { internal: Vec::::new() }, + } + } + + #[inline] + pub fn with_capacity(cap: usize) -> Arena { + Arena { + node_layout: NodeHierarchy { internal: Vec::with_capacity(cap) }, + node_data: NodeDataContainer { internal: Vec::::with_capacity(cap) }, + } + } + + /// Create a new node from its associated data. + #[inline] + pub(crate) fn new_node(&mut self, data: T) -> NodeId { + let next_index = self.node_layout.len(); + self.node_layout.internal.push(Node { + parent: None, + first_child: None, + last_child: None, + previous_sibling: None, + next_sibling: None, + }); + self.node_data.internal.push(data); + NodeId::new(next_index) + } + + // Returns how many nodes there are in the arena + #[inline] + pub fn len(&self) -> usize { + self.node_layout.len() + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Return an iterator over the indices in the internal arenas Vec + #[inline] + pub fn linear_iter(&self) -> LinearIterator { + LinearIterator { + arena_len: self.node_layout.len(), + position: 0, + } + } + + /// Appends another arena to the end of the current arena + /// (by simply appending the two Vec of nodes) + /// Can potentially mess up internal IDs, only use this if you + /// know what you're doing + #[inline] + pub fn append_arena(&mut self, other: &mut Arena) { + self.node_layout.internal.append(&mut other.node_layout.internal); + self.node_data.internal.append(&mut other.node_data.internal); + } + + /// Transform keeps the relative order of parents / children + /// but transforms an Arena into an Arena, by running the closure on each of the + /// items. The `NodeId` for the root is then valid for the newly created `Arena`, too. + #[inline] + pub(crate) fn transform(&self, closure: F) -> Arena where F: Fn(&T, NodeId) -> U { + // TODO if T: Send (which is usually the case), then we could use rayon here! + Arena { + node_layout: self.node_layout.clone(), + node_data: self.node_data.transform(closure), + } + } +} + +impl Arena> { + + /// Prints the debug version of the arena, without printing the actual arena + pub(crate) fn print_tree) -> String + Copy>(&self, format_cb: F) -> String { + let mut s = String::new(); + if self.len() > 0 { + self.print_tree_recursive(format_cb, &mut s, NodeId::new(0), 0); + } + s + } + + fn print_tree_recursive) -> String + Copy>(&self, format_cb: F, string: &mut String, current_node_id: NodeId, indent: usize) { + let node = &self.node_layout[current_node_id]; + let tabs = String::from(" ").repeat(indent); + string.push_str(&format!("{}{}\n", tabs, format_cb(&self.node_data[current_node_id]))); + + if let Some(first_child) = node.first_child { + self.print_tree_recursive(format_cb, string, first_child, indent + 1); + if node.last_child.is_some() { + string.push_str(&format!("{}\n", tabs, self.node_data[current_node_id].node_type.get_path())); + } + } + + if let Some(next_sibling) = node.next_sibling { + self.print_tree_recursive(format_cb, string, next_sibling, indent); + } + } +} + +impl NodeId { + + /// Return an iterator of references to this node and its ancestors. + /// + /// Call `.next().unwrap()` once on the iterator to skip the node itself. + #[inline] + pub const fn ancestors(self, node_layout: &NodeHierarchy) -> Ancestors { + Ancestors { + node_layout, + node: Some(self), + } + } + + /// Return an iterator of references to this node and the siblings before it. + /// + /// Call `.next().unwrap()` once on the iterator to skip the node itself. + #[inline] + pub const fn preceding_siblings(self, node_layout: &NodeHierarchy) -> PrecedingSiblings { + PrecedingSiblings { + node_layout, + node: Some(self), + } + } + + /// Return an iterator of references to this node and the siblings after it. + /// + /// Call `.next().unwrap()` once on the iterator to skip the node itself. + #[inline] + pub const fn following_siblings(self, node_layout: &NodeHierarchy) -> FollowingSiblings { + FollowingSiblings { + node_layout, + node: Some(self), + } + } + + /// Return an iterator of references to this node’s children. + #[inline] + pub fn children(self, node_layout: &NodeHierarchy) -> Children { + Children { + node_layout, + node: node_layout[self].first_child, + } + } + + /// Return an iterator of references to this node’s children, in reverse order. + #[inline] + pub fn reverse_children(self, node_layout: &NodeHierarchy) -> ReverseChildren { + ReverseChildren { + node_layout, + node: node_layout[self].last_child, + } + } + + /// Return an iterator of references to this node and its descendants, in tree order. + /// + /// Parent nodes appear before the descendants. + /// Call `.next().unwrap()` once on the iterator to skip the node itself. + #[inline] + pub const fn descendants(self, node_layout: &NodeHierarchy) -> Descendants { + Descendants(self.traverse(node_layout)) + } + + /// Return an iterator of references to this node and its descendants, in tree order. + #[inline] + pub const fn traverse(self, node_layout: &NodeHierarchy) -> Traverse { + Traverse { + node_layout, + root: self, + next: Some(NodeEdge::Start(self)), + } + } + + /// Return an iterator of references to this node and its descendants, in tree order. + #[inline] + pub const fn reverse_traverse(self, node_layout: &NodeHierarchy) -> ReverseTraverse { + ReverseTraverse { + node_layout, + root: self, + next: Some(NodeEdge::End(self)), + } + } +} + + +macro_rules! impl_node_iterator { + ($name: ident, $next: expr) => { + impl<'a> Iterator for $name<'a> { + type Item = NodeId; + + fn next(&mut self) -> Option { + match self.node.take() { + Some(node) => { + self.node = $next(&self.node_layout[node]); + Some(node) + } + None => None + } + } + } + } +} + +/// An linear iterator, does not respect the DOM in any way, +/// it just iterates over the nodes like a Vec +#[derive(Debug, Copy, Clone)] +pub struct LinearIterator { + arena_len: usize, + position: usize, +} + +impl Iterator for LinearIterator { + type Item = NodeId; + + fn next(&mut self) -> Option { + if self.arena_len < 1 || self.position > (self.arena_len - 1){ + None + } else { + let new_id = Some(NodeId::new(self.position)); + self.position += 1; + new_id + } + } +} + +/// An iterator of references to the ancestors a given node. +pub struct Ancestors<'a> { + node_layout: &'a NodeHierarchy, + node: Option, +} + +impl_node_iterator!(Ancestors, |node: &Node| node.parent); + +/// An iterator of references to the siblings before a given node. +pub struct PrecedingSiblings<'a> { + node_layout: &'a NodeHierarchy, + node: Option, +} + +impl_node_iterator!(PrecedingSiblings, |node: &Node| node.previous_sibling); + +/// An iterator of references to the siblings after a given node. +pub struct FollowingSiblings<'a> { + node_layout: &'a NodeHierarchy, + node: Option, +} + +impl_node_iterator!(FollowingSiblings, |node: &Node| node.next_sibling); + +/// An iterator of references to the children of a given node. +pub struct Children<'a> { + node_layout: &'a NodeHierarchy, + node: Option, +} + +impl_node_iterator!(Children, |node: &Node| node.next_sibling); + +/// An iterator of references to the children of a given node, in reverse order. +pub struct ReverseChildren<'a> { + node_layout: &'a NodeHierarchy, + node: Option, +} + +impl_node_iterator!(ReverseChildren, |node: &Node| node.previous_sibling); + +/// An iterator of references to a given node and its descendants, in tree order. +pub struct Descendants<'a>(Traverse<'a>); + +impl<'a> Iterator for Descendants<'a> { + type Item = NodeId; + + fn next(&mut self) -> Option { + loop { + match self.0.next() { + Some(NodeEdge::Start(node)) => return Some(node), + Some(NodeEdge::End(_)) => {} + None => return None + } + } + } +} + +#[derive(Debug, Clone)] +pub enum NodeEdge { + /// Indicates that start of a node that has children. + /// Yielded by `Traverse::next` before the node’s descendants. + /// In HTML or XML, this corresponds to an opening tag like `
` + Start(T), + + /// Indicates that end of a node that has children. + /// Yielded by `Traverse::next` after the node’s descendants. + /// In HTML or XML, this corresponds to a closing tag like `
` + End(T), +} + +impl NodeEdge { + pub fn inner_value(self) -> T { + use self::NodeEdge::*; + match self { + Start(t) => t, + End(t) => t, + } + } +} + +/// An iterator of references to a given node and its descendants, in tree order. +pub struct Traverse<'a> { + node_layout: &'a NodeHierarchy, + root: NodeId, + next: Option>, +} + +impl<'a> Iterator for Traverse<'a> { + type Item = NodeEdge; + + fn next(&mut self) -> Option> { + match self.next.take() { + Some(item) => { + self.next = match item { + NodeEdge::Start(node) => { + match self.node_layout[node].first_child { + Some(first_child) => Some(NodeEdge::Start(first_child)), + None => Some(NodeEdge::End(node.clone())) + } + } + NodeEdge::End(node) => { + if node == self.root { + None + } else { + match self.node_layout[node].next_sibling { + Some(next_sibling) => Some(NodeEdge::Start(next_sibling)), + None => match self.node_layout[node].parent { + Some(parent) => Some(NodeEdge::End(parent)), + + // `node.parent()` here can only be `None` + // if the tree has been modified during iteration, + // but silently stopping iteration + // seems a more sensible behavior than panicking. + None => None + } + } + } + } + }; + Some(item) + } + None => None + } + } +} + +/// An iterator of references to a given node and its descendants, in reverse tree order. +pub struct ReverseTraverse<'a> { + node_layout: &'a NodeHierarchy, + root: NodeId, + next: Option>, +} + +impl<'a> Iterator for ReverseTraverse<'a> { + type Item = NodeEdge; + + fn next(&mut self) -> Option> { + match self.next.take() { + Some(item) => { + self.next = match item { + NodeEdge::End(node) => { + match self.node_layout[node].last_child { + Some(last_child) => Some(NodeEdge::End(last_child)), + None => Some(NodeEdge::Start(node.clone())) + } + } + NodeEdge::Start(node) => { + if node == self.root { + None + } else { + match self.node_layout[node].previous_sibling { + Some(previous_sibling) => Some(NodeEdge::End(previous_sibling)), + None => match self.node_layout[node].parent { + Some(parent) => Some(NodeEdge::Start(parent)), + + // `node.parent()` here can only be `None` + // if the tree has been modified during iteration, + // but silently stopping iteration + // seems a more sensible behavior than panicking. + None => None + } + } + } + } + }; + Some(item) + } + None => None + } + } +} \ No newline at end of file diff --git a/azul/src/lib.rs b/azul/src/lib.rs new file mode 100644 index 000000000..1f2ae05ac --- /dev/null +++ b/azul/src/lib.rs @@ -0,0 +1,299 @@ +//! Azul is a free, functional, immediate-mode GUI framework for rapid development +//! of desktop applications written in Rust, supported by the Mozilla WebRender +//! rendering engine, using a flexbox-based CSS / DOM model for layout and styling. +//! +//! # Concept +//! +//! Azul is largely based on the principle of immediate-mode GUI frameworks, which +//! is that the entire UI (in Azuls case the DOM) is reconstructed and re-rendered +//! on every frame (instead of having functions that mutate the UI state like +//! `button.setText()`). This method of constructing UIs has a performance overhead +//! over methods that retain the UI, therefore Azul only calls the [`Layout::layout()`] +//! function when its absolutely necessary - inside of a callback, you can return +//! whether it is necessary to redraw the screen or not (by returning +//! [`Redraw`] or [`DontRedraw`], respectively). +//! +//! In difference to other immediate-mode frameworks, Azul does not immediately +//! draw to the screen, but rather "draws" to a `Dom`. This has several advantages, +//! such as making it possible to layout code at runtime, [loading a `Dom` from +//! an XML file], recognizing state changes by diffing two frames, as well as being +//! able to reparent DOMs into almost any configuration to make components reusable +//! independent of the context they are in. +//! +//! # Development lifecycle +//! +//! A huge problem when working with GUI applications in Rust is managing the +//! compile time. Having to recompile your entire code when you just want to +//! shift an element a pixel to the right is not a good developer experience. +//! Azul has three main methods of combating compile time: +//! +//! - The [XML] system, which allows you to load DOMs at runtime [from a file] +//! - The [CSS] system, which allows you to [load and parse stylesheets] +//! +//! Due to Azuls stateless rendering architecutre, hot-reloading also preserves +//! the current application state. Once you are done layouting your applications +//! UI, you can [transpile the XML code to valid Rust source code] using [azulc], +//! the Azul-XML-to-Rust compiler. +//! +//! Please note that the compiler isn't perfect - the XML system is very limited, +//! and parsing XML has a certain performance overhead, since it's done on every frame. +//! That is fine for debug builds, but the XML system should not be used in release mode. +//! +//! When you are done with designing the callbacks of your widget, you may want to +//! package the widget up to autmatically react to certain events without having the +//! user of your widget write any code to hook up the callbacks - for this purpose, +//! Azul features a [two way data binding] system. +//! +//! # Custom drawing and embedding external applications +//! +//! Azul is mostly concerned with rendering text, images and rectangular boxes (divs). +//! Any other content can be drawn by drawing to an OpenGL texture (using a +//! [`GlTextureCallback`]) and handing the texture as an "image" to Azul. This is also how +//! components like a video player or other OpenGL-based visualizations can exist +//! outside of the core library and be "injected" into the UI. +//! +//! You can draw to an OpenGL texture and hand it to Azul in order to display it +//! in the UI - the texture doesn't have to come from Azul itself, you can inject +//! it from an external application. +//! +//! # Limitations +//! +//! There are a few limitations that should be noted: +//! +//! - There are no scrollbars yet. Creating scrollable frames can be done by +//! [creating an `IFrameCallback`]. +//! - Similarly, there is no clipping of overflowing content yet - clipping only +//! works for `IFrameCallback`s. +//! - There is no support for CSS animations of any kind yet +//! - Changing dynamic variables will trigger an entire UI relayout and restyling +//! +//! # Hello world +//! +//! ```no_run +//! extern crate azul; +//! +//! use azul::prelude::*; +//! +//! struct MyDataModel { } +//! +//! impl Layout for MyDataModel { +//! fn layout(&self, _: LayoutInfo) -> Dom { +//! Dom::label("Hello World") +//! } +//! } +//! +//! fn main() { +//! let mut app = App::new(MyDataModel { }, AppConfig::default()).unwrap(); +//! let window = app.create_window(WindowCreateOptions::default(), css::native()).unwrap(); +//! app.run(window).unwrap(); +//! } +//! ``` +//! +//! Running this code should return a window similar to this: +//! +//! ![Opening a blank window](https://raw.githubusercontent.com/maps4print/azul/master/doc/azul_tutorial_empty_window.png) +//! +//! # Tutorials +//! +//! Explaining all concepts and examples is too much to be included in +//! this API reference. Please refer to the [wiki](https://github.com/maps4print/azul/wiki) +//! or use the links below to learn about how to use Azul. +//! +//! - [Getting Started](https://github.com/maps4print/azul/wiki/Getting-Started) +//! - [A simple counter](https://github.com/maps4print/azul/wiki/A-simple-counter) +//! - [Styling your app with CSS](https://github.com/maps4print/azul/wiki/Styling-your-application-with-CSS) +//! - [SVG drawing](https://github.com/maps4print/azul/wiki/SVG-drawing) +//! - [OpenGL drawing](https://github.com/maps4print/azul/wiki/OpenGL-drawing) +//! - [Timers, timers, tasks and async IO](https://github.com/maps4print/azul/wiki/Timers,-timers,-tasks-and-async-IO) +//! - [Two-way data binding](https://github.com/maps4print/azul/wiki/Two-way-data-binding) +//! - [Unit testing](https://github.com/maps4print/azul/wiki/Unit-testing) +//! +//! [`Layout::layout()`]: ../azul/traits/trait.Layout.html +//! [widgets]: ../azul/widgets/index.html +//! [loading a `Dom` from an XML file]: ../azul/dom/struct.Dom.html#method.from_file +//! [XML]: ../azul/xml/index.html +//! [`Redraw`]: ../azul/callbacks/constant.Redraw.html +//! [`DontRedraw`]: ../azul/callbacks/constant.DontRedraw.html +//! [`GlTextureCallback`]: ../azul/callbacks/struct.GlTextureCallback.html +//! [creating an `IFrameCallback`]: ../azul/dom/struct.Dom.html#method.iframe +//! [from a file]: ../azul/dom/struct.Dom.html#method.from_file +//! [CSS]: ../azul/css/index.html +//! [load and parse stylesheets]: ../azul/css/fn.from_str.html +//! [transpile the XML code to valid Rust source code]: https://github.com/maps4print/azul/wiki/XML-to-Rust-compilation +//! [azulc]: https://crates.io/crates/azulc +//! [two way data binding]: https://github.com/maps4print/azul/wiki/Two-way-data-binding + +#![doc( + html_logo_url = "https://raw.githubusercontent.com/maps4print/azul/master/assets/images/azul_logo_full_min.svg.png", + html_favicon_url = "https://raw.githubusercontent.com/maps4print/azul/master/assets/images/favicon.ico", +)] + +#![allow(dead_code)] +#![deny(unused_must_use)] +#![deny(unreachable_patterns)] +#![deny(missing_copy_implementations)] +#![deny(clippy::all)] + +#[macro_use(warn, error, lazy_static)] +#[cfg_attr(feature = "svg", macro_use(implement_vertex, uniform))] +pub extern crate azul_dependencies; +#[cfg(feature = "serde_serialization")] +#[cfg_attr(feature = "serde_serialization", macro_use)] +extern crate serde; +#[cfg(feature = "serde_serialization")] +#[cfg_attr(feature = "serde_serialization", macro_use)] +extern crate serde_derive; + +pub(crate) use azul_dependencies::glium as glium; +pub(crate) use azul_dependencies::gleam as gleam; +pub(crate) use azul_dependencies::euclid; +pub(crate) use azul_dependencies::webrender; +pub(crate) use azul_dependencies::app_units; +pub(crate) use azul_dependencies::unicode_normalization; +pub(crate) use azul_dependencies::tinyfiledialogs; +pub(crate) use azul_dependencies::clipboard2; +pub(crate) use azul_dependencies::font_loader; +pub(crate) use azul_dependencies::xmlparser; +pub(crate) use azul_dependencies::harfbuzz_sys; + +#[cfg(feature = "logging")] +pub(crate) use azul_dependencies::log; +#[cfg(feature = "svg")] +pub(crate) use azul_dependencies::stb_truetype; +#[cfg(feature = "logging")] +pub(crate) use azul_dependencies::fern; +#[cfg(feature = "logging")] +pub(crate) use azul_dependencies::backtrace; +#[cfg(feature = "image_loading")] +pub(crate) use azul_dependencies::image; +#[cfg(feature = "svg")] +pub(crate) use azul_dependencies::lyon; +#[cfg(feature = "svg_parsing")] +pub(crate) use azul_dependencies::usvg; +#[cfg(feature = "faster-hashing")] +pub(crate) use azul_dependencies::twox_hash; + +#[cfg(feature = "css_parser")] +extern crate azul_css; +extern crate azul_native_style; +extern crate azul_css_parser; + +// Crate-internal macros +#[macro_use] +mod macros; + +/// Manages application state (`App` / `AppState` / `AppResources`), wrapping resources and app state +pub mod app; +/// Async IO helpers / (`Task` / `Timer` / `Thread`) +pub mod async; +/// Type definitions for various types of callbacks, as well as focus and scroll handling +pub mod callbacks; +/// CSS type definitions / CSS parsing functions +#[cfg(any(feature = "css_parser", feature = "native_style"))] +pub mod css; +/// Bindings to the native file-chooser, color picker, etc. dialogs +pub mod dialogs; +/// DOM / HTML node handling +pub mod dom; +/// Re-exports of errors +pub mod error; +/// Handles text layout (modularized, can be used as a standalone module) +pub mod text_layout; +/// Main `Layout` trait definition + convenience traits for `Arc>` +pub mod traits; +/// Container for default widgets (`TextInput` / `Button` / `Label`, `TableView`, ...) +pub mod widgets; +/// Window state handling and window-related information +pub mod window; +/// XML-based DOM serialization and XML-to-Rust compiler implementation +pub mod xml; + +/// UI Description & display list handling (webrender) +mod ui_description; +/// HarfBuzz text shaping utilities +mod text_shaping; +/// Converts the UI description (the styled HTML nodes) +/// to an actual display list (+ layout) +mod display_list; +/// Slab allocator for nodes, based on IDs (replaces kuchiki + markup5ever) +mod id_tree; +/// State handling for user interfaces +mod ui_state; +/// The compositor takes all textures (user-defined + the UI texture(s)) and draws them on +/// top of each other +mod compositor; +/// Default logger, can be turned off with `feature = "logging"` +#[cfg(feature = "logging")] +mod logging; +/// Flexbox-based UI solver +mod ui_solver; +/// DOM styling module +mod style; +/// DOM diffing +mod diff; +/// Checks that two-way bound values are on the stack +mod stack_checked_pointer; +/// Window state handling and diffing +mod window_state; +/// ImageId / FontId handling and caching +mod app_resources; + +/// Font & image resource handling, lookup and caching +pub mod resources { + // re-export everything *except* the AppResources (which are exported under the "app" module) + pub use app_resources::{ + FontId, ImageId, LoadedFont, RawImage, FontReloadError, FontSource, ImageReloadError, + ImageSource, RawImageFormat, CssFontId, CssImageId, + TextCache, TextId, + }; +} + +// Faster implementation of a HashMap (optional, disabled by default, turn on with --feature="faster-hashing") + +#[cfg(feature = "faster-hashing")] +type FastHashMap = ::std::collections::HashMap>; +#[cfg(feature = "faster-hashing")] +type FastHashSet = ::std::collections::HashSet>; +#[cfg(not(feature = "faster-hashing"))] +type FastHashMap = ::std::collections::HashMap; +#[cfg(not(feature = "faster-hashing"))] +type FastHashSet = ::std::collections::HashSet; + +/// Quick exports of common types +pub mod prelude { + #[cfg(feature = "css_parser")] + pub use azul_css::*; + pub use app::{App, AppConfig, AppState, AppResources}; + pub use async::{Task, TerminateTimer, TimerId, Timer, DropCheck}; + pub use resources::{ + RawImageFormat, ImageId, FontId, FontSource, ImageSource, + TextCache, TextId, + }; + pub use callbacks::{ + Callback, TimerCallback, IFrameCallback, GlTextureCallback, + UpdateScreen, Redraw, DontRedraw, + CallbackInfo, FocusTarget, LayoutInfo, HidpiAdjustedBounds, Texture, + }; + pub use dom::{ + Dom, DomHash, NodeType, NodeData, On, DomString, TabIndex, + EventFilter, HoverEventFilter, FocusEventFilter, NotEventFilter, WindowEventFilter, + }; + pub use traits::{Layout, Modify}; + pub use window::{ + MonitorIter, Window, WindowCreateOptions, + WindowMonitorTarget, RendererType, ReadOnlyWindow + }; + pub use window_state::{WindowState, KeyboardState, MouseState, DebugState, keymap, AcceleratorKey}; + pub use glium::glutin::{ + dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}, + VirtualKeyCode, ScanCode, Icon, + }; + pub use stack_checked_pointer::StackCheckedPointer; + pub use text_layout::{TextLayoutOptions, GlyphInstance}; + pub use xml::{XmlComponent, XmlComponentMap}; + + #[cfg(any(feature = "css_parser", feature = "native_style"))] + pub use css; + #[cfg(feature = "logging")] + pub use log::LevelFilter; +} diff --git a/azul/src/logging.rs b/azul/src/logging.rs new file mode 100644 index 000000000..13a4653a1 --- /dev/null +++ b/azul/src/logging.rs @@ -0,0 +1,192 @@ +use dialogs::msg_box_ok; +use log::LevelFilter; +use std::sync::atomic::{Ordering, AtomicBool}; + +pub(crate) static SHOULD_ENABLE_PANIC_HOOK: AtomicBool = AtomicBool::new(false); + +pub(crate) fn set_up_logging(log_file_path: Option<&str>, log_level: LevelFilter) { + + use fern::InitError; + use std::error::Error; + + /// Sets up the global logger + fn set_up_logging_internal(log_file_path: Option<&str>, log_level: LevelFilter) + -> Result<(), InitError> + { + use std::io::{Error as IoError, ErrorKind as IoErrorKind}; + use fern::{Dispatch, log_file}; + + let log_location = { + use std::env; + + let mut exe_location = env::current_exe() + .map_err(|_| InitError::Io(IoError::new(IoErrorKind::Other, + "Executable has no executable path (?), can't open log file")))?; + + exe_location.pop(); + exe_location.push(log_file_path.unwrap_or("error.log")); + exe_location + }; + + Dispatch::new() + .format(|out, message, record| { + out.finish(format_args!( + "[{}][{}] {}", + record.level(), + record.target(), + message + )) + }) + .level(log_level) + .chain(::std::io::stdout()) + .chain(log_file(log_location)?) + .apply()?; + Ok(()) + } + + match set_up_logging_internal(log_file_path, log_level) { + Ok(_) => { }, + Err(e) => match e { + InitError::Io(e) => { + println!("[WARN] Logging IO init error: \r\nkind: {:?}\r\n\r\ndescription:\r\n{}\r\n\r\ncause:\r\n{:?}\r\n", + e.kind(), e.description(), e.source()); + }, + InitError::SetLoggerError(e) => { + println!("[WARN] Logging initalization error: \r\ndescription:\r\n{}\r\n\r\ncause:\r\n{:?}\r\n", + e.description(), e.source()); + } + } + } +} + +/// In the (rare) case of a panic, print it to the stdout, log it to the file and +/// prompt the user with a message box. +pub(crate) fn set_up_panic_hooks() { + + use std::panic::{self, PanicInfo}; + use backtrace::{Backtrace, BacktraceFrame}; + + fn panic_fn(panic_info: &PanicInfo) { + + use std::thread; + + let payload = panic_info.payload(); + let location = panic_info.location(); + + let payload_str = format!("{:?}", payload); + let panic_str = payload.downcast_ref::() + .map(|s| s.as_ref()) + .or_else(|| + payload.downcast_ref::<&str>() + .map(|s| *s) + ) + .unwrap_or(payload_str.as_str()); + + let location_str = location.map(|loc| format!("{} at line {}", loc.file(), loc.line())); + let backtrace_str_old = format_backtrace(&Backtrace::new()); + let backtrace_str = backtrace_str_old + .lines() + .filter(|l| !l.is_empty()) + .collect::>() + .join("\r\n"); + + let thread = thread::current(); + let thread_name = thread.name().unwrap_or(""); + + let error_str = format!( + "An unexpected panic ocurred, the program has to exit.\r\n\ + Please report this error and attach the log file found in the directory of the executable.\r\n\ + \r\n\ + The error ocurred in: {} in thread {}\r\n\ + \r\n\ + Error information:\r\n\ + {}\r\n\ + \r\n\ + Backtrace:\r\n\ + \r\n\ + {}\r\n", + location_str.unwrap_or(format!("")), thread_name, panic_str, backtrace_str); + + #[cfg(target_os = "linux")] + let mut error_str_clone = error_str.clone(); + #[cfg(target_os = "linux")] { + error_str_clone = error_str_clone.replace("<", "<"); + error_str_clone = error_str_clone.replace(">", ">"); + } + + // TODO: invoke external app crash handler with the location to the log file + error!("{}", error_str); + + if SHOULD_ENABLE_PANIC_HOOK.load(Ordering::SeqCst) { + #[cfg(not(target_os = "linux"))] + msg_box_ok("Unexpected fatal error", &error_str, ::tinyfiledialogs::MessageBoxIcon::Error); + #[cfg(target_os = "linux")] + msg_box_ok("Unexpected fatal error", &error_str_clone, ::tinyfiledialogs::MessageBoxIcon::Error); + } + } + + fn format_backtrace(backtrace: &Backtrace) -> String { + + fn format_frame(frame: &BacktraceFrame) -> String { + + use std::ffi::OsStr; + + let ip = frame.ip(); + let symbols = frame.symbols(); + + const UNRESOLVED_FN_STR: &str = "unresolved function"; + + if symbols.is_empty() { + return format!("{} @ {:?}", UNRESOLVED_FN_STR, ip); + } + + // skip the first 10 symbols because they belong to the + // backtrace library and aren't relevant for debugging + symbols.iter().map(|symbol| { + + let mut nice_string = String::new(); + + if let Some(name) = symbol.name() { + let name_demangled = format!("{}", name); + let name_demangled_new = name_demangled.rsplit("::").skip(1).map(|e| e.to_string()).collect::>(); + let name_demangled = name_demangled_new.into_iter().rev().collect::>().join("::"); + nice_string.push_str(&name_demangled); + } else { + nice_string.push_str(UNRESOLVED_FN_STR); + } + + let mut file_string = String::new(); + if let Some(file) = symbol.filename() { + let origin_file_name = file.file_name() + .unwrap_or(OsStr::new("unresolved file name")) + .to_string_lossy(); + file_string.push_str(&format!("{}", origin_file_name)); + } + + if let Some(line) = symbol.lineno() { + file_string.push_str(&format!(":{}", line)); + } + + if !file_string.is_empty() { + nice_string.push_str(" @ "); + nice_string.push_str(&file_string); + if !nice_string.ends_with("\n") { + nice_string.push_str("\n"); + } + } + + nice_string + + }).collect::>().join("") + } + + backtrace + .frames() + .iter() + .map(|frame| format_frame(frame)) + .collect::>() + .join("\r\n") + } + + panic::set_hook(Box::new(panic_fn)); +} \ No newline at end of file diff --git a/azul/src/macros.rs b/azul/src/macros.rs new file mode 100644 index 000000000..91400527e --- /dev/null +++ b/azul/src/macros.rs @@ -0,0 +1,436 @@ +/// Implement the `From` trait for any type. +/// Example usage: +/// ``` +/// enum MyError<'a> { +/// Bar(BarError<'a>) +/// Foo(FooError<'a>) +/// } +/// +/// impl_from!(BarError<'a>, Error::Bar); +/// impl_from!(BarError<'a>, Error::Bar); +/// +/// ``` +macro_rules! impl_from { + // From a type with a lifetime to a type which also has a lifetime + ($a:ident<$c:lifetime>, $b:ident::$enum_type:ident) => { + impl<$c> From<$a<$c>> for $b<$c> { + fn from(e: $a<$c>) -> Self { + $b::$enum_type(e) + } + } + }; + + // From a type without a lifetime to a type which also does not have a lifetime + ($a:ident, $b:ident::$enum_type:ident) => { + impl From<$a> for $b { + fn from(e: $a) -> Self { + $b::$enum_type(e) + } + } + }; +} + +/// Implement `Display` for an enum. +/// +/// Example usage: +/// ``` +/// enum Foo<'a> { +/// Bar(&'a str) +/// Baz(i32) +/// } +/// +/// impl_display!{ Foo<'a>, { +/// Bar(s) => s, +/// Baz(i) => format!("{}", i) +/// }} +/// ``` +macro_rules! impl_display { + // For a type with a lifetime + ($enum:ident<$lt:lifetime>, {$($variant:pat => $fmt_string:expr),+$(,)* }) => { + + impl<$lt> ::std::fmt::Display for $enum<$lt> { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + use self::$enum::*; + match &self { + $( + $variant => write!(f, "{}", $fmt_string), + )+ + } + } + } + + }; + + // For a type without a lifetime + ($enum:ident, {$($variant:pat => $fmt_string:expr),+$(,)* }) => { + + impl ::std::fmt::Display for $enum { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + use self::$enum::*; + match &self { + $( + $variant => write!(f, "{}", $fmt_string), + )+ + } + } + } + + }; +} + +/// Implements `Display, Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Hash` +/// for a Callback with a `.0` field: +/// +/// ``` +/// struct MyCallback(fn (&T)); +/// +/// // impl Display, Debug, etc. for MyCallback +/// impl_callback!(MyCallback); +/// ``` +/// +/// This is necessary to work around for https://github.com/rust-lang/rust/issues/54508 +macro_rules! impl_callback {($callback_value:ident<$t:ident>) => ( + + impl<$t> ::std::fmt::Display for $callback_value<$t> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) + } + } + + impl<$t> ::std::fmt::Debug for $callback_value<$t> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let callback = stringify!($callback_value); + write!(f, "{} @ 0x{:x}", callback, self.0 as usize) + } + } + + impl<$t> Clone for $callback_value<$t> { + fn clone(&self) -> Self { + $callback_value(self.0.clone()) + } + } + + impl<$t> ::std::hash::Hash for $callback_value<$t> { + fn hash(&self, state: &mut H) where H: Hasher { + state.write_usize(self.0 as usize); + } + } + + impl<$t> PartialEq for $callback_value<$t> { + fn eq(&self, rhs: &Self) -> bool { + self.0 as usize == rhs.0 as usize + } + } + + impl<$t> PartialOrd for $callback_value<$t> { + fn partial_cmp(&self, other: &Self) -> Option<::std::cmp::Ordering> { + Some((self.0 as usize).cmp(&(other.0 as usize))) + } + } + + impl<$t> Ord for $callback_value<$t> { + fn cmp(&self, other: &Self) -> ::std::cmp::Ordering { + (self.0 as usize).cmp(&(other.0 as usize)) + } + } + + impl<$t> Eq for $callback_value<$t> { } + + impl<$t> Copy for $callback_value<$t> { } +)} + +macro_rules! image_api {($struct_name:ident::$struct_field:ident) => ( + +impl $struct_name { + + /// See [`AppResources::get_loaded_font_ids`] + /// + /// [`AppResources::get_loaded_font_ids`]: ../app_resources/struct.AppResources.html#method.get_loaded_font_ids + pub fn get_loaded_font_ids(&self) -> Vec { + self.$struct_field.get_loaded_font_ids() + } + + /// See [`AppResources::get_loaded_image_ids`] + /// + /// [`AppResources::get_loaded_image_ids`]: ../app_resources/struct.AppResources.html#method.get_loaded_image_ids + pub fn get_loaded_image_ids(&self) -> Vec { + self.$struct_field.get_loaded_image_ids() + } + + /// See [`AppResources::get_loaded_css_image_ids`] + /// + /// [`AppResources::get_loaded_css_image_ids`]: ../app_resources/struct.AppResources.html#method.get_loaded_css_image_ids + pub fn get_loaded_css_image_ids(&self) -> Vec { + self.$struct_field.get_loaded_css_image_ids() + } + + /// See [`AppResources::get_loaded_css_font_ids`] + /// + /// [`AppResources::get_loaded_css_font_ids`]: ../app_resources/struct.AppResources.html#method.get_loaded_css_font_ids + pub fn get_loaded_css_font_ids(&self) -> Vec { + self.$struct_field.get_loaded_css_font_ids() + } + + /// See [`AppResources::get_loaded_text_ids`] + /// + /// [`AppResources::get_loaded_text_ids`]: ../app_resources/struct.AppResources.html#method.get_loaded_text_ids + pub fn get_loaded_text_ids(&self) -> Vec { + self.$struct_field.get_loaded_text_ids() + } + + // -- ImageId cache + + /// See [`AppResources::add_image`] + /// + /// [`AppResources::add_image`]: ../app_resources/struct.AppResources.html#method.add_image + #[cfg(feature = "image_loading")] + pub fn add_image(&mut self, image_id: ImageId, image_source: ImageSource) { + self.$struct_field.add_image(image_id, image_source) + } + + /// See [`AppResources::has_image`] + /// + /// [`AppResources::has_image`]: ../app_resources/struct.AppResources.html#method.has_image + pub fn has_image(&self, image_id: &ImageId) -> bool { + self.$struct_field.has_image(image_id) + } + + /// Given an `ImageId`, returns the bytes for that image or `None`, if the `ImageId` is invalid. + /// + /// See [`AppResources::get_image_bytes`] + /// + /// [`AppResources::get_image_bytes`]: ../app_resources/struct.AppResources.html#method.get_image_bytes + pub fn get_image_bytes(&self, image_id: &ImageId) -> Option> { + self.$struct_field.get_image_bytes(image_id) + } + + /// See [`AppResources::delete_image`] + /// + /// [`AppResources::delete_image`]: ../app_resources/struct.AppResources.html#method.delete_image + pub fn delete_image(&mut self, image_id: &ImageId) { + self.$struct_field.delete_image(image_id) + } + + /// See [`AppResources::add_css_image_id`] + /// + /// [`AppResources::add_css_image_id`]: ../app_resources/struct.AppResources.html#method.add_css_image_id + pub fn add_css_image_id>(&mut self, css_id: S) -> ImageId { + self.$struct_field.add_css_image_id(css_id) + } + + /// See [`AppResources::has_css_image_id`] + /// + /// [`AppResources::has_css_image_id`]: ../app_resources/struct.AppResources.html#method.has_css_image_id + pub fn has_css_image_id(&self, css_id: &str) -> bool { + self.$struct_field.has_css_image_id(css_id) + } + + /// See [`AppResources::get_css_image_id`] + /// + /// [`AppResources::get_css_image_id`]: ../app_resources/struct.AppResources.html#method.get_css_image_id + pub fn get_css_image_id(&self, css_id: &str) -> Option<&ImageId> { + self.$struct_field.get_css_image_id(css_id) + } + + /// See [`AppResources::delete_css_image_id`] + /// + /// [`AppResources::delete_css_image_id`]: ../app_resources/struct.AppResources.html#method.delete_css_image_id + pub fn delete_css_image_id(&mut self, css_id: &str) -> Option { + self.$struct_field.delete_css_image_id(css_id) + } + + /// See [`AppResources::add_css_font_id`] + /// + /// [`AppResources::add_css_font_id`]: ../app_resources/struct.AppResources.html#method.add_css_font_id + pub fn add_css_font_id>(&mut self, css_id: S) -> FontId { + self.$struct_field.add_css_font_id(css_id) + } + + /// See [`AppResources::has_css_font_id`] + /// + /// [`AppResources::has_css_font_id`]: ../app_resources/struct.AppResources.html#method.has_css_font_id + pub fn has_css_font_id(&self, css_id: &str) -> bool { + self.$struct_field.has_css_font_id(css_id) + } + + /// See [`AppResources::get_css_font_id`] + /// + /// [`AppResources::get_css_font_id`]: ../app_resources/struct.AppResources.html#method.get_css_font_id + pub fn get_css_font_id(&self, css_id: &str) -> Option<&FontId> { + self.$struct_field.get_css_font_id(css_id) + } + + /// See [`AppResources::delete_css_font_id`] + /// + /// [`AppResources::delete_css_font_id`]: ../app_resources/struct.AppResources.html#method.delete_css_font_id + pub fn delete_css_font_id(&mut self, css_id: &str) -> Option { + self.$struct_field.delete_css_font_id(css_id) + } +} + +)} + +macro_rules! font_api {($struct_name:ident::$struct_field:ident) => ( + +impl $struct_name { + + /// Given a `FontId`, returns the bytes for that font or `None`, if the `FontId` is invalid. + /// See [`AppResources::get_font_bytes`] + /// + /// [`AppResources::get_font_bytes`]: ../app_resources/struct.AppResources.html#method.get_font_bytes + pub fn get_font_bytes(&self, font_id: &FontId) -> Option, i32), FontReloadError>> { + self.$struct_field.get_font_bytes(font_id) + } + + /// See [`AppResources::add_font`] + /// + /// [`AppResources::add_font`]: ../app_resources/struct.AppResources.html#method.add_font + pub fn add_font(&mut self, font_id: FontId, font_source: FontSource) { + self.$struct_field.add_font(font_id, font_source) + } + + /// See [`AppResources::has_font`] + /// + /// [`AppResources::has_font`]: ../app_resources/struct.AppResources.html#method.has_font + pub fn has_font(&self, font_id: &FontId) -> bool { + self.$struct_field.has_font(font_id) + } + + /// See [`AppResources::delete_font`] + /// + /// [`AppResources::delete_font`]: ../app_resources/struct.AppResources.html#method.delete_font + pub fn delete_font(&mut self, font_id: &FontId) { + self.$struct_field.delete_font(font_id) + } +} + +)} + +macro_rules! text_api {($struct_name:ident::$struct_field:ident) => ( + +impl $struct_name { + + /// Adds a string to the internal text cache, but only store it as a string, + /// without caching the layout of the string. + /// + /// See [`AppResources::add_text`]. + /// + /// [`AppResources::add_text`]: ../app_resources/struct.AppResources.html#method.add_text + pub fn add_text(&mut self, text: &str) -> TextId { + self.$struct_field.add_text(text) + } + + /// Removes a string from both the string cache and the layouted text cache + /// + /// See [`AppResources::delete_text`]. + /// + /// [`AppResources::delete_text`]: ../app_resources/struct.AppResources.html#method.delete_text + pub fn delete_text(&mut self, id: TextId) { + self.$struct_field.delete_text(id) + } + + /// Empties the entire internal text cache, invalidating all `TextId`s. + /// If the given TextId is used after this call, the text will not render in the UI. + /// Use with care. + /// + /// See [`AppResources::clear_all_texts`]. + /// + /// [`AppResources::clear_all_texts`]: ../app_resources/struct.AppResources.html#method.clear_all_texts + pub fn clear_all_texts(&mut self) { + self.$struct_field.clear_all_texts() + } +} + +)} + +macro_rules! clipboard_api {($struct_name:ident::$struct_field:ident) => ( + +impl $struct_name { + + /// See [`AppResources::get_clipboard_string`] + /// + /// [`AppResources::get_clipboard_string`]: ../app_resources/struct.AppResources.html#method.get_clipboard_string + pub fn get_clipboard_string(&mut self) -> Result { + self.$struct_field.get_clipboard_string() + } + + /// See [`AppResources::set_clipboard_string`] + /// + /// [`AppResources::set_clipboard_string`]: ../app_resources/struct.AppResources.html#method.set_clipboard_string + pub fn set_clipboard_string>(&mut self, contents: I) -> Result<(), ClipboardError> { + self.$struct_field.set_clipboard_string(contents) + } +} + +)} + +macro_rules! timer_api {($struct_name:ident::$struct_field:ident) => ( + +impl $struct_name { + + /// See [`AppState::add_timer`] + /// + /// [`AppState::add_timer`]: ../app_state/struct.AppState.html#method.add_timer + pub fn add_timer(&mut self, timer_id: TimerId, timer: Timer) { + self.$struct_field.add_timer(timer_id, timer) + } + + /// See [`AppState::has_timer`] + /// + /// [`AppState::has_timer`]: ../app_state/struct.AppState.html#method.has_timer + pub fn has_timer(&self, timer_id: &TimerId) -> bool { + self.$struct_field.has_timer(timer_id) + } + + /// See [`AppState::get_timer`] + /// + /// [`AppState::get_timer`]: ../app_state/struct.AppState.html#method.get_timer + pub fn get_timer(&self, timer_id: &TimerId) -> Option> { + self.$struct_field.get_timer(timer_id) + } + + /// See [`AppState::delete_timer`] + /// + /// [`AppState::delete_timer`]: ../app_state/struct.AppState.html#method.delete_timer + pub fn delete_timer(&mut self, timer_id: &TimerId) -> Option> { + self.$struct_field.delete_timer(timer_id) + } +} + +)} + +macro_rules! impl_deamon_api {() => ( + + /// Insert a timer into the list of active timers. + /// Replaces the existing timer if called with the same TimerId. + pub fn add_timer(&mut self, id: TimerId, timer: Timer) { + self.timers.insert(id, timer); + } + + pub fn has_timer(&self, timer_id: &TimerId) -> bool { + self.get_timer(timer_id).is_some() + } + + pub fn get_timer(&self, timer_id: &TimerId) -> Option> { + self.timers.get(&timer_id).cloned() + } + + pub fn delete_timer(&mut self, timer_id: &TimerId) -> Option> { + self.timers.remove(timer_id) + } + + /// Custom tasks can be used when the `AppState` isn't `Send`. For example + /// `SvgCache` isn't thread-safe, since it has to interact with OpenGL, so + /// it can't be sent to other threads safely. + /// + /// What you can do instead, is take a part of your application data, wrap + /// that in an `Arc>` and push a task that takes it onto the queue. + /// This way you can modify a part of the application state on a different + /// thread, while not requiring that everything is thread-safe. + /// + /// While you can't modify the `SvgCache` from a different thread, you can + /// modify other things in the `AppState` and leave the SVG cache alone. + pub fn add_task(&mut self, task: Task) { + self.tasks.push(task); + } +)} \ No newline at end of file diff --git a/azul/src/stack_checked_pointer.rs b/azul/src/stack_checked_pointer.rs new file mode 100644 index 000000000..e5d9f4da6 --- /dev/null +++ b/azul/src/stack_checked_pointer.rs @@ -0,0 +1,180 @@ + +use std::{ + fmt, + hash::{Hash, Hasher}, + marker::PhantomData, +}; +use { + dom::Dom, + callbacks::{ + DefaultCallbackType, CallbackInfo, LayoutInfo, HidpiAdjustedBounds, + UpdateScreen, Texture, + }, + app::AppStateNoData, +}; + +/// A `StackCheckedPointer` is a type-erased, raw pointer to a +/// value **inside** of `T`. +/// +/// Since we know that the pointer is "checked" to be contained (on the stack) +/// within `&T as usize` and `&T as usize + mem::size_of::()`, +/// `StackCheckedPointer` has the same lifetime as `T` +/// (but the type is erased, so it can be stored independent from `T`s lifetime). +/// +/// Note for enums: Should the pointer point to an enum instead of a struct and +/// the enum (which in Rust is a union) changes its variant, the behaviour of +/// invoking this pointer is undefined (likely to segfault). +pub struct StackCheckedPointer { + /// Type-erased pointer to a value on the stack in the `app_data.data` + /// model. When invoking default methods, we have to store a pointer to + /// the data we should update, but storing it in a `Box` to + /// erase the type doesn't help anything - we trust the user of this + /// pointer to know the exact type of this pointer. + internal: *const (), + /// Marker so that one stack checked pointer can't be shared across + /// two data models that are both `T: Layout`. + marker: PhantomData, +} + +impl StackCheckedPointer { + + /// Validates that the pointer to U is contained in T. + /// + /// This means that the lifetime of U is the same lifetime as T - + /// the returned `StackCheckedPointer` is valid for as long as `stack` + /// is valid. + pub fn new(stack: &T, pointer: &U) -> Option { + if is_subtype_of(stack, pointer) { + Some(Self { + internal: pointer as *const _ as *const (), + marker: PhantomData, + }) + } else { + None + } + } + + /// **UNSAFE**: Invoke the pointer with a function pointer that can + /// modify the pointer. It isn't checked that the type that the + /// `StackCheckedPointer` was created with is the same as this `U`, + /// but they **must be the same type**. This can't be checked since + /// the type has been (deliberately) erased. + /// + /// **NOTE**: To avoid undefined behaviour, you **must** check that + /// the `StackCheckedPointer` isn't mutably aliased at the time of + /// calling the callback. + pub unsafe fn invoke_mut( + &self, + callback: DefaultCallbackType, + app_state_no_data: &mut AppStateNoData, + window_event: &mut CallbackInfo) + -> UpdateScreen + { + // VERY UNSAFE, TRIPLE-CHECK FOR UNDEFINED BEHAVIOUR + callback(&mut *(self.internal as *mut U), app_state_no_data, window_event) + } + + pub unsafe fn invoke_mut_iframe( + &self, + callback: fn(&mut U, LayoutInfo, HidpiAdjustedBounds) -> Dom, + window_info: LayoutInfo, + dimensions: HidpiAdjustedBounds) + -> Dom + { + callback(&mut *(self.internal as *mut U), window_info, dimensions) + } + + pub unsafe fn invoke_mut_texture( + &self, + callback: fn(&mut U, LayoutInfo, HidpiAdjustedBounds) -> Option, + window_info: LayoutInfo, + dimensions: HidpiAdjustedBounds) + -> Option + { + callback(&mut *(self.internal as *mut U), window_info, dimensions) + } +} + +// #[derive(Debug, Clone, PartialEq, Hash, Eq)] for StackCheckedPointer + +impl fmt::Debug for StackCheckedPointer { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, + "StackCheckedPointer {{ internal: 0x{:x}, marker: {:?} }}", + self.internal as usize, self.marker + ) + } +} + +impl Clone for StackCheckedPointer { + fn clone(&self) -> Self { + StackCheckedPointer { internal: self.internal, marker: self.marker.clone() } + } +} + +impl Hash for StackCheckedPointer { + fn hash(&self, state: &mut H) where H: Hasher { + state.write_usize(self.internal as usize); + } +} + +impl PartialEq for StackCheckedPointer { + fn eq(&self, rhs: &Self) -> bool { + self.internal as usize == rhs.internal as usize + } +} + +impl Eq for StackCheckedPointer { } +impl Copy for StackCheckedPointer { } + + +/// Returns true if U is a type inside of T +/// +/// i.e: +/// +/// ```ignore +/// # struct Data { i: usize, p: Vec } +/// let data = Data { i: 5, p: vec![5] }; +/// +/// // true because i is inside of data +/// assert_eq!(is_subtype_of(&data, &data.i), true); +/// // true because p is inside of data +/// assert_eq!(is_subtype_of(&data, &data.p), true); +/// // false because p is heap-allocated, therefore not inside of data +/// assert_eq!(is_subtype_of(&data, &data.p[0]), false); +/// ``` +fn is_subtype_of(data: &T, subtype: &U) -> bool { + + // determine in which direction the stack grows + use std::mem::size_of; + + struct Invalid { + a: u64, + b: u64, + } + + let invalid = Invalid { a: 0, b: 0 }; + + let stack_grows_down = &invalid.b as *const _ as usize > &invalid.a as *const _ as usize; + + // calculate if U is a subtype of T + let st = subtype as *const _ as usize; + let t = data as *const _ as usize; + + if stack_grows_down { + st >= t && st + size_of::() <= t + size_of::() + } else { + st <= t && st - size_of::() >= t - size_of::() + } +} + +#[test] +fn test_reflection_subtyping() { + + struct Data { i: usize, p: Vec } + let data = Data { i: 5, p: vec![5] }; + + assert_eq!(is_subtype_of(&data, &data.i), true); + assert_eq!(is_subtype_of(&data, &data.p), true); + assert_eq!(is_subtype_of(&data, &data.p[0]), false); +} \ No newline at end of file diff --git a/azul/src/style.rs b/azul/src/style.rs new file mode 100644 index 000000000..8d8209cb5 --- /dev/null +++ b/azul/src/style.rs @@ -0,0 +1,618 @@ +//! DOM-tree to CSS style tree stying + +use std::{fmt, collections::BTreeMap}; +use azul_css::{ + Css, CssContentGroup, CssPath, + CssPathSelector, CssPathPseudoSelector, CssNthChildSelector::*, +}; +use webrender::api::HitTestItem; +use { + ui_description::{UiDescription, StyledNode}, + dom::NodeData, + ui_state::UiState, + id_tree::{NodeId, NodeHierarchy, NodeDataContainer}, + callbacks::FocusTarget, +}; + +/// Has all the necessary information about the style CSS path +pub(crate) struct HtmlCascadeInfo<'a, T: 'a> { + pub node_data: &'a NodeData, + pub index_in_parent: usize, + pub is_last_child: bool, + pub is_hovered_over: bool, + pub is_focused: bool, + pub is_active: bool, +} + +impl<'a, T: 'a> fmt::Debug for HtmlCascadeInfo<'a, T> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "HtmlCascadeInfo {{ \ + node_data: {:?}, \ + index_in_parent: {}, \ + is_last_child: {:?}, \ + is_hovered_over: {:?}, \ + is_focused: {:?}, \ + is_active: {:?}, \ + }}", + self.node_data, + self.index_in_parent, + self.is_last_child, + self.is_hovered_over, + self.is_focused, + self.is_active, + ) + } +} + +/// Returns if the style CSS path matches the DOM node (i.e. if the DOM node should be styled by that element) +pub(crate) fn matches_html_element<'a, T>( + css_path: &CssPath, + node_id: NodeId, + node_hierarchy: &NodeHierarchy, + html_node_tree: &NodeDataContainer>) +-> bool +{ + use self::CssGroupSplitReason::*; + + if css_path.selectors.is_empty() { + return false; + } + + let mut current_node = Some(node_id); + let mut direct_parent_has_to_match = false; + let mut last_selector_matched = true; + + for (content_group, reason) in CssGroupIterator::new(&css_path.selectors) { + let cur_node_id = match current_node { + Some(c) => c, + None => { + // The node has no parent, but the CSS path + // still has an extra limitation - only valid if the + // next content group is a "*" element + return *content_group == [&CssPathSelector::Global]; + }, + }; + let current_selector_matches = selector_group_matches(&content_group, &html_node_tree[cur_node_id]); + + if direct_parent_has_to_match && !current_selector_matches { + // If the element was a ">" element and the current, + // direct parent does not match, return false + return false; // not executed (maybe this is the bug) + } + + // If the current selector matches, but the previous one didn't, + // that means that the CSS path chain is broken and therefore doesn't match the element + if current_selector_matches && !last_selector_matched { + return false; + } + + // Important: Set if the current selector has matched the element + last_selector_matched = current_selector_matches; + // Select if the next content group has to exactly match or if it can potentially be skipped + direct_parent_has_to_match = reason == DirectChildren; + current_node = node_hierarchy[cur_node_id].parent; + } + + last_selector_matched +} + +struct CssGroupIterator<'a> { + pub css_path: &'a Vec, + pub current_idx: usize, + pub last_reason: CssGroupSplitReason, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum CssGroupSplitReason { + Children, + DirectChildren, +} + +impl<'a> CssGroupIterator<'a> { + pub fn new(css_path: &'a Vec) -> Self { + let initial_len = css_path.len(); + Self { + css_path, + current_idx: initial_len, + last_reason: CssGroupSplitReason::Children, + } + } +} + +impl<'a> Iterator for CssGroupIterator<'a> { + type Item = (CssContentGroup<'a>, CssGroupSplitReason); + + fn next(&mut self) -> Option<(CssContentGroup<'a>, CssGroupSplitReason)> { + use self::CssPathSelector::*; + + let mut new_idx = self.current_idx; + + if new_idx == 0 { + return None; + } + + let mut current_path = Vec::new(); + + while new_idx != 0 { + match self.css_path.get(new_idx - 1)? { + Children => { + self.last_reason = CssGroupSplitReason::Children; + break; + }, + DirectChildren => { + self.last_reason = CssGroupSplitReason::DirectChildren; + break; + }, + other => current_path.push(other), + } + new_idx -= 1; + } + + // NOTE: Order inside of a ContentGroup is not important + // for matching elements, only important for testing + #[cfg(test)] + current_path.reverse(); + + if new_idx == 0 { + if current_path.is_empty() { + None + } else { + // Last element of path + self.current_idx = 0; + Some((current_path, self.last_reason)) + } + } else { + // skip the "Children | DirectChildren" element itself + self.current_idx = new_idx - 1; + Some((current_path, self.last_reason)) + } + } +} + +fn construct_html_cascade_tree<'a, T>( + input: &'a NodeDataContainer>, + node_hierarchy: &NodeHierarchy, + node_depths_sorted: &[(usize, NodeId)], + focused_item: Option, + hovered_items: &BTreeMap, + is_mouse_down: bool +) -> NodeDataContainer> { + + let mut nodes = (0..node_hierarchy.len()).map(|_| HtmlCascadeInfo { + node_data: &input[NodeId::new(0)], + index_in_parent: 0, + is_last_child: false, + is_hovered_over: false, + is_active: false, + is_focused: false, + }).collect::>(); + + for (_depth, parent_id) in node_depths_sorted { + + // Note: :nth-child() starts at 1 instead of 0 + let index_in_parent = parent_id.preceding_siblings(node_hierarchy).count(); + + let is_parent_hovered_over = hovered_items.contains_key(parent_id); + let parent_html_matcher = HtmlCascadeInfo { + node_data: &input[*parent_id], + index_in_parent: index_in_parent, // necessary for nth-child + is_last_child: node_hierarchy[*parent_id].next_sibling.is_none(), // Necessary for :last selectors + is_hovered_over: is_parent_hovered_over, + is_active: is_parent_hovered_over && is_mouse_down, + is_focused: focused_item == Some(*parent_id), + }; + + nodes[parent_id.index()] = parent_html_matcher; + + for (child_idx, child_id) in parent_id.children(node_hierarchy).enumerate() { + let is_child_hovered_over = hovered_items.contains_key(&child_id); + let child_html_matcher = HtmlCascadeInfo { + node_data: &input[child_id], + index_in_parent: child_idx + 1, // necessary for nth-child + is_last_child: node_hierarchy[child_id].next_sibling.is_none(), + is_hovered_over: is_child_hovered_over, + is_active: is_child_hovered_over && is_mouse_down, + is_focused: focused_item == Some(child_id), + }; + + nodes[child_id.index()] = child_html_matcher; + } + } + + NodeDataContainer { internal: nodes } +} + +/// In order to support :hover, the element must have a TagId, otherwise it +/// will be disregarded in the hit-testing. A hover group +#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)] +pub struct HoverGroup { + /// Whether any property in the hover group will trigger a re-layout. + /// This is important for creating + pub affects_layout: bool, + /// Whether this path ends with `:active` or with `:hover` + pub active_or_hover: ActiveHover, +} + +/// Sets whether an element needs to be selected for `:active` or for `:hover` +#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)] +pub enum ActiveHover { + Active, + Hover, +} + +/// Returns all CSS paths that have a `:hover` or `:active` in their path +/// (since they need to have tags for hit-testing) +fn collect_hover_groups(css: &Css) -> BTreeMap { + use azul_css::{CssPathSelector::*, CssPathPseudoSelector::*}; + + let hover_rule = PseudoSelector(Hover); + let active_rule = PseudoSelector(Active); + + // Filter out all :hover and :active rules, since we need to create tags + // for them after the main CSS styling has been done + css.rules().filter_map(|rule_block| { + let pos = rule_block.path.selectors.iter().position(|x| *x == hover_rule || *x == active_rule)?; + if rule_block.declarations.is_empty() { + return None; + } + + let active_or_hover = match rule_block.path.selectors.get(pos)? { + PseudoSelector(Hover) => ActiveHover::Hover, + PseudoSelector(Active) => ActiveHover::Active, + _ => return None, + }; + + let css_path = CssPath { selectors: rule_block.path.selectors.iter().cloned().take(pos).collect() }; + let hover_group = HoverGroup { + affects_layout: rule_block.declarations.iter().any(|hover_rule| hover_rule.can_trigger_relayout()), + active_or_hover, + }; + Some((css_path, hover_group)) + }).collect() +} + +/// In order to figure out on which nodes to insert the :hover and :active hit-test tags, +/// we need to select all items that have a :hover or :active tag. +fn match_hover_selectors<'a, T>( + hover_selectors: BTreeMap, + node_hierarchy: &NodeHierarchy, + html_node_tree: &NodeDataContainer>, +) -> BTreeMap +{ + let mut btree_map = BTreeMap::new(); + + for (css_path, hover_selector) in hover_selectors { + btree_map.extend( + html_node_tree + .linear_iter() + .filter(|node_id| matches_html_element(&css_path, *node_id, node_hierarchy, html_node_tree)) + .map(|node_id| (node_id, hover_selector)) + ); + } + + btree_map +} + +/// Matches a single group of items, panics on Children or DirectChildren selectors +/// +/// The intent is to "split" the CSS path into groups by selectors, then store and cache +/// whether the direct or any parent has matched the path correctly +fn selector_group_matches<'a, T>(selectors: &[&CssPathSelector], html_node: &HtmlCascadeInfo<'a, T>) -> bool { + use self::CssPathSelector::*; + + for selector in selectors { + match selector { + Global => { }, + Type(t) => { + if html_node.node_data.node_type.get_path() != *t { + return false; + } + }, + Class(c) => { + if !html_node.node_data.classes.iter().any(|class| class.equals_str(c)) { + return false; + } + }, + Id(id) => { + if !html_node.node_data.ids.iter().any(|html_id| html_id.equals_str(id)) { + return false; + } + }, + PseudoSelector(CssPathPseudoSelector::First) => { + // Notice: index_in_parent is 1-indexed + if html_node.index_in_parent != 1 { return false; } + }, + PseudoSelector(CssPathPseudoSelector::Last) => { + // Notice: index_in_parent is 1-indexed + if !html_node.is_last_child { return false; } + }, + PseudoSelector(CssPathPseudoSelector::NthChild(x)) => { + match *x { + Number(value) => if html_node.index_in_parent != value { return false; }, + Even => if html_node.index_in_parent % 2 == 0 { return false; }, + Odd => if html_node.index_in_parent % 2 == 1 { return false; }, + Pattern { repeat, offset } => if html_node.index_in_parent >= offset && + ((html_node.index_in_parent - offset) % repeat != 0) { return false; }, + } + }, + PseudoSelector(CssPathPseudoSelector::Hover) => { + if !html_node.is_hovered_over { return false; } + }, + PseudoSelector(CssPathPseudoSelector::Active) => { + if !html_node.is_active { return false; } + }, + PseudoSelector(CssPathPseudoSelector::Focus) => { + if !html_node.is_focused { return false; } + }, + DirectChildren | Children => { + panic!("Unreachable: DirectChildren or Children in CSS path!"); + }, + } + } + + true +} + +pub(crate) fn match_dom_selectors( + ui_state: &UiState, + css: &Css, + focused_node: &mut Option, + pending_focus_target: &mut Option, + hovered_nodes: &BTreeMap, + is_mouse_down: bool, +) -> UiDescription { + + use azul_css::CssDeclaration; + + let non_leaf_nodes = ui_state.dom.arena.node_layout.get_parents_sorted_by_depth(); + + let mut html_tree = construct_html_cascade_tree( + &ui_state.dom.arena.node_data, + &ui_state.dom.arena.node_layout, + &non_leaf_nodes, + *focused_node, + hovered_nodes, + is_mouse_down, + ); + + // Update the current focused field if the callbacks of the + // previous frame has overridden the focus field + update_focus_from_callbacks( + pending_focus_target, + focused_node, + &ui_state.dom.arena.node_layout, + &mut html_tree, + ); + + // First, apply all rules normally (no inheritance) of CSS values + // This is an O(n^2) operation, but it can be parallelized in the future + let mut styled_nodes = ui_state.dom.arena.node_data.transform(|_, node_id| StyledNode { + css_constraints: css + .rules() + .filter(|rule| matches_html_element(&rule.path, node_id, &ui_state.dom.arena.node_layout, &html_tree)) + .flat_map(|matched_rule| matched_rule.declarations.iter().map(|declaration| (declaration.get_type(), declaration.clone()))) + .collect(), + }); + + // Then, inherit all values of the parent to the children, but only if the property is + // inheritable and isn't yet set. NOTE: This step can't be parallelized! + for (_depth, parent_id) in non_leaf_nodes { + + let inherited_rules: Vec = styled_nodes[parent_id].css_constraints.values().filter(|prop| prop.is_inheritable()).cloned().collect(); + if inherited_rules.is_empty() { + continue; + } + + for child_id in parent_id.children(&ui_state.dom.arena.node_layout) { + for inherited_rule in &inherited_rules { + // Only override the rule if the child already has an inherited rule, don't override it + let inherited_rule_type = inherited_rule.get_type(); + styled_nodes[child_id].css_constraints.entry(inherited_rule_type).or_insert_with(|| inherited_rule.clone()); + } + } + } + + // In order to hit-test :hover and :active nodes, need to select them + // first (to insert their TagId later) + let selected_hover_nodes = match_hover_selectors( + collect_hover_groups(css), + &ui_state.dom.arena.node_layout, + &html_tree, + ); + + UiDescription { + + // NOTE: this clone is necessary, otherwise we wouldn't be able to + // update the UiState + // + // WARNING: The UIState can modify the `arena` with its copy of the Rc ! + // Be careful about appending things to the arena, since that could modify + // the UiDescription without you knowing! + // + // NOTE: This deep-clones the entire arena, which may be a + // performance-sensitive operation! + + ui_descr_arena: ui_state.dom.arena.clone(), + dynamic_css_overrides: ui_state.dynamic_css_overrides.clone(), + ui_descr_root: ui_state.dom.root, + styled_nodes, + selected_hover_nodes, + } +} + +/// Update the WindowStates focus node in case the previous +/// frames callbacks set the focus to a specific node +/// +/// Takes the `WindowState.pending_focus_target` and `WindowState.focused_node` +/// and updates the `WindowState.focused_node` accordingly. +/// Should be called before `` +fn update_focus_from_callbacks<'a, T: 'a>( + pending_focus_target: &mut Option, + focused_node: &mut Option, + node_hierarchy: &NodeHierarchy, + html_node_tree: &mut NodeDataContainer>, +) { + // `pending_focus_target` is `None` in most cases, since usually the callbacks + // don't mess with the current focused item. + let new_focus_target = match pending_focus_target { + Some(s) => s.clone(), + None => return, + }; + + match new_focus_target { + FocusTarget::Id(node_id) => { + if html_node_tree.len() < node_id.index() { + *focused_node = Some(node_id); + } else { + warn!("Focusing on node with invalid ID: {}", node_id); + } + }, + FocusTarget::NoFocus => { *focused_node = None; }, + FocusTarget::Path(css_path) => { + if let Some(new_focused_node_id) = html_node_tree.linear_iter() + .find(|node_id| matches_html_element(&css_path, *node_id, &node_hierarchy, &html_node_tree)) { + *focused_node = Some(new_focused_node_id); + } else { + warn!("Could not find focus node for path: {}", css_path); + } + }, + } + + // Set all items to None, no matter what - this takes care of clearing the current + // focused item, in case the `pending_focus_target` is set to `Some(FocusTarget::NoFocus)`. + for html_node in &mut html_node_tree.internal { + html_node.is_focused = false; + } + + if let Some(focused_node) = focused_node { + html_node_tree[*focused_node].is_focused = true; + } + + *pending_focus_target = None; +} + +#[test] +fn test_case_issue_93() { + + use azul_css::CssPathSelector::*; + use azul_css::*; + use prelude::*; + + struct DataModel; + + fn render_tab() -> Dom { + Dom::div().with_class("tabwidget-tab") + .with_child(Dom::label("").with_class("tabwidget-tab-label")) + .with_child(Dom::label("").with_class("tabwidget-tab-close")) + } + + let dom = Dom::div().with_id("editor-rooms") + .with_child( + Dom::div().with_class("tabwidget-bar") + .with_child(render_tab().with_class("active")) + .with_child(render_tab()) + .with_child(render_tab()) + .with_child(render_tab()) + ); + + let tab_active_close = CssPath { selectors: vec![ + Class("tabwidget-tab".into()), + Class("active".into()), + Children, + Class("tabwidget-tab-close".into()) + ] }; + + let node_hierarchy = &dom.arena.node_layout; + let nodes_sorted = node_hierarchy.get_parents_sorted_by_depth(); + let html_node_tree = construct_html_cascade_tree( + &dom.arena.node_data, + &node_hierarchy, + &nodes_sorted, + None, + &BTreeMap::new(), + false, + ); + + // rules: [ + // ".tabwidget-tab-label" : ColorU::BLACK, + // ".tabwidget-tab.active .tabwidget-tab-label" : ColorU::WHITE, + // ".tabwidget-tab.active .tabwidget-tab-close" : ColorU::RED, + // ] + + // 0: [div #editor-rooms ] + // |-- 1: [div .tabwidget-bar] + // | |-- 2: [div .tabwidget-tab .active] + // | | |-- 3: [p .tabwidget-tab-label] + // | | |-- 4: [p .tabwidget-tab-close] + // | |-- 5: [div .tabwidget-tab] + // | | |-- 6: [p .tabwidget-tab-label] + // | | |-- 7: [p .tabwidget-tab-close] + // | |-- 8: [div .tabwidget-tab] + // | | |-- 9: [p .tabwidget-tab-label] + // | | |-- 10: [p .tabwidget-tab-close] + // | |-- 11: [div .tabwidget-tab] + // | | |-- 12: [p .tabwidget-tab-label] + // | | |-- 13: [p .tabwidget-tab-close] + + // Test 1: + // ".tabwidget-tab.active .tabwidget-tab-label" + // should not match + // ".tabwidget-tab.active .tabwidget-tab-close" + assert_eq!(matches_html_element(&tab_active_close, NodeId::new(3), &node_hierarchy, &html_node_tree), false); + + // Test 2: + // ".tabwidget-tab.active .tabwidget-tab-close" + // should match + // ".tabwidget-tab.active .tabwidget-tab-close" + assert_eq!(matches_html_element(&tab_active_close, NodeId::new(4), &node_hierarchy, &html_node_tree), true); +} + +#[test] +fn test_css_group_iterator() { + + use self::CssPathSelector::*; + use azul_css::NodeTypePath; + + // ".hello > #id_text.new_class div.content" + // -> ["div.content", "#id_text.new_class", ".hello"] + let selectors = vec![ + Class("hello".into()), + DirectChildren, + Id("id_test".into()), + Class("new_class".into()), + Children, + Type(NodeTypePath::Div), + Class("content".into()), + ]; + + let mut it = CssGroupIterator::new(&selectors); + + assert_eq!(it.next(), Some((vec![ + &Type(NodeTypePath::Div), + &Class("content".into()), + ], CssGroupSplitReason::Children))); + + assert_eq!(it.next(), Some((vec![ + &Id("id_test".into()), + &Class("new_class".into()), + ], CssGroupSplitReason::DirectChildren))); + + assert_eq!(it.next(), Some((vec![ + &Class("hello".into()), + ], CssGroupSplitReason::DirectChildren))); // technically not correct + + assert_eq!(it.next(), None); + + // Test single class + let selectors_2 = vec![ + Class("content".into()), + ]; + + let mut it = CssGroupIterator::new(&selectors_2); + + assert_eq!(it.next(), Some((vec![ + &Class("content".into()), + ], CssGroupSplitReason::Children))); + + assert_eq!(it.next(), None); +} \ No newline at end of file diff --git a/azul/src/text_layout.rs b/azul/src/text_layout.rs new file mode 100644 index 000000000..b9bd6549a --- /dev/null +++ b/azul/src/text_layout.rs @@ -0,0 +1,1210 @@ +#![allow(unused_variables, dead_code)] + +use azul_css::{ + StyleTextAlignmentHorz, StyleTextAlignmentVert, ScrollbarInfo, +}; +pub use webrender::api::{ + GlyphInstance, LayoutSize, LayoutRect, LayoutPoint, +}; +pub use harfbuzz_sys::{hb_glyph_info_t as GlyphInfo, hb_glyph_position_t as GlyphPosition}; + +pub type WordIndex = usize; +pub type GlyphIndex = usize; +pub type LineLength = f32; +pub type IndexOfLineBreak = usize; +pub type RemainingSpaceToRight = f32; +pub type LineBreaks = Vec<(GlyphIndex, RemainingSpaceToRight)>; + +const DEFAULT_LINE_HEIGHT: f32 = 1.0; +const DEFAULT_WORD_SPACING: f32 = 1.0; +const DEFAULT_LETTER_SPACING: f32 = 0.0; +const DEFAULT_TAB_WIDTH: f32 = 4.0; + +/// Text broken up into `Tab`, `Word()`, `Return` characters +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Words { + pub items: Vec, + // NOTE: Can't be a string, because it wouldn't be possible to take substrings + // (since in UTF-8, multiple characters can be encoded in one byte). + internal_str: String, + internal_chars: Vec, +} + +impl Words { + + pub fn get_substr(&self, word: &Word) -> String { + self.internal_chars[word.start..word.end].iter().collect() + } + + pub fn get_str(&self) -> &str { + &self.internal_str + } + + pub fn get_char(&self, idx: usize) -> Option { + self.internal_chars.get(idx).cloned() + } +} + +/// Section of a certain type +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Word { + pub start: usize, + pub end: usize, + pub word_type: WordType, +} + +/// Either a white-space delimited word, tab or return character +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum WordType { + /// Encountered a word (delimited by spaces) + Word, + // `\t` or `x09` + Tab, + /// `\r`, `\n` or `\r\n`, escaped: `\x0D`, `\x0A` or `\x0D\x0A` + Return, + /// Space character + Space, +} + +/// A paragraph of words that are shaped and scaled (* but not yet layouted / positioned*!) +/// according to their final size in pixels. +#[derive(Debug, Clone)] +pub struct ScaledWords { + /// Font size (in pixels) that was used to scale these words + pub font_size_px: f32, + /// Words scaled to their appropriate font size, but not yet positioned on the screen + pub items: Vec, + /// Longest word in the `self.scaled_words`, necessary for + /// calculating overflow rectangles. + pub longest_word_width: f32, + /// Horizontal advance of the space glyph + pub space_advance_px: f32, + /// Glyph index of the space character + pub space_codepoint: u32, +} + +/// Word that is scaled (to a font / font instance), but not yet positioned +#[derive(Debug, Clone)] +pub struct ScaledWord { + /// Glyphs, positions are relative to the first character of the word + pub glyph_infos: Vec, + /// Horizontal advances of each glyph, necessary for + /// hit-testing characters later on (for text selection). + pub glyph_positions: Vec, + /// The sum of the width of all the characters in this word + pub word_width: f32, +} + +/// Stores the positions of the vertically laid out texts +#[derive(Debug, Clone, PartialEq)] +pub struct WordPositions { + /// Font size that was used to layout this text (value in pixels) + pub font_size_px: f32, + /// Options like word spacing, character spacing, etc. that were + /// used to layout these glyphs + pub text_layout_options: TextLayoutOptions, + /// Stores the positions of words. + pub word_positions: Vec, + /// Index of the word at which the line breaks + length of line + /// (useful for text selection + horizontal centering) + pub line_breaks: Vec<(WordIndex, LineLength)>, + /// Horizontal width of the last line (in pixels), necessary for inline layout later on, + /// so that the next text run can contine where the last text run left off. + /// + /// Usually, the "trailing" of the current text block is the "leading" of the + /// next text block, to make it seem like two text runs push into each other. + pub trailing: f32, + /// How many words are in the text? + pub number_of_words: usize, + /// How many lines (NOTE: virtual lines, meaning line breaks in the layouted text) are there? + pub number_of_lines: usize, + /// Horizontal and vertical boundaries of the layouted words. + /// + /// Note that the vertical extent can be larger than the last words' position, + /// because of trailing negative glyph advances. + pub content_size: LayoutSize, +} + +/// Width and height of the scrollbars at the side of the text field. +/// +/// This information is necessary in order to reserve space at +/// the side of the text field so that the text doesn't overlap the scrollbar. +/// In some cases (when the scrollbar is set to "auto"), the scrollbar space +/// is only taken up when the text overflows the rectangle itself. +#[derive(Debug, Default, Clone, PartialEq, PartialOrd)] +pub struct ScrollbarStyle { + /// Vertical scrollbar style, if any + pub horizontal: Option, + /// Horizontal scrollbar style, if any + pub vertical: Option, +} + +/// Layout options that can impact the flow of word positions +#[derive(Debug, Clone, PartialEq, Default)] +pub struct TextLayoutOptions { + /// Multiplier for the line height, default to 1.0 + pub line_height: Option, + /// Additional spacing between glyphs (in pixels) + pub letter_spacing: Option, + /// Additional spacing between words (in pixels) + pub word_spacing: Option, + /// How many spaces should a tab character emulate + /// (multiplying value, i.e. `4.0` = one tab = 4 spaces)? + pub tab_width: Option, + /// Maximum width of the text (in pixels) - if the text is set to `overflow:visible`, set this to None. + pub max_horizontal_width: Option, + /// How many pixels of leading does the first line have? Note that this added onto to the holes, + /// so for effects like `:first-letter`, use a hole instead of a leading. + pub leading: Option, + /// This is more important for inline text layout where items can punch "holes" + /// into the text flow, for example an image that floats to the right. + /// + /// TODO: Currently unused! + pub holes: Vec, +} + +/// Given the scale of words + the word positions, lays out the words in a +#[derive(Debug, Clone, PartialEq)] +pub struct LeftAlignedGlyphs<'a> { + /// Width that was used to layout these glyphs (or None if the text has overflow:visible) + pub max_horizontal_width: Option, + /// Actual glyph instances, copied + pub glyphs: Vec<&'a GlyphInstance>, + /// Rectangles of the different lines, necessary for text selection + /// and hovering over text, etc. + pub line_rects: &'a Vec, + /// Horizontal and vertical extent of the text + pub text_bbox: LayoutSize, +} + +/// Returns the layouted glyph instances +#[derive(Debug, Clone, PartialEq)] +pub struct LayoutedGlyphs { + pub glyphs: Vec, +} + +/// Whether the text overflows the parent rectangle, and if yes, by how many pixels, +/// necessary for determining if / how to show a scrollbar + aligning / centering text. +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] +pub enum TextOverflow { + /// Text is overflowing, by how much (in pixels)? + IsOverflowing(f32), + /// Text is in bounds, how much space is available until the edge of the rectangle (in pixels)? + InBounds(f32), +} + +/// Iterator over glyphs that returns information about the cluster that this glyph belongs to. +/// Returned by the `ScaledWord::cluster_iter()` function. +/// +/// For each glyph, returns information about what cluster this glyph belongs to. Useful for +/// doing operations per-cluster instead of per-glyph. +/// *Note*: The iterator returns once-per-glyph, not once-per-cluster, however +/// you can merge the clusters into groups by using the `ClusterInfo.cluster_idx`. +#[derive(Debug, Clone)] +pub struct ClusterIterator<'a> { + /// What codepoint does the current glyph have - set to `None` if the first character isn't yet processed. + cur_codepoint: Option, + /// What cluster *index* are we currently at - default: 0 + cluster_count: usize, + word: &'a ScaledWord, + /// Store what glyph we are currently processing in this word + cur_glyph_idx: usize, +} + +/// Info about what cluster a certain glyph belongs to. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ClusterInfo { + /// Cluster index in this word + pub cluster_idx: usize, + /// Codepoint of this cluster + pub codepoint: u32, + /// What the glyph index of this cluster is + pub glyph_idx: usize, +} + +impl<'a> Iterator for ClusterIterator<'a> { + + type Item = ClusterInfo; + + /// Returns an iterator over the clusters in this word. + /// + /// Note: This will return one `ClusterInfo` per glyph, so you can't just + /// use `.cluster_iter().count()` to count the glyphs: Instead, use `.cluster_iter().last().cluster_idx`. + fn next(&mut self) -> Option { + + let next_glyph = self.word.glyph_infos.get(self.cur_glyph_idx)?; + + let glyph_idx = self.cur_glyph_idx; + + if self.cur_codepoint != Some(next_glyph.cluster) { + self.cur_codepoint = Some(next_glyph.cluster); + self.cluster_count += 1; + } + + self.cur_glyph_idx += 1; + + Some(ClusterInfo { + cluster_idx: self.cluster_count, + codepoint: self.cur_codepoint.unwrap_or(0), + glyph_idx, + }) + } +} + +impl ScaledWord { + + /// Creates an iterator over clusters instead of glyphs + pub fn cluster_iter<'a>(&'a self) -> ClusterIterator<'a> { + ClusterIterator { + cur_codepoint: None, + cluster_count: 0, + word: &self, + cur_glyph_idx: 0, + } + } + + pub fn number_of_clusters(&self) -> usize { + self.cluster_iter().last().map(|l| l.cluster_idx).unwrap_or(0) + } +} + +/// Splits the text by whitespace into logical units (word, tab, return, whitespace). +pub fn split_text_into_words(text: &str) -> Words { + + use unicode_normalization::UnicodeNormalization; + + // Necessary because we need to handle both \n and \r\n characters + // If we just look at the characters one-by-one, this wouldn't be possible. + let normalized_string = text.nfc().collect::(); + let normalized_chars = normalized_string.chars().collect::>(); + + let mut words = Vec::new(); + + // Instead of storing the actual word, the word is only stored as an index instead, + // which reduces allocations and is important for later on introducing RTL text + // (where the position of the character data does not correspond to the actual glyph order). + let mut current_word_start = 0; + let mut last_char_idx = 0; + let mut last_char_was_whitespace = false; + + let char_len = normalized_chars.len(); + + for (ch_idx, ch) in normalized_chars.iter().enumerate() { + + let ch = *ch; + let current_char_is_whitespace = ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n'; + + let should_push_delimiter = match ch { + ' ' => { + Some(Word { + start: last_char_idx + 1, + end: ch_idx + 1, + word_type: WordType::Space + }) + }, + '\t' => { + Some(Word { + start: last_char_idx + 1, + end: ch_idx + 1, + word_type: WordType::Tab + }) + }, + '\n' => { + Some(if normalized_chars[last_char_idx] == '\r' { + // "\r\n" return + Word { + start: last_char_idx, + end: ch_idx + 1, + word_type: WordType::Return, + } + } else { + // "\n" return + Word { + start: last_char_idx + 1, + end: ch_idx + 1, + word_type: WordType::Return, + } + }) + }, + _ => None, + }; + + // Character is a whitespace or the character is the last character in the text (end of text) + let should_push_word = if current_char_is_whitespace && !last_char_was_whitespace { + Some(Word { + start: current_word_start, + end: ch_idx, + word_type: WordType::Word + }) + } else { + None + }; + + if current_char_is_whitespace { + current_word_start = ch_idx + 1; + } + + let mut push_words = |arr: [Option;2]| { + words.extend(arr.into_iter().filter_map(|e| *e)); + }; + + push_words([should_push_word, should_push_delimiter]); + + last_char_was_whitespace = current_char_is_whitespace; + last_char_idx = ch_idx; + } + + // Push the last word + if current_word_start != last_char_idx + 1 { + words.push(Word { + start: current_word_start, + end: normalized_chars.len(), + word_type: WordType::Word + }); + } + + // If the last item is a `Return`, remove it + if let Some(Word { word_type: WordType::Return, .. }) = words.last() { + words.pop(); + } + + Words { + items: words, + internal_str: normalized_string, + internal_chars: normalized_chars, + } +} + +/// Takes a text broken into semantic items and a font instance and +/// scales the font accordingly. +pub fn words_to_scaled_words( + words: &Words, + font_bytes: &[u8], + font_index: u32, + font_size_px: f32, +) -> ScaledWords { + + use text_shaping::{self, HbBuffer, HbFont, HbScaledFont}; + + let hb_font = HbFont::from_bytes(font_bytes, font_index); + let hb_scaled_font = HbScaledFont::from_font(&hb_font, font_size_px); + + // Get the dimensions of the space glyph + let hb_space_buffer = HbBuffer::from_str(" "); + let hb_shaped_space = text_shaping::shape_word_hb(&hb_space_buffer, &hb_scaled_font); + let space_advance_px = hb_shaped_space.glyph_positions[0].x_advance as f32 / 128.0; // TODO: Half width for spaces? + let space_codepoint = hb_shaped_space.glyph_infos[0].codepoint; + + let hb_buffer_entire_paragraph = HbBuffer::from_str(&words.internal_str); + let hb_shaped_entire_paragraph = text_shaping::shape_word_hb(&hb_buffer_entire_paragraph, &hb_scaled_font); + + let mut shaped_word_positions = Vec::new(); + let mut shaped_word_infos = Vec::new(); + let mut current_word_positions = Vec::new(); + let mut current_word_infos = Vec::new(); + + for i in 0..hb_shaped_entire_paragraph.glyph_positions.len() { + let glyph_info = hb_shaped_entire_paragraph.glyph_infos[i]; + let glyph_position = hb_shaped_entire_paragraph.glyph_positions[i]; + + let is_space = glyph_info.codepoint == space_codepoint; + if is_space { + shaped_word_positions.push(current_word_positions.clone()); + shaped_word_infos.push(current_word_infos.clone()); + current_word_positions.clear(); + current_word_infos.clear(); + } else { + current_word_positions.push(glyph_position); + current_word_infos.push(glyph_info); + } + } + + if !current_word_positions.is_empty() { + shaped_word_positions.push(current_word_positions); + shaped_word_infos.push(current_word_infos); + } + + let mut longest_word_width = 0.0_f32; + + let scaled_words = words.items.iter() + .filter(|w| w.word_type == WordType::Word) + .enumerate() + .filter_map(|(word_idx, word)| { + + let hb_glyph_positions = shaped_word_positions.get(word_idx)?; + let hb_glyph_infos = shaped_word_infos.get(word_idx)?; + + let hb_word_width = text_shaping::get_word_visual_width_hb(&hb_glyph_positions); + let hb_glyph_positions = text_shaping::get_glyph_positions_hb(&hb_glyph_positions); + let hb_glyph_infos = text_shaping::get_glyph_infos_hb(&hb_glyph_infos); + + longest_word_width = longest_word_width.max(hb_word_width.abs()); + + Some(ScaledWord { + glyph_infos: hb_glyph_infos, + glyph_positions: hb_glyph_positions, + word_width: hb_word_width, + }) + }).collect(); + + ScaledWords { + items: scaled_words, + longest_word_width: longest_word_width, + space_advance_px, + space_codepoint, + font_size_px, + } +} + +/// Positions the words on the screen (does not layout any glyph positions!), necessary for estimating +/// the intrinsic width + height of the text content. +pub fn position_words( + words: &Words, + scaled_words: &ScaledWords, + text_layout_options: &TextLayoutOptions, + font_size_px: f32, +) -> WordPositions { + + use self::WordType::*; + use std::f32; + + let space_advance = scaled_words.space_advance_px; + let word_spacing_px = space_advance * text_layout_options.word_spacing.unwrap_or(DEFAULT_WORD_SPACING); + let line_height_px = space_advance * text_layout_options.line_height.unwrap_or(DEFAULT_LINE_HEIGHT); + let tab_width_px = space_advance * text_layout_options.tab_width.unwrap_or(DEFAULT_TAB_WIDTH); + let letter_spacing_px = text_layout_options.letter_spacing.unwrap_or(DEFAULT_LETTER_SPACING); + + let mut line_breaks = Vec::new(); + let mut word_positions = Vec::new(); + + let mut line_number = 0; + let mut line_caret_x = 0.0; + let mut current_word_idx = 0; + + macro_rules! advance_caret {($line_caret_x:expr) => ({ + let caret_intersection = caret_intersects_with_holes( + $line_caret_x, + line_number, + font_size_px, + line_height_px, + &text_layout_options.holes, + text_layout_options.max_horizontal_width, + ); + + if let LineCaretIntersection::PushCaretOntoNextLine(_, _) = caret_intersection { + line_breaks.push((current_word_idx, line_caret_x)); + } + + // Correct and advance the line caret position + advance_caret( + &mut $line_caret_x, + &mut line_number, + caret_intersection, + ); + })} + + advance_caret!(line_caret_x); + + if let Some(leading) = text_layout_options.leading { + line_caret_x += leading; + advance_caret!(line_caret_x); + } + + // NOTE: word_idx increases only on words, not on other symbols! + let mut word_idx = 0; + + macro_rules! handle_word {() => ({ + + let scaled_word = match scaled_words.items.get(word_idx) { + Some(s) => s, + None => continue, + }; + + let reserved_letter_spacing_px = match text_layout_options.letter_spacing { + None => 0.0, + Some(spacing_multiplier) => spacing_multiplier * scaled_word.number_of_clusters().saturating_sub(1) as f32, + }; + + // Calculate where the caret would be for the next word + let word_advance_x = scaled_word.word_width + reserved_letter_spacing_px; + + let mut new_caret_x = line_caret_x + word_advance_x; + + // NOTE: Slightly modified "advance_caret!(new_caret_x);" - due to line breaking behaviour + + let caret_intersection = caret_intersects_with_holes( + new_caret_x, + line_number, + font_size_px, + line_height_px, + &text_layout_options.holes, + text_layout_options.max_horizontal_width, + ); + + let mut is_line_break = false; + if let LineCaretIntersection::PushCaretOntoNextLine(_, _) = caret_intersection { + line_breaks.push((current_word_idx, line_caret_x)); + is_line_break = true; + } + + if !is_line_break { + let line_caret_y = get_line_y_position(line_number, font_size_px, line_height_px); + word_positions.push(LayoutPoint::new(line_caret_x, line_caret_y)); + } + + // Correct and advance the line caret position + advance_caret( + &mut new_caret_x, + &mut line_number, + caret_intersection, + ); + + line_caret_x = new_caret_x; + + // If there was a line break, the position needs to be determined after the line break happened + if is_line_break { + let line_caret_y = get_line_y_position(line_number, font_size_px, line_height_px); + word_positions.push(LayoutPoint::new(line_caret_x, line_caret_y)); + // important! - if the word is pushed onto the next line, the caret has to be + // advanced by that words width! + line_caret_x += word_advance_x; + } + + // NOTE: Word index is increased before pushing, since word indices are 1-indexed + // (so that paragraphs can be selected via "(0..word_index)"). + word_idx += 1; + current_word_idx = word_idx; + })} + + // The last word is a bit special: Any text must have at least one line break! + for word in words.items.iter().take(words.items.len().saturating_sub(1)) { + match word.word_type { + Word => { + handle_word!(); + }, + Return => { + line_breaks.push((current_word_idx, line_caret_x)); + line_number += 1; + let mut new_caret_x = 0.0; + advance_caret!(new_caret_x); + line_caret_x = new_caret_x; + }, + Space => { + let mut new_caret_x = line_caret_x + word_spacing_px; + advance_caret!(new_caret_x); + line_caret_x = new_caret_x; + }, + Tab => { + let mut new_caret_x = line_caret_x + word_spacing_px + tab_width_px; + advance_caret!(new_caret_x); + line_caret_x = new_caret_x; + }, + } + } + + // Handle the last word, but ignore any last Return, Space or Tab characters + for word in &words.items[words.items.len().saturating_sub(1)..] { + if word.word_type == Word { + handle_word!(); + } + line_breaks.push((current_word_idx, line_caret_x)); + } + + let trailing = line_caret_x; + let number_of_lines = line_number + 1; + let number_of_words = current_word_idx + 1; + + let longest_line_width = line_breaks.iter().map(|(_word_idx, line_length)| *line_length).fold(0.0_f32, f32::max); + let content_size_y = get_line_y_position(line_number, font_size_px, line_height_px); + let content_size_x = text_layout_options.max_horizontal_width.unwrap_or(longest_line_width); + let content_size = LayoutSize::new(content_size_x, content_size_y); + + WordPositions { + font_size_px, + text_layout_options: text_layout_options.clone(), + trailing, + number_of_words, + number_of_lines, + content_size, + word_positions, + line_breaks, + } +} + +pub fn get_layouted_glyphs_unpositioned( + word_positions: &WordPositions, + scaled_words: &ScaledWords, +) -> LayoutedGlyphs { + + use text_shaping; + + let mut glyphs = Vec::with_capacity(scaled_words.items.len()); + + let letter_spacing_px = word_positions.text_layout_options.letter_spacing.unwrap_or(0.0); + + for (scaled_word, word_position) in scaled_words.items.iter() + .zip(word_positions.word_positions.iter()) { + glyphs.extend( + text_shaping::get_glyph_instances_hb(&scaled_word.glyph_infos, &scaled_word.glyph_positions) + .into_iter() + .zip(scaled_word.cluster_iter()) + .map(|(mut glyph, cluster_info)| { + glyph.point.x += word_position.x; + glyph.point.y += word_position.y; + glyph.point.x += letter_spacing_px * cluster_info.cluster_idx as f32; + glyph + }) + ) + } + + LayoutedGlyphs { glyphs } +} + +pub fn get_layouted_glyphs_with_horizonal_alignment( + word_positions: &WordPositions, + scaled_words: &ScaledWords, + alignment_horz: StyleTextAlignmentHorz, +) -> (LayoutedGlyphs, LineBreaks) { + let mut glyphs = get_layouted_glyphs_unpositioned(word_positions, scaled_words); + + // Align glyphs horizontal + let line_breaks = get_char_indices(&word_positions, &scaled_words); + align_text_horz(&mut glyphs.glyphs, alignment_horz, &line_breaks); + + (glyphs, line_breaks) +} + +/// Returns the final glyphs and positions them relative to the `rect_offset`, +/// ready for webrender to display +pub fn get_layouted_glyphs( + word_positions: &WordPositions, + scaled_words: &ScaledWords, + alignment_horz: StyleTextAlignmentHorz, + alignment_vert: StyleTextAlignmentVert, + rect_offset: LayoutPoint, + bounding_size_height_px: f32, +) -> LayoutedGlyphs { + + let (mut glyphs, line_breaks) = get_layouted_glyphs_with_horizonal_alignment(word_positions, scaled_words, alignment_horz); + + // Align glyphs vertically + let vertical_overflow = get_vertical_overflow(&word_positions, bounding_size_height_px); + align_text_vert(&mut glyphs.glyphs, alignment_vert, &line_breaks, vertical_overflow); + + add_origin(&mut glyphs.glyphs, rect_offset.x, rect_offset.y); + + glyphs +} + +/// Given a width, returns the vertical height and width of the text +pub fn get_positioned_word_bounding_box(word_positions: &WordPositions) -> LayoutSize { + word_positions.content_size +} + +pub fn get_vertical_overflow(word_positions: &WordPositions, bounding_size_height_px: f32) -> TextOverflow { + let content_size = word_positions.content_size; + if bounding_size_height_px > content_size.height { + TextOverflow::InBounds(bounding_size_height_px - content_size.height) + } else { + TextOverflow::IsOverflowing(content_size.height - bounding_size_height_px) + } +} + +pub fn word_item_is_return(item: &Word) -> bool { + item.word_type == WordType::Return +} + +pub fn text_overflow_is_overflowing(overflow: &TextOverflow) -> bool { + use self::TextOverflow::*; + match overflow { + IsOverflowing(_) => true, + InBounds(_) => false, + } +} + +pub fn get_char_indices(word_positions: &WordPositions, scaled_words: &ScaledWords) -> LineBreaks { + + let width = word_positions.content_size.width; + + if scaled_words.items.is_empty() { + return Vec::new(); + } + + let mut current_glyph_count = 0; + let mut last_word_idx = 0; + + word_positions.line_breaks.iter().map(|(current_word_idx, line_length)| { + let remaining_space_px = width - line_length; + let words = &scaled_words.items[last_word_idx..*current_word_idx]; + let glyphs_in_this_line: usize = words.iter().map(|w| w.glyph_infos.len()).sum::(); + + current_glyph_count += glyphs_in_this_line; + last_word_idx = *current_word_idx; + + (current_glyph_count, remaining_space_px) + }).collect() +} + +/// For a given line number (**NOTE: 0-indexed!**), calculates the Y +/// position of the bottom left corner +pub fn get_line_y_position(line_number: usize, font_size_px: f32, line_height_px: f32) -> f32 { + ((font_size_px + line_height_px) * line_number as f32) + font_size_px +} + +#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)] +enum LineCaretIntersection { + /// OK: Caret does not interset any elements + NoIntersection, + /// In order to not intersect with any holes, the caret needs to + /// be advanced to the position x, but can stay on the same line. + AdvanceCaretTo(f32), + /// Caret needs to advance X number of lines and be positioned + /// with a leading of x + PushCaretOntoNextLine(usize, f32), +} + +/// Check if the caret intersects with any holes and if yes, if the cursor should move to a new line. +/// +/// # Inputs +/// +/// - `line_caret_x`: The current horizontal caret position +/// - `line_number`: The current line number +/// - `holes`: Whether the text should respect any rectangular regions +/// where the text can't flow (preparation for inline / float layout). +/// - `max_width`: Does the text have a restriction on how wide it can be (in pixels) +fn caret_intersects_with_holes( + line_caret_x: f32, + line_number: usize, + font_size_px: f32, + line_height_px: f32, + holes: &[LayoutRect], + max_width: Option, +) -> LineCaretIntersection { + + let mut new_line_caret_x = None; + let mut line_advance = 0; + + // If the caret is outside of the max_width, move it to the start of a new line + if let Some(max_width) = max_width { + if line_caret_x > max_width { + new_line_caret_x = Some(0.0); + line_advance += 1; + } + } + + for hole in holes { + + let mut should_move_caret = false; + let mut current_line_advance = 0; + let mut new_line_number = line_number + current_line_advance; + let mut current_caret = LayoutPoint::new( + new_line_caret_x.unwrap_or(line_caret_x), + get_line_y_position(new_line_number, font_size_px, line_height_px) + ); + + // NOTE: holes need to be sorted by Y origin (from smallest to largest Y), + // and be sorted from left to right + while hole.contains(¤t_caret) { + should_move_caret = true; + if let Some(max_width) = max_width { + if hole.origin.x + hole.size.width >= max_width { + // Need to break the line here + current_line_advance += 1; + new_line_number = line_number + current_line_advance; + current_caret = LayoutPoint::new( + new_line_caret_x.unwrap_or(line_caret_x), + get_line_y_position(new_line_number, font_size_px, line_height_px) + ); + } else { + new_line_number = line_number + current_line_advance; + current_caret = LayoutPoint::new( + hole.origin.x + hole.size.width, + get_line_y_position(new_line_number, font_size_px, line_height_px) + ); + } + } else { + // No max width, so no need to break the line, move the caret to the right side of the hole + new_line_number = line_number + current_line_advance; + current_caret = LayoutPoint::new( + hole.origin.x + hole.size.width, + get_line_y_position(new_line_number, font_size_px, line_height_px) + ); + } + } + + if should_move_caret { + new_line_caret_x = Some(current_caret.x); + line_advance += current_line_advance; + } + } + + if let Some(new_line_caret_x) = new_line_caret_x { + if line_advance == 0 { + LineCaretIntersection::AdvanceCaretTo(new_line_caret_x) + } else { + LineCaretIntersection::PushCaretOntoNextLine(line_advance, new_line_caret_x) + } + } else { + LineCaretIntersection::NoIntersection + } +} + +fn advance_caret(caret: &mut f32, line_number: &mut usize, intersection: LineCaretIntersection) { + use self::LineCaretIntersection::*; + match intersection { + NoIntersection => { }, + AdvanceCaretTo(x) => { *caret = x; }, + PushCaretOntoNextLine(num_lines, x) => { *line_number += num_lines; *caret = x; }, + } +} + +pub fn align_text_horz( + glyphs: &mut [GlyphInstance], + alignment: StyleTextAlignmentHorz, + line_breaks: &[(usize, f32)] +) { + use azul_css::StyleTextAlignmentHorz::*; + + // Text alignment is theoretically very simple: + // + // If we have a bunch of text, such as this (the `glyphs`): + + // ^^^^^^^^^^^^ + // ^^^^^^^^ + // ^^^^^^^^^^^^^^^^ + // ^^^^^^^^^^ + + // and we have information about how much space each line has to the right: + // (the "---" is the space) + + // ^^^^^^^^^^^^---- + // ^^^^^^^^-------- + // ^^^^^^^^^^^^^^^^ + // ^^^^^^^^^^------ + + // Then we can center-align the text, by just taking the "-----", dividing + // it by 2 and moving all characters to the right: + + // --^^^^^^^^^^^^-- + // ----^^^^^^^^---- + // ^^^^^^^^^^^^^^^^ + // ---^^^^^^^^^^--- + + // Same for right-aligned text, but without the "divide by 2 step" + + if line_breaks.is_empty() || glyphs.is_empty() { + return; // ??? maybe a 0-height rectangle? + } + + // // assert that the last info in the line_breaks vec has the same glyph index + // // i.e. the last line has to end with the last glyph + // assert!(glyphs.len() - 1 == line_breaks[line_breaks.len() - 1].0); + + let multiply_factor = match alignment { + Left => return, + Center => 0.5, // move the line by the half width + Right => 1.0, // move the line by the full width + }; + + // If we have the characters "ABC\n\nDEF", this will result in: + // + // [ Glyph(A), Glyph(B), Glyph(C), Glyph(D), Glyph(E), Glyph(F)] + // + // [LineBreak(2), LineBreak(2), LineBreak(5)] + // + // If we'd just shift every character after the line break, we'd get into + // the problem of shifting the 3rd character twice, because of the \n\n. + // + // To avoid the double-line-break problem, we can use ranges: + // + // - from 0..=2, shift the characters at i by X amount + // - from 3..3 (e.g. 0 characters) shift the characters at i by X amount + // - from 3..=5 shift the characters by X amount + // + // Because the middle range selects 0 characters, the shift is effectively + // ignored, which is what we want - because there are no characters to shift. + + let mut start_range_char = 0; + + for (line_break_char, line_break_amount) in line_breaks { + + // NOTE: Inclusive range - beware: off-by-one-errors! + for glyph in &mut glyphs[start_range_char..*line_break_char] { + let old_glyph_x = glyph.point.x; + glyph.point.x += line_break_amount * multiply_factor; + } + start_range_char = *line_break_char; // NOTE: beware off-by-one error - note the +1! + } +} + +pub fn align_text_vert( + glyphs: &mut [GlyphInstance], + alignment: StyleTextAlignmentVert, + line_breaks: &[(usize, f32)], + vertical_overflow: TextOverflow, +){ + use self::TextOverflow::*; + use self::StyleTextAlignmentVert::*; + + if line_breaks.is_empty() || glyphs.is_empty() { + return; + } + + // // Die if we have a line break at a position bigger than the position of the last glyph, + // // because something went horribly wrong! + // // + // // The next unwrap is always safe as line_breaks will have a minimum of one entry! + // assert!(glyphs.len() - 1 == line_breaks.last().unwrap().0); + + let multiply_factor = match alignment { + Top => return, + Center => 0.5, + Bottom => 1.0, + }; + + let space_to_add = match vertical_overflow { + IsOverflowing(_) => return, + InBounds(remaining_space_px) => { + // Total text height (including last leading!) + // All metrics in pixels + (remaining_space_px * multiply_factor) + }, + }; + + glyphs.iter_mut().for_each(|g| g.point.y += space_to_add); +} + +/// Adds the X and Y offset to each glyph in the positioned glyph +pub fn add_origin(positioned_glyphs: &mut [GlyphInstance], x: f32, y: f32) { + for c in positioned_glyphs { + c.point.x += x; + c.point.y += y; + } +} + +#[test] +fn test_split_words() { + + fn print_words(w: &Words) { + println!("-- string: {:?}", w.get_str()); + for item in &w.items { + println!("{:?} - ({}..{}) = {:?}", w.get_substr(item), item.start, item.end, item.word_type); + } + } + + fn string_to_vec(s: String) -> Vec { + s.chars().collect() + } + + fn assert_words(expected: &Words, got_words: &Words) { + for (idx, expected_word) in expected.items.iter().enumerate() { + let got = got_words.items.get(idx); + if got != Some(expected_word) { + println!("expected: "); + print_words(expected); + println!("got: "); + print_words(got_words); + panic!("Expected word idx {} - expected: {:#?}, got: {:#?}", idx, Some(expected_word), got); + } + } + } + + let ascii_str = String::from("abc\tdef \nghi\r\njkl"); + let words_ascii = split_text_into_words(&ascii_str); + let words_ascii_expected = Words { + internal_str: ascii_str.clone(), + internal_chars: string_to_vec(ascii_str), + items: vec![ + Word { start: 0, end: 3, word_type: WordType::Word }, // "abc" - (0..3) = Word + Word { start: 3, end: 4, word_type: WordType::Tab }, // "\t" - (3..4) = Tab + Word { start: 4, end: 7, word_type: WordType::Word }, // "def" - (4..7) = Word + Word { start: 7, end: 8, word_type: WordType::Space }, // " " - (7..8) = Space + Word { start: 8, end: 9, word_type: WordType::Space }, // " " - (8..9) = Space + Word { start: 9, end: 10, word_type: WordType::Return }, // "\n" - (9..10) = Return + Word { start: 10, end: 13, word_type: WordType::Word }, // "ghi" - (10..13) = Word + Word { start: 13, end: 15, word_type: WordType::Return }, // "\r\n" - (13..15) = Return + Word { start: 15, end: 18, word_type: WordType::Word }, // "jkl" - (15..18) = Word + ], + }; + + assert_words(&words_ascii_expected, &words_ascii); + + let unicode_str = String::from("㌊㌋㌌㌍㌎㌏㌐㌑ ㌒㌓㌔㌕㌖㌗"); + let words_unicode = split_text_into_words(&unicode_str); + let words_unicode_expected = Words { + internal_str: unicode_str.clone(), + internal_chars: string_to_vec(unicode_str), + items: vec![ + Word { start: 0, end: 8, word_type: WordType::Word }, // "㌊㌋㌌㌍㌎㌏㌐㌑" + Word { start: 8, end: 9, word_type: WordType::Space }, // " " + Word { start: 9, end: 15, word_type: WordType::Word }, // "㌒㌓㌔㌕㌖㌗" + ], + }; + + assert_words(&words_unicode_expected, &words_unicode); + + let single_str = String::from("A"); + let words_single_str = split_text_into_words(&single_str); + let words_single_str_expected = Words { + internal_str: single_str.clone(), + internal_chars: string_to_vec(single_str), + items: vec![ + Word { start: 0, end: 1, word_type: WordType::Word }, // "A" + ], + }; + + assert_words(&words_single_str_expected, &words_single_str); +} + +#[test] +fn test_get_line_y_position() { + + assert_eq!(get_line_y_position(0, 20.0, 0.0), 20.0); + assert_eq!(get_line_y_position(1, 20.0, 0.0), 40.0); + assert_eq!(get_line_y_position(2, 20.0, 0.0), 60.0); + + // lines: + // 0 - height 20, padding 5 = 20.0 (padding is for the next line) + // 1 - height 20, padding 5 = 45.0 ( = 20 + 20 + 5) + // 2 - height 20, padding 5 = 70.0 ( = 20 + 20 + 5 + 20 + 5) + assert_eq!(get_line_y_position(0, 20.0, 5.0), 20.0); + assert_eq!(get_line_y_position(1, 20.0, 5.0), 45.0); + assert_eq!(get_line_y_position(2, 20.0, 5.0), 70.0); +} + +// Scenario 1: +// +// +---------+ +// |+ ------>|+ +// | | +// +---------+ +// rectangle: 100x200 +// max-width: none, line-height 1.0, font-size: 20 +// cursor is at: 0x, 20y +// expect cursor to advance to 100x, 20y +// +#[test] +fn test_caret_intersects_with_holes_1() { + let line_caret_x = 0.0; + let line_number = 0; + let font_size_px = 20.0; + let line_height_px = 0.0; + let max_width = None; + let holes = vec![LayoutRect::new(LayoutPoint::new(0.0, 0.0), LayoutSize::new(200.0, 100.0))]; + + let result = caret_intersects_with_holes( + line_caret_x, + line_number, + font_size_px, + line_height_px, + &holes, + max_width, + ); + + assert_eq!(result, LineCaretIntersection::AdvanceCaretTo(200.0)); +} + +// Scenario 2: +// +// +---------+ +// |+ -----> | +// |-------> | +// |---------| +// |+ | +// | | +// +---------+ +// rectangle: 100x200 +// max-width: 200px, line-height 1.0, font-size: 20 +// cursor is at: 0x, 20y +// expect cursor to advance to 0x, 100y (+= 4 lines) +// +#[test] +fn test_caret_intersects_with_holes_2() { + let line_caret_x = 0.0; + let line_number = 0; + let font_size_px = 20.0; + let line_height_px = 0.0; + let max_width = Some(200.0); + let holes = vec![LayoutRect::new(LayoutPoint::new(0.0, 0.0), LayoutSize::new(200.0, 100.0))]; + + let result = caret_intersects_with_holes( + line_caret_x, + line_number, + font_size_px, + line_height_px, + &holes, + max_width, + ); + + assert_eq!(result, LineCaretIntersection::PushCaretOntoNextLine(4, 0.0)); +} + +// Scenario 3: +// +// +----------------+ +// | | | +-----> +// |------->+ | +// |------+ | +// | | +// | | +// +----------------+ +// rectangle: 100x200 +// max-width: 400px, line-height 1.0, font-size: 20 +// cursor is at: 450x, 20y +// expect cursor to advance to 200x, 40y (+= 1 lines, leading of 200px) +// +#[test] +fn test_caret_intersects_with_holes_3() { + let line_caret_x = 450.0; + let line_number = 0; + let font_size_px = 20.0; + let line_height_px = 0.0; + let max_width = Some(400.0); + let holes = vec![LayoutRect::new(LayoutPoint::new(0.0, 0.0), LayoutSize::new(200.0, 100.0))]; + + let result = caret_intersects_with_holes( + line_caret_x, + line_number, + font_size_px, + line_height_px, + &holes, + max_width, + ); + + assert_eq!(result, LineCaretIntersection::PushCaretOntoNextLine(1, 200.0)); +} + +// Scenario 4: +// +// +----------------+ +// | + +------+ | +// | | | | +// | | | | +// | +------+ | +// | | +// +----------------+ +// rectangle: 100x200 @ 80.0x, 20.0y +// max-width: 400px, line-height 1.0, font-size: 20 +// cursor is at: 40x, 20y +// expect cursor to not advance at all +// +#[test] +fn test_caret_intersects_with_holes_4() { + let line_caret_x = 40.0; + let line_number = 0; + let font_size_px = 20.0; + let line_height_px = 0.0; + let max_width = Some(400.0); + let holes = vec![LayoutRect::new(LayoutPoint::new(80.0, 20.0), LayoutSize::new(200.0, 100.0))]; + + let result = caret_intersects_with_holes( + line_caret_x, + line_number, + font_size_px, + line_height_px, + &holes, + max_width, + ); + + assert_eq!(result, LineCaretIntersection::NoIntersection); +} diff --git a/azul/src/text_shaping.rs b/azul/src/text_shaping.rs new file mode 100644 index 000000000..c420c2936 --- /dev/null +++ b/azul/src/text_shaping.rs @@ -0,0 +1,272 @@ +//! Contains functions for laying out single words (uses HarfBuzz for context-aware font shaping). +//! Right now, words are laid out on a word-per-word basis, no inter-word font shaping is done. + +use std::{slice, ptr, u32, ops::Deref, os::raw::{c_char, c_uint}}; +use webrender::api::{LayoutPoint, GlyphInstance as WrGlyphInstance}; +use harfbuzz_sys::{ + hb_blob_create, hb_blob_destroy, + hb_font_create, hb_font_destroy, + hb_face_create, hb_face_destroy, + hb_buffer_create, hb_buffer_destroy, + hb_shape, hb_font_set_scale, hb_buffer_add_utf8, hb_ot_font_set_funcs, + hb_buffer_get_glyph_infos, hb_buffer_get_glyph_positions, + hb_buffer_guess_segment_properties, hb_buffer_allocation_successful, + hb_blob_t, hb_memory_mode_t, hb_buffer_t, + hb_glyph_position_t, hb_glyph_info_t, hb_font_t, hb_face_t, + hb_feature_t, hb_tag_t, + HB_MEMORY_MODE_READONLY, +}; + +pub type GlyphInfo = hb_glyph_info_t; +pub type GlyphPosition = hb_glyph_position_t; + +const MEMORY_MODE_READONLY: hb_memory_mode_t = HB_MEMORY_MODE_READONLY; +const HB_SCALE_FACTOR: f32 = 128.0; + +// NOTE: hb_tag_t = u32 +// See: https://github.com/tangrams/harfbuzz-example/blob/master/src/hbshaper.h +// +// Translation of the original HB_TAG macro, defined in: +// https://github.com/harfbuzz/harfbuzz/blob/90dd255e570bf8ea3436e2f29242068845256e55/src/hb-common.h#L89 +// +// NOTE: Minimum required rustc version for const fn is 1.31. +const fn create_hb_tag(tag: (char, char, char, char)) -> hb_tag_t { + (((tag.0 as hb_tag_t) & 0xFF) << 24) | + (((tag.1 as hb_tag_t) & 0xFF) << 16) | + (((tag.2 as hb_tag_t) & 0xFF) << 8) | + (((tag.3 as hb_tag_t) & 0xFF) << 0) +} + +// Kerning operations +const KERN_TAG: hb_tag_t = create_hb_tag(('k', 'e', 'r', 'n')); +// Standard ligature substitution +const LIGA_TAG: hb_tag_t = create_hb_tag(('l', 'i', 'g', 'a')); +// Contextual ligature substitution +const CLIG_TAG: hb_tag_t = create_hb_tag(('c', 'l', 'i', 'g')); + +const FEATURE_KERNING_OFF: hb_feature_t = hb_feature_t { tag: KERN_TAG, value: 0, start: 0, end: u32::MAX }; +const FEATURE_KERNING_ON: hb_feature_t = hb_feature_t { tag: KERN_TAG, value: 1, start: 0, end: u32::MAX }; +const FEATURE_LIGATURE_OFF: hb_feature_t = hb_feature_t { tag: LIGA_TAG, value: 0, start: 0, end: u32::MAX }; +const FEATURE_LIGATURE_ON: hb_feature_t = hb_feature_t { tag: LIGA_TAG, value: 1, start: 0, end: u32::MAX }; +const FEATURE_CLIG_OFF: hb_feature_t = hb_feature_t { tag: CLIG_TAG, value: 0, start: 0, end: u32::MAX }; +const FEATURE_CLIG_ON: hb_feature_t = hb_feature_t { tag: CLIG_TAG, value: 1, start: 0, end: u32::MAX }; + +// NOTE: kerning is a "feature" and has to be specifically turned on. +static ACTIVE_HB_FEATURES: [hb_feature_t;3] = [ + FEATURE_KERNING_ON, + FEATURE_LIGATURE_ON, + FEATURE_CLIG_ON, +]; + +#[derive(Debug, Clone)] +pub struct ShapedWord { + pub glyph_infos: Vec, + pub glyph_positions: Vec, +} + +#[derive(Debug)] +pub struct HbFont<'a> { + font_bytes: &'a [u8], + font_index: u32, + hb_face_bytes: *mut hb_blob_t, + hb_face: *mut hb_face_t, + hb_font: *mut hb_font_t, +} + +impl<'a> HbFont<'a> { + pub fn from_bytes(font_bytes: &'a [u8], font_index: u32) -> Self { + + // Create a HbFont with no destroy function (font is cleaned up by Rust destructor) + + let user_data_ptr = ptr::null_mut(); + let destroy_func = None; + + let font_ptr = font_bytes.as_ptr() as *const i8; + let hb_face_bytes = unsafe { + hb_blob_create(font_ptr, font_bytes.len() as u32, MEMORY_MODE_READONLY, user_data_ptr, destroy_func) + }; + let hb_face = unsafe { hb_face_create(hb_face_bytes, font_index as c_uint) }; + let hb_font = unsafe { hb_font_create(hb_face) }; + unsafe { hb_ot_font_set_funcs(hb_font) }; + + Self { + font_bytes, + font_index, + hb_face_bytes, + hb_face, + hb_font, + } + } +} + +impl<'a> Drop for HbFont<'a> { + fn drop(&mut self) { + unsafe { hb_font_destroy(self.hb_font) }; + unsafe { hb_face_destroy(self.hb_face) }; + // TODO: Is this safe - memory may be deleted twice? + unsafe { hb_blob_destroy(self.hb_face_bytes) }; + } +} + +#[derive(Debug)] +pub struct HbScaledFont<'a> { + pub font: &'a HbFont<'a>, + pub font_size_px: f32, +} + +impl<'a> HbScaledFont<'a> { + /// Create a `HbScaledFont` from a + pub fn from_font(font: &'a HbFont<'a>, font_size_px: f32) -> Self { + let px = (font_size_px * HB_SCALE_FACTOR) as i32; + unsafe { hb_font_set_scale(font.hb_font, px, px) }; + Self { + font, + font_size_px, + } + } +} + +#[derive(Debug)] +pub struct HbBuffer<'a> { + words: &'a str, + hb_buffer: *mut hb_buffer_t, +} + +impl<'a> HbBuffer<'a> { + pub fn from_str(words: &'a str) -> Self { + + let hb_buffer = unsafe { hb_buffer_create() }; + unsafe { hb_buffer_allocation_successful(hb_buffer); }; + let word_ptr = words.as_ptr() as *const c_char; // HB handles UTF-8 + + let word_len = words.len() as i32; + + // NOTE: It's not possible to take a sub-string into a UTF-8 buffer! + + unsafe { + hb_buffer_add_utf8(hb_buffer, word_ptr, word_len, 0, word_len); + // Guess the script, language and direction from the buffer + hb_buffer_guess_segment_properties(hb_buffer); + } + + Self { + words, + hb_buffer, + } + } +} + +impl<'a> Drop for HbBuffer<'a> { + fn drop(&mut self) { + unsafe { hb_buffer_destroy(self.hb_buffer) }; + } +} + +// The glyph infos are allocated by HarfBuzz and freed +// when the font is destroyed. This is a convenience wrapper that +// directly dereferences the internal hb_glyph_info_t and +// hb_glyph_position_t, to avoid extra allocations. +#[derive(Debug)] +pub struct CVec { + ptr: *const T, + len: usize, +} + +impl Deref for CVec { + type Target = [T]; + + fn deref(&self) -> &[T] { + unsafe { slice::from_raw_parts(self.ptr, self.len) } + } +} + +pub type HbGlyphInfo = hb_glyph_info_t; +pub type HbGlyphPosition = hb_glyph_position_t; + +/// Shaped word - memory of the glyph_infos and glyph_positions is owned by HarfBuzz, +/// therefore the `buf` and `font` have to live as least as long as the word is in use. +#[derive(Debug)] +pub struct HbShapedWord<'a> { + pub buf: &'a HbBuffer<'a>, + pub scaled_font: &'a HbScaledFont<'a>, + pub glyph_infos: CVec, + pub glyph_positions: CVec, +} + +pub(crate) fn shape_word_hb<'a>( + text: &'a HbBuffer<'a>, + scaled_font: &'a HbScaledFont<'a>, +) -> HbShapedWord<'a> { + + let features = if ACTIVE_HB_FEATURES.is_empty() { + ptr::null() + } else { + &ACTIVE_HB_FEATURES as *const _ + }; + + let num_features = ACTIVE_HB_FEATURES.len() as u32; + + unsafe { hb_shape(scaled_font.font.hb_font, text.hb_buffer, features, num_features) }; + + let mut glyph_count = 0; + let glyph_infos = unsafe { hb_buffer_get_glyph_infos(text.hb_buffer, &mut glyph_count) }; + + let mut position_count = glyph_count; + let glyph_positions = unsafe { hb_buffer_get_glyph_positions(text.hb_buffer, &mut position_count) }; + + // Assert that there are as many glyph infos as there are glyph positions + assert_eq!(glyph_count, position_count); + + HbShapedWord { + buf: text, + scaled_font, + glyph_infos: CVec { + ptr: glyph_infos, + len: glyph_count as usize, + }, + glyph_positions: CVec { + ptr: glyph_positions, + len: glyph_count as usize, + }, + } +} + +pub(crate) fn get_word_visual_width_hb(glyph_positions: &[GlyphPosition]) -> f32 { + glyph_positions.iter().map(|pos| pos.x_advance as f32 / HB_SCALE_FACTOR).sum() +} + +pub(crate) fn get_glyph_infos_hb(glyph_infos: &[GlyphInfo]) -> Vec { + glyph_infos.iter().cloned().collect() +} + +pub(crate) fn get_glyph_positions_hb(glyph_positions: &[GlyphPosition]) -> Vec { + glyph_positions.iter().cloned().collect() +} + +pub(crate) fn get_glyph_instances_hb( + glyph_infos: &[GlyphInfo], + glyph_positions: &[GlyphPosition], +) -> Vec { + + let mut current_cursor_x = 0.0; + let mut current_cursor_y = 0.0; + + glyph_infos.iter().zip(glyph_positions.iter()).map(|(glyph_info, glyph_pos)| { + let glyph_index = glyph_info.codepoint; + + let x_offset = glyph_pos.x_offset as f32 / HB_SCALE_FACTOR; + let y_offset = glyph_pos.y_offset as f32 / HB_SCALE_FACTOR; + let x_advance = glyph_pos.x_advance as f32 / HB_SCALE_FACTOR; + let y_advance = glyph_pos.y_advance as f32 / HB_SCALE_FACTOR; + + let point = LayoutPoint::new(current_cursor_x + x_offset, current_cursor_y + y_offset); + + current_cursor_x += x_advance; + current_cursor_y += y_advance; + + WrGlyphInstance { + index: glyph_index, + point, + } + }).collect() +} diff --git a/azul/src/traits.rs b/azul/src/traits.rs new file mode 100644 index 000000000..952fcea36 --- /dev/null +++ b/azul/src/traits.rs @@ -0,0 +1,86 @@ +use std::sync::{Arc, Mutex}; +use { + dom::Dom, +}; + +use callbacks::LayoutInfo; + +/// The core trait that has to be implemented for the app model to provide a mapping from an +/// application state to a user interface state (Model -> View). +pub trait Layout { + /// This function is called on each frame - if Azul determines that the screen + /// needs to be redrawn, it calls this function on the application data, in order + /// to get the new UI. This prevents the UI state from getting out-of-sync + /// with the application state. + /// + /// The `layout_info` can give you important information about the window states current + /// size (for example, to return a different DOM depending on the viewport size) as well as + /// giving access to the `AppResources` (in order to look up image IDs or create OpenGL textures). + /// + /// You can append DOMs recursively, i.e. appending a DOM to a DOM - as well as + /// breaking up your rendering into separate functions (to re-use DOM components as widgets). + /// + /// ## Example + /// + /// ```rust + /// use azul::{dom::Dom, traits::Layout, callbacks::LayoutInfo}; + /// + /// struct MyDataModel { } + /// + /// impl Layout for MyDataModel { + /// fn layout(&self, _: LayoutInfo) -> Dom { + /// Dom::label("Hello World!").with_id("my-label") + /// } + /// } + /// + /// // This is done by azul internally + /// // let new_ui = MyDataModel::layout(); + /// ``` + fn layout(&self, layout_info: LayoutInfo) -> Dom where Self: Sized; +} + +/// Convenience trait that allows the `app_state.modify()` - only implemented for +/// `Arc` - shortly locks the app state mutex, modifies it and unlocks +/// it again. +/// +/// Note: Usually when doing asynchronous programming you don't want to block the main +/// UI. While Rust executes the `app_state.modify()` closure, your `AppState` gets +/// locked, meaning that no layout can happen and no other thread or callback can write +/// to the apps data. In order to make your app performant, don't do heavy computations +/// inside the closure, only use it to write or copy data in and out of the application +/// state. +pub trait Modify { + + /// Modifies the app state and then returns if the modification was successful + /// Takes a FnMut that modifies the state + fn modify(&self, closure: F) -> Option<()> where F: FnOnce(&mut T); + + /// Same as `.modify`, but the closure can now copy some data out of the locked data model. + fn modify_clone(&self, closure: F) -> Option where F: FnOnce(&mut T) -> S { + let mut initial: Option = None; + self.modify(|lock| initial = Some(closure(lock)))?; + initial + } + + /// Same as `.modify`, but the closure can now return `Option<()>` + fn modify_opt(&self, closure: F) -> Option<()> where F: FnOnce(&mut T) -> Option<()> { + self.modify_clone(|lock| { + let result: Option<()> = closure(lock); + result + })? + } + /// Same as `.modify_opt`, but the closure returns `Option` instead of `S`. + fn modify_opt_clone(&self, closure: F) -> Option where F: FnOnce(&mut T) -> Option { + let mut initial: Option = None; + self.modify(|lock| initial = closure(lock))?; + initial + } +} + +impl Modify for Arc> { + fn modify(&self, closure: F) -> Option<()> where F: FnOnce(&mut T) { + let mut lock = self.lock().ok()?; + closure(&mut *lock); + Some(()) + } +} diff --git a/azul/src/ui_description.rs b/azul/src/ui_description.rs new file mode 100644 index 000000000..8fd7db738 --- /dev/null +++ b/azul/src/ui_description.rs @@ -0,0 +1,111 @@ +use std::{ + fmt, + collections::BTreeMap, +}; +use azul_css::{ Css, CssDeclaration, CssProperty, CssPropertyType }; +use webrender::api::HitTestItem; +use { + FastHashMap, + id_tree::{Arena, NodeId, NodeDataContainer}, + dom::{Dom, NodeData, DomString}, + ui_state::UiState, + style::HoverGroup, + callbacks::FocusTarget, +}; + +pub struct UiDescription { + pub(crate) ui_descr_arena: Arena>, + /// ID of the root node of the arena (usually NodeId(0)) + pub(crate) ui_descr_root: NodeId, + /// This field is created from the Css + pub(crate) styled_nodes: NodeDataContainer, + /// The style properties that should be overridden for this frame, cloned from the `Css` + pub(crate) dynamic_css_overrides: BTreeMap>, + /// In order to hit-test :hover and :active selectors, need to insert tags for all rectangles + /// that have a non-:hover path, for example if we have `#thing:hover`, then all nodes selected by `#thing` + /// need to get a TagId, otherwise, they can't be hit-tested. + pub(crate) selected_hover_nodes: BTreeMap, +} + +impl fmt::Debug for UiDescription { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "UiDescription {{ \ + ui_descr_arena: {:?}, + ui_descr_root: {:?}, + styled_nodes: {:?}, + dynamic_css_overrides: {:?}, + selected_hover_nodes: {:?}, + }}", + self.ui_descr_arena, + self.ui_descr_root, + self.styled_nodes, + self.dynamic_css_overrides, + self.selected_hover_nodes, + ) + } +} + +impl Clone for UiDescription { + fn clone(&self) -> Self { + Self { + ui_descr_arena: self.ui_descr_arena.clone(), + ui_descr_root: self.ui_descr_root, + styled_nodes: self.styled_nodes.clone(), + dynamic_css_overrides: self.dynamic_css_overrides.clone(), + selected_hover_nodes: self.selected_hover_nodes.clone(), + } + } +} + +impl Default for UiDescription { + fn default() -> Self { + use dom::NodeType; + let default_dom = Dom::new(NodeType::Div); + let hovered_nodes = BTreeMap::new(); + let is_mouse_down = false; + let mut focused_node = None; + let mut focus_target = None; + Self::match_css_to_dom( + &mut default_dom.into_ui_state(), + &Css::default(), + &mut focused_node, + &mut focus_target, + &hovered_nodes, + is_mouse_down, + ) + } +} + +impl UiDescription { + /// Applies the styles to the nodes calculated from the `layout_screen` + /// function and calculates the final display list that is submitted to the + /// renderer. + pub fn match_css_to_dom( + ui_state: &mut UiState, + style: &Css, + focused_node: &mut Option, + pending_focus_target: &mut Option, + hovered_nodes: &BTreeMap, + is_mouse_down: bool, + ) -> Self + { + let ui_description = ::style::match_dom_selectors( + ui_state, + &style, + focused_node, + pending_focus_target, + hovered_nodes, + is_mouse_down + ); + + // Important: Create all the tags for the :hover and :active selectors + ui_state.create_tags_for_hover_nodes(&ui_description.selected_hover_nodes); + ui_description + } +} + +#[derive(Debug, Default, Clone, PartialEq, Hash, PartialOrd, Eq, Ord)] +pub(crate) struct StyledNode { + /// The CSS constraints, after the cascading step + pub(crate) css_constraints: BTreeMap, +} diff --git a/azul/src/ui_solver.rs b/azul/src/ui_solver.rs new file mode 100644 index 000000000..933cd8041 --- /dev/null +++ b/azul/src/ui_solver.rs @@ -0,0 +1,1760 @@ +use std::{f32, collections::BTreeMap}; +use azul_css::{ + LayoutPosition, LayoutMargin, LayoutPadding, + RectLayout, StyleFontSize, RectStyle, + StyleTextAlignmentHorz, StyleTextAlignmentVert, PixelValue, +}; +use app_units::Au; +use { + id_tree::{NodeId, NodeDataContainer, NodeHierarchy}, + display_list::DisplayRectangle, + dom::{NodeData, NodeType}, + app_resources::AppResources, + text_layout::{Words, ScaledWords, TextLayoutOptions, WordPositions}, +}; +use webrender::api::{LayoutRect, LayoutPoint, LayoutSize, FontInstanceKey}; + +const DEFAULT_FLEX_GROW_FACTOR: f32 = 1.0; +const DEFAULT_FONT_SIZE: StyleFontSize = StyleFontSize(PixelValue::const_px(10)); +const DEFAULT_FONT_ID: &str = "sans-serif"; + +type PixelSize = f32; + +#[derive(Debug, Copy, Clone, PartialEq)] +enum WhConstraint { + /// between min, max + Between(f32, f32), + /// Value needs to be exactly X + EqualTo(f32), + /// Value can be anything + Unconstrained, +} + +impl WhConstraint { + + /// Returns the minimum value or 0 on `Unconstrained` + /// (warning: this might not be what you want) + pub fn min_needed_space(&self) -> Option { + use self::WhConstraint::*; + match self { + Between(min, _) => Some(*min), + EqualTo(exact) => Some(*exact), + Unconstrained => None, + } + } + + /// Returns the maximum space until the constraint is violated - returns + /// `None` if the constraint is unbounded + pub fn max_available_space(&self) -> Option { + use self::WhConstraint::*; + match self { + Between(_, max) => { Some(*max) }, + EqualTo(exact) => Some(*exact), + Unconstrained => None, + } + } + + /// Returns if this `WhConstraint` is an `EqualTo` constraint + pub fn is_fixed_constraint(&self) -> bool { + use self::WhConstraint::*; + match self { + EqualTo(_) => true, + _ => false, + } + } +} + +macro_rules! determine_preferred { + ($fn_name:ident, $width:ident, $min_width:ident, $max_width:ident) => ( + + /// - `preferred_inner_width` denotes the preferred width of the width or height got from the + /// from the rectangles content. + /// + /// For example, if you have an image, the `preferred_inner_width` is the images width, + /// if the node type is an text, the `preferred_inner_width` is the text height. + fn $fn_name(layout: &RectLayout, preferred_inner_width: Option) -> WhConstraint { + + let width = layout.$width.map(|w| w.0.to_pixels()); + let min_width = layout.$min_width.map(|w| w.0.to_pixels()); + let max_width = layout.$max_width.map(|w| w.0.to_pixels()); + + // TODO: correct for width / height less than 0 - "negative" width is impossible! + + let (absolute_min, absolute_max) = { + if let (Some(min), Some(max)) = (min_width, max_width) { + if min_width < max_width { + (Some(min), Some(max)) + } else { + // min-width > max_width: max_width wins + (Some(max), Some(max)) + } + } else { + (min_width, max_width) + } + }; + + if let Some(width) = width { + if let Some(max_width) = absolute_max { + if let Some(min_width) = absolute_min { + if min_width < width && width < max_width { + // normal: min_width < width < max_width + WhConstraint::EqualTo(width) + } else if width > max_width { + WhConstraint::EqualTo(max_width) + } else if width < min_width { + WhConstraint::EqualTo(min_width) + } else { + WhConstraint::Unconstrained /* unreachable */ + } + } else { + // width & max_width + WhConstraint::EqualTo(width.min(max_width)) + } + } else if let Some(min_width) = absolute_min { + // no max width, only width & min_width + WhConstraint::EqualTo(width.max(min_width)) + } else { + // no min-width or max-width + WhConstraint::EqualTo(width) + } + } else { + if let Some(width) = preferred_inner_width { + // -- same as the width() block: width takes precedence over + // no width, only min_width and max_width + if let Some(max_width) = absolute_max { + if let Some(min_width) = absolute_min { + if min_width < width && width < max_width { + // normal: min_width < width < max_width + WhConstraint::Between(width, max_width) + } else if width > max_width { + WhConstraint::EqualTo(max_width) + } else if width < min_width { + WhConstraint::EqualTo(min_width) + } else { + WhConstraint::Unconstrained /* unreachable */ + } + } else { + // width & max_width + let min = width.min(max_width); + let max = width.max(max_width); + WhConstraint::Between(min, max) + } + } else if let Some(min_width) = absolute_min { + // no max width, only width & min_width + let min = width.min(min_width); + let max = width.max(min_width); + WhConstraint::Between(min, max) + } else { + // no min-width or max-width + WhConstraint::Between(width, f32::MAX) + } + } else { + // no width, no preferred width, + if let Some(max_width) = absolute_max { + if let Some(min_width) = absolute_min { + WhConstraint::Between(min_width, max_width) + } else { + // TODO: check sign positive on max_width! + WhConstraint::Between(0.0, max_width) + } + } else { + if let Some(min_width) = absolute_min { + WhConstraint::Between(min_width, f32::MAX) + } else { + // no width, min_width or max_width + WhConstraint::Unconstrained + } + } + } + } + }) +} + +// Returns the preferred width, given [width, min_width, max_width] inside a RectLayout +// or `None` if the height can't be determined from the node alone. +// +// fn determine_preferred_width(layout: &RectLayout) -> Option +determine_preferred!(determine_preferred_width, width, min_width, max_width); + +// Returns the preferred height, given [height, min_height, max_height] inside a RectLayout +// or `None` if the height can't be determined from the node alone. +// +// fn determine_preferred_height(layout: &RectLayout) -> Option +determine_preferred!(determine_preferred_height, height, min_height, max_height); + +#[derive(Debug, Copy, Clone, PartialEq)] +struct WidthCalculatedRect { + pub preferred_width: WhConstraint, + pub margin: LayoutMargin, + pub padding: LayoutPadding, + pub flex_grow_px: f32, + pub min_inner_size_px: f32, +} + +impl WidthCalculatedRect { + /// Get the flex basis in the horizontal direction - vertical axis has to be calculated differently + pub fn get_flex_basis_horizontal(&self) -> f32 { + self.preferred_width.min_needed_space().unwrap_or(0.0) + + self.margin.left.map(|px| px.to_pixels()).unwrap_or(0.0) + + self.margin.right.map(|px| px.to_pixels()).unwrap_or(0.0) + + self.padding.left.map(|px| px.to_pixels()).unwrap_or(0.0) + + self.padding.right.map(|px| px.to_pixels()).unwrap_or(0.0) + } + + /// Get the sum of the horizontal padding amount (`padding.left + padding.right`) + pub fn get_horizontal_padding(&self) -> f32 { + self.padding.left.map(|px| px.to_pixels()).unwrap_or(0.0) + + self.padding.right.map(|px| px.to_pixels()).unwrap_or(0.0) + } + + /// Called after solver has run: Solved width of rectangle + pub fn solved_result(&self) -> WidthSolvedResult { + WidthSolvedResult { + min_width: self.min_inner_size_px, + space_added: self.flex_grow_px, + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq)] +struct HeightCalculatedRect { + pub preferred_height: WhConstraint, + pub margin: LayoutMargin, + pub padding: LayoutPadding, + pub flex_grow_px: f32, + pub min_inner_size_px: f32, +} + +impl HeightCalculatedRect { + /// Get the flex basis in the horizontal direction - vertical axis has to be calculated differently + pub fn get_flex_basis_vertical(&self) -> f32 { + self.preferred_height.min_needed_space().unwrap_or(0.0) + + self.margin.top.map(|px| px.to_pixels()).unwrap_or(0.0) + + self.margin.bottom.map(|px| px.to_pixels()).unwrap_or(0.0) + + self.padding.top.map(|px| px.to_pixels()).unwrap_or(0.0) + + self.padding.bottom.map(|px| px.to_pixels()).unwrap_or(0.0) + } + + /// Get the sum of the horizontal padding amount (`padding.top + padding.bottom`) + pub fn get_vertical_padding(&self) -> f32 { + self.padding.top.map(|px| px.to_pixels()).unwrap_or(0.0) + + self.padding.bottom.map(|px| px.to_pixels()).unwrap_or(0.0) + } + + /// Called after solver has run: Solved width of rectangle + pub fn solved_result(&self) -> HeightSolvedResult { + HeightSolvedResult { + min_height: self.min_inner_size_px, + space_added: self.flex_grow_px, + } + } +} + +// `typed_arena!(WidthCalculatedRect, preferred_width, determine_preferred_width, get_horizontal_padding, get_flex_basis_horizontal)` +macro_rules! typed_arena {( + $struct_name:ident, + $preferred_field:ident, + $determine_preferred_fn:ident, + $get_padding_fn:ident, + $get_flex_basis:ident, + $bubble_fn_name:ident, + $main_axis:ident +) => ( + +impl NodeDataContainer<$struct_name> { + + /// Fill out the preferred width of all nodes. + /// + /// We could operate on the Arena directly, but that makes testing very + /// hard since we are only interested in testing or touching the layout. So this makes the + /// calculation maybe a few microseconds slower, but gives better testing capabilities + /// + /// NOTE: Later on, this could maybe be a NodeDataContainer<&'a RectLayout>. + #[must_use] + fn from_rect_layout_arena(node_data: &NodeDataContainer, widths: &NodeDataContainer>) -> Self { + let new_nodes = node_data.internal.iter().enumerate().map(|(node_id, node_data)|{ + let id = NodeId::new(node_id); + $struct_name { + // TODO: get the initial width of the rect content + $preferred_field: $determine_preferred_fn(&node_data, widths[id]), + margin: node_data.margin.unwrap_or_default(), + padding: node_data.padding.unwrap_or_default(), + flex_grow_px: 0.0, + min_inner_size_px: 0.0, + } + }).collect(); + NodeDataContainer { internal: new_nodes } + } + + /// Bubble the inner sizes to their parents - on any parent nodes, fill out + /// the width so that the `preferred_width` can contain the child nodes (if + /// that doesn't violate the constraints of the parent) + fn $bubble_fn_name( + &mut self, + node_hierarchy: &NodeHierarchy, + arena_data: &NodeDataContainer, + non_leaf_nodes: &[(usize, NodeId)]) + { + // Reverse, since we want to go from the inside out (depth 5 needs to be filled out first) + // + // Set the preferred_width of the parent nodes + for (_node_depth, non_leaf_id) in non_leaf_nodes.iter().rev() { + + use self::WhConstraint::*; + + // Sum of the direct children's flex-basis = the parents preferred width + let children_flex_basis = self.sum_children_flex_basis(*non_leaf_id, node_hierarchy, arena_data); + + // Calculate the new flex-basis width + let parent_width_metrics = self[*non_leaf_id]; + + // For calculating the inner width, subtract the parents padding + let parent_padding = self[*non_leaf_id].$get_padding_fn(); + + // If the children are larger than the parents preferred max-width or smaller + // than the parents min-width, adjust + let child_width = match parent_width_metrics.$preferred_field { + Between(min, max) => { + if children_flex_basis > (max - parent_padding) { + max + } else if children_flex_basis < (min + parent_padding) { + min + } else { + children_flex_basis + } + }, + EqualTo(exact) => exact - parent_padding, + Unconstrained => children_flex_basis, + }; + + self[*non_leaf_id].min_inner_size_px = child_width; + } + + // Now, the width of all elements should be filled, + // but they aren't flex-growed or flex-shrinked yet + } + + /// Go from the root down and flex_grow the children if needed - respects the `width`, `min_width` and `max_width` properties + /// The layout step doesn't account for the min_width and max_width constraints, so we have to adjust them manually + fn apply_flex_grow( + &mut self, + node_hierarchy: &NodeHierarchy, + arena_data: &NodeDataContainer, + parent_ids_sorted_by_depth: &[(usize, NodeId)], + root_width: f32 + ) { + use azul_css::LayoutAlignItems; + + debug_assert!(self[NodeId::new(0)].flex_grow_px == 0.0); + + // Set the window width on the root node (since there is only one root node, we can + // calculate the `flex_grow_px` directly) + // + // Usually `top_level_flex_basis` is NOT 0.0, rather it's the sum of all widths in the DOM, + // i.e. the sum of the whole DOM tree + let top_level_flex_basis = self[NodeId::new(0)].min_inner_size_px; + + // The root node can still have some sort of max-width attached, so we need to check for that + let root_preferred_width = if let Some(max_width) = self[NodeId::new(0)].$preferred_field.max_available_space() { + if root_width > max_width { max_width } else { root_width } + } else { + root_width + }; + + self[NodeId::new(0)].flex_grow_px = root_preferred_width - top_level_flex_basis; + + // Keep track of the nearest relative or absolute positioned element + let mut positioned_node_stack = vec![NodeId::new(0)]; + + for (_node_depth, parent_id) in parent_ids_sorted_by_depth { + + use azul_css::{LayoutAxis, LayoutPosition}; + + let parent_node = &arena_data[*parent_id]; + let parent_is_positioned = parent_node.position.unwrap_or_default() != LayoutPosition::Static; + + if parent_is_positioned { + positioned_node_stack.push(*parent_id); + } + + // How much width is there to distribute along the main and cross axis? + let (width_main_axis, width_cross_axis) = { + let parent_width_metrics = &self[*parent_id]; + + let width_horizontal_axis = { + let children_margin: f32 = parent_id.children(node_hierarchy).map(|child_id| arena_data[child_id].get_horizontal_margin()).sum(); + parent_width_metrics.min_inner_size_px + parent_width_metrics.flex_grow_px - parent_node.get_horizontal_padding() - children_margin + }; + + let width_vertical_axis = { + // let children_margin: f32 = parent_id.children(node_hierarchy).map(|child_id| arena_data[child_id].get_vertical_margin()).sum(); + parent_width_metrics.min_inner_size_px + parent_width_metrics.flex_grow_px - parent_node.get_vertical_padding() + }; + + let width_main_axis = match LayoutAxis::$main_axis { + LayoutAxis::Horizontal => width_horizontal_axis, + LayoutAxis::Vertical => width_vertical_axis, + }; + + let width_cross_axis = match LayoutAxis::$main_axis { + LayoutAxis::Horizontal => width_vertical_axis, + LayoutAxis::Vertical => width_horizontal_axis, + }; + + (width_main_axis, width_cross_axis) + }; + + // Only stretch the items, if they have a align-items: stretch! + if parent_node.align_items.unwrap_or_default() == LayoutAlignItems::Stretch { + if parent_node.direction.unwrap_or_default().get_axis() == LayoutAxis::$main_axis { + Self::distribute_space_along_main_axis(parent_id, width_main_axis, node_hierarchy, arena_data, self, &positioned_node_stack); + } else { + Self::distribute_space_along_cross_axis(parent_id, width_cross_axis, node_hierarchy, arena_data, self, &positioned_node_stack); + } + } + + if parent_is_positioned { + positioned_node_stack.pop(); + } + } + } + + /// Returns the sum of the flex-basis of the current nodes' children + fn sum_children_flex_basis( + &self, + node_id: NodeId, + node_hierarchy: &NodeHierarchy, + display_arena: &NodeDataContainer) + -> f32 + { + node_id + .children(node_hierarchy) + .filter(|child_node_id| display_arena[*child_node_id].position != Some(LayoutPosition::Absolute)) + .map(|child_node_id| self[child_node_id].$get_flex_basis()) + .sum() + } + + /// Does the actual width layout, respects the `width`, `min_width` and `max_width` + /// properties as well as the `flex_grow` factor. `flex_shrink` currently does nothing. + fn distribute_space_along_main_axis( + node_id: &NodeId, + width_to_distribute: f32, + node_hierarchy: &NodeHierarchy, + arena_data: &NodeDataContainer, + width_calculated_arena: &mut NodeDataContainer<$struct_name>, + positioned_node_stack: &[NodeId] + ) { + let mut parent_node_inner_width = width_to_distribute; + + // 1. Set all child elements that have an exact width to that width, record their violations + // and add their violation to the leftover horizontal space. + // let mut horizontal_space_from_fixed_width_items = 0.0; + let mut horizontal_space_taken_up_by_fixed_width_items = 0.0; + + { + // Vec<(NodeId, PreferredWidth)> + let exact_width_childs = node_id + .children(node_hierarchy) + .filter_map(|id| if let WhConstraint::EqualTo(exact) = width_calculated_arena[id].$preferred_field { + Some((id, exact)) + } else { + None + }) + .collect::>(); + + for (exact_width_child_id, exact_width) in exact_width_childs { + + // If this child node is `position: absolute`, it doesn't take any space away from + // its siblings, since it is taken out of the regular content flow + if arena_data[exact_width_child_id].position.unwrap_or_default() != LayoutPosition::Absolute { + horizontal_space_taken_up_by_fixed_width_items += exact_width; + } + + // so that node.min_inner_size_px + node.flex_grow_px = exact_width + width_calculated_arena[exact_width_child_id].flex_grow_px = + exact_width - width_calculated_arena[exact_width_child_id].min_inner_size_px; + } + } + + // The fixed-width items are now considered solved, so subtract them out of the width of the parent. + parent_node_inner_width -= horizontal_space_taken_up_by_fixed_width_items; + + // Now we can be sure that if we write #x { width: 500px; } that it will actually be 500px large + // and not be influenced by flex in any way. + + // 2. Set all items to their minimum width. Record how much space is gained by doing so. + let mut horizontal_space_taken_up_by_variable_items = 0.0; + + use FastHashSet; + + let mut variable_width_childs = node_id + .children(node_hierarchy) + .filter(|id| !width_calculated_arena[*id].$preferred_field.is_fixed_constraint()) + .collect::>(); + + let mut absolute_variable_width_nodes = Vec::new(); + + for variable_child_id in &variable_width_childs { + + if arena_data[*variable_child_id].position.unwrap_or_default() != LayoutPosition::Absolute { + + let min_width = width_calculated_arena[*variable_child_id].$preferred_field.min_needed_space().unwrap_or(0.0); + + horizontal_space_taken_up_by_variable_items += min_width; + + // so that node.min_inner_size_px + node.flex_grow_px = min_width + width_calculated_arena[*variable_child_id].flex_grow_px = + min_width - width_calculated_arena[*variable_child_id].min_inner_size_px; + + } else { + + // `position: absolute` items don't take space away from their siblings, rather + // they take the minimum needed space by their content + + let root_id = NodeId::new(0); + let nearest_relative_parent_node = positioned_node_stack.get(positioned_node_stack.len() - 1).unwrap_or(&root_id); + let relative_parent_width = { + let relative_parent_node = &width_calculated_arena[*nearest_relative_parent_node]; + relative_parent_node.flex_grow_px + relative_parent_node.min_inner_size_px + }; + + // By default, absolute positioned elements take the width of their content + // let min_inner_width = width_calculated_arena[*variable_child_id].$preferred_field.min_needed_space().unwrap_or(0.0); + + // The absolute positioned node might have a max-width constraint, which has a + // higher precedence than `top, bottom, left, right`. + let max_space_current_node = match width_calculated_arena[*variable_child_id].$preferred_field { + WhConstraint::EqualTo(e) => e, + WhConstraint::Between(min, max) => { + if relative_parent_width > min { + if relative_parent_width < max { + relative_parent_width + } else { + max + } + } else { + min + } + }, + WhConstraint::Unconstrained => relative_parent_width, + }; + + // so that node.min_inner_size_px + node.flex_grow_px = max_space_current_node + width_calculated_arena[*variable_child_id].flex_grow_px = + max_space_current_node - width_calculated_arena[*variable_child_id].min_inner_size_px; + + absolute_variable_width_nodes.push(*variable_child_id); + } + + } + + // Absolute positioned nodes aren't in the space-to-distribute set + for absolute_node in absolute_variable_width_nodes { + variable_width_childs.remove(&absolute_node); + } + + // This satisfies the `width` and `min_width` constraints. However, we still need to worry about + // the `max_width` and unconstrained children. + // + // By setting the items to their minimum size, we've gained some space that we now need to distribute + // according to the flex_grow values + parent_node_inner_width -= horizontal_space_taken_up_by_variable_items; + + let mut total_horizontal_space_available = parent_node_inner_width; + let mut max_width_violations = Vec::new(); + + loop { + + if total_horizontal_space_available <= 0.0 || variable_width_childs.is_empty() { + break; + } + + // In order to apply flex-grow correctly, we need the sum of + // the flex-grow factors of all the variable-width children + // + // NOTE: variable_width_childs can change its length, have to recalculate every loop! + let children_combined_flex_grow: f32 = variable_width_childs + .iter() + .map(|child_id| + // Prevent flex-grow and flex-shrink to be less than 1 + arena_data[*child_id].flex_grow + .map(|grow| grow.0.get().max(1.0)) + .unwrap_or(DEFAULT_FLEX_GROW_FACTOR)) + .sum(); + + // Grow all variable children by the same amount. + for variable_child_id in &variable_width_childs { + + let flex_grow = arena_data[*variable_child_id].flex_grow + .and_then(|grow| Some(grow.0.get())) + .unwrap_or(DEFAULT_FLEX_GROW_FACTOR); + + // Do not expand the item on "flex-grow: 0" (prevent division by 0) + if flex_grow as usize == 0 { + continue; + } + + let added_space_for_one_child = total_horizontal_space_available * (flex_grow / children_combined_flex_grow); + + let current_width_of_child = width_calculated_arena[*variable_child_id].min_inner_size_px + + width_calculated_arena[*variable_child_id].flex_grow_px; + + if let Some(max_width) = width_calculated_arena[*variable_child_id].$preferred_field.max_available_space() { + if (current_width_of_child + added_space_for_one_child) > max_width { + // so that node.min_inner_size_px + node.flex_grow_px = max_width + width_calculated_arena[*variable_child_id].flex_grow_px = + max_width - width_calculated_arena[*variable_child_id].min_inner_size_px; + + max_width_violations.push(*variable_child_id); + } else { + // so that node.min_inner_size_px + node.flex_grow_px = added_space_for_one_child + width_calculated_arena[*variable_child_id].flex_grow_px = + added_space_for_one_child - width_calculated_arena[*variable_child_id].min_inner_size_px; + } + } else { + // so that node.min_inner_size_px + node.flex_grow_px = added_space_for_one_child + width_calculated_arena[*variable_child_id].flex_grow_px = + added_space_for_one_child - width_calculated_arena[*variable_child_id].min_inner_size_px; + } + } + + // If we haven't violated any max_width constraints, then we have + // added all remaining widths and thereby successfully solved the layout + if max_width_violations.is_empty() { + break; + } else { + // Nodes that were violated can't grow anymore in the next iteration, + // so we remove them from the solution and consider them "solved". + // Their amount of violation then gets distributed across the remaining + // items in the next iteration. + for solved_node_id in max_width_violations.drain(..) { + + // Since the node now gets removed, it doesn't contribute to the pool anymore + total_horizontal_space_available -= + width_calculated_arena[solved_node_id].min_inner_size_px + + width_calculated_arena[solved_node_id].flex_grow_px; + + variable_width_childs.remove(&solved_node_id); + } + } + } + } + + fn distribute_space_along_cross_axis( + node_id: &NodeId, + width_to_distribute: f32, + node_hierarchy: &NodeHierarchy, + arena_data: &NodeDataContainer, + width_calculated_arena: &mut NodeDataContainer<$struct_name>, + positioned_node_stack: &[NodeId]) + { + let parent_node_inner_width = width_to_distribute; + + let last_relative_node_width = { + let zero_node = NodeId::new(0); + let last_relative_node_id = positioned_node_stack.get(positioned_node_stack.len() - 1).unwrap_or(&zero_node); + let last_relative_node = &width_calculated_arena[*last_relative_node_id]; + last_relative_node.min_inner_size_px + last_relative_node.flex_grow_px - last_relative_node.$get_padding_fn() + }; + + for child_id in node_id.children(node_hierarchy) { + + let parent_node_inner_width = if arena_data[child_id].position.unwrap_or_default() != LayoutPosition::Absolute { + parent_node_inner_width + } else { + last_relative_node_width + }; + + let preferred_width = { + let min_width = width_calculated_arena[child_id].$preferred_field.min_needed_space().unwrap_or(0.0); + // In this case we want to overflow if the min width of the cross axis + if min_width > parent_node_inner_width { + min_width + } else { + if let Some(max_width) = width_calculated_arena[child_id].$preferred_field.max_available_space() { + if max_width > parent_node_inner_width { + parent_node_inner_width + } else { + max_width + } + } else { + parent_node_inner_width + } + } + }; + + // so that node.min_inner_size_px + node.flex_grow_px = preferred_width + width_calculated_arena[child_id].flex_grow_px = + preferred_width - width_calculated_arena[child_id].min_inner_size_px; + } + } +} + +)} + +typed_arena!( + WidthCalculatedRect, + preferred_width, + determine_preferred_width, + get_horizontal_padding, + get_flex_basis_horizontal, + bubble_preferred_widths_to_parents, + Horizontal +); + +typed_arena!( + HeightCalculatedRect, + preferred_height, + determine_preferred_height, + get_vertical_padding, + get_flex_basis_vertical, + bubble_preferred_heights_to_parents, + Vertical +); + +#[derive(Debug, Copy, Clone, PartialEq)] +pub(crate) struct WidthSolvedResult { + pub min_width: f32, + pub space_added: f32, +} + +impl WidthSolvedResult { + pub fn total(&self) -> f32 { + self.min_width + self.space_added + } +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub(crate) struct HeightSolvedResult { + pub min_height: f32, + pub space_added: f32, +} + +impl HeightSolvedResult { + pub fn total(&self) -> f32 { + self.min_height + self.space_added + } +} + +#[derive(Debug, Clone)] +pub(crate) struct SolvedWidthLayout { + pub solved_widths: NodeDataContainer, + pub layout_only_arena: NodeDataContainer, + pub non_leaf_nodes_sorted_by_depth: Vec<(usize, NodeId)>, +} + +#[derive(Debug, Clone)] +pub(crate) struct SolvedHeightLayout { + pub solved_heights: NodeDataContainer, +} + +/// Returns the solved widths of the items in a BTree form +pub(crate) fn solve_flex_layout_width<'a>( + node_hierarchy: &NodeHierarchy, + display_rectangles: &NodeDataContainer>, + preferred_widths: &NodeDataContainer>, + window_width: f32 +) -> SolvedWidthLayout { + let layout_only_arena = display_rectangles.transform(|node, _| node.layout); + let mut width_calculated_arena = NodeDataContainer::::from_rect_layout_arena(&layout_only_arena, preferred_widths); + let non_leaf_nodes_sorted_by_depth = node_hierarchy.get_parents_sorted_by_depth(); + width_calculated_arena.bubble_preferred_widths_to_parents(node_hierarchy, &layout_only_arena, &non_leaf_nodes_sorted_by_depth); + width_calculated_arena.apply_flex_grow(node_hierarchy, &layout_only_arena, &non_leaf_nodes_sorted_by_depth, window_width); + let solved_widths = width_calculated_arena.transform(|node, _| node.solved_result()); + SolvedWidthLayout { solved_widths , layout_only_arena, non_leaf_nodes_sorted_by_depth } +} + +/// Returns the solved height of the items in a BTree form +pub(crate) fn solve_flex_layout_height( + node_hierarchy: &NodeHierarchy, + solved_widths: &SolvedWidthLayout, + preferred_heights: &NodeDataContainer>, + window_height: f32 +) -> SolvedHeightLayout { + let SolvedWidthLayout { layout_only_arena, .. } = solved_widths; + let mut height_calculated_arena = NodeDataContainer::::from_rect_layout_arena(&layout_only_arena, preferred_heights); + height_calculated_arena.bubble_preferred_heights_to_parents(node_hierarchy, &layout_only_arena, &solved_widths.non_leaf_nodes_sorted_by_depth); + height_calculated_arena.apply_flex_grow(node_hierarchy, &layout_only_arena, &solved_widths.non_leaf_nodes_sorted_by_depth, window_height); + let solved_heights = height_calculated_arena.transform(|node, _| node.solved_result()); + SolvedHeightLayout { solved_heights } +} + +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] +pub(crate) struct HorizontalSolvedPosition(pub f32); + +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] +pub(crate) struct VerticalSolvedPosition(pub f32); + +macro_rules! get_position { +($fn_name:ident, + $width_layout:ident, + $height_solved_position:ident, + $solved_widths_field:ident, + $min_width:ident, + $left:ident, + $right:ident, + $axis:ident +) => ( + +/// Traverses along the DOM and solve for the X or Y position +fn $fn_name( + node_hierarchy: &NodeHierarchy, + node_data: &NodeDataContainer, + non_leaf_nodes: &[(usize, NodeId)], + solved_widths: &$width_layout) +-> NodeDataContainer<$height_solved_position> +{ + fn determine_child_x_absolute( + child_id: NodeId, + positioned_node_stack: &[NodeId], + arena_data: &NodeDataContainer, + arena_solved_data: &mut NodeDataContainer<$height_solved_position>, + solved_widths: &$width_layout, + ) { + let child_width_with_padding = { + let child_node = &solved_widths.$solved_widths_field[child_id]; + child_node.$min_width + child_node.space_added + }; + + let child_node = &arena_data[child_id]; + let child_margin = child_node.margin.unwrap_or_default(); + let child_margin_left = child_margin.$left.map(|x| x.to_pixels()).unwrap_or(0.0); + let child_margin_right = child_margin.$right.map(|x| x.to_pixels()).unwrap_or(0.0); + + let zero_node = NodeId::new(0); + let last_relative_node_id = positioned_node_stack.get(positioned_node_stack.len() - 1).unwrap_or(&zero_node); + + let last_relative_node = arena_data[*last_relative_node_id]; + let last_relative_padding = last_relative_node.padding.unwrap_or_default(); + let last_relative_padding_left = last_relative_padding.$left.map(|x| x.to_pixels()).unwrap_or(0.0); + let last_relative_padding_right = last_relative_padding.$right.map(|x| x.to_pixels()).unwrap_or(0.0); + + let last_relative_node_x = arena_solved_data[*last_relative_node_id].0 + last_relative_padding_left; + let last_relative_node_inner_width = { + let last_relative_node = &solved_widths.$solved_widths_field[*last_relative_node_id]; + last_relative_node.$min_width + last_relative_node.space_added - (last_relative_padding_left + last_relative_padding_right) + }; + + let child_left = &arena_data[child_id].$left.map(|s| s.0.to_pixels()); + let child_right = &arena_data[child_id].$right.map(|s| s.0.to_pixels()); + + if let Some(child_right) = child_right { + // align right / bottom of last relative parent + arena_solved_data[child_id].0 = + last_relative_node_x + + last_relative_node_inner_width + - child_width_with_padding + - child_margin_right + - child_right; + } else { + // align left / top of last relative parent + arena_solved_data[child_id].0 = + last_relative_node_x + + child_margin_left + + child_left.unwrap_or(0.0); + } + } + + fn determine_child_x_along_main_axis( + parent_id: NodeId, + main_axis_alignment: LayoutJustifyContent, + arena_data: &NodeDataContainer, + arena_solved_data: &mut NodeDataContainer<$height_solved_position>, + solved_widths: &$width_layout, + child_id: NodeId, + parent_x_position: f32, + parent_inner_width: f32, + sum_x_of_children_so_far: &mut f32, + positioned_node_stack: &[NodeId], + ) { + use azul_css::LayoutJustifyContent::*; + + let child_width_with_padding = { + let child_node = &solved_widths.$solved_widths_field[child_id]; + child_node.$min_width + child_node.space_added + }; + + // width: increase X according to the main axis, Y according to the cross_axis + let child_node = &arena_data[child_id]; + let child_margin = child_node.margin.unwrap_or_default(); + let child_margin_left = child_margin.$left.map(|x| x.to_pixels()).unwrap_or(0.0); + let child_margin_right = child_margin.$right.map(|x| x.to_pixels()).unwrap_or(0.0); + + if child_node.position.unwrap_or_default() == LayoutPosition::Absolute { + determine_child_x_absolute( + child_id, + positioned_node_stack, + arena_data, + arena_solved_data, + solved_widths + ); + } else { + // X position of the top left corner + // WARNING: End has to be added after all children! + let x_of_top_left_corner = match main_axis_alignment { + Start | End => { + parent_x_position + *sum_x_of_children_so_far + child_margin_left + }, + Center => { + parent_x_position + + ((parent_inner_width / 2.0) + - ((*sum_x_of_children_so_far + child_margin_right + child_width_with_padding) / 2.0)) + }, + SpaceBetween => { + parent_x_position // TODO! + }, + SpaceAround => { + parent_x_position // TODO! + }, + }; + + arena_solved_data[child_id].0 = x_of_top_left_corner; + *sum_x_of_children_so_far += child_margin_right + child_width_with_padding + child_margin_left; + } + } + + fn determine_child_x_along_cross_axis( + arena_data: &NodeDataContainer, + solved_widths: &$width_layout, + child_id: NodeId, + positioned_node_stack: &[NodeId], + arena_solved_data: &mut NodeDataContainer<$height_solved_position>, + parent_x_position: f32) + { + let child_node = &arena_data[child_id]; + let child_margin_left = child_node.margin.unwrap_or_default().$left.map(|x| x.to_pixels()).unwrap_or(0.0); + + if child_node.position.unwrap_or_default() == LayoutPosition::Absolute { + determine_child_x_absolute( + child_id, + positioned_node_stack, + arena_data, + arena_solved_data, + solved_widths + ); + } else { + arena_solved_data[child_id].0 = parent_x_position + child_margin_left; + } + } + + use azul_css::{LayoutAxis, LayoutJustifyContent}; + + let mut arena_solved_data = NodeDataContainer::new(vec![$height_solved_position(0.0); node_data.len()]); + + // Stack of the positioned nodes (nearest relative or absolute positioned node) + let mut positioned_node_stack = vec![NodeId::new(0)]; + + for (_node_depth, parent_id) in non_leaf_nodes { + + let parent_node = node_data[*parent_id]; + + let parent_padding = parent_node.padding.unwrap_or_default(); + let parent_padding_left = parent_padding.$left.map(|x| x.to_pixels()).unwrap_or(0.0); + let parent_padding_right = parent_padding.$right.map(|x| x.to_pixels()).unwrap_or(0.0); + + let parent_x_position = arena_solved_data[*parent_id].0 + parent_padding_left; + let parent_direction = parent_node.direction.unwrap_or_default(); + + // Push nearest relative or absolute positioned element + let parent_is_positioned = parent_node.position.unwrap_or_default() != LayoutPosition::Static; + if parent_is_positioned { + positioned_node_stack.push(*parent_id); + } + + let parent_inner_width = { + let parent_node = &solved_widths.$solved_widths_field[*parent_id]; + parent_node.$min_width + parent_node.space_added - (parent_padding_left + parent_padding_right) + }; + + if parent_direction.get_axis() == LayoutAxis::$axis { + // Along main axis: Take X of parent + let main_axis_alignment = node_data[*parent_id].justify_content.unwrap_or_default(); + let mut sum_x_of_children_so_far = 0.0; + + if parent_direction.is_reverse() { + for child_id in parent_id.reverse_children(node_hierarchy) { + determine_child_x_along_main_axis( + *parent_id, + main_axis_alignment, + &node_data, + &mut arena_solved_data, + solved_widths, + child_id, + parent_x_position, + parent_inner_width, + &mut sum_x_of_children_so_far, + &positioned_node_stack, + ); + } + } else { + for child_id in parent_id.children(node_hierarchy) { + determine_child_x_along_main_axis( + *parent_id, + main_axis_alignment, + &node_data, + &mut arena_solved_data, + solved_widths, + child_id, + parent_x_position, + parent_inner_width, + &mut sum_x_of_children_so_far, + &positioned_node_stack, + ); + } + } + + // If the direction is `flex-end`, we can't add the X position during the iteration, + // so we have to "add" the diff to the parent_inner_width at the end + let should_align_towards_end = + (!parent_direction.is_reverse() && main_axis_alignment == LayoutJustifyContent::End) || + (parent_direction.is_reverse() && main_axis_alignment == LayoutJustifyContent::Start); + + if should_align_towards_end { + let diff = parent_inner_width - sum_x_of_children_so_far; + for child_id in parent_id.children(node_hierarchy).filter(|ch| { + node_data[*ch].position.unwrap_or_default() != LayoutPosition::Absolute + }) { + arena_solved_data[child_id].0 += diff; + } + } + + } else { + // Along cross axis: Increase X with width of current element + + if parent_direction.is_reverse() { + for child_id in parent_id.reverse_children(node_hierarchy) { + determine_child_x_along_cross_axis( + node_data, + solved_widths, + child_id, + &positioned_node_stack, + &mut arena_solved_data, + parent_x_position, + ); + } + } else { + for child_id in parent_id.children(node_hierarchy) { + determine_child_x_along_cross_axis( + node_data, + solved_widths, + child_id, + &positioned_node_stack, + &mut arena_solved_data, + parent_x_position, + ); + } + } + } + + if parent_is_positioned { + positioned_node_stack.pop(); + } + + } + + arena_solved_data +} + +)} + +fn get_x_positions( + solved_widths: &SolvedWidthLayout, + node_hierarchy: &NodeHierarchy, + origin: LayoutPoint, +) -> NodeDataContainer +{ + get_position!(get_pos_x, SolvedWidthLayout, HorizontalSolvedPosition, solved_widths, min_width, left, right, Horizontal); + let mut arena = get_pos_x(node_hierarchy, &solved_widths.layout_only_arena, &solved_widths.non_leaf_nodes_sorted_by_depth, solved_widths); + + // Add the origin on top of the position + let x = origin.x as f32; + if x > 0.5 || x < -0.5 { + for item in &mut arena.internal { + item.0 += x; + } + } + arena +} + +fn get_y_positions( + solved_heights: &SolvedHeightLayout, + solved_widths: &SolvedWidthLayout, + node_hierarchy: &NodeHierarchy, + origin: LayoutPoint +) -> NodeDataContainer +{ + get_position!(get_pos_y, SolvedHeightLayout, VerticalSolvedPosition, solved_heights, min_height, top, bottom, Vertical); + let mut arena = get_pos_y(node_hierarchy, &solved_widths.layout_only_arena, &solved_widths.non_leaf_nodes_sorted_by_depth, solved_heights); + + // Add the origin on top of the position + let y = origin.y as f32; + if y > 0.5 || y < -0.5 { + for item in &mut arena.internal { + item.0 += y; + } + } + arena +} + +/// Returns the preferred width, for example for an image, that would be the +/// original width (an image always wants to take up the original space) +fn get_content_width( + node_id: &NodeId, + node_type: &NodeType, + app_resources: &AppResources, + positioned_words: &BTreeMap, +) -> Option { + use dom::NodeType::*; + match node_type { + Image(image_id) => app_resources.get_image_info(image_id).map(|info| info.descriptor.size.width as f32), + Label(_) | Text(_) => positioned_words.get(node_id).map(|pos| pos.0.content_size.width), + _ => None, + } +} + +fn get_content_height( + node_id: &NodeId, + node_type: &NodeType, + app_resources: &AppResources, + positioned_words: &BTreeMap, + div_width: f32, +) -> Option { + use dom::NodeType::*; + match &node_type { + Image(i) => { + let image_size = &app_resources.get_image_info(i)?.descriptor.size; + let aspect_ratio = image_size.width as f32 / image_size.height as f32; + let preferred_height = div_width * aspect_ratio; + Some(PreferredHeight::Image { + original_dimensions: (image_size.width as usize, image_size.height as usize), + aspect_ratio, + preferred_height, + }) + }, + Label(_) | Text(_) => { + positioned_words + .get(node_id) + .map(|pos| PreferredHeight::Text { content_size: pos.0.content_size }) + }, + _ => None, + } +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) enum PreferredHeight { + Image { original_dimensions: (usize, usize), aspect_ratio: f32, preferred_height: f32 }, + Text { content_size: LayoutSize } +} + +impl PreferredHeight { + + /// Returns the preferred size of the div content. + /// Note that this can be larger than the actual div content! + pub fn get_content_size(&self) -> f32 { + use self::PreferredHeight::*; + match self { + Image { preferred_height, .. } => *preferred_height, + Text { content_size } => content_size.height, + } + } +} + +pub(crate) fn font_size_to_au(font_size: StyleFontSize) -> Au { + px_to_au(font_size.0.to_pixels()) +} + +pub(crate) fn px_to_au(px: f32) -> Au { + use app_units::{AU_PER_PX, MIN_AU, MAX_AU}; + let target_app_units = Au((px * AU_PER_PX as f32) as i32); + target_app_units.min(MAX_AU).max(MIN_AU) +} + +pub(crate) fn get_font_id(rect_style: &RectStyle) -> &str { + let font_id = rect_style.font_family.as_ref().and_then(|family| family.fonts.get(0)); + font_id.map(|f| f.get_str()).unwrap_or(DEFAULT_FONT_ID) +} + +pub(crate) fn get_font_size(rect_style: &RectStyle) -> StyleFontSize { + rect_style.font_size.unwrap_or(DEFAULT_FONT_SIZE) +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct PositionedRectangle { + pub bounds: LayoutRect, + /// Size of the content, for example if a div contains an image, + /// that image can be bigger than the actual rect + pub content_width: Option, + pub content_height: Option, +} + +#[derive(Debug, Clone)] +pub struct LayoutResult { + pub rects: NodeDataContainer, + pub word_cache: BTreeMap, + pub scaled_words: BTreeMap, + pub positioned_word_cache: BTreeMap, + pub node_depths: Vec<(usize, NodeId)>, +} + +#[derive(Debug, Copy, Clone, PartialEq)] +struct InlineText { + /// Horizontal padding of the text in pixels + horizontal_padding: f32, + /// Horizontal margin of the text in pixels + horizontal_margin: f32, +} + +/// At this point in time, all font keys, image keys, etc. have +/// to be already submitted in the RenderApi! +pub(crate) fn do_the_layout<'a,'b, T>( + node_hierarchy: &NodeHierarchy, + node_data: &NodeDataContainer>, + display_rects: &NodeDataContainer>, + app_resources: &'b AppResources, + rect_size: LayoutSize, + rect_offset: LayoutPoint, +) -> LayoutResult { + + // Determine what the width for each div would be if the content size didn't matter + let widths_content_ignored = solve_flex_layout_width( + node_hierarchy, + &display_rects, + &node_data.transform(|_, _| None), + rect_size.width as f32, + ); + + // Determine what the "maximum width" for each div is, except for divs where overflow:visible is set + // I.e. for a div width 800px, with 4 text child nodes, each text node gets a width of 200px + let max_widths = node_hierarchy + .linear_iter() + .filter(|node_id| !display_rects[*node_id].layout.is_horizontal_overflow_visible()) + .map(|node_id| (node_id, widths_content_ignored.solved_widths[node_id].total())) + .collect::>(); + + // TODO: Filter all inline text blocks (prepare inline text layout run) + let inline_text_blocks = BTreeMap::::new(); + + // Resolve cached text IDs or break new, uncached strings into words / text runs + let word_cache = create_word_cache(app_resources, node_data); + // Scale the words to the correct size - TODO: Caching / GC! + let scaled_words = create_scaled_words(app_resources, &word_cache, display_rects); + // Layout all words as if there was no max-width constraint + let word_positions_no_max_width = create_word_positions( + &word_cache, + &scaled_words, + display_rects, + &max_widths, + &inline_text_blocks + ); + + // Determine the preferred **content** width, without any max-width restrictions - + // For images that would be the image width / height, for text it would be the text + // laid out without any width constraints. + let content_widths = node_data.transform(|node, node_id| + get_content_width(&node_id, &node.node_type, app_resources, &word_positions_no_max_width) + ); + + // Solve the widths again, this time incorporating the maximum widths + let solved_widths = solve_flex_layout_width( + node_hierarchy, + &display_rects, + &content_widths, + rect_size.width as f32, + ); + + // Layout all texts again with the resolved width constraints + let proper_max_widths = solved_widths.solved_widths.linear_iter().map(|node_id| { + (node_id, solved_widths.solved_widths[node_id].total() - display_rects[node_id].layout.get_horizontal_padding()) + }).collect(); + + // Resolve the word positions relative to each divs upper left corner + let word_positions_with_max_width = create_word_positions( + &word_cache, + &scaled_words, + display_rects, + &proper_max_widths, + &inline_text_blocks + ); + + // Given the final width of a node and the height of the content, resolve the div + // height and return whether the node content overflows its parent (width-in-height-out) + let content_heights = node_data.transform(|node, node_id| { + let div_width = solved_widths.solved_widths[node_id].total(); + get_content_height( + &node_id, + &node.node_type, + app_resources, + &word_positions_with_max_width, + div_width + ).map(|ch| ch.get_content_size()) + }); + + // Given the final heights, resolve the heights for flexible-size divs + // TODO: Fix justify-content:flex-start: The content height is not the final height! + let solved_heights = solve_flex_layout_height( + node_hierarchy, + &solved_widths, + &content_heights, + rect_size.height as f32, + ); + + let x_positions = get_x_positions(&solved_widths, node_hierarchy, rect_offset.clone()); + let y_positions = get_y_positions(&solved_heights, &solved_widths, node_hierarchy, rect_offset); + + let layouted_rects = node_data.transform(|_node, node_id| { + PositionedRectangle { + bounds: LayoutRect::new( + LayoutPoint::new(x_positions[node_id].0, y_positions[node_id].0), + LayoutSize::new( + solved_widths.solved_widths[node_id].total(), + solved_heights.solved_heights[node_id].total(), + ) + ), + content_width: Some(proper_max_widths[&node_id]), + content_height: content_heights[node_id], + } + }); + + LayoutResult { + rects: layouted_rects, + word_cache, + scaled_words, + positioned_word_cache: word_positions_with_max_width, + node_depths: solved_widths.non_leaf_nodes_sorted_by_depth, + } +} + +fn create_word_cache( + app_resources: &AppResources, + node_data: &NodeDataContainer>, +) -> BTreeMap +{ + use text_layout::split_text_into_words; + node_data + .linear_iter() + .filter_map(|node_id| { + match &node_data[node_id].node_type { + NodeType::Label(string) => Some((node_id, split_text_into_words(string.as_str()))), + NodeType::Text(text_id) => { + app_resources.get_text(text_id).map(|words| (node_id, words.clone())) + }, + _ => None, + } + }).collect() +} + +fn create_scaled_words<'a>( + app_resources: &AppResources, + words: &BTreeMap, + display_rects: &NodeDataContainer>, +) -> BTreeMap { + + use text_layout::words_to_scaled_words; + use app_resources::ImmediateFontId; + + words.iter().filter_map(|(node_id, words)| { + let style = &display_rects[*node_id].style; + let font_size = get_font_size(&style); + let font_size_au = font_size_to_au(font_size); + let css_font_id = get_font_id(&style); + let font_id = match app_resources.get_css_font_id(css_font_id) { + Some(s) => ImmediateFontId::Resolved(*s), + None => ImmediateFontId::Unresolved(css_font_id.to_string()), + }; + + let loaded_font = app_resources.get_loaded_font(&font_id)?; + let font_instance_key = loaded_font.font_instances.get(&font_size_au)?; + + let font_bytes = &loaded_font.font_bytes; + let font_index = loaded_font.font_index as u32; + + let scaled_words = words_to_scaled_words( + words, + font_bytes, + font_index, + font_size.0.to_pixels(), + ); + Some((*node_id, (scaled_words, *font_instance_key))) + }).collect() +} + +fn create_word_positions<'a>( + words: &BTreeMap, + scaled_words: &BTreeMap, + display_rects: &NodeDataContainer>, + max_widths: &BTreeMap, + inline_texts: &BTreeMap, +) -> BTreeMap { + + use text_layout; + + words.iter().filter_map(|(node_id, words)| { + + let rect = &display_rects[*node_id]; + let (scaled_words, font_instance_key) = scaled_words.get(&node_id)?; + + let font_size = get_font_size(&rect.style).0; + let max_horizontal_width = max_widths.get(&node_id).cloned(); + let leading = inline_texts.get(&node_id).map(|inline_text| inline_text.horizontal_margin + inline_text.horizontal_padding); + + // TODO: Make this configurable + let text_holes = Vec::new(); + let text_layout_options = get_text_layout_options(&rect, max_horizontal_width, leading, text_holes); + + // TODO: handle overflow / scrollbar_style ! + let positioned_words = text_layout::position_words( + words, scaled_words, + &text_layout_options, + font_size.to_pixels() + ); + + Some((*node_id, (positioned_words, *font_instance_key))) + }).collect() +} + +fn get_text_layout_options( + rect: &DisplayRectangle, + max_horizontal_width: Option, + leading: Option, + holes: Vec, +) -> TextLayoutOptions { + TextLayoutOptions { + line_height: rect.style.line_height.map(|lh| lh.0.get()), + letter_spacing: rect.style.letter_spacing.map(|ls| ls.0.to_pixels()), + word_spacing: rect.style.word_spacing.map(|ws| ws.0.to_pixels()), + tab_width: rect.style.tab_width.map(|tw| tw.0.get()), + max_horizontal_width, + leading, + holes, + } +} + +/// For a given rectangle, determines what text alignment should be used +pub(crate) fn determine_text_alignment(rect_style: &RectStyle, rect_layout: &RectLayout) + -> (StyleTextAlignmentHorz, StyleTextAlignmentVert) +{ + let mut horz_alignment = StyleTextAlignmentHorz::default(); + let mut vert_alignment = StyleTextAlignmentVert::default(); + + if let Some(align_items) = rect_layout.align_items { + // Vertical text alignment + use azul_css::LayoutAlignItems; + match align_items { + LayoutAlignItems::Start => vert_alignment = StyleTextAlignmentVert::Top, + LayoutAlignItems::End => vert_alignment = StyleTextAlignmentVert::Bottom, + // technically stretch = blocktext, but we don't have that yet + _ => vert_alignment = StyleTextAlignmentVert::Center, + } + } + + if let Some(justify_content) = rect_layout.justify_content { + use azul_css::LayoutJustifyContent; + // Horizontal text alignment + match justify_content { + LayoutJustifyContent::Start => horz_alignment = StyleTextAlignmentHorz::Left, + LayoutJustifyContent::End => horz_alignment = StyleTextAlignmentHorz::Right, + _ => horz_alignment = StyleTextAlignmentHorz::Center, + } + } + + if let Some(text_align) = rect_style.text_align { + // Horizontal text alignment with higher priority + horz_alignment = text_align; + } + + (horz_alignment, vert_alignment) +} + +#[cfg(test)] +mod layout_tests { + + use azul_css::RectLayout; + use id_tree::{Node, NodeId}; + use super::*; + + /// Returns a DOM for testing so we don't have to construct it every time. + /// The DOM structure looks like this: + /// + /// ```no_run + /// 0 + /// '- 1 + /// '-- 2 + /// ' '-- 3 + /// ' '--- 4 + /// '-- 5 + /// ``` + fn get_testing_hierarchy() -> NodeHierarchy { + NodeHierarchy { + internal: vec![ + // 0 + Node { + parent: None, + previous_sibling: None, + next_sibling: None, + first_child: Some(NodeId::new(1)), + last_child: Some(NodeId::new(1)), + }, + // 1 + Node { + parent: Some(NodeId::new(0)), + previous_sibling: None, + next_sibling: Some(NodeId::new(5)), + first_child: Some(NodeId::new(2)), + last_child: Some(NodeId::new(2)), + }, + // 2 + Node { + parent: Some(NodeId::new(1)), + previous_sibling: None, + next_sibling: None, + first_child: Some(NodeId::new(3)), + last_child: Some(NodeId::new(4)), + }, + // 3 + Node { + parent: Some(NodeId::new(2)), + previous_sibling: None, + next_sibling: Some(NodeId::new(4)), + first_child: None, + last_child: None, + }, + // 4 + Node { + parent: Some(NodeId::new(2)), + previous_sibling: Some(NodeId::new(3)), + next_sibling: None, + first_child: None, + last_child: None, + }, + // 5 + Node { + parent: Some(NodeId::new(1)), + previous_sibling: Some(NodeId::new(2)), + next_sibling: None, + first_child: None, + last_child: None, + }, + ] + } + } + + /// Returns the same arena, but pre-fills nodes at [(NodeId, RectLayout)] + /// with the layout rect + fn get_display_rectangle_arena(constraints: &[(usize, RectLayout)]) -> (NodeHierarchy, NodeDataContainer) { + let arena = get_testing_hierarchy(); + let mut arena_data = vec![RectLayout::default(); arena.len()]; + for (id, rect) in constraints { + arena_data[*id] = *rect; + } + (arena, NodeDataContainer { internal: arena_data }) + } + + #[test] + fn test_determine_preferred_width() { + use azul_css::{LayoutMinWidth, LayoutMaxWidth, PixelValue, LayoutWidth}; + + let layout = RectLayout { + width: None, + min_width: None, + max_width: None, + .. Default::default() + }; + assert_eq!(determine_preferred_width(&layout, None), WhConstraint::Unconstrained); + + let layout = RectLayout { + width: Some(LayoutWidth(PixelValue::px(500.0))), + min_width: None, + max_width: None, + .. Default::default() + }; + assert_eq!(determine_preferred_width(&layout, None), WhConstraint::EqualTo(500.0)); + + let layout = RectLayout { + width: Some(LayoutWidth(PixelValue::px(500.0))), + min_width: Some(LayoutMinWidth(PixelValue::px(600.0))), + max_width: None, + .. Default::default() + }; + assert_eq!(determine_preferred_width(&layout, None), WhConstraint::EqualTo(600.0)); + + let layout = RectLayout { + width: Some(LayoutWidth(PixelValue::px(10000.0))), + min_width: Some(LayoutMinWidth(PixelValue::px(600.0))), + max_width: Some(LayoutMaxWidth(PixelValue::px(800.0))), + .. Default::default() + }; + assert_eq!(determine_preferred_width(&layout, None), WhConstraint::EqualTo(800.0)); + + let layout = RectLayout { + width: None, + min_width: Some(LayoutMinWidth(PixelValue::px(600.0))), + max_width: Some(LayoutMaxWidth(PixelValue::px(800.0))), + .. Default::default() + }; + assert_eq!(determine_preferred_width(&layout, None), WhConstraint::Between(600.0, 800.0)); + + let layout = RectLayout { + width: None, + min_width: None, + max_width: Some(LayoutMaxWidth(PixelValue::px(800.0))), + .. Default::default() + }; + assert_eq!(determine_preferred_width(&layout, None), WhConstraint::Between(0.0, 800.0)); + + let layout = RectLayout { + width: Some(LayoutWidth(PixelValue::px(1000.0))), + min_width: None, + max_width: Some(LayoutMaxWidth(PixelValue::px(800.0))), + .. Default::default() + }; + assert_eq!(determine_preferred_width(&layout, None), WhConstraint::EqualTo(800.0)); + + let layout = RectLayout { + width: Some(LayoutWidth(PixelValue::px(1200.0))), + min_width: Some(LayoutMinWidth(PixelValue::px(1000.0))), + max_width: Some(LayoutMaxWidth(PixelValue::px(800.0))), + .. Default::default() + }; + assert_eq!(determine_preferred_width(&layout, None), WhConstraint::EqualTo(800.0)); + + let layout = RectLayout { + width: Some(LayoutWidth(PixelValue::px(1200.0))), + min_width: Some(LayoutMinWidth(PixelValue::px(1000.0))), + max_width: Some(LayoutMaxWidth(PixelValue::px(400.0))), + .. Default::default() + }; + assert_eq!(determine_preferred_width(&layout, None), WhConstraint::EqualTo(400.0)); + } + + /// Tests that the nodes get filled correctly + #[test] + fn test_fill_out_preferred_width() { + + use azul_css::*; + + let (node_hierarchy, node_data) = get_display_rectangle_arena(&[ + (0, RectLayout { + direction: Some(LayoutDirection::Row), + .. Default::default() + }), + (1, RectLayout { + max_width: Some(LayoutMaxWidth(PixelValue::px(200.0))), + padding: Some(LayoutPadding { left: Some(PixelValue::px(20.0)), right: Some(PixelValue::px(20.0)), .. Default::default() }), + direction: Some(LayoutDirection::Row), + .. Default::default() + }), + (2, RectLayout { + direction: Some(LayoutDirection::Row), + .. Default::default() + }) + ]); + + let preferred_widths = node_data.transform(|_, _| None); + let mut width_filled_out_data = NodeDataContainer::::from_rect_layout_arena(&node_data, &preferred_widths); + + // Test some basic stuff - test that `get_flex_basis` works + + // Nodes 0, 2, 3, 4 and 5 have no basis + assert_eq!(width_filled_out_data[NodeId::new(0)].get_flex_basis_horizontal(), 0.0); + + // Node 1 has a padding on left and right of 20, so a flex-basis of 40.0 + assert_eq!(width_filled_out_data[NodeId::new(1)].get_flex_basis_horizontal(), 40.0); + assert_eq!(width_filled_out_data[NodeId::new(1)].get_horizontal_padding(), 40.0); + + assert_eq!(width_filled_out_data[NodeId::new(2)].get_flex_basis_horizontal(), 0.0); + assert_eq!(width_filled_out_data[NodeId::new(3)].get_flex_basis_horizontal(), 0.0); + assert_eq!(width_filled_out_data[NodeId::new(4)].get_flex_basis_horizontal(), 0.0); + assert_eq!(width_filled_out_data[NodeId::new(5)].get_flex_basis_horizontal(), 0.0); + + assert_eq!(width_filled_out_data[NodeId::new(0)].preferred_width, WhConstraint::Unconstrained); + assert_eq!(width_filled_out_data[NodeId::new(1)].preferred_width, WhConstraint::Between(0.0, 200.0)); + assert_eq!(width_filled_out_data[NodeId::new(2)].preferred_width, WhConstraint::Unconstrained); + assert_eq!(width_filled_out_data[NodeId::new(3)].preferred_width, WhConstraint::Unconstrained); + assert_eq!(width_filled_out_data[NodeId::new(4)].preferred_width, WhConstraint::Unconstrained); + assert_eq!(width_filled_out_data[NodeId::new(5)].preferred_width, WhConstraint::Unconstrained); + + // Test the flex-basis sum + assert_eq!(width_filled_out_data.sum_children_flex_basis(NodeId::new(2), &node_hierarchy, &node_data), 0.0); + assert_eq!(width_filled_out_data.sum_children_flex_basis(NodeId::new(1), &node_hierarchy, &node_data), 0.0); + assert_eq!(width_filled_out_data.sum_children_flex_basis(NodeId::new(0), &node_hierarchy, &node_data), 40.0); + + // -- Section 2: Test that size-bubbling works: + // + // Size-bubbling should take the 40px padding and "bubble" it towards the + let non_leaf_nodes_sorted_by_depth = node_hierarchy.get_parents_sorted_by_depth(); + + // ID 5 has no child, so it's not returned, same as 3 and 4 + assert_eq!(non_leaf_nodes_sorted_by_depth, vec![ + (0, NodeId::new(0)), + (1, NodeId::new(1)), + (2, NodeId::new(2)), + ]); + + width_filled_out_data.bubble_preferred_widths_to_parents(&node_hierarchy, &node_data, &non_leaf_nodes_sorted_by_depth); + + // This step shouldn't have touched the flex_grow_px + for node in &width_filled_out_data.internal { + assert_eq!(node.flex_grow_px, 0.0); + } + + // This step should not modify the `preferred_width` + assert_eq!(width_filled_out_data[NodeId::new(0)].preferred_width, WhConstraint::Unconstrained); + assert_eq!(width_filled_out_data[NodeId::new(1)].preferred_width, WhConstraint::Between(0.0, 200.0)); + assert_eq!(width_filled_out_data[NodeId::new(2)].preferred_width, WhConstraint::Unconstrained); + assert_eq!(width_filled_out_data[NodeId::new(3)].preferred_width, WhConstraint::Unconstrained); + assert_eq!(width_filled_out_data[NodeId::new(4)].preferred_width, WhConstraint::Unconstrained); + assert_eq!(width_filled_out_data[NodeId::new(5)].preferred_width, WhConstraint::Unconstrained); + + // The padding of the Node 1 should have bubbled up to be the minimum width of Node 0 + assert_eq!(width_filled_out_data[NodeId::new(0)].min_inner_size_px, 40.0); + assert_eq!(width_filled_out_data[NodeId::new(1)].get_flex_basis_horizontal(), 40.0); + assert_eq!(width_filled_out_data[NodeId::new(1)].min_inner_size_px, 0.0); + assert_eq!(width_filled_out_data[NodeId::new(2)].get_flex_basis_horizontal(), 0.0); + assert_eq!(width_filled_out_data[NodeId::new(2)].min_inner_size_px, 0.0); + assert_eq!(width_filled_out_data[NodeId::new(3)].get_flex_basis_horizontal(), 0.0); + assert_eq!(width_filled_out_data[NodeId::new(3)].min_inner_size_px, 0.0); + assert_eq!(width_filled_out_data[NodeId::new(4)].get_flex_basis_horizontal(), 0.0); + assert_eq!(width_filled_out_data[NodeId::new(4)].min_inner_size_px, 0.0); + assert_eq!(width_filled_out_data[NodeId::new(5)].get_flex_basis_horizontal(), 0.0); + assert_eq!(width_filled_out_data[NodeId::new(5)].min_inner_size_px, 0.0); + + // -- Section 3: Test if growing the sizes works + + let window_width = 754.0; // pixel + + // - window_width: 754px + // 0 -- [] - expecting width to stretch to 754 px + // '- 1 -- [max-width: 200px; padding: 20px] - expecting width to stretch to 200 px + // '-- 2 -- [] - expecting width to stretch to 160px + // ' '-- 3 -- [] - expecting width to stretch to 80px (half of 160) + // ' '-- 4 -- [] - expecting width to stretch to 80px (half of 160) + // '-- 5 -- [] - expecting width to stretch to 554px (754 - 200px max-width of earlier sibling) + + width_filled_out_data.apply_flex_grow(&node_hierarchy, &node_data, &non_leaf_nodes_sorted_by_depth, window_width); + + assert_eq!(width_filled_out_data[NodeId::new(0)].solved_result(), WidthSolvedResult { + min_width: 40.0, + space_added: window_width - 40.0, + }); + assert_eq!(width_filled_out_data[NodeId::new(1)].solved_result(), WidthSolvedResult { + min_width: 0.0, + space_added: 200.0, + }); + assert_eq!(width_filled_out_data[NodeId::new(2)].solved_result(), WidthSolvedResult { + min_width: 0.0, + space_added: 160.0, + }); + assert_eq!(width_filled_out_data[NodeId::new(3)].solved_result(), WidthSolvedResult { + min_width: 0.0, + space_added: 80.0, + }); + assert_eq!(width_filled_out_data[NodeId::new(4)].solved_result(), WidthSolvedResult { + min_width: 0.0, + space_added: 80.0, + }); + assert_eq!(width_filled_out_data[NodeId::new(5)].solved_result(), WidthSolvedResult { + min_width: 0.0, + space_added: window_width - 200.0, + }); + } +} diff --git a/azul/src/ui_state.rs b/azul/src/ui_state.rs new file mode 100644 index 000000000..f3e82509a --- /dev/null +++ b/azul/src/ui_state.rs @@ -0,0 +1,144 @@ +use std::{ + fmt, + collections::BTreeMap, +}; +use glium::glutin::WindowId as GliumWindowId; +use azul_css::CssProperty; +use { + FastHashMap, + app::RuntimeError, + dom::{ + Dom, TagId, TabIndex, DomString, + HoverEventFilter, FocusEventFilter, NotEventFilter, + WindowEventFilter + }, + app::AppState, + id_tree::NodeId, + style::HoverGroup, + callbacks::{Callback, LayoutInfo, DefaultCallbackId}, +}; + +pub struct UiState { + /// The actual DOM, rendered from the .layout() function + pub dom: Dom, + /// The style properties that should be overridden for this frame, cloned from the `Css` + pub dynamic_css_overrides: BTreeMap>, + /// Stores all tags for nodes that need to activate on a `:hover` or `:active` event. + pub tag_ids_to_hover_active_states: BTreeMap, + + /// Tags -> Focusable nodes + pub tab_index_tags: BTreeMap, + /// Tags -> Draggable nodes + pub draggable_tags: BTreeMap, + /// Tag IDs -> Node IDs + pub tag_ids_to_node_ids: BTreeMap, + /// Reverse of `tag_ids_to_node_ids`. + pub node_ids_to_tag_ids: BTreeMap, + + // For hover, focus and not callbacks, there needs to be a tag generated + // for hit-testing. Since window and desktop callbacks are not attached to + // any element, they only store the NodeId (where the event came from), but have + // no tag themselves. + // + // There are two maps per event, one for the regular callbacks and one for + // the default callbacks. This is done for consistency, since otherwise the + // event filtering logic gets much more complicated than it already is. + pub hover_callbacks: BTreeMap>>, + pub hover_default_callbacks: BTreeMap>, + pub focus_callbacks: BTreeMap>>, + pub focus_default_callbacks: BTreeMap>, + pub not_callbacks: BTreeMap>>, + pub not_default_callbacks: BTreeMap>, + pub window_callbacks: BTreeMap>>, + pub window_default_callbacks: BTreeMap>, +} + +impl fmt::Debug for UiState { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, + "UiState {{ \ + + dom: {:?}, \ + dynamic_css_overrides: {:?}, \ + tag_ids_to_hover_active_states: {:?}, \ + tab_index_tags: {:?}, \ + draggable_tags: {:?}, \ + tag_ids_to_node_ids: {:?}, \ + node_ids_to_tag_ids: {:?}, \ + hover_callbacks: {:?}, \ + hover_default_callbacks: {:?}, \ + focus_callbacks: {:?}, \ + focus_default_callbacks: {:?}, \ + not_callbacks: {:?}, \ + not_default_callbacks: {:?}, \ + window_callbacks: {:?}, \ + window_default_callbacks: {:?}, \ + }}", + + self.dom, + self.dynamic_css_overrides, + self.tag_ids_to_hover_active_states, + self.tab_index_tags, + self.draggable_tags, + self.tag_ids_to_node_ids, + self.node_ids_to_tag_ids, + self.hover_callbacks, + self.hover_default_callbacks, + self.focus_callbacks, + self.focus_default_callbacks, + self.not_callbacks, + self.not_default_callbacks, + self.window_callbacks, + self.window_default_callbacks, + ) + } +} + +impl UiState { + + #[allow(unused_imports, unused_variables)] + pub(crate) fn from_app_state( + app_state: &mut AppState, + window_id: &GliumWindowId, + layout_callback: fn(&T, layout_info: LayoutInfo) -> Dom + ) -> Result> { + + use dom::{Dom, On, NodeType}; + use std::sync::atomic::Ordering; + use app::RuntimeError::*; + + let mut fake_window = app_state.windows.get_mut(window_id).ok_or(WindowIndexError)?; + let window_info = LayoutInfo { + window: &mut fake_window, + resources: &app_state.resources, + }; + + // Only shortly lock the data to get the dom out + let dom: Dom = { + #[cfg(test)]{ + Dom::::new(NodeType::Div) + } + + #[cfg(not(test))]{ + let dom_lock = app_state.data.lock().unwrap(); + (layout_callback)(&*dom_lock, window_info) + } + }; + + Ok(dom.into_ui_state()) + } + + pub(crate) fn create_tags_for_hover_nodes(&mut self, hover_nodes: &BTreeMap) { + use dom::new_tag_id; + for (hover_node_id, hover_group) in hover_nodes { + let hover_tag = match self.node_ids_to_tag_ids.get(hover_node_id) { + Some(tag_id) => *tag_id, + None => new_tag_id(), + }; + + self.node_ids_to_tag_ids.insert(*hover_node_id, hover_tag); + self.tag_ids_to_node_ids.insert(hover_tag, *hover_node_id); + self.tag_ids_to_hover_active_states.insert(hover_tag, (*hover_node_id, *hover_group)); + } + } +} diff --git a/azul/src/widgets/button.rs b/azul/src/widgets/button.rs new file mode 100644 index 000000000..eaee52666 --- /dev/null +++ b/azul/src/widgets/button.rs @@ -0,0 +1,60 @@ +use { + dom::{Dom, DomString, TabIndex}, + app_resources::ImageId, +}; + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct Button { + pub content: ButtonContent, +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub enum ButtonContent { + Image(ImageId), + // Buttons should only contain short amounts of text + Text(DomString), +} + +impl Button { + pub fn with_label>(text: S) -> Self { + Self { + content: ButtonContent::Text(text.into()), + } + } + + pub fn with_image(image: ImageId) -> Self { + Self { + content: ButtonContent::Image(image), + } + } + + pub fn dom(self) -> Dom { + use self::ButtonContent::*; + + let mut button_root = Dom::div() + .with_class("__azul-native-button") + .with_tab_index(TabIndex::Auto); + + button_root.add_child(match self.content { + Text(s) => Dom::label(s), + Image(i) => Dom::image(i), + }); + + button_root + } +} + +#[test] +fn test_button_ui_1() { + + struct Mock; + + let expected = r#" +
+

Hello

+
+ "#; + let button: Dom = Button::with_label("Hello").dom(); + + button.assert_eq(expected); +} diff --git a/azul/src/widgets/label.rs b/azul/src/widgets/label.rs new file mode 100644 index 000000000..0fdc2c7f6 --- /dev/null +++ b/azul/src/widgets/label.rs @@ -0,0 +1,21 @@ +use { + dom::{Dom, DomString}, +}; + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct Label { + pub text: DomString, +} + +impl Label { + + #[inline] + pub fn new>(text: S) -> Self { + Self { text: text.into() } + } + + #[inline] + pub fn dom(self) -> Dom { + Dom::label(self.text).with_class("__azul-native-label") + } +} diff --git a/azul/src/widgets/mod.rs b/azul/src/widgets/mod.rs new file mode 100644 index 000000000..17908356f --- /dev/null +++ b/azul/src/widgets/mod.rs @@ -0,0 +1,11 @@ +#[cfg(feature = "svg")] +pub mod svg; +pub mod button; +pub mod label; +pub mod text_input; +pub mod table_view; + +pub mod errors { + #[cfg(all(feature = "svg", feature = "svg_parsing"))] + pub use super::svg::SvgParseError; +} \ No newline at end of file diff --git a/azul/src/widgets/svg.rs b/azul/src/widgets/svg.rs new file mode 100644 index 000000000..faf0911ad --- /dev/null +++ b/azul/src/widgets/svg.rs @@ -0,0 +1,2180 @@ +use std::{ + fmt, + rc::Rc, + sync::{Mutex, atomic::{Ordering, AtomicUsize}}, + cell::{RefCell, RefMut}, + collections::hash_map::Entry::*, +}; +#[cfg(feature = "svg_parsing")] +use std::io::{Error as IoError}; +use glium::{ + backend::Facade, index::PrimitiveType, + DrawParameters, IndexBuffer, VertexBuffer, + Program, Api, Surface, +}; +use lyon::{ + tessellation::{ + FillOptions, BuffersBuilder, FillVertex, FillTessellator, + LineCap, LineJoin, StrokeTessellator, StrokeOptions, StrokeVertex, + basic_shapes::{ + fill_circle, stroke_circle, fill_rounded_rectangle, + stroke_rounded_rectangle, BorderRadii + }, + }, + path::{ + default::{Builder, Path}, + builder::{PathBuilder, FlatPathBuilder}, + }, + geom::euclid::{TypedRect, TypedPoint2D, TypedSize2D, TypedVector2D, UnknownUnit}, +}; +#[cfg(feature = "svg_parsing")] +use usvg::{Error as SvgError}; +use azul_css::{ColorU, ColorF, StyleTextAlignmentHorz}; +use { + FastHashMap, + prelude::GlyphInstance, + callbacks::Texture, + window::FakeWindow, + app_resources::{AppResources, FontId}, + text_layout::{Words, ScaledWords, WordPositions, LineBreaks, LayoutedGlyphs, TextLayoutOptions}, +}; + +pub use lyon::tessellation::VertexBuffers; +pub use lyon::path::PathEvent; +pub use lyon::geom::math::Point; + +static SVG_LAYER_ID: AtomicUsize = AtomicUsize::new(0); +static SVG_TRANSFORM_ID: AtomicUsize = AtomicUsize::new(0); +static SVG_VIEW_BOX_ID: AtomicUsize = AtomicUsize::new(0); + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct SvgTransformId(usize); + +pub fn new_svg_transform_id() -> SvgTransformId { + SvgTransformId(SVG_TRANSFORM_ID.fetch_add(1, Ordering::SeqCst)) +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct SvgViewBoxId(usize); + +pub fn new_view_box_id() -> SvgViewBoxId { + SvgViewBoxId(SVG_VIEW_BOX_ID.fetch_add(1, Ordering::SeqCst)) +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] +pub struct SvgLayerId(usize); + +pub fn new_svg_layer_id() -> SvgLayerId { + SvgLayerId(SVG_LAYER_ID.fetch_add(1, Ordering::SeqCst)) +} + +const SHADER_VERSION_GL: &str = "#version 150"; +const SHADER_VERSION_GLES: &str = "#version 300 es"; +const DEFAULT_GLYPH_TOLERANCE: f32 = 0.01; + +const SVG_VERTEX_SHADER: &str = " + + precision highp float; + + #define attribute in + #define varying out + + in vec2 xy; + in vec2 normal; + + uniform vec2 bbox_size; + uniform vec2 offset; + uniform float z_index; + uniform float zoom; + uniform vec2 rotation_center; + uniform float rotation_sin; + uniform float rotation_cos; + uniform vec2 scale_factor; + uniform vec2 translate_px; + + void main() { + // Rotation first, then scale, then translation -- all in pixel space + vec2 rotation_center_xy = xy - rotation_center; + float new_x = (rotation_center_xy.x * rotation_cos) - (rotation_center_xy.y * rotation_sin); + float new_y = (xy.x * rotation_sin) + (xy.y * rotation_cos); + vec2 rotated_xy = vec2(new_x, new_y); + vec2 scaled_xy = rotated_xy * scale_factor; + vec2 translated_xy = scaled_xy + translate_px + rotation_center; + + vec2 position_centered = translated_xy / bbox_size; + vec2 position_zoomed = position_centered * vec2(zoom); + gl_Position = vec4(position_zoomed + (offset / bbox_size) - vec2(1.0), z_index, 1.0); + }"; + +const SVG_FRAGMENT_SHADER: &str = " + + precision highp float; + + #define attribute in + #define varying out + + uniform vec4 color; + out vec4 out_color; + + // The shader output is in SRGB color space, + // and the shader assumes that the input colors are in SRGB, too. + + void main() { + out_color = color; + } +"; + +fn prefix_gl_version(shader: &str, gl: Api) -> String { + match gl { + Api::Gl => format!("{}\n{}", SHADER_VERSION_GL, shader), + Api::GlEs => format!("{}\n{}", SHADER_VERSION_GLES, shader), + } +} + +#[derive(Debug, Clone)] +pub struct SvgShader { + pub program: Rc, +} + +impl SvgShader { + pub fn new(display: &F) -> Self { + use glium::program::ProgramCreationInput; + + let current_gl_api = display.get_context().get_opengl_version().0; + let vertex_source_prefixed = prefix_gl_version(SVG_VERTEX_SHADER, current_gl_api); + let fragment_source_prefixed = prefix_gl_version(SVG_FRAGMENT_SHADER, current_gl_api); + + let program_creation_input = ProgramCreationInput::SourceCode { + vertex_shader: &vertex_source_prefixed, + fragment_shader: &fragment_source_prefixed, + geometry_shader: None, + tessellation_control_shader: None, + tessellation_evaluation_shader: None, + transform_feedback_varyings: None, + + // Important: Disable automatic gl::GL_FRAMEBUFFER_SRGB - + // webrender expects SRGB textures and will handle this conversion for us + // See https://github.com/servo/webrender/issues/3262 + + outputs_srgb: true, + uses_point_size: false, + }; + + Self { + program: Rc::new(Program::new(display, program_creation_input).unwrap()), + } + } +} + +pub struct SvgCache { + // Stores the vertices and indices necessary for drawing. Must be synchronized with the `layers` + gpu_ready_to_upload_cache: FastHashMap, Vec)>, + stroke_gpu_ready_to_upload_cache: FastHashMap, Vec)>, + vertex_index_buffer_cache: RefCell, IndexBuffer)>>>, + stroke_vertex_index_buffer_cache: RefCell, IndexBuffer)>>>, + shader: Mutex>, +} + +impl Default for SvgCache { + fn default() -> Self { + Self { + gpu_ready_to_upload_cache: FastHashMap::default(), + stroke_gpu_ready_to_upload_cache: FastHashMap::default(), + vertex_index_buffer_cache: RefCell::new(FastHashMap::default()), + stroke_vertex_index_buffer_cache: RefCell::new(FastHashMap::default()), + shader: Mutex::new(None), + } + } +} + +fn fill_vertex_buffer_cache<'a, F: Facade>( + id: &SvgLayerId, + mut rmut: RefMut<'a, FastHashMap, IndexBuffer)>>>, + rnotmut: &FastHashMap, Vec)>, + window: &F) +{ + use std::collections::hash_map::Entry::*; + + match rmut.entry(*id) { + Occupied(_) => { }, + Vacant(v) => { + let (vbuf, ibuf) = match rnotmut.get(id).as_ref() { + Some(s) => s, + None => return, + }; + let vertex_buffer = VertexBuffer::new(window, vbuf).unwrap(); + let index_buffer = IndexBuffer::new(window, PrimitiveType::TrianglesList, ibuf).unwrap(); + v.insert(Rc::new((vertex_buffer, index_buffer))); + } + } +} + +impl SvgCache { + + /// Creates an empty SVG cache + pub fn empty() -> Self { + Self::default() + } + + /// Builds and compiles the SVG shader if the shader isn't already present + fn init_shader(&self, display: &F) -> SvgShader { + let mut shader_lock = self.shader.lock().unwrap(); + if shader_lock.is_none() { + *shader_lock = Some(SvgShader::new(display)); + } + shader_lock.as_ref().map(|s| s.clone()).unwrap() + } + + fn get_stroke_vertices_and_indices<'a, F: Facade>(&'a self, window: &F, id: &SvgLayerId) + -> Option, IndexBuffer)>> + { + + { + let rmut = self.stroke_vertex_index_buffer_cache.borrow_mut(); + let rnotmut = &self.stroke_gpu_ready_to_upload_cache; + fill_vertex_buffer_cache(id, rmut, rnotmut, window); + } + + self.stroke_vertex_index_buffer_cache.borrow().get(id).map(|x| x.clone()) + } + + /// Note: panics if the ID isn't found. + /// + /// Since we are required to keep the `self.layers` and the `self.gpu_buffer_cache` + /// in sync, a panic should never happen + fn get_vertices_and_indices<'a, F: Facade>(&'a self, window: &F, id: &SvgLayerId) + -> Option, IndexBuffer)>> + { + // We need the SvgCache to call this function immutably, otherwise we can't + // use it from the Layout::layout() function + { + let rmut = self.vertex_index_buffer_cache.borrow_mut(); + let rnotmut = &self.gpu_ready_to_upload_cache; + + fill_vertex_buffer_cache(id, rmut, rnotmut, window); + } + + self.vertex_index_buffer_cache.borrow().get(id).map(|x| x.clone()) + } + + pub fn add_layer(&mut self, layer: SvgLayerResourceDirect) -> (SvgLayerId, SvgStyle) { + + let SvgLayerResourceDirect { style, stroke, fill } = layer; + + let new_svg_id = new_svg_layer_id(); + + if let Some(fill) = fill { + self.gpu_ready_to_upload_cache.insert(new_svg_id, (fill.vertices, fill.indices)); + } + + if let Some(stroke) = stroke { + self.stroke_gpu_ready_to_upload_cache.insert(new_svg_id, (stroke.vertices, stroke.indices)); + } + + (new_svg_id, style) + } + + pub fn delete_layer(&mut self, svg_id: SvgLayerId) { + self.gpu_ready_to_upload_cache.remove(&svg_id); + self.stroke_gpu_ready_to_upload_cache.remove(&svg_id); + let rmut = self.vertex_index_buffer_cache.get_mut(); + let stroke_rmut = self.stroke_vertex_index_buffer_cache.get_mut(); + rmut.remove(&svg_id); + stroke_rmut.remove(&svg_id); + } + + pub fn clear_all_layers(&mut self) { + self.gpu_ready_to_upload_cache.clear(); + self.stroke_gpu_ready_to_upload_cache.clear(); + + let rmut = self.vertex_index_buffer_cache.get_mut(); + rmut.clear(); + + let stroke_rmut = self.stroke_vertex_index_buffer_cache.get_mut(); + stroke_rmut.clear(); + } + + /// Creates a new, identical, copied instance of the given layer - duplicates + /// the object in GPU memory, but this way re-tesselation can be avoided. + pub fn clone_layer(&mut self, svg_id: SvgLayerId) -> SvgLayerId { + + let new_svg_id = new_svg_layer_id(); + let old_svg_id = svg_id; + + if let Some(vertices_indices) = self.gpu_ready_to_upload_cache.get(&old_svg_id).cloned() { + self.gpu_ready_to_upload_cache.insert(new_svg_id, vertices_indices); + } + + if let Some(stroke_vertices_indices) = self.stroke_gpu_ready_to_upload_cache.get(&old_svg_id).cloned() { + self.stroke_gpu_ready_to_upload_cache.insert(new_svg_id, stroke_vertices_indices); + } + + // Needs to be on a separate line, otherwise it would get a BorrowMut error! + let vertices_indices_clone = self.vertex_index_buffer_cache.borrow().get(&old_svg_id).cloned(); + if let Some(vertices_indices) = vertices_indices_clone { + self.vertex_index_buffer_cache.borrow_mut().insert(new_svg_id, vertices_indices); + } + + let stroke_vertices_indices_clone = self.stroke_vertex_index_buffer_cache.borrow().get(&old_svg_id).cloned(); + if let Some(stroke_vertices_indices) = stroke_vertices_indices_clone { + self.stroke_vertex_index_buffer_cache.borrow_mut().insert(new_svg_id, stroke_vertices_indices); + } + + new_svg_id + } + + /// Parses an input source, parses the SVG, adds the shapes as layers into + /// the registry, returns the IDs of the added shapes, in the order that they appeared in the Svg + #[cfg(feature = "svg_parsing")] + pub fn add_svg>(&mut self, input: S) -> Result, SvgParseError> { + let layers = self::svg_to_lyon::parse_from(input)?; + Ok(layers + .into_iter() + .map(|(layer, style)| SvgLayerResourceDirect::tesselate_from_layer(&layer, style)) + .map(|tesselated_layer| self.add_layer(tesselated_layer)) + .collect()) + } +} + +impl fmt::Debug for SvgCache { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + for layer_id in self.gpu_ready_to_upload_cache.keys() { + write!(f, "{:?}", layer_id)?; + } + Ok(()) + } +} + +const GL_RESTART_INDEX: u32 = ::std::u32::MAX; + +/// Returns the (fill, stroke) vertices of a layer +pub fn tesselate_polygon_data(layer_data: &[SvgLayerType], style: SvgStyle) +-> SvgLayerResourceDirect // (Option<(Vec, Vec)>, Option<(Vec, Vec)>) +{ + let tolerance = 0.01; + let fill = style.fill.is_some(); + let stroke_options = style.stroke.map(|s| s.1); + + let mut last_index = 0; + let mut fill_vertex_buf = Vec::::new(); + let mut fill_index_buf = Vec::::new(); + + let mut last_stroke_index = 0; + let mut stroke_vertex_buf = Vec::::new(); + let mut stroke_index_buf = Vec::::new(); + + for layer in layer_data { + + let mut path = None; + + if fill { + let VertexBuffers { vertices, indices } = layer.tesselate_fill(tolerance, &mut path); + let fill_vertices_len = vertices.len(); + fill_vertex_buf.extend(vertices.into_iter()); + fill_index_buf.extend(indices.into_iter().map(|i| i as u32 + last_index as u32)); + fill_index_buf.push(GL_RESTART_INDEX); + last_index += fill_vertices_len; + } + + if let Some(stroke_options) = &stroke_options { + let VertexBuffers { vertices, indices } = layer.tesselate_stroke(tolerance, &mut path, *stroke_options); + let stroke_vertices_len = vertices.len(); + stroke_vertex_buf.extend(vertices.into_iter()); + stroke_index_buf.extend(indices.into_iter().map(|i| i as u32 + last_stroke_index as u32)); + stroke_index_buf.push(GL_RESTART_INDEX); + last_stroke_index += stroke_vertices_len; + } + } + + let fill_verts = if fill { + Some(VerticesIndicesBuffer { + vertices: fill_vertex_buf, + indices: fill_index_buf, + }) + } else { None }; + + let stroke_verts = if stroke_options.is_some() { + Some(VerticesIndicesBuffer { + vertices: stroke_vertex_buf, + indices: stroke_index_buf, + }) + } else { None }; + + SvgLayerResourceDirect { + style, + fill: fill_verts, + stroke: stroke_verts, + } +} + +/// Quick helper function to generate the vertices for a black circle at runtime +pub fn quick_circle(circle: SvgCircle, fill_color: ColorU) -> SvgLayerResourceDirect { + let style = SvgStyle::filled(fill_color); + tesselate_polygon_data(&[SvgLayerType::Circle(circle)], style) +} + +/// Quick helper function to generate the layer for **multiple** circles (in one draw call) +pub fn quick_circles(circles: &[SvgCircle], fill_color: ColorU) -> SvgLayerResourceDirect { + let circles = circles.iter().map(|c| SvgLayerType::Circle(*c)).collect::>(); + let style = SvgStyle::filled(fill_color); + tesselate_polygon_data(&circles, style) +} + +/// Helper function to easily draw some lines at runtime +/// +/// ## Inputs +/// +/// - `lines`: Each item in `lines` is a line (represented by a `Vec<(x, y)>`). +/// Lines that are shorter than 2 points are ignored / not rendered. +/// - `stroke_color`: The color of the line +/// - `stroke_options`: If the line should be round, square, etc. +pub fn quick_lines(lines: &[Vec<(f32, f32)>], stroke_color: ColorU, stroke_options: Option) +-> SvgLayerResourceDirect +{ + let stroke_options = stroke_options.unwrap_or_default(); + let style = SvgStyle::stroked(stroke_color, stroke_options); + + let polygons = lines.iter() + .filter(|line| line.len() >= 2) + .map(|line| { + + let first_point = &line[0]; + let mut poly_events = vec![PathEvent::MoveTo(TypedPoint2D::new(first_point.0, first_point.1))]; + + for (x, y) in line.iter().skip(1) { + poly_events.push(PathEvent::LineTo(TypedPoint2D::new(*x, *y))); + } + + SvgLayerType::Polygon(poly_events) + }).collect::>(); + + tesselate_polygon_data(&polygons, style) +} + +pub fn quick_rects(rects: &[SvgRect], stroke_color: Option, fill_color: Option, stroke_options: Option) +-> SvgLayerResourceDirect +{ + let style = SvgStyle { + stroke: stroke_color.map(|col| (col, stroke_options.unwrap_or_default())), + fill: fill_color, + .. Default::default() + }; + let rects = rects.iter().map(|r| SvgLayerType::Rect(*r)).collect::>(); + tesselate_polygon_data(&rects, style) +} + +const BEZIER_SAMPLE_RATE: usize = 20; + +type ArcLength = f32; + +/// The sampled bezier curve stores information about 10 points that lie along the +/// bezier curve. +/// +/// For example: To place a text on a curve, we only have the layout +/// of the text in pixels. In order to calculate the position and rotation of +/// the individual characters (to place the text on the curve) we need to know +/// what the percentage offset (from 0.0 to 1.0) of the current character is +/// (which we can then give to the bezier formula, which will calculate the position +/// and rotation of the character) +/// +/// Calculating the position accurately is an unsolvable problem, but we can +/// "estimate" where the character would be, by solving 10 bezier points +/// for the offsets 0.0, 0.1, 0.2, and so on and storing the arc length from the +/// start for each position, ex. the position 0.1 is at 20 pixels, the position +/// 0.5 at 500 pixels, etc. Since a bezier curve is, well, curved, this offset is +/// not constantly increasing, it can vary from point to point. +/// +/// Lastly, to get the percentage of the string on the curve, we simply interpolate +/// linearly between the two nearest values. I.e. if we need to place a character +/// at 300 pixels from the start, we interpolate linearly between 0.1 +/// (which we know is at 20 pixels) and 0.5 (which we know is at 500 pixels). +/// +/// This process is called "arc length parametrization". For more info + diagrams, see: +/// http://www.planetclegg.com/projects/WarpingTextToSplines.html +#[derive(Debug, Copy, Clone)] +pub struct SampledBezierCurve { + /// Copy of the original curve which the SampledBezierCurve was created from + original_curve: [BezierControlPoint;4], + /// Total length of the arc of the curve (from 0.0 to 1.0) + arc_length: f32, + /// Stores the x and y position of the sampled bezier points + sampled_bezier_points: [BezierControlPoint;BEZIER_SAMPLE_RATE + 1], + /// Each index is the bezier value * 0.1, i.e. index 1 = 0.1, + /// index 2 = 0.2 and so on. + /// + /// Stores the length of the BezierControlPoint at i from the + /// start of the curve + arc_length_parametrization: [ArcLength; BEZIER_SAMPLE_RATE + 1], +} + +/// NOTE: The inner value is in **radians**, not degrees! +#[derive(Debug, Copy, Clone)] +pub struct BezierCharacterRotation(pub f32); + +impl SampledBezierCurve { + + /// Roughly estimate the length of a bezier curve arc using 10 samples + pub fn from_curve(curve: &[BezierControlPoint;4]) -> Self { + + let mut sampled_bezier_points = [curve[0]; BEZIER_SAMPLE_RATE + 1]; + let mut arc_length_parametrization = [0.0; BEZIER_SAMPLE_RATE + 1]; + + for i in 1..(BEZIER_SAMPLE_RATE + 1) { + sampled_bezier_points[i] = cubic_interpolate_bezier( + curve, i as f32 / BEZIER_SAMPLE_RATE as f32 + ); + } + + sampled_bezier_points[BEZIER_SAMPLE_RATE] = curve[3]; + + // arc_length represents the sum of all sampled arcs up until the + // current sampled iteration point + let mut arc_length = 0.0; + + for (i, w) in sampled_bezier_points.windows(2).enumerate() { + let dist_current = w[0].distance(&w[1]); + arc_length_parametrization[i] = arc_length; + arc_length += dist_current; + } + + arc_length_parametrization[BEZIER_SAMPLE_RATE] = arc_length; + + SampledBezierCurve { + original_curve: *curve, + arc_length, + sampled_bezier_points, + arc_length_parametrization, + } + } + + /// Offset should be the point you seek from the start, i.e. 500 pixels for example. + /// + /// NOTE: Currently this function assumes a value that will be on the curve, + /// not past the 1.0 mark. + pub fn get_bezier_percentage_from_offset(&self, offset: f32) -> f32 { + + let mut lower_bound = 0; + let mut upper_bound = BEZIER_SAMPLE_RATE; + + // If the offset is too high (past 1.0) we simply interpolate between the 0.9 + // and 1.0 point. Because of this we don't want to include the last point when iterating + for (i, param) in self.arc_length_parametrization.iter().take(BEZIER_SAMPLE_RATE).enumerate() { + if *param < offset { + lower_bound = i; + } else if *param > offset { + upper_bound = i; + break; + } + } + + // Now we know that the offset lies between the lower and upper bound, we need to + // find out how much we should (linearly) interpolate + let lower_bound_value = self.arc_length_parametrization[lower_bound]; + let upper_bound_value = self.arc_length_parametrization[upper_bound]; + let lower_upper_diff = upper_bound_value - lower_bound_value; + let interpolate_percent = (offset - lower_bound_value) / lower_upper_diff; + + let lower_bound_percent = lower_bound as f32 / BEZIER_SAMPLE_RATE as f32; + let upper_bound_percent = upper_bound as f32 / BEZIER_SAMPLE_RATE as f32; + + let lower_upper_diff_percent = upper_bound_percent - lower_bound_percent; + lower_bound_percent + (lower_upper_diff_percent * interpolate_percent) + } + + /// Place some glyphs on a curve and calculate the respective offsets and rotations + /// for the glyphs + /// + /// ## Inputs + /// + /// - `glyphs`: The glyph positions of the text you want to place on the curve + /// - `start_offset` The offset of the first character from the start of the curve: + /// **Note**: `start_offset` is measured in pixels, not percent! + /// + /// ## Returns + /// + /// - `Vec<(f32, f32)>`: the x and y offsets of the glyph characters + /// - `Vec`: The rotations in degrees of the glyph characters + pub fn get_text_offsets_and_rotations(&self, glyphs: &[GlyphInstance], start_offset: f32) + -> (Vec<(f32, f32)>, Vec) + { + let mut glyph_offsets = Vec::new(); + let mut glyph_rotations = Vec::new(); + + // NOTE: g.point.x is the offset from the start, not the advance! + let mut current_offset = start_offset + glyphs.get(0).and_then(|g| Some(g.point.x)).unwrap_or(0.0); + let mut last_offset = start_offset; + + for glyph_idx in 0..glyphs.len() { + let char_bezier_percentage = self.get_bezier_percentage_from_offset(current_offset); + let char_bezier_pt = cubic_interpolate_bezier(&self.original_curve, char_bezier_percentage); + glyph_offsets.push((char_bezier_pt.x / SVG_FAKE_FONT_SIZE, char_bezier_pt.y / SVG_FAKE_FONT_SIZE)); + + let char_rotation_percentage = self.get_bezier_percentage_from_offset(last_offset); + let rotation = cubic_bezier_normal(&self.original_curve, char_rotation_percentage).to_rotation(); + glyph_rotations.push(rotation); + + last_offset = current_offset; + current_offset = start_offset + glyphs.get(glyph_idx + 1).map(|g| g.point.x).unwrap_or(0.0); + } + + (glyph_offsets, glyph_rotations) + } + + /// Returns the bounding box of the 4 points making up the curve. + /// + /// Since a bezier curve is always contained within the 4 control points, + /// the returned Bbox can be used for hit-testing. + pub fn get_bbox(&self) -> (SvgBbox, [(usize, usize);2]) { + + let mut lowest_x = self.original_curve[0].x; + let mut highest_x = self.original_curve[0].x; + let mut lowest_y = self.original_curve[0].y; + let mut highest_y = self.original_curve[0].y; + + let mut lowest_x_idx = 0; + let mut highest_x_idx = 0; + let mut lowest_y_idx = 0; + let mut highest_y_idx = 0; + + for (idx, BezierControlPoint { x, y }) in self.original_curve.iter().enumerate().skip(1) { + if *x < lowest_x { + lowest_x = *x; + lowest_x_idx = idx; + } + if *x > highest_x { + highest_x = *x; + highest_x_idx = idx; + } + if *y < lowest_y { + lowest_y = *y; + lowest_y_idx = idx; + } + if *y > highest_y { + highest_y = *y; + highest_y_idx = idx; + } + } + + ( + SvgBbox(TypedRect::new(TypedPoint2D::new(lowest_x, lowest_y), TypedSize2D::new(highest_x - lowest_x, highest_y - lowest_y))), + [(lowest_x_idx, lowest_y_idx), (highest_x_idx, highest_y_idx)] + ) + } + + /// Returns the geometry necessary for drawing the points from `self.sampled_bezier_points`. + /// Usually only good for debugging + pub fn draw_circles(&self, color: ColorU) -> SvgLayerResourceDirect { + quick_circles( + &self.sampled_bezier_points + .iter() + .map(|c| SvgCircle { center_x: c.x, center_y: c.y, radius: 1.0 }) + .collect::>(), + color) + } + + /// Returns the geometry necessary to draw the control handles of this curve + pub fn draw_control_handles(&self, color: ColorU) -> SvgLayerResourceDirect { + quick_circles( + &self.original_curve + .iter() + .map(|c| SvgCircle { center_x: c.x, center_y: c.y, radius: 3.0 }) + .collect::>(), + color) + } + + /// Returns the geometry necessary to draw the bezier curve (the actual line) + pub fn draw_lines(&self, stroke_color: ColorU) -> SvgLayerResourceDirect { + let line = [self.sampled_bezier_points.iter().map(|b| (b.x, b.y)).collect()]; + quick_lines(&line, stroke_color, None) + } + + /// Returns the sampled points from this bezier curve + pub fn get_sampled_points<'a>(&'a self) -> &'a [BezierControlPoint;BEZIER_SAMPLE_RATE + 1] { + &self.sampled_bezier_points + } +} + +/// Joins multiple SvgVert buffers to one and calculates the indices +/// +/// TODO: Wrap this in a nicer API +pub fn join_vertex_buffers(input: &[VertexBuffers]) -> VerticesIndicesBuffer { + + let mut last_index = 0; + let mut vertex_buf = Vec::::new(); + let mut index_buf = Vec::::new(); + + for VertexBuffers { vertices, indices } in input { + let vertices_len = vertices.len(); + vertex_buf.extend(vertices.into_iter()); + index_buf.extend(indices.into_iter().map(|i| *i as u32 + last_index as u32)); + index_buf.push(GL_RESTART_INDEX); + last_index += vertices_len; + } + + VerticesIndicesBuffer { vertices: vertex_buf, indices: index_buf } +} + +pub fn transform_vertex_buffer(input: &mut [SvgVert], x: f32, y: f32) { + for vert in input { + vert.xy.0 += x; + vert.xy.1 += y; + } +} + +/// sin and cos are the sinus and cosinus of the rotation +pub fn rotate_vertex_buffer(input: &mut [SvgVert], sin: f32, cos: f32) { + for vert in input { + let (x, y) = vert.xy; + let new_x = (x * cos) - (y * sin); + let new_y = (x * sin) + (y * cos); + vert.xy = (new_x, new_y); + } +} + +#[cfg(feature = "svg_parsing")] +#[derive(Debug)] +pub enum SvgParseError { + /// Syntax error in the Svg + FailedToParseSvg(SvgError), + /// Io error reading the Svg + IoError(IoError), +} + +#[cfg(feature = "svg_parsing")] +impl From for SvgParseError { + fn from(e: SvgError) -> Self { + SvgParseError::FailedToParseSvg(e) + } +} + +#[cfg(feature = "svg_parsing")] +impl From for SvgParseError { + fn from(e: IoError) -> Self { + SvgParseError::IoError(e) + } +} + +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub struct SvgStyle { + /// Stroke color + pub stroke: Option<(ColorU, SvgStrokeOptions)>, + /// Fill color + pub fill: Option, + /// Stores rotation, translation + pub transform: SvgTransform, + // TODO: stroke-dasharray +} + +impl SvgStyle { + + /// If the style already has a translation, adds the new translation, + /// otherwise initializes the value to the new translation + pub fn translate(&mut self, x_px: f32, y_px: f32) { + let (cur_x, cur_y) = self.transform.translation.map(|t| (t.x, t.y)).unwrap_or((0.0, 0.0)); + self.transform.translation = Some(SvgTranslation { x: cur_x + x_px, y: cur_y + y_px }); + } + + /// If the style already has a rotation, adds the rotation, otherwise sets the rotation + /// + /// Input is in degrees. + pub fn rotate(&mut self, degrees: f32) { + let current_rotation = self.transform.rotation.map(|r| r.1.to_degrees()).unwrap_or(0.0); + let current_rotation_point = self.transform.rotation.map(|r| r.0).unwrap_or_default(); + self.transform.rotation = Some((current_rotation_point, SvgRotation::degrees(current_rotation + degrees))); + } + + /// If the style already has a scale, adds the rotation, otherwise sets the scale. + pub fn scale(&mut self, scale_factor_x: f32, scale_factor_y: f32) { + let (new_scale_x, new_scale_y) = match self.transform.scale { + Some(s) => (s.x * scale_factor_x, s.y * scale_factor_y), + None => (scale_factor_x, scale_factor_y), + }; + self.transform.scale = Some(SvgScaleFactor { x: new_scale_x, y: new_scale_y }); + } + + /// If the style already has a rotation, adds the rotation, otherwise sets the rotation point to the new value + pub fn move_rotation_point(&mut self, rotation_point_x: f32, rotation_point_y: f32) { + let current_rotation_point = self.transform.rotation.map(|r| r.0).unwrap_or_default(); + let current_rotation = self.transform.rotation.unwrap_or_default().1; + let new_rotation_point = SvgRotationPoint { x: current_rotation_point.x + rotation_point_x, y: current_rotation_point.y + rotation_point_y }; + self.transform.rotation = Some((new_rotation_point, current_rotation)); + } + + /// Replaces the translation value with the new x and y values - or initializes it to the new value + pub fn set_translation(&mut self, x_px: f32, y_px: f32) { + self.transform.translation = Some(SvgTranslation { x: x_px, y: y_px }); + } + + /// Replaces the rotation value with the new rotation values - or initializes it, if set to None + pub fn set_rotation(&mut self, degrees: f32) { + let current_rotation_point = self.transform.rotation.map(|r| r.0).unwrap_or_default(); + let new_rotation = SvgRotation { degrees }; + self.transform.rotation = Some((current_rotation_point, new_rotation)); + } + + /// Replaces the scale value with the new x and y values - or initializes it to the new value + pub fn set_scale(&mut self, scale_factor_x: f32, scale_factor_y: f32) { + self.transform.scale = Some(SvgScaleFactor { x: scale_factor_x, y: scale_factor_y }); + } + + /// Replaces the rotation value with the new rotation values - or initializes it, if set to None + pub fn set_rotation_point(&mut self, rotation_point_x: f32, rotation_point_y: f32) { + let current_rotation = self.transform.rotation.unwrap_or_default().1; + let new_rotation_point = SvgRotationPoint { x: rotation_point_x, y: rotation_point_y }; + self.transform.rotation = Some((new_rotation_point, current_rotation)); + } +} + +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub struct SvgTransform { + /// Rotation of this SVG layer in degrees, around the point specified in the SvgRotationPoint + pub rotation: Option<(SvgRotationPoint, SvgRotation)>, + /// Translates the individual layer additionally to the whole SVG + pub translation: Option, + /// Scaling factor of this shape + pub scale: Option, +} + +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub struct SvgRotation { degrees: f32 } + +impl SvgRotation { + /// Note: Assumes that the input is in degrees, not radians! + pub fn degrees(degrees: f32) -> Self { Self { degrees } } + + pub fn to_degrees(&self) -> f32 { self.degrees } + + // Returns the (sin, cos) in radians + fn to_rotation(&self) -> (f32, f32) { + let rad = self.degrees.to_radians(); + (rad.sin(), rad.cos()) + } +} + +/// Rotation point, local to the current SVG layer, i.e. (0.0, 0.0) will +/// rotate the shape on the top left corner +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub struct SvgRotationPoint { pub x: f32, pub y: f32 } + +/// Scale factor (1.0, 1.0) by default. Unit is in normalized percent. +/// Shapes can be stretched and squished. +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct SvgScaleFactor { pub x: f32, pub y: f32 } + +impl Default for SvgScaleFactor { + fn default() -> Self { + SvgScaleFactor { x: 1.0, y: 1.0 } + } +} + +/// Translation **in pixels** (or whatever the source unit for rendered SVG data +/// is, but usually this will be pixels) +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub struct SvgTranslation { pub x: f32, pub y: f32 } + +impl SvgStyle { + pub fn stroked(color: ColorU, stroke_opts: SvgStrokeOptions) -> Self { + Self { + stroke: Some((color, stroke_opts)), + .. Default::default() + } + } + + pub fn filled(color: ColorU) -> Self { + Self { + fill: Some(color), + .. Default::default() + } + } +} +// similar to lyon::SvgStrokeOptions, except the +// thickness is a usize (f32 * 1000 as usize), in order +// to implement Hash +#[derive(Debug, Copy, Clone, PartialEq, Hash)] +pub struct SvgStrokeOptions { + /// What cap to use at the start of each sub-path. + /// + /// Default value: `LineCap::Butt`. + pub start_cap: SvgLineCap, + + /// What cap to use at the end of each sub-path. + /// + /// Default value: `LineCap::Butt`. + pub end_cap: SvgLineCap, + + /// See the SVG specification. + /// + /// Default value: `LineJoin::Miter`. + pub line_join: SvgLineJoin, + + /// Line width + /// + /// Default value: `StrokeOptions::DEFAULT_LINE_WIDTH`. + line_width: usize, + + /// See the SVG specification. + /// + /// Must be greater than or equal to 1.0. + /// Default value: `StrokeOptions::DEFAULT_MITER_LIMIT`. + miter_limit: usize, + + /// Maximum allowed distance to the path when building an approximation. + /// + /// See [Flattening and tolerance](index.html#flattening-and-tolerance). + /// Default value: `StrokeOptions::DEFAULT_TOLERANCE`. + tolerance: usize, + + /// Apply line width + /// + /// When set to false, the generated vertices will all be positioned in the centre + /// of the line. The width can be applied later on (eg in a vertex shader) by adding + /// the vertex normal multiplied by the line with to each vertex position. + /// + /// Default value: `true`. + pub apply_line_width: bool, +} + +const SVG_LINE_PRECISION: f32 = 1000.0; + +impl SvgStrokeOptions { + /// NOTE: Getters and setters are necessary here, because the line width, miter limit, etc. + /// are all normalized to fit into a usize + pub fn with_line_width(mut self, line_width: f32) -> Self { self.set_line_width(line_width); self } + pub fn set_line_width(&mut self, line_width: f32) { self.line_width = (line_width * SVG_LINE_PRECISION) as usize; } + pub fn get_line_width(&self) -> f32 { self.line_width as f32 / SVG_LINE_PRECISION } + pub fn with_miter_limit(mut self, miter_limit: f32) -> Self { self.set_miter_limit(miter_limit); self } + pub fn set_miter_limit(&mut self, miter_limit: f32) { self.miter_limit = (miter_limit * SVG_LINE_PRECISION) as usize; } + pub fn get_miter_limit(&self) -> f32 { self.miter_limit as f32 / SVG_LINE_PRECISION } + pub fn with_tolerance(mut self, tolerance: f32) -> Self { self.set_tolerance(tolerance); self } + pub fn set_tolerance(&mut self, tolerance: f32) { self.tolerance = (tolerance * SVG_LINE_PRECISION) as usize; } + pub fn get_tolerance(&self) -> f32 { self.tolerance as f32 / SVG_LINE_PRECISION } +} + +impl Into for SvgStrokeOptions { + fn into(self) -> StrokeOptions { + let target = StrokeOptions::default() + .with_tolerance(self.get_tolerance()) + .with_start_cap(self.start_cap.into()) + .with_end_cap(self.end_cap.into()) + .with_line_join(self.line_join.into()) + .with_line_width(self.get_line_width()) + .with_miter_limit(self.get_miter_limit()); + + if !self.apply_line_width { + target.dont_apply_line_width() + } else { + target + } + } +} + +impl Default for SvgStrokeOptions { + fn default() -> Self { + const DEFAULT_MITER_LIMIT: f32 = 4.0; + const DEFAULT_LINE_WIDTH: f32 = 1.0; + const DEFAULT_TOLERANCE: f32 = 0.1; + + Self { + start_cap: SvgLineCap::default(), + end_cap: SvgLineCap::default(), + line_join: SvgLineJoin::default(), + line_width: (DEFAULT_LINE_WIDTH * SVG_LINE_PRECISION) as usize, + miter_limit: (DEFAULT_MITER_LIMIT * SVG_LINE_PRECISION) as usize, + tolerance: (DEFAULT_TOLERANCE * SVG_LINE_PRECISION) as usize, + apply_line_width: true, + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Hash)] +pub enum SvgLineCap { + Butt, + Square, + Round, +} + +impl Default for SvgLineCap { + fn default() -> Self { + SvgLineCap::Butt + } +} + +impl Into for SvgLineCap { + #[inline] + fn into(self) -> LineCap { + use self::SvgLineCap::*; + match self { + Butt => LineCap::Butt, + Square => LineCap::Square, + Round => LineCap::Round, + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Hash)] +pub enum SvgLineJoin { + Miter, + MiterClip, + Round, + Bevel, +} + +impl Default for SvgLineJoin { + fn default() -> Self { + SvgLineJoin::Miter + } +} + +impl Into for SvgLineJoin { + #[inline] + fn into(self) -> LineJoin { + use self::SvgLineJoin::*; + match self { + Miter => LineJoin::Miter, + MiterClip => LineJoin::MiterClip, + Round => LineJoin::Round, + Bevel => LineJoin::Bevel, + } + } +} + +/// One "layer" is simply one or more polygons that get drawn using the same style +/// i.e. one SVG `` element +/// +/// Note: If you want to draw text in a SVG element, you need to convert the character +/// of the font to a `Vec), + Circle(SvgCircle), + Rect(SvgRect), +} + +#[derive(Debug, Copy, Clone)] +pub struct SvgVert { + pub xy: (f32, f32), + pub normal: (f32, f32), +} + +implement_vertex!(SvgVert, xy, normal); + +#[derive(Debug, Copy, Clone)] +pub struct SvgWorldPixel; + +pub type GlyphId = u32; + +/// A vectorized font holds the glyphs for a given font, but in a vector format +#[derive(Debug, Clone, Default)] +pub struct VectorizedFont { + /// Glyph -> Polygon map + glyph_polygon_map: RefCell>>, + /// Glyph -> Stroke map + glyph_stroke_map: RefCell>>, + /// Original font bytes + font_bytes: Vec, +} + +use stb_truetype::{FontInfo, Vertex}; + +impl VectorizedFont { + + /// Prepares a vectorized font from a set of bytes + pub fn from_bytes(font_bytes: Vec) -> Self { + Self { + font_bytes, + .. Default::default() + } + } + + pub fn get_fill_vertices(&self, glyphs: &[GlyphInstance]) -> Vec> { + + let font_info = match FontInfo::new(self.font_bytes.clone(), 0) { + Some(s) => s, + None => return Vec::new(), + }; + + let mut borrow_mut = self.glyph_polygon_map.borrow_mut(); + glyphs.iter().filter_map(|glyph| { + match borrow_mut.entry(glyph.index) { + Occupied(o) => Some(o.get().clone()), + Vacant(v) => { + let glyph_shape = font_info.get_glyph_shape(glyph.index)?; + let poly = glyph_to_svg_layer_type(glyph_shape); + let mut path = None; + let polygon_verts = poly.tesselate_fill(DEFAULT_GLYPH_TOLERANCE, &mut path); + v.insert(polygon_verts.clone()); + Some(polygon_verts) + } + } + }).collect() + } + + pub fn get_stroke_vertices(&self, glyphs: &[GlyphInstance], stroke_options: &SvgStrokeOptions) -> Vec> { + + let font_info = match FontInfo::new(self.font_bytes.clone(), 0) { + Some(s) => s, + None => return Vec::new(), + }; + + let mut borrow_mut = self.glyph_stroke_map.borrow_mut(); + glyphs.iter().filter_map(|glyph| { + match borrow_mut.entry(glyph.index) { + Occupied(o) => Some(o.get().clone()), + Vacant(v) => { + let glyph_shape = font_info.get_glyph_shape(glyph.index)?; + let poly = glyph_to_svg_layer_type(glyph_shape); + let mut path = None; + let stroke_verts = poly.tesselate_stroke(DEFAULT_GLYPH_TOLERANCE, &mut path, *stroke_options); + v.insert(stroke_verts.clone()); + Some(stroke_verts) + } + } + }).collect() + } +} + + +/// Converts a `Vec` to a `SvgLayerType::Polygon` +fn glyph_to_svg_layer_type(vertices: Vec) -> SvgLayerType { + SvgLayerType::Polygon(vertices.into_iter().map(rusttype_glyph_to_path_events).collect()) +} + +// Convert a Rusttype glyph to a Vec of PathEvents, +// in order to turn a glyph into a polygon +fn rusttype_glyph_to_path_events(vertex: Vertex) -> PathEvent { + + use stb_truetype::VertexType; + + // Rusttypes vertex type needs to be inverted in the Y axis + // in order to work with lyon correctly + match vertex.vertex_type() { + VertexType::CurveTo => PathEvent::QuadraticTo( + Point::new(vertex.cx as f32, -(vertex.cy as f32)), + Point::new(vertex.x as f32, -(vertex.y as f32)) + ), + VertexType::MoveTo => PathEvent::MoveTo(Point::new(vertex.x as f32, -(vertex.y as f32))), + VertexType::LineTo => PathEvent::LineTo(Point::new(vertex.x as f32, -(vertex.y as f32))), + } +} + +#[derive(Debug, Clone, Default)] +pub struct VectorizedFontCache { + /// Font -> Vectorized glyph map + /// + /// Needs to be wrapped in a RefCell / Rc since we want to lazy-load the + /// fonts to keep the memory usage down + vectorized_fonts: RefCell>>, +} + +impl VectorizedFontCache { + + pub fn new() -> Self { + Self::default() + } + + pub fn insert_if_not_exist(&mut self, id: FontId, font_bytes: Vec) { + self.vectorized_fonts.borrow_mut().entry(id).or_insert_with(|| Rc::new(VectorizedFont::from_bytes(font_bytes))); + } + + pub fn insert(&mut self, id: FontId, font: VectorizedFont) { + self.vectorized_fonts.borrow_mut().insert(id, Rc::new(font)); + } + + /// Returns true if the font cache has the respective font + pub fn has_font(&self, id: &FontId) -> bool { + self.vectorized_fonts.borrow().get(id).is_some() + } + + pub fn get_font(&self, id: &FontId, app_resources: &AppResources) -> Option> { + use std::collections::hash_map::Entry::*; + + match self.vectorized_fonts.borrow_mut().entry(id.clone()) { + Occupied(_) => { }, + Vacant(v) => { + let font_bytes = app_resources.get_font_bytes(&id)?; + let (font_bytes, _) = font_bytes.ok()?; + v.insert(Rc::new(VectorizedFont::from_bytes(font_bytes))); + } + } + + self.vectorized_fonts.borrow().get(&id).map(|font| font.clone()) + } + + pub fn remove_font(&mut self, id: &FontId) { + self.vectorized_fonts.borrow_mut().remove(id); + } +} + +impl SvgLayerType { + + pub fn tesselate_fill(&self, tolerance: f32, polygon: &mut Option) + -> VertexBuffers + { + let mut geometry = VertexBuffers::new(); + + match self { + SvgLayerType::Polygon(p) => { + if polygon.is_none() { + *polygon = Some(build_path_from_polygon(&p, tolerance)); + } + + let path = polygon.as_ref().unwrap(); + + let mut tessellator = FillTessellator::new(); + tessellator.tessellate_path( + path.path_iter(), + &FillOptions::default(), + &mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex| { + SvgVert { + xy: (vertex.position.x, vertex.position.y), + normal: (vertex.normal.x, vertex.position.y), + } + }), + ).unwrap(); + }, + SvgLayerType::Circle(c) => { + let center = TypedPoint2D::new(c.center_x, c.center_y); + fill_circle(center, c.radius, &FillOptions::default(), + &mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex| { + SvgVert { + xy: (vertex.position.x, vertex.position.y), + normal: (vertex.normal.x, vertex.position.y), + } + } + )); + }, + SvgLayerType::Rect(r) => { + let (rect, radii) = get_radii(&r); + fill_rounded_rectangle(&rect, &radii, &FillOptions::default(), + &mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex| { + SvgVert { + xy: (vertex.position.x, vertex.position.y), + normal: (vertex.normal.x, vertex.position.y), + } + } + )); + } + } + + geometry + } + + pub fn tesselate_stroke( + &self, + tolerance: f32, + polygon: &mut Option, + stroke: SvgStrokeOptions + ) -> VertexBuffers { + + let mut stroke_geometry = VertexBuffers::new(); + let stroke_options: StrokeOptions = stroke.into(); + let stroke_options = stroke_options.with_tolerance(tolerance); + + match self { + SvgLayerType::Polygon(p) => { + if polygon.is_none() { + *polygon = Some(build_path_from_polygon(&p, tolerance)); + } + + let path = polygon.as_ref().unwrap(); + + let mut stroke_tess = StrokeTessellator::new(); + stroke_tess.tessellate_path( + path.path_iter(), + &stroke_options, + &mut BuffersBuilder::new(&mut stroke_geometry, |vertex: StrokeVertex| { + SvgVert { + xy: (vertex.position.x, vertex.position.y), + normal: (vertex.normal.x, vertex.position.y), + } + }), + ); + }, + SvgLayerType::Circle(c) => { + let center = TypedPoint2D::new(c.center_x, c.center_y); + stroke_circle(center, c.radius, &stroke_options, + &mut BuffersBuilder::new(&mut stroke_geometry, |vertex: StrokeVertex| { + SvgVert { + xy: (vertex.position.x, vertex.position.y), + normal: (vertex.normal.x, vertex.position.y), + } + } + )); + }, + SvgLayerType::Rect(r) => { + let (rect, radii) = get_radii(&r); + stroke_rounded_rectangle(&rect, &radii, &stroke_options, + &mut BuffersBuilder::new(&mut stroke_geometry, |vertex: StrokeVertex| { + SvgVert { + xy: (vertex.position.x, vertex.position.y), + normal: (vertex.normal.x, vertex.position.y), + } + } + )); + }, + } + + stroke_geometry + } +} + +fn get_radii(r: &SvgRect) -> (TypedRect, BorderRadii) { + let rect = TypedRect::new(TypedPoint2D::new(r.x, r.y), TypedSize2D::new(r.width, r.height)); + let radii = BorderRadii { + top_left: r.rx, + top_right: r.rx, + bottom_left: r.rx, + bottom_right: r.rx, + }; + (rect, radii) +} + +fn build_path_from_polygon(polygon: &[PathEvent], tolerance: f32) -> Path { + let mut builder = Builder::with_capacity(polygon.len()).flattened(tolerance); + for event in polygon { + builder.path_event(*event); + } + builder.with_svg().build() +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct SvgCircle { + pub center_x: f32, + pub center_y: f32, + pub radius: f32, +} + +impl SvgCircle { + pub fn contains_point(&self, x: f32, y: f32) -> bool { + let x_diff = (x - self.center_x).abs(); + let y_diff = (y - self.center_y).abs(); + (x_diff * x_diff) + (y_diff * y_diff) < (self.radius * self.radius) + } +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct SvgRect { + pub width: f32, + pub height: f32, + pub x: f32, + pub y: f32, + pub rx: f32, + pub ry: f32, +} + +impl SvgRect { + /// Note: does not incorporate rounded edges! + /// Origin of x and y is assumed to be the top left corner + pub fn contains_point(&self, x: f32, y: f32) -> bool { + x > self.x && + x < self.x + self.width && + y > self.y && + y < self.y + self.height + } +} + +#[cfg(feature = "svg_parsing")] +mod svg_to_lyon { + + use lyon::{ + math::Point, + path::PathEvent, + }; + use usvg::{Tree, PathSegment, Color, Options, Paint, Stroke, LineCap, LineJoin, NodeKind}; + use widgets::svg::{ + SvgStrokeOptions, SvgLineCap, SvgLineJoin, + SvgLayerType, SvgStyle, SvgParseError + }; + use azul_css::ColorU; + + pub fn parse_from>(svg_source: S) -> Result, SvgStyle)>, SvgParseError> { + + let opt = Options::default(); + let rtree = Tree::from_str(svg_source.as_ref(), &opt).unwrap(); + + let mut layer_data = Vec::new(); + + for node in rtree.root().descendants() { + if let NodeKind::Path(p) = &*node.borrow() { + let mut style = SvgStyle::default(); + + if let Some(ref fill) = p.fill { + + // fall back to always use color fill + // no gradients (yet?) + let color = match fill.paint { + Paint::Color(c) => c, + _ => FALLBACK_COLOR, + }; + + style.fill = Some(ColorU { + r: color.red, + g: color.green, + b: color.blue, + a: (fill.opacity.value() * 255.0) as u8 + }); + } + + if let Some(ref stroke) = p.stroke { + style.stroke = Some(convert_stroke(stroke)); + } + + let layer = vec![SvgLayerType::Polygon(p.segments.iter().map(|e| as_event(e)).collect())]; + layer_data.push((layer, style)); + } + } + + Ok(layer_data) + } + + // Map resvg::tree::PathSegment to lyon::path::PathEvent + fn as_event(ps: &PathSegment) -> PathEvent { + match *ps { + PathSegment::MoveTo { x, y } => PathEvent::MoveTo(Point::new(x as f32, y as f32)), + PathSegment::LineTo { x, y } => PathEvent::LineTo(Point::new(x as f32, y as f32)), + PathSegment::CurveTo { x1, y1, x2, y2, x, y, } => { + PathEvent::CubicTo( + Point::new(x1 as f32, y1 as f32), + Point::new(x2 as f32, y2 as f32), + Point::new(x as f32, y as f32)) + } + PathSegment::ClosePath => PathEvent::Close, + } + } + + pub const FALLBACK_COLOR: Color = Color { + red: 0, + green: 0, + blue: 0, + }; + + // dissect a resvg::Stroke into a webrender::ColorU + SvgStrokeOptions + pub fn convert_stroke(s: &Stroke) -> (ColorU, SvgStrokeOptions) { + + let color = match s.paint { + Paint::Color(c) => c, + _ => FALLBACK_COLOR, + }; + let line_cap = match s.linecap { + LineCap::Butt => SvgLineCap::Butt, + LineCap::Square => SvgLineCap::Square, + LineCap::Round => SvgLineCap::Round, + }; + let line_join = match s.linejoin { + LineJoin::Miter => SvgLineJoin::Miter, + LineJoin::Bevel => SvgLineJoin::Bevel, + LineJoin::Round => SvgLineJoin::Round, + }; + + let opts = SvgStrokeOptions { + start_cap: line_cap, + end_cap: line_cap, + line_join, + .. SvgStrokeOptions::default().with_line_width(s.width as f32) + }; + + (ColorU { + r: color.red, + g: color.green, + b: color.blue, + a: (s.opacity.value() * 255.0) as u8 + }, opts) + } +} + +#[derive(Debug, Clone)] +pub struct Svg { + /// Currently active layers + pub layers: Vec, + /// Pan (horizontal, vertical) in pixels + pub pan: (f32, f32), + /// 1.0 = default zoom + pub zoom: f32, + /// Whether an FXAA shader should be applied to the resulting OpenGL texture + pub enable_fxaa: bool, + /// Should the SVG add the current HiDPI factor to the zoom? + pub enable_hidpi: bool, + /// Background color (default: transparent) + pub background_color: ColorU, + /// Multisampling (default: 1.0) - since there is no anti-aliasing yet, simply + /// increases the texture size that is drawn to. + pub multisampling_factor: f32, +} + +impl Default for Svg { + fn default() -> Self { + Self { + layers: Vec::new(), + pan: (0.0, 0.0), + zoom: 1.0, + enable_fxaa: false, + enable_hidpi: true, + background_color: ColorU { r: 0, b: 0, g: 0, a: 0 }, + multisampling_factor: 1.0, + } + } +} + +#[derive(Debug, Clone)] +pub enum SvgLayerResource { + Reference((SvgLayerId, SvgStyle)), + Direct(SvgLayerResourceDirect), +} + +#[derive(Debug, Clone)] +pub struct SvgLayerResourceDirect { + pub style: SvgStyle, + pub fill: Option, + pub stroke: Option, +} + +impl SvgLayerResourceDirect { + pub fn tesselate_from_layer(data: &[SvgLayerType], style: SvgStyle) -> Self { + tesselate_polygon_data(data, style) + } +} + +#[derive(Debug, Clone, Default)] +pub struct VerticesIndicesBuffer { + pub vertices: Vec, + pub indices: Vec, +} + +#[cfg_attr(feature = "serde_serialization", derive(Serialize, Deserialize))] +#[derive(Debug, Copy, Clone)] +pub struct BezierControlPoint { + pub x: f32, + pub y: f32, +} + +impl BezierControlPoint { + /// Distance of two points + pub fn distance(&self, other: &Self) -> f32 { + ((other.x - self.x).powi(2) + (other.y - self.y).powi(2)).sqrt() + } +} + +/// Bezier formula for cubic curves (start, handle 1, handle 2, end). +/// +/// ## Inputs +/// +/// - `curve`: The 4 handles of the curve +/// - `t`: The interpolation amount - usually between 0.0 and 1.0 if the point +/// should be between the start and end +/// +/// ## Returns +/// +/// - `BezierControlPoint`: The calculated point which lies on the curve, +/// according the the bezier formula +pub fn cubic_interpolate_bezier(curve: &[BezierControlPoint;4], t: f32) -> BezierControlPoint { + let one_minus = 1.0 - t; + let one_minus_square = one_minus.powi(2); + let one_minus_cubic = one_minus.powi(3); + + let t_pow2 = t.powi(2); + let t_pow3 = t.powi(3); + + let x = one_minus_cubic * curve[0].x + + 3.0 * one_minus_square * t * curve[1].x + + 3.0 * one_minus * t_pow2 * curve[2].x + + t_pow3 * curve[3].x; + + let y = one_minus_cubic * curve[0].y + + 3.0 * one_minus_square * t * curve[1].y + + 3.0 * one_minus * t_pow2 * curve[2].y + + t_pow3 * curve[3].y; + + BezierControlPoint { x, y } +} + +pub fn quadratic_interpolate_bezier(curve: &[BezierControlPoint;3], t: f32) -> BezierControlPoint { + let one_minus = 1.0 - t; + let one_minus_square = one_minus.powi(2); + + let t_pow2 = t.powi(2); + + // TODO: Why 3.0 and not 2.0? + + let x = one_minus_square * curve[0].x + + 2.0 * one_minus * t * curve[1].x + + 3.0 * t_pow2 * curve[2].x; + + let y = one_minus_square * curve[0].y + + 2.0 * one_minus * t * curve[1].y + + 3.0 * t_pow2 * curve[2].y; + + BezierControlPoint { x, y } +} + +#[derive(Debug, Copy, Clone)] +pub struct BezierNormalVector { + pub x: f32, + pub y: f32, +} + +impl BezierNormalVector { + pub fn to_rotation(&self) -> BezierCharacterRotation { + BezierCharacterRotation((-self.x).atan2(self.y)) + } +} + +/// Calculates the normal vector at a certain point (perpendicular to the curve) +pub fn cubic_bezier_normal(curve: &[BezierControlPoint;4], t: f32) -> BezierNormalVector { + + // 1. Calculate the derivative of the bezier curve + // + // This means, we go from 4 control points to 3 control points and redistribute + // the weights of the control points according to the formula: + // + // w'0 = 3(w1-w0) + // w'1 = 3(w2-w1) + // w'2 = 3(w3-w2) + + let weight_1_x = 3.0 * (curve[1].x - curve[0].x); + let weight_1_y = 3.0 * (curve[1].y - curve[0].y); + + let weight_2_x = 3.0 * (curve[2].x - curve[1].x); + let weight_2_y = 3.0 * (curve[2].y - curve[1].y); + + let weight_3_x = 3.0 * (curve[3].x - curve[2].x); + let weight_3_y = 3.0 * (curve[3].y - curve[2].y); + + // The first derivative of a cubic bezier curve is a quadratic bezier curve + // Luckily, the first derivative is also the tangent vector. So all we need to do + // is to get the quadratic bezier + let mut tangent = quadratic_interpolate_bezier(&[ + BezierControlPoint { x: weight_1_x, y: weight_1_y }, + BezierControlPoint { x: weight_2_x, y: weight_2_y }, + BezierControlPoint { x: weight_3_x, y: weight_3_y }, + ], t); + + // We normalize the tangent to have a lenght of 1 + let tangent_length = (tangent.x.powi(2) + tangent.y.powi(2)).sqrt(); + tangent.x /= tangent_length; + tangent.y /= tangent_length; + + // The tangent is the vector that runs "along" the curve at a specific point. + // To get the normal (to calcuate the rotation of the characters), we need to + // rotate the tangent vector by 90 degrees. + // + // Rotating by 90 degrees is very simple, as we only need to flip the x and y axis + + BezierNormalVector { + x: -tangent.y, + y: tangent.x, + } +} + +#[derive(Debug, Copy, Clone)] +pub enum SvgTextPlacement { + /// Text is simply layouted from left-to-right + Unmodified, + /// Text is rotated by X degrees + Rotated(f32), + /// Text is placed on a cubic bezier curve + OnCubicBezierCurve(SampledBezierCurve), +} + +#[derive(Debug, Clone)] +pub struct SvgText { + /// Font size of the text, in pixels + pub font_size_px: f32, + /// Font ID, such as FontId(0) + pub font_id: FontId, + /// What are the words / glyphs in this text + pub text_layout: SvgTextLayout, + /// What is the font color & stroke (if any)? + pub style: SvgStyle, + /// Is the text rotated or on a curve? + pub placement: SvgTextPlacement, +} + +/// An axis-aligned bounding box (not rotated / skewed) +#[derive(Debug, Copy, Clone)] +pub struct SvgBbox(pub TypedRect); + +impl SvgBbox { + + /// Simple function for drawing a single bounding box + pub fn draw_lines(&self, color: ColorU, line_width: f32) -> SvgLayerResourceDirect { + quick_rects(&[SvgRect { + width: self.0.size.width, + height: self.0.size.height, + x: self.0.origin.x, + y: self.0.origin.y, + rx: 0.0, + ry: 0.0, + }], + Some(color), + None, + Some(SvgStrokeOptions::default().with_line_width(line_width)) + )} + + /// Checks if the bounding box contains a point + pub fn contains_point(&self, x: f32, y: f32) -> bool { + self.0.contains(&TypedPoint2D::new(x, y)) + } + + /// Translate the SvgBbox by x / y + pub fn translate(&mut self, x: f32, y: f32) { + self.0 = self.0.translate(&TypedVector2D::new(x, y)); + } +} + +#[test] +fn translate_bbox() { + let mut bbox = SvgBbox(TypedRect::zero()); + bbox.translate(200.0, 300.0); + assert_eq!(bbox.0.origin.x, 200.0); + assert_eq!(bbox.0.origin.y, 300.0); +} + +pub fn is_point_in_shape(point: (f32, f32), shape: &[(f32, f32)]) -> bool { + if shape.len() < 3 { + // Shape must at least have 3 points, i.e. be a triangle + return false; + } + + // We iterate over the shape in 2 points. + // + // If the mouse cursor (target point) is on the left side for all points, + // then cursor is inside of the shape. If it appears on the right side for + // only one point, we know that it isn't inside the target shape. + // all() is lazy and will quit on the first result where the target is not + // inside the shape. + shape.iter().zip(shape.iter().skip(1)).all(|(start, end)| { + !(side_of_point(point, *start, *end).is_sign_positive()) + }) +} + +/// Determine which side of a vector the point is on. +/// +/// Depending on if the result of this function is positive or negative, +/// the target point lies either right or left to the imaginary line from (start -> end) +#[inline] +pub fn side_of_point(target: (f32, f32), start: (f32, f32), end: (f32, f32)) -> f32 { + ((target.0 - start.0) * (end.1 - start.1)) - + ((target.1 - start.1) * (end.0 - start.0)) +} + +/// Creates a text layout for a single string of text +#[derive(Debug, Clone)] +pub struct SvgTextLayout { + /// The words, broken up by whitespace + pub words: Words, + /// Words, scaled by a certain font size (with font metrics) + pub scaled_words: ScaledWords, + /// Layout of the positions, word-by-word + pub word_positions: WordPositions, + /// Positioned and horizontally aligned glyphs + pub layouted_glyphs: LayoutedGlyphs, + /// At what glyphs does the line actually break (necessary for aligning content) + pub line_breaks: LineBreaks, +} + +/// Since the SvgText is scaled on the GPU, the font size doesn't matter here +const SVG_FAKE_FONT_SIZE: f32 = 64.0; + +impl SvgTextLayout { + + /// Calculate the text layout from a font and a font size - + /// note: the idea is to let the user cache the returned result, + /// it is not recommended to run this function on every frame, + /// since it can be very expensive + pub fn from_str( + text: &str, + font_bytes: &[u8], + font_index: u32, + text_layout_options: &TextLayoutOptions, + horizontal_alignment: StyleTextAlignmentHorz, + ) -> Self { + + use text_layout; + + let words = text_layout::split_text_into_words(text); + let scaled_words = text_layout::words_to_scaled_words(&words, font_bytes, font_index, SVG_FAKE_FONT_SIZE); + let word_positions = text_layout::position_words(&words, &scaled_words, text_layout_options, SVG_FAKE_FONT_SIZE); + let (layouted_glyphs, line_breaks) = text_layout::get_layouted_glyphs_with_horizonal_alignment(&word_positions, &scaled_words, horizontal_alignment); + + SvgTextLayout { + words, scaled_words, word_positions, layouted_glyphs, line_breaks + } + } + + /// Get the bounding box of a layouted text + pub fn get_bbox(&self, placement: &SvgTextPlacement) -> SvgBbox { + use self::SvgTextPlacement::*; + + // TODO: Scale by font size! + + let normal_width = self.word_positions.content_size.width; + let normal_height = self.word_positions.content_size.height; + + SvgBbox(match placement { + Unmodified => { + TypedRect::new( + TypedPoint2D::new(0.0, 0.0), + TypedSize2D::new(normal_width, normal_height) + ) + }, + Rotated(r) => { + + fn rotate_point((x, y): (f32, f32), sin: f32, cos: f32) -> (f32, f32) { + ((x * cos) - (y * sin), (x * sin) + (y * cos)) + } + + let rot_radians = r.to_radians(); + let sin = rot_radians.sin(); + let cos = rot_radians.cos(); + + let top_left = (0.0, 0.0); + let top_right = (0.0 + normal_width, 0.0); + let bottom_right = (0.0 + normal_width, normal_height); + let bottom_left = (0.0, normal_height); + + let (top_left_x, top_left_y) = rotate_point(top_left, sin, cos); + let (top_right_x, top_right_y) = rotate_point(top_right, sin, cos); + let (bottom_right_x, bottom_right_y) = rotate_point(bottom_right, sin, cos); + let (bottom_left_x, bottom_left_y) = rotate_point(bottom_left, sin, cos); + + let min_x = top_left_x.min(top_right_x).min(bottom_right_x).min(bottom_left_x); + let max_x = top_left_x.max(top_right_x).max(bottom_right_x).max(bottom_left_x); + let min_y = top_left_y.min(top_right_y).min(bottom_right_y).min(bottom_left_y); + let max_y = top_left_y.max(top_right_y).max(bottom_right_y).max(bottom_left_y); + + TypedRect::new( + TypedPoint2D::new(min_x, min_y), + TypedSize2D::new(max_x - min_x, max_y - min_y) + ) + }, + OnCubicBezierCurve(curve) => { + let (mut bbox, _bbox_indices) = curve.get_bbox(); + + // TODO: There should be a more sophisticated Bbox calculation here + // that takes the rotation of the text into account. Right now we simply + // add the font size to the BBox height, so that we can still select text + // even when the control points are aligned in a horizontal line. + // + // This is not so much about correctness as it is about simply making + // it work for now. + + bbox.0.origin.y -= SVG_FAKE_FONT_SIZE; + bbox.0.size.height += SVG_FAKE_FONT_SIZE; + bbox.0 + } + }) + } +} + +impl SvgText { + + pub fn to_svg_layer(&self, vectorized_fonts_cache: &VectorizedFontCache, resources: &AppResources) -> SvgLayerResourceDirect { + + let vectorized_font = vectorized_fonts_cache.get_font(&self.font_id, resources).unwrap(); + + // The text contains the vertices and indices in unscaled units. This is so that the font + // can be cached and later on be scaled and rotated on the GPU instead of the CPU. + let mut text = match self.placement { + SvgTextPlacement::Unmodified => { + normal_text(&self.text_layout, self.style, &*vectorized_font) + }, + SvgTextPlacement::Rotated(degrees) => { + let mut text = normal_text(&self.text_layout, self.style, &*vectorized_font); + text.style.rotate(degrees); + text + }, + SvgTextPlacement::OnCubicBezierCurve(curve) => { + text_on_curve(&self.text_layout, self.style, &*vectorized_font, &curve) + }, + }; + + // The glyphs are laid out to be 1px high, they are then later scaled to the correct font size + text.style.scale(self.font_size_px, self.font_size_px); + text + } + + pub fn get_bbox(&self) -> SvgBbox { + let mut bbox = self.text_layout.get_bbox(&self.placement); + let translation = self.style.transform.translation.unwrap_or_default(); + bbox.translate(translation.x, translation.y); + bbox + } +} + +pub fn normal_text( + layout: &SvgTextLayout, + text_style: SvgStyle, + vectorized_font: &VectorizedFont, +) -> SvgLayerResourceDirect +{ + let fill_vertices = text_style.fill.map(|_| { + let fill_verts = vectorized_font.get_fill_vertices(&layout.layouted_glyphs.glyphs); + normal_text_to_vertices(&layout.layouted_glyphs.glyphs, fill_verts) + }); + + let stroke_vertices = text_style.stroke.map(|stroke| { + let stroke_verts = vectorized_font.get_stroke_vertices(&layout.layouted_glyphs.glyphs, &stroke.1); + normal_text_to_vertices(&layout.layouted_glyphs.glyphs, stroke_verts) + }); + + SvgLayerResourceDirect { + style: text_style, + fill: fill_vertices, + stroke: stroke_vertices, + } +} + +pub fn normal_text_to_vertices( + glyph_ids: &[GlyphInstance], + mut vertex_buffers: Vec>, +) -> VerticesIndicesBuffer +{ + normal_text_to_vertices_inner(glyph_ids, &mut vertex_buffers); + join_vertex_buffers(&vertex_buffers) +} + +fn normal_text_to_vertices_inner( + glyph_ids: &[GlyphInstance], + vertex_buffers: &mut Vec>, +) { + vertex_buffers.iter_mut().zip(glyph_ids).for_each(|(vertex_buf, gid)| { + // NOTE: The gid.point has the font size already applied to it, + // so we have to un-do the scaling for the glyph offsets, so all other scaling can be done on the GPU + transform_vertex_buffer(&mut vertex_buf.vertices, gid.point.x / SVG_FAKE_FONT_SIZE, gid.point.y / SVG_FAKE_FONT_SIZE); + }); +} + +pub fn text_on_curve( + layout: &SvgTextLayout, + text_style: SvgStyle, + vectorized_font: &VectorizedFont, + curve: &SampledBezierCurve +) -> SvgLayerResourceDirect +{ + // NOTE: char offsets are now in unscaled glyph space! + let (char_offsets, char_rotations) = curve.get_text_offsets_and_rotations(&layout.layouted_glyphs.glyphs, 0.0); + + let fill_vertices = text_style.fill.map(|_| { + let fill_verts = vectorized_font.get_fill_vertices(&layout.layouted_glyphs.glyphs); + curved_vector_text_to_vertices(&char_offsets, &char_rotations, fill_verts) + }); + + let stroke_vertices = text_style.stroke.map(|stroke| { + let stroke_verts = vectorized_font.get_stroke_vertices(&layout.layouted_glyphs.glyphs, &stroke.1); + curved_vector_text_to_vertices(&char_offsets, &char_rotations, stroke_verts) + }); + + SvgLayerResourceDirect { + style: text_style, + fill: fill_vertices, + stroke: stroke_vertices, + } +} + +// Calculates the layout for one word block +pub fn curved_vector_text_to_vertices( + char_offsets: &[(f32, f32)], + char_rotations: &[BezierCharacterRotation], + mut vertex_buffers: Vec>, +) -> VerticesIndicesBuffer +{ + vertex_buffers.iter_mut() + .zip(char_rotations.into_iter()) + .zip(char_offsets.iter()) + .for_each(|((vertex_buf, char_rot), char_offset)| { + let (char_offset_x, char_offset_y) = char_offset; // weird borrow issue + // 1. Rotate individual characters inside of the word + let (char_sin, char_cos) = (char_rot.0.sin(), char_rot.0.cos()); + rotate_vertex_buffer(&mut vertex_buf.vertices, char_sin, char_cos); + // 2. Transform characters to their respective positions + transform_vertex_buffer(&mut vertex_buf.vertices, *char_offset_x, *char_offset_y); + }); + + join_vertex_buffers(&vertex_buffers) +} + +impl Svg { + + #[inline] + pub fn with_layers(layers: Vec) -> Self { + Self { layers: layers, .. Default::default() } + } + + #[inline] + pub fn with_pan(mut self, horz: f32, vert: f32) -> Self { + self.pan = (horz, vert); + self + } + + #[inline] + pub fn with_zoom(mut self, zoom: f32) -> Self { + self.zoom = zoom; + self + } + + #[inline] + pub fn with_hidpi_enabled(mut self, hidpi_enabled: bool) -> Self { + self.enable_hidpi = hidpi_enabled; + self + } + + #[inline] + pub fn with_background_color(mut self, color: ColorU) -> Self { + self.background_color = color; + self + } + + /// Since there is no anti-aliasing yet, this will enlarge the texture that is drawn to by + /// the factor X. Default is `1.0`, but you could for example, render to a `1.2x` texture. + #[inline] + pub fn with_multisampling_factor(mut self, multisampling_factor: f32) -> Self { + self.multisampling_factor = multisampling_factor; + self + } + + #[inline] + pub fn with_fxaa(mut self, enable_fxaa: bool) -> Self { + self.enable_fxaa = enable_fxaa; + self + } + + /// Renders the SVG to a texture. This should be called in a callback, since + /// during DOM construction, the items don't know how large they will be. + /// + /// The final texture will be width * height large. Note that width and height + /// need to be multiplied with the current `HiDPI` factor, otherwise the texture + /// will be blurry on HiDPI screens. This isn't done automatically. + pub fn render_svg( + &self, + svg_cache: &SvgCache, + window: &FakeWindow, + width: usize, + height: usize + ) -> Texture + { + let read_only_window = window.read_only_window(); + + let texture_width = (width as f32 * self.multisampling_factor) as u32; + let texture_height = (height as f32 * self.multisampling_factor) as u32; + + // let (window_width, window_height) = window.get_physical_size(); + + // TODO: This currently doesn't work - only the first draw call is drawn + // This is probably because either webrender or glium messes with the texture + // in some way. Need to investigate. + let bg_col: ColorF = self.background_color.into(); + + let z_index: f32 = 0.5; + // let bbox_size = TypedSize2D::new(window_width as f32, window_height as f32); + let bbox_size = TypedSize2D::new(texture_width as f32, texture_height as f32); + let shader = svg_cache.init_shader(&read_only_window); + + let hidpi = window.get_hidpi_factor() as f32; + + let zoom = if self.enable_hidpi { self.zoom * hidpi } else { self.zoom } * self.multisampling_factor; + let pan = if self.enable_hidpi { (self.pan.0 * hidpi, self.pan.1 * hidpi) } else { self.pan }; + let pan = (pan.0 * self.multisampling_factor, pan.1 * self.multisampling_factor); + + let draw_options = DrawParameters { + primitive_restart_index: true, + .. Default::default() + }; + + let tex = read_only_window.create_texture(texture_width, texture_height); + + { + + let mut surface = tex.as_surface(); + surface.clear_color(bg_col.r, bg_col.g, bg_col.b, bg_col.a); + + for layer in &self.layers { + + let style = match &layer { + SvgLayerResource::Reference((_, style)) => *style, + SvgLayerResource::Direct(d) => d.style, + }; + + let fill_vi = match &layer { + SvgLayerResource::Reference((layer_id, _)) => svg_cache.get_vertices_and_indices(&read_only_window, layer_id), + SvgLayerResource::Direct(d) => d.fill.as_ref().map(|f| { + let vertex_buffer = VertexBuffer::new(&read_only_window, &f.vertices).unwrap(); + let index_buffer = IndexBuffer::new(&read_only_window, PrimitiveType::TrianglesList, &f.indices).unwrap(); + Rc::new((vertex_buffer, index_buffer)) + }), + }; + + let stroke_vi = match &layer { + SvgLayerResource::Reference((layer_id, _)) => svg_cache.get_stroke_vertices_and_indices(&read_only_window, layer_id), + SvgLayerResource::Direct(d) => d.stroke.as_ref().map(|f| { + let vertex_buffer = VertexBuffer::new(&read_only_window, &f.vertices).unwrap(); + let index_buffer = IndexBuffer::new(&read_only_window, PrimitiveType::TrianglesList, &f.indices).unwrap(); + Rc::new((vertex_buffer, index_buffer)) + }), + }; + + if let (Some(fill_color), Some(fill_vi)) = (style.fill, fill_vi) { + let (fill_vertices, fill_indices) = &*fill_vi; + draw_vertex_buffer_to_surface( + &mut surface, &shader.program, &fill_vertices, &fill_indices, + &draw_options, &bbox_size, fill_color, z_index, pan, zoom, &style.transform); + } + + if let (Some(stroke_color), Some(stroke_vi)) = (style.stroke, stroke_vi) { + let (stroke_vertices, stroke_indices) = &*stroke_vi; + draw_vertex_buffer_to_surface(&mut surface, &shader.program, &stroke_vertices, &stroke_indices, + &draw_options, &bbox_size, stroke_color.0, z_index, pan, zoom, &style.transform); + } + } + + // TODO: apply FXAA shader + + } // unbind surface framebuffer + + tex + } +} + +fn draw_vertex_buffer_to_surface( + surface: &mut S, + shader: &Program, + vertices: &VertexBuffer, + indices: &IndexBuffer, + draw_options: &DrawParameters, + bbox_size: &TypedSize2D, + color: ColorU, + z_index: f32, + pan: (f32, f32), + zoom: f32, + layer_transform: &SvgTransform) +{ + let color: ColorF = color.into(); + + let (layer_rotation_center, layer_rotation_degrees) = layer_transform.rotation.unwrap_or_default(); + let (rotation_sin, rotation_cos) = layer_rotation_degrees.to_rotation(); + let layer_translation = layer_transform.translation.unwrap_or_default(); + let layer_scale_factor = layer_transform.scale.unwrap_or_default(); + + let uniforms = uniform! { + + // vertex shader + bbox_size: (bbox_size.width / 2.0, bbox_size.height / 2.0), + offset: (pan.0, pan.1), + z_index: z_index, + zoom: zoom, + rotation_center: (layer_rotation_center.x, layer_rotation_center.y), + rotation_sin: rotation_sin, + rotation_cos: rotation_cos, + scale_factor: (layer_scale_factor.x, layer_scale_factor.y), + translate_px: (layer_translation.x, layer_translation.y), + + // fragment shader + color: ( + color.r as f32, + color.g as f32, + color.b as f32, + color.a as f32 + ), + }; + + surface.draw(vertices, indices, shader, &uniforms, draw_options).unwrap(); +} diff --git a/azul/src/widgets/table_view.rs b/azul/src/widgets/table_view.rs new file mode 100644 index 000000000..2f11b70cd --- /dev/null +++ b/azul/src/widgets/table_view.rs @@ -0,0 +1,229 @@ +//! Table view + +use std::collections::BTreeMap; +use { + app::AppStateNoData, + callbacks::{IFrameCallback, HidpiAdjustedBounds, UpdateScreen, DontRedraw}, + dom::{Dom, On, NodeData, DomString, NodeType}, + callbacks::{LayoutInfo, CallbackInfo}, + callbacks::{StackCheckedPointer, DefaultCallback}, + window::FakeWindow, +}; + +#[derive(Debug, Default, Copy, Clone)] +pub struct TableView { + +} + +#[derive(Debug, Clone)] +pub struct TableViewState { + pub work_sheet: Worksheet, + pub column_width: f32, + pub row_height: f32, + pub selected_cell: Option<(usize, usize)>, +} + +impl Default for TableViewState { + fn default() -> Self { + Self { + work_sheet: Worksheet::default(), + column_width: 100.0, + row_height: 20.0, + selected_cell: None, + } + } +} + +#[derive(Debug, Default, Clone)] +pub struct Worksheet { + pub data: BTreeMap>, +} + +impl Worksheet { + pub fn set_cell>(&mut self, x: usize, y: usize, value: I) { + self.data + .entry(x) + .or_insert_with(|| BTreeMap::new()) + .insert(y, value.into()); + } +} + +#[derive(Debug, Default, Clone)] +pub struct TableColumn { + cells: Vec, +} + +impl TableView { + pub fn new() -> Self { + Self { + + } + } + + pub fn dom(&self, data: &TableViewState, t: &T, window: &mut FakeWindow) -> Dom { + if let Some(ptr) = StackCheckedPointer::new(t, data) { + let mut dom = Dom::iframe(IFrameCallback(render_table_callback), ptr); + let callback_id = window.add_callback(ptr, DefaultCallback(Self::table_view_on_click)); + dom.add_default_callback_id(On::MouseUp, callback_id); + dom + } else { + Dom::label( + "Cannot create table from heap-allocated TableViewState, \ + please call TableViewState::render_dom manually" + ) + } + } + + fn table_view_on_click(ptr: &StackCheckedPointer, data: &mut AppStateNoData, event: &mut CallbackInfo) + -> UpdateScreen + { + unsafe { ptr.invoke_mut(TableViewState::on_click, data, event) } + } +} + +fn render_table_callback(ptr: &StackCheckedPointer, info: LayoutInfo, dimensions: HidpiAdjustedBounds) +-> Dom +{ + unsafe { ptr.invoke_mut_iframe(TableViewState::render, info, dimensions) } +} + + +impl TableViewState { + pub fn render(state: &mut TableViewState, _info: LayoutInfo, dimensions: HidpiAdjustedBounds) + -> Dom + { + let logical_size = dimensions.get_logical_size(); + let necessary_columns = (logical_size.width as f32 / state.column_width).ceil() as usize; + let necessary_rows = (logical_size.height as f32 / state.row_height).ceil() as usize; + + // div.__azul-native-table-container + // |-> div.__azul-native-table-column (Column 0) + // |-> div.__azul-native-table-top-left-rect .__azul-native-table-column-name + // '-> div.__azul-native-table-row-numbers .__azul-native-table-row + // + // |-> div.__azul-native-table-column-container + // |-> div.__azul-native-table-column (Column 1 ...) + // |-> div.__azul-native-table-column-name + // '-> div.__azul-native-table-row + // '-> div.__azul-native-table-cell + + Dom::div() + .with_class("__azul-native-table-container") + .with_child( + Dom::div() + .with_class("__azul-native-table-row-number-wrapper") + .with_child( + // Empty rectangle at the top left of the table + Dom::div() + .with_class("__azul-native-table-top-left-rect") + ) + .with_child( + // Rows - "1", "2", "3" + (0..necessary_rows.saturating_sub(1)) + .map(|row_idx| + NodeData { + node_type: NodeType::Label(DomString::Heap(format!("{}", row_idx + 1))), + classes: vec![DomString::Static("__azul-native-table-row")], + .. Default::default() + } + ) + .collect::>() + .with_class("__azul-native-table-row-numbers") + ) + ) + .with_child( + (0..necessary_columns) + .map(|col_idx| + // Column name + Dom::new(NodeType::Div) + .with_class("__azul-native-table-column") + .with_child(Dom::label(column_name_from_number(col_idx)).with_class("__azul-native-table-column-name")) + .with_child( + // Actual rows - if no content is given, they are simply empty + (0..necessary_rows) + .map(|row_idx| + NodeData { + node_type: if let Some(data) = state.work_sheet.data.get(&col_idx).and_then(|col| col.get(&row_idx)) { + NodeType::Label(DomString::Heap(data.clone())) + } else { + NodeType::Div + }, + classes: vec![DomString::Static("__azul-native-table-cell")], + .. Default::default() + } + ) + .collect::>() + .with_class("__azul-native-table-rows") + ) + ) + .collect::>() + .with_class("__azul-native-table-column-container") + // current active selection (s) + .with_child( + Dom::div() + .with_class("__azul-native-table-selection") + .with_child(Dom::div().with_class("__azul-native-table-selection-handle")) + ) + ) + } + + pub fn on_click( + &mut self, + _app_state: &mut AppStateNoData, + _window_event: &mut CallbackInfo) + -> UpdateScreen + { + println!("table was clicked"); + DontRedraw + } +} + +/// Maps an index number to a value, necessary for creating the column name: +/// +/// ```no_run,ignore +/// 0 -> A +/// 25 -> Z +/// 26 -> AA +/// 27 -> AB +/// ``` +/// +/// ... and so on. This implementation is very fast, takes ~50 to 100 +/// nanoseconds for 1 iteration due to almost pure-stack allocated data. +/// For an explanation of the algorithm with comments, see: +/// https://github.com/fschutt/street_index/blob/78b935a1303070947c0854b6d01f540ec298c9d5/src/gridconfig.rs#L155-L209 +pub fn column_name_from_number(num: usize) -> String { + const ALPHABET_LEN: usize = 26; + // usize::MAX is "GKGWBYLWRXTLPP" with a length of 15 characters + const MAX_LEN: usize = 15; + + #[inline(always)] + fn u8_to_char(input: u8) -> u8 { + 'A' as u8 + input + } + + let mut result = [0;MAX_LEN + 1]; + let mut multiple_of_alphabet = num / ALPHABET_LEN; + let mut character_count = 0; + + while multiple_of_alphabet != 0 && character_count < MAX_LEN { + let remainder = (multiple_of_alphabet - 1) % ALPHABET_LEN; + result[(MAX_LEN - 1) - character_count] = u8_to_char(remainder as u8); + character_count += 1; + multiple_of_alphabet = (multiple_of_alphabet - 1) / ALPHABET_LEN; + } + + result[MAX_LEN] = u8_to_char((num % ALPHABET_LEN) as u8); + let zeroed_characters = MAX_LEN.saturating_sub(character_count); + let slice = &result[zeroed_characters..]; + unsafe { ::std::str::from_utf8_unchecked(slice) }.to_string() +} + +#[test] +fn test_column_name_from_number() { + assert_eq!(column_name_from_number(0), String::from("A")); + assert_eq!(column_name_from_number(1), String::from("B")); + assert_eq!(column_name_from_number(6), String::from("G")); + assert_eq!(column_name_from_number(26), String::from("AA")); + assert_eq!(column_name_from_number(27), String::from("AB")); + assert_eq!(column_name_from_number(225), String::from("HR")); +} \ No newline at end of file diff --git a/azul/src/widgets/text_input.rs b/azul/src/widgets/text_input.rs new file mode 100644 index 000000000..39fb03fa1 --- /dev/null +++ b/azul/src/widgets/text_input.rs @@ -0,0 +1,233 @@ +//! Text input (demonstrates two-way data binding) + +use std::ops::Range; +use { + callbacks::{UpdateScreen, Redraw, DontRedraw}, + dom::{Dom, EventFilter, FocusEventFilter, TabIndex}, + window::FakeWindow, + prelude::VirtualKeyCode, + callbacks::{CallbackInfo, StackCheckedPointer, DefaultCallback, DefaultCallbackId}, + app::AppStateNoData, +}; + +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] +pub struct TextInput { + on_text_input_callback: Option<(DefaultCallbackId, DefaultCallbackId)>, +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct TextInputState { + pub text: String, + pub selection: Option, + pub cursor: usize, +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub enum Selection { + All, + FromTo(Range), +} + +impl Default for TextInputState { + fn default() -> Self { + TextInputState { + text: String::new(), + selection: None, + cursor: 0, + } + } +} + +impl TextInputState { + pub fn new>(input: S) -> Self { + let input_str: String = input.into(); + let len = input_str.len(); + Self { + text: input_str, + selection: None, + cursor: len, + } + } +} + +impl TextInput { + + pub fn new() -> Self { + TextInput { on_text_input_callback: None } + } + + pub fn bind(self, window: &mut FakeWindow, field: &TextInputState, data: &T) -> Self { + let ptr = StackCheckedPointer::new(data, field); + let on_text_input_callback = ptr.map(|ptr|{( + window.add_callback(ptr, DefaultCallback(TextInputState::on_text_input_private)), + window.add_callback(ptr, DefaultCallback(TextInputState::on_virtual_key_down_private)) + )}); + + Self { + on_text_input_callback, + .. self + } + } + + pub fn dom(&self, field: &TextInputState) -> Dom { + + let mut parent_div = + Dom::div() + .with_class("__azul-native-input-text") + .with_tab_index(TabIndex::Auto); + + if let Some((text_input_callback, vk_callback)) = self.on_text_input_callback { + parent_div.add_default_callback_id(EventFilter::Focus(FocusEventFilter::TextInput), text_input_callback); + parent_div.add_default_callback_id(EventFilter::Focus(FocusEventFilter::VirtualKeyDown), vk_callback); + } + + let label = Dom::label(field.text.clone()).with_class("__azul-native-input-text-label"); + parent_div.with_child(label) + } +} + +impl TextInputState { + + fn on_virtual_key_down_private(data: &StackCheckedPointer, app_state_no_data: &mut AppStateNoData, window_event: &mut CallbackInfo) -> UpdateScreen { + unsafe { data.invoke_mut(Self::on_virtual_key_down, app_state_no_data, window_event) } + } + + fn on_text_input_private(data: &StackCheckedPointer, app_state_no_data: &mut AppStateNoData, window_event: &mut CallbackInfo) -> UpdateScreen { + unsafe { data.invoke_mut(Self::on_text_input, app_state_no_data, window_event) } + } + + pub fn on_virtual_key_down(&mut self, app_state_no_data: &mut AppStateNoData, event: &mut CallbackInfo) -> UpdateScreen { + + let keyboard_state = app_state_no_data.windows[event.window_id].get_keyboard_state(); + + match keyboard_state.latest_virtual_keycode { + Some(VirtualKeyCode::Back) => { + // TODO: shift + back = delete last word + let selection = self.selection.clone(); + match selection { + None => { + if self.cursor == self.text.len() { + self.text.pop(); + } else { + let mut a = self.text.chars().take(self.cursor).collect::(); + let new = self.text.len().min(self.cursor.saturating_add(1)); + a.extend(self.text.chars().skip(new)); + self.text = a; + } + self.cursor = self.cursor.saturating_sub(1); + }, + Some(Selection::All) => { + self.text.clear(); + self.cursor = 0; + self.selection = None; + }, + Some(Selection::FromTo(range)) => { + delete_selection(self, range, None); + }, + } + + Redraw + }, + Some(VirtualKeyCode::Return) => { + // TODO: selection! + self.text.push('\n'); + self.cursor = self.cursor.saturating_add(1); + /* + match self.selection { + None => { }, + } + */ + Redraw + }, + Some(VirtualKeyCode::Home) => { + self.cursor = 0; + self.selection = None; + Redraw + }, + Some(VirtualKeyCode::End) => { + self.cursor = self.text.len(); + self.selection = None; + Redraw + }, + Some(VirtualKeyCode::A) if keyboard_state.ctrl_down => { + self.selection = Some(Selection::All); + Redraw + }, + Some(VirtualKeyCode::Escape) => { + self.selection = None; + Redraw + }, + Some(VirtualKeyCode::Right) => { + self.cursor = self.text.len().min(self.cursor.saturating_add(1)); + Redraw + }, + Some(VirtualKeyCode::Left) => { + self.cursor = (0.max(self.cursor.saturating_sub(1))).min(self.cursor.saturating_add(1)); + Redraw + }, + Some(VirtualKeyCode::C) => { + // TODO: copy + DontRedraw + }, + Some(VirtualKeyCode::V) => { + // TODO: paste + DontRedraw + }, + _ => DontRedraw, + } + } + + pub fn on_text_input(&mut self, app_state_no_data: &mut AppStateNoData, event: &mut CallbackInfo) -> UpdateScreen { + + let keyboard_state = app_state_no_data.windows[event.window_id].get_keyboard_state(); + + match keyboard_state.current_char { + Some(c) => { + let selection = self.selection.clone(); + match selection { + None => { + if self.cursor == self.text.len() { + self.text.push(c); + } else { + // TODO: insert character at the cursor location! + self.text.push(c); + } + self.cursor = self.cursor.saturating_add(1); + }, + Some(Selection::All) => { + self.text = format!("{}", c); + self.cursor = 1; + self.selection = None; + }, + Some(Selection::FromTo(range)) => { + delete_selection(self, range, Some(c)); + }, + } + Redraw + }, + None => DontRedraw, + } + } +} + +fn delete_selection(state: &mut TextInputState, selection: Range, new_text: Option) { + let Range { start, end } = selection; + let max = if end > state.text.len() { state.text.len() } else { end }; + + let mut cur = start; + if max == state.text.len() { + state.text.truncate(start); + } else { + let mut a = state.text.chars().take(start).collect::(); + + if let Some(new) = new_text { + a.push(new); + cur += 1; + } + + a.extend(state.text.chars().skip(end)); + state.text = a; + } + + state.cursor = cur; +} \ No newline at end of file diff --git a/azul/src/window.rs b/azul/src/window.rs new file mode 100644 index 000000000..9f4d16017 --- /dev/null +++ b/azul/src/window.rs @@ -0,0 +1,1039 @@ +use std::{ + fmt, + rc::Rc, + marker::PhantomData, + io::Error as IoError, + sync::atomic::{AtomicUsize, Ordering}, +}; +use webrender::{ + api::{ + PipelineId, Epoch, DocumentId, + RenderApi, ExternalScrollId, RenderNotifier, DeviceIntSize, + }, + Renderer, RendererOptions, RendererKind, ShaderPrecacheFlags, WrShaders, + // renderer::RendererError; -- not currently public in WebRender +}; +use glium::{ + IncompatibleOpenGl, Display, SwapBuffersError, + debug::DebugCallbackBehavior, + glutin::{ + self, EventsLoop, AvailableMonitorsIter, ContextTrait, CombinedContext, CreationError, + MonitorId, ContextError, ContextBuilder, WindowId as GliumWindowId, + Window as GliumWindow, WindowBuilder as GliumWindowBuilder, Icon, Context, + dpi::LogicalSize, + }, + backend::{Context as BackendContext, Facade, glutin::DisplayCreationError}, +}; +use gleam::gl::{self, Gl}; +use azul_css::{Css, ColorU}; +#[cfg(debug_assertions)] +use azul_css::HotReloadHandler; +use { + FastHashMap, + compositor::Compositor, + app::FrameEventInfo, + callbacks::{ + Callback, DefaultCallbackSystem, StackCheckedPointer, + DefaultCallback, DefaultCallbackId, Texture, + }, + display_list::ScrolledNodes, +}; +pub use webrender::api::HitTestItem; +pub use window_state::*; + +static LAST_PIPELINE_ID: AtomicUsize = AtomicUsize::new(0); + +fn new_pipeline_id() -> PipelineId { + PipelineId(LAST_PIPELINE_ID.fetch_add(1, Ordering::SeqCst) as u32, 0) +} + +/// User-modifiable fake window +#[derive(Clone)] +pub struct FakeWindow { + /// The window state for the next frame + pub state: WindowState, + /// The user can push default callbacks in this `DefaultCallbackSystem`, + /// which get called later in the hit-testing logic + pub(crate) default_callbacks: DefaultCallbackSystem, + /// An Rc to the original WindowContext - this is only so that + /// the user can create textures and other OpenGL content in the window + /// but not change any window properties from underneath - this would + /// lead to mismatch between the + pub(crate) read_only_window: Rc, +} + +impl FakeWindow { + + /// Returns a read-only window which can be used to create / draw + /// custom OpenGL texture during the `.layout()` phase + pub fn read_only_window(&self) -> ReadOnlyWindow { + ReadOnlyWindow { + inner: self.read_only_window.clone() + } + } + + pub fn get_physical_size(&self) -> (u32, u32) { + let hidpi = self.get_hidpi_factor(); + self.state.size.dimensions.to_physical(hidpi).into() + } + + /// Returns the current HiDPI factor. + pub fn get_hidpi_factor(&self) -> f64 { + self.state.size.hidpi_factor + } + + pub(crate) fn set_keyboard_state(&mut self, kb: &KeyboardState) { + self.state.internal.keyboard_state = kb.clone(); + } + + pub(crate) fn set_mouse_state(&mut self, mouse: &MouseState) { + self.state.internal.mouse_state = *mouse; + } + + /// Returns the current keyboard keyboard state. We don't want the library + /// user to be able to modify this state, only to read it. + pub fn get_keyboard_state<'a>(&'a self) -> &'a KeyboardState { + self.state.get_keyboard_state() + } + + /// Returns the current windows mouse state. We don't want the library + /// user to be able to modify this state, only to read it + pub fn get_mouse_state<'a>(&'a self) -> &'a MouseState { + self.state.get_mouse_state() + } + + /// Adds a default callback to the window. The default callbacks are + /// cleared after every frame, so two-way data binding widgets have to call this + /// on every frame they want to insert a default callback. + /// + /// Returns an ID by which the callback can be uniquely identified (used for hit-testing) + #[must_use] + pub fn add_callback( + &mut self, + callback_ptr: StackCheckedPointer, + callback_fn: DefaultCallback + ) -> DefaultCallbackId { + + use callbacks::get_new_unique_default_callback_id; + + let default_callback_id = get_new_unique_default_callback_id(); + self.default_callbacks.add_callback(default_callback_id, callback_ptr, callback_fn); + default_callback_id + } +} + +/// Read-only window which can be used to create / draw +/// custom OpenGL texture during the `.layout()` phase +#[derive(Clone)] +pub struct ReadOnlyWindow { + pub inner: Rc, +} + +impl Facade for ReadOnlyWindow { + fn get_context(&self) -> &Rc { + self.inner.get_context() + } +} + +impl ReadOnlyWindow { + + // Since webrender is asynchronous, we can't let the user draw + // directly onto the frame or the texture since that has to be timed + // with webrender + pub fn create_texture(&self, width: u32, height: u32) -> Texture { + use glium::texture::texture2d::Texture2d; + let tex = Texture2d::empty(&*self.inner, width, height).unwrap(); + Texture::new(tex) + } + + /// Make the window active (OpenGL) - necessary before + /// starting to draw on any window-owned texture + pub fn make_current(&self) { + let gl_window = self.inner.gl_window(); + unsafe { gl_window.make_current().unwrap() }; + } + + /// Unbind the current framebuffer manually. Is also executed on `Drop`. + /// + /// TODO: Is it necessary to expose this or is it enough to just + /// unbind the framebuffer on drop? + pub fn unbind_framebuffer(&self) { + let gl = get_gl_context(&*self.inner).unwrap(); + + gl.bind_framebuffer(gl::FRAMEBUFFER, 0); + } + + pub fn get_gl_context(&self) -> Rc { + // Can only fail when the API was initialized from WebGL, + // which can't happen, since that would already crash on startup + get_gl_context(&*self.inner).unwrap() + } +} + +impl Drop for ReadOnlyWindow { + fn drop(&mut self) { + self.unbind_framebuffer(); + } +} + +impl fmt::Debug for FakeWindow { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, + "FakeWindow {{\ + state: {:?}, \ + read_only_window: Rc, \ + }}", self.state) + } +} + +/// Options on how to initially create the window +#[derive(Debug, Clone)] +pub struct WindowCreateOptions { + /// State of the window, set the initial title / width / height here. + pub state: WindowState, + /// Which monitor should the window be created on? + pub monitor: WindowMonitorTarget, + /// Renderer type: Hardware-with-software-fallback, pure software or pure hardware renderer? + pub renderer_type: RendererType, + /// Win32 menu callbacks + pub menu_callbacks: FastHashMap>, + /// Sets the window icon (Windows and Linux only). Usually 16x16 px or 32x32px + pub window_icon: Option, + /// Windows only: Sets the 256x256 taskbar icon during startup + pub taskbar_icon: Option, +} + +impl Default for WindowCreateOptions { + fn default() -> Self { + Self { + state: WindowState::default(), + monitor: WindowMonitorTarget::default(), + renderer_type: RendererType::default(), + menu_callbacks: FastHashMap::default(), + window_icon: None, + taskbar_icon: None, + } + } +} + +/// Force a specific renderer. +/// By default, Azul will try to use the hardware renderer and fall +/// back to the software renderer if it can't create an OpenGL 3.2 context. +/// However, in some cases a hardware renderer might create problems +/// or you want to force either a software or hardware renderer. +/// +/// If the field `renderer_type` on the `WindowCreateOptions` is not +/// `RendererType::Default`, the `create_window` method will try to create +/// a window with the specific renderer type and **crash** if the renderer is +/// not available for whatever reason. +/// +/// If you don't know what any of this means, leave it at `Default`. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum RendererType { + Default, + Hardware, + Software, +} + +impl Default for RendererType { + fn default() -> Self { + RendererType::Default + } +} + +/// Error that could happen during window creation +#[derive(Debug)] +pub enum WindowCreateError { + /// WebGl is not supported by WebRender + WebGlNotSupported, + /// Couldn't create the display from the window and the EventsLoop + DisplayCreateError(DisplayCreationError), + /// OpenGL version is either too old or invalid + Gl(IncompatibleOpenGl), + /// Could not create an OpenGL context + Context(ContextError), + /// Could not create a window + CreateError(CreationError), + /// Could not swap the front & back buffers + SwapBuffers(::glium::SwapBuffersError), + /// IO error + Io(::std::io::Error), + /// WebRender creation error (probably OpenGL missing?) + Renderer/*(RendererError)*/, +} + +impl_display! { + WindowCreateError, + { + DisplayCreateError(e) => format!("Could not create the display from the window and the EventsLoop: {}", e), + Gl(e) => format!("{}", e), + Context(e) => format!("{}", e), + CreateError(e) => format!("{}", e), + SwapBuffers(e) => format!("{}", e), + Io(e) => format!("{}", e), + WebGlNotSupported => "WebGl is not supported by WebRender", + Renderer => "Webrender creation error (probably OpenGL missing?)", + } +} + +impl_from!(SwapBuffersError, WindowCreateError::SwapBuffers); +impl_from!(CreationError, WindowCreateError::CreateError); +impl_from!(IoError, WindowCreateError::Io); +impl_from!(IncompatibleOpenGl, WindowCreateError::Gl); +impl_from!(DisplayCreationError, WindowCreateError::DisplayCreateError); +impl_from!(ContextError, WindowCreateError::Context); + +struct Notifier { } + +impl RenderNotifier for Notifier { + fn clone(&self) -> Box { + Box::new(Notifier { }) + } + + // NOTE: Rendering is single threaded (because that's the nature of OpenGL), + // so when the Renderer::render() function is finished, then the rendering + // is finished and done, the rendering is currently blocking (but only takes about 0.. There is no point in implementing RenderNotifier, it only leads to + // synchronization problems (when handling Event::Awakened). + + fn wake_up(&self) { } + fn new_frame_ready(&self, _id: DocumentId, _scrolled: bool, _composite_needed: bool, _render_time: Option) { } +} + +/// Iterator over connected monitors (for positioning, etc.) +pub struct MonitorIter { + inner: AvailableMonitorsIter, +} + +impl Iterator for MonitorIter { + type Item = MonitorId; + fn next(&mut self) -> Option { + self.inner.next() + } +} + +/// Select on which monitor the window should pop up. +#[derive(Clone)] +pub enum WindowMonitorTarget { + /// Window should appear on the primary monitor + Primary, + /// Use `Window::get_available_monitors()` to select the correct monitor + Custom(MonitorId) +} + +impl fmt::Debug for WindowMonitorTarget { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use self::WindowMonitorTarget::*; + match *self { + Primary => write!(f, "WindowMonitorTarget::Primary"), + Custom(_) => write!(f, "WindowMonitorTarget::Custom(_)"), + } + } +} + +impl Default for WindowMonitorTarget { + fn default() -> Self { + WindowMonitorTarget::Primary + } +} + +/// Represents one graphical window to be rendered +pub struct Window { + /// System that can identify this window + pub(crate) id: GliumWindowId, + /// Stores the create_options: necessary because by default, the window is hidden + /// and only gets shown after the first redraw. + pub(crate) create_options: WindowCreateOptions, + /// Current state of the window, stores the keyboard / mouse state, + /// visibility of the window, etc. of the LAST frame. The user never sets this + /// field directly, but rather sets the WindowState he wants to have for the NEXT frame, + /// then azul compares the changes (i.e. if we are currently in fullscreen mode and + /// the user wants the next screen to be in fullscreen mode, too, simply do nothing), then it + /// updates this field to reflect the changes. + /// + /// This field is initialized from the `WindowCreateOptions`. + pub(crate) state: WindowState, + /// The display, i.e. the window + pub(crate) display: Rc, + /// The `WindowInternal` allows us to solve some borrowing issues + pub(crate) internal: WindowInternal, + /// States of scrolling animations, updated every frame + pub(crate) scroll_states: ScrollStates, + // The background thread that is running for this window. + // pub(crate) background_thread: Option>, + /// The style applied to the current window + pub(crate) css: Css, + /// An optional style hot-reloader for the current window, only available with debug_assertions + /// enabled + #[cfg(debug_assertions)] + pub(crate) css_loader: Option>, + /// Purely a marker, so that `app.run()` can infer the type of `T: Layout` + /// of the `WindowCreateOptions`, so that we can write: + /// + /// ```rust,ignore + /// app.run(Window::new(WindowCreateOptions::new(), Css::default()).unwrap()); + /// ``` + /// + /// instead of having to annotate the type: + /// + /// ```rust,ignore + /// app.run(Window::new(WindowCreateOptions::::new(), Css::default()).unwrap()); + /// ``` + marker: PhantomData, +} + +pub(crate) struct ScrollStates(pub(crate) FastHashMap); + +impl ScrollStates { + pub fn new() -> ScrollStates { + ScrollStates(FastHashMap::default()) + } + + /// NOTE: This has to be a getter, because we need to update + #[must_use] + pub(crate) fn get_scroll_amount(&mut self, scroll_id: &ExternalScrollId) -> Option<(f32, f32)> { + let entry = self.0.get_mut(&scroll_id)?; + Some(entry.get()) + } + + /// Updating the scroll amount does not update the `entry.used_this_frame`, + /// since that is only relevant when we are actually querying the renderer. + pub(crate) fn scroll_node(&mut self, scroll_id: &ExternalScrollId, scroll_by_x: f32, scroll_by_y: f32) { + if let Some(entry) = self.0.get_mut(scroll_id) { + entry.add(scroll_by_x, scroll_by_y); + } + } + + pub(crate) fn ensure_initialized_scroll_state(&mut self, scroll_id: ExternalScrollId, overflow_x: f32, overflow_y: f32) { + self.0.entry(scroll_id).or_insert_with(|| ScrollState::new(overflow_x, overflow_y)); + } + + /// Removes all scroll states that weren't used in the last frame + pub(crate) fn remove_unused_scroll_states(&mut self) { + self.0.retain(|_, state| state.used_this_frame); + } +} + +#[derive(Debug, Copy, Clone)] +pub struct ScrollState { + /// Amount in pixel that the current node is scrolled + scroll_amount_x: f32, + scroll_amount_y: f32, + overflow_x: f32, + overflow_y: f32, + /// Was the scroll amount used in this frame? + used_this_frame: bool, +} + +impl ScrollState { + + fn new(overflow_x: f32, overflow_y: f32) -> Self { + ScrollState { + scroll_amount_x: 0.0, + scroll_amount_y: 0.0, + overflow_x, + overflow_y, + used_this_frame: true, + } + } + + pub fn get(&mut self) -> (f32, f32) { + self.used_this_frame = true; + (self.scroll_amount_x, self.scroll_amount_y) + } + + pub fn add(&mut self, x: f32, y: f32) { + self.scroll_amount_x = self.overflow_x.min(self.scroll_amount_x + x).max(0.0); + self.scroll_amount_y = self.overflow_y.min(self.scroll_amount_y + y).max(0.0); + } +} + +impl Default for ScrollState { + fn default() -> Self { + ScrollState { + scroll_amount_x: 0.0, + scroll_amount_y: 0.0, + overflow_x: 0.0, + overflow_y: 0.0, + used_this_frame: true, + } + } +} + +pub(crate) struct WindowInternal { + pub(crate) last_scrolled_nodes: ScrolledNodes, + pub(crate) epoch: Epoch, + pub(crate) pipeline_id: PipelineId, + pub(crate) document_id: DocumentId, +} + +// TODO: Right now it's not very ergonomic to cache shaders between +// renderers - notify webrender about this. +const WR_SHADER_CACHE: Option<&mut WrShaders> = None; + +impl<'a, T> Window { + + /// Creates a new window + pub(crate) fn new( + render_api: &mut RenderApi, + shared_context: &Context, + events_loop: &EventsLoop, + options: WindowCreateOptions, + mut css: Css, + background_color: ColorU, + ) -> Result { + + // NOTE: It would be OK to use &RenderApi here, but it's better + // to make sure that the RenderApi is currently not in use by anything else. + + // NOTE: Creating a new EventsLoop for the new window causes a segfault. + // Report this to the winit developers. + // let events_loop = EventsLoop::new(); + + let is_transparent_background = background_color.a != 0; + + let mut window = GliumWindowBuilder::new() + .with_title(options.state.title.clone()) + .with_maximized(options.state.is_maximized) + .with_decorations(options.state.has_decorations) + .with_visibility(false) + .with_transparency(is_transparent_background) + .with_multitouch(); + + // TODO: Update winit to have: + // .with_always_on_top(options.state.is_always_on_top) + // + // winit 0.13 -> winit 0.15 + + // TODO: Add all the extensions for X11 / Mac / Windows, + // like setting the taskbar icon, setting the titlebar icon, etc. + + if let Some(icon) = options.window_icon.clone() { + window = window.with_window_icon(Some(icon)); + } + + // TODO: Platform-specific options! + #[cfg(target_os = "windows")] { + if let Some(icon) = options.taskbar_icon.clone() { + use glium::glutin::os::windows::WindowBuilderExt; + window = window.with_taskbar_icon(Some(icon)); + } + + // if options.no_redirection_bitmap { + // use glium::glutin::os::windows::WindowBuilderExt; + // window = window.with_no_redirection_bitmap(true); + // } + } + + if let Some(min_dim) = options.state.size.min_dimensions { + window = window.with_min_dimensions(min_dim); + } + + if let Some(max_dim) = options.state.size.max_dimensions { + window = window.with_max_dimensions(max_dim); + } + + // Only create a context with VSync and SRGB if the context creation works + let gl_window = create_gl_window(window, &events_loop, Some(shared_context))?; + + // Hide the window until the first draw (prevents flash on startup) + gl_window.hide(); + + let (hidpi_factor, winit_hidpi_factor) = get_hidpi_factor(&gl_window.window(), &events_loop); + let mut state = options.state.clone(); + state.size.hidpi_factor = hidpi_factor as f64; + state.size.winit_hidpi_factor = winit_hidpi_factor as f64; + + if options.state.is_fullscreen { + gl_window.window().set_fullscreen(Some(gl_window.window().get_current_monitor())); + } + + if let Some(pos) = options.state.position { + gl_window.window().set_position(pos); + } + + if options.state.is_maximized && !options.state.is_fullscreen { + gl_window.window().set_maximized(true); + } else if !options.state.is_fullscreen { + gl_window.window().set_inner_size(options.state.size.get_inner_logical_size()); + } + + // #[cfg(debug_assertions)] + // let display = Display::with_debug(gl_window, DebugCallbackBehavior::DebugMessageOnError)?; + // #[cfg(not(debug_assertions))] + let display = Display::with_debug(gl_window, DebugCallbackBehavior::Ignore)?; + + let framebuffer_size = { + let inner_logical_size = display.gl_window().get_inner_size().unwrap(); + let (width, height): (u32, u32) = inner_logical_size.to_physical(hidpi_factor as f64).into(); + DeviceIntSize::new(width as i32, height as i32) + }; + + let document_id = render_api.add_document(framebuffer_size, 0); + let epoch = Epoch(0); + + // TODO: The PipelineId is what gets passed to the OutputImageHandler + // (the code that coordinates displaying the rendered texture). + // + // Each window is a "pipeline", i.e a new web page in webrender terms, + // however, there is only one global renderer, in order to save on memory, + // The pipeline ID is important, in order to coordinate the rendered textures + // back to their windows and window positions. + let pipeline_id = new_pipeline_id(); + + let window_id = display.gl_window().id(); + + // let (sender, receiver) = channel(); + // let thread = Builder::new().name(options.title.clone()).spawn(move || Self::handle_event(receiver))?; + + css.sort_by_specificity(); + + let last_scrolled_nodes = ScrolledNodes::default(); + + let window = Window { + id: window_id, + create_options: options, + state: state, + display: Rc::new(display), + css, + #[cfg(debug_assertions)] + css_loader: None, + scroll_states: ScrollStates::new(), + internal: WindowInternal { epoch, pipeline_id, document_id, last_scrolled_nodes }, + marker: PhantomData, + }; + + Ok(window) + } + + /// Creates a new window that will automatically load a new style from a given HotReloadHandler. + /// Only available with debug_assertions enabled. + #[cfg(debug_assertions)] + pub(crate) fn new_hot_reload( + render_api: &mut RenderApi, + shared_context: &Context, + events_loop: &EventsLoop, + options: WindowCreateOptions, + css_loader: Box, + background_color: ColorU, + ) -> Result { + let mut window = Window::new(render_api, shared_context, events_loop, options, Css::default(), background_color)?; + window.css_loader = Some(css_loader); + Ok(window) + } + + /// Returns an iterator over all given monitors + pub fn get_available_monitors() -> MonitorIter { + MonitorIter { + inner: EventsLoop::new().get_available_monitors(), + } + } + + /// Returns what monitor the window is currently residing on (to query monitor size, etc.). + pub fn get_current_monitor(&self) -> MonitorId { + self.display.gl_window().window().get_current_monitor() + } + + /// Updates the window state, diff the `self.state` with the `new_state` + /// and updating the platform window to reflect the changes + /// + /// Note: Currently, setting `mouse_state.position`, `window.size` or + /// `window.position` has no effect on the platform window, since they are very + /// frequently modified by the user (other properties are always set by the + /// application developer) + pub(crate) fn update_from_user_window_state(&mut self, new_state: WindowState) { + + let gl_window = self.display.gl_window(); + let window = gl_window.window(); + let old_state = &mut self.state; + + // Compare the old and new state, field by field + + if old_state.title != new_state.title { + window.set_title(&new_state.title); + old_state.title = new_state.title; + } + + if old_state.internal.mouse_state.mouse_cursor_type != new_state.internal.mouse_state.mouse_cursor_type { + window.set_cursor(new_state.internal.mouse_state.mouse_cursor_type); + old_state.internal.mouse_state.mouse_cursor_type = new_state.internal.mouse_state.mouse_cursor_type; + } + + if old_state.is_maximized != new_state.is_maximized { + window.set_maximized(new_state.is_maximized); + old_state.is_maximized = new_state.is_maximized; + } + + if old_state.is_fullscreen != new_state.is_fullscreen { + if new_state.is_fullscreen { + window.set_fullscreen(Some(window.get_current_monitor())); + } else { + window.set_fullscreen(None); + } + old_state.is_fullscreen = new_state.is_fullscreen; + } + + if old_state.has_decorations != new_state.has_decorations { + window.set_decorations(new_state.has_decorations); + old_state.has_decorations = new_state.has_decorations; + } + + if old_state.is_visible != new_state.is_visible { + if new_state.is_visible { + window.show(); + } else { + window.hide(); + } + old_state.is_visible = new_state.is_visible; + } + + if old_state.size.min_dimensions != new_state.size.min_dimensions { + window.set_min_dimensions(new_state.size.min_dimensions.map(Into::into)); + old_state.size.min_dimensions = new_state.size.min_dimensions; + } + + if old_state.size.max_dimensions != new_state.size.max_dimensions { + window.set_max_dimensions(new_state.size.max_dimensions.map(Into::into)); + old_state.size.max_dimensions = new_state.size.max_dimensions; + } + } + + #[allow(unused_variables)] + pub(crate) fn update_from_external_window_state( + &mut self, + frame_event_info: &mut FrameEventInfo, + events_loop: &EventsLoop, + ) { + + if frame_event_info.new_window_size.is_some() || frame_event_info.new_dpi_factor.is_some() { + #[cfg(target_os = "linux")] { + self.state.size.hidpi_factor = linux_get_hidpi_factor( + &self.display.gl_window().window().get_current_monitor(), + events_loop + ); + } + } + + if let Some(new_size) = frame_event_info.new_window_size { + self.state.size.dimensions = new_size; + } + + if let Some(dpi) = frame_event_info.new_dpi_factor { + self.state.size.winit_hidpi_factor = dpi; + frame_event_info.should_redraw_window = true; + } + } + + /// Resets the mouse states `scroll_x` and `scroll_y` to 0 + pub(crate) fn clear_scroll_state(&mut self) { + self.state.internal.mouse_state.scroll_x = 0.0; + self.state.internal.mouse_state.scroll_y = 0.0; + } +} + +/// Since the rendering is single-threaded anyways, the renderer is shared across windows. +/// Second, in order to use the font-related functions on the `RenderApi`, we need to +/// store the RenderApi somewhere in the AppResources. However, the `RenderApi` is bound +/// to a window (because OpenGLs function pointer is bound to a window). +/// +/// This means that on startup (when calling App::new()), azul creates a fake, hidden display +/// that handles all the rendering, outputs the rendered frames onto a texture, so that the +/// other windows can use said texture. This is also important for animations and multi-window +/// apps later on, but for now the only reason is so that `AppResources::add_font()` has +/// the proper access to the `RenderApi` +pub(crate) struct FakeDisplay { + /// Main render API that can be used to register and un-register fonts and images + pub(crate) render_api: RenderApi, + /// Main renderer, responsible for rendering all windows + pub(crate) renderer: Option, + /// Fake / invisible display, only used because OpenGL is tied to a display context + /// (offscreen rendering is not supported out-of-the-box on many platforms) + pub(crate) hidden_display: Display, + /// TODO: Not sure if we even need this, the events loop isn't important + /// for a window that is never shown + pub(crate) hidden_events_loop: EventsLoop, +} + +impl FakeDisplay { + + /// Creates a new render + a new display, given a renderer type (software or hardware) + pub(crate) fn new(renderer_type: RendererType) + -> Result + { + let events_loop = EventsLoop::new(); + let window = GliumWindowBuilder::new().with_dimensions(LogicalSize::new(10.0, 10.0)).with_visibility(false); + let gl_window = create_gl_window(window, &events_loop, None)?; + let (dpi_factor, _) = get_hidpi_factor(&gl_window.window(), &events_loop); + gl_window.hide(); + + let display = Display::with_debug(gl_window, DebugCallbackBehavior::Ignore)?; + let gl = get_gl_context(&display)?; + + // Note: Notifier is fairly useless, since rendering is completely single-threaded, see comments on RenderNotifier impl + let notifier = Box::new(Notifier { }); + let (mut renderer, render_api) = create_renderer(gl.clone(), notifier, renderer_type, dpi_factor)?; + + renderer.set_external_image_handler(Box::new(Compositor::default())); + + Ok(Self { + render_api, + renderer: Some(renderer), + hidden_display: display, + hidden_events_loop: events_loop, + }) + } +} + +impl Drop for FakeDisplay { + fn drop(&mut self) { + + // NOTE: For some reason this is necessary, otherwise the renderer crashes on shutdown + // + // TODO: This still crashes on Linux because the makeCurrent call doesn't succeed + // (likely because the underlying surface has been destroyed). In those cases, + // we don't de-initialize the rendered (since this is an application shutdown it + // doesn't matter, the resources are going to get cleaned up by the OS). + match unsafe { self.hidden_display.gl_window().make_current() } { + Ok(_) => { }, + Err(e) => { + error!("Shutdown error: {}", e); + return; + }, + } + + let gl_context = match get_gl_context(&self.hidden_display) { + Ok(o) => o, + Err(e) => { + error!("Shutdown error: {}", e); + return; + }, + }; + + gl_context.disable(gl::FRAMEBUFFER_SRGB); + gl_context.disable(gl::MULTISAMPLE); + gl_context.disable(gl::POLYGON_SMOOTH); + + if let Some(renderer) = self.renderer.take() { + renderer.deinit(); + } + } +} + +/// Returns the actual hidpi factor and the winit DPI factor for the current window +#[allow(unused_variables)] +fn get_hidpi_factor(window: &GliumWindow, events_loop: &EventsLoop) -> (f64, f64) { + let monitor = window.get_current_monitor(); + let winit_hidpi_factor = monitor.get_hidpi_factor(); + + #[cfg(target_os = "linux")] { + (linux_get_hidpi_factor(&monitor, &events_loop), winit_hidpi_factor) + } + #[cfg(not(target_os = "linux"))] { + (winit_hidpi_factor, winit_hidpi_factor) + } +} + + +fn create_gl_window(window: GliumWindowBuilder, events_loop: &EventsLoop, shared_context: Option<&Context>) +-> Result +{ + // The shared_context is reversed: If the shared_context is None, then this window is the root window, + // so the window should be created with new_shared (so the context can be shared to all other windows). + // + // If the shared_context is Some() then the window is not a root window, so it should share the existing + // context, but not re-share it (so, create it normally via ::new() instead of ::new_shared()). + + CombinedContext::new(window.clone(), create_context_builder(true, true, shared_context), &events_loop).or_else(|_| + CombinedContext::new(window.clone(), create_context_builder(true, false, shared_context), &events_loop)).or_else(|_| + CombinedContext::new(window.clone(), create_context_builder(false, true, shared_context), &events_loop)).or_else(|_| + CombinedContext::new(window.clone(), create_context_builder(false, false,shared_context), &events_loop)) + .map_err(|e| WindowCreateError::CreateError(e)) +} + +/// ContextBuilder is sadly not clone-able, which is why it has to be re-created +/// every time you want to create a new context. The goals is to not crash on +/// platforms that don't have VSync or SRGB (which are OpenGL extensions) installed. +/// +/// Secondly, in order to support multi-window apps, all windows need to share +/// the same OpenGL context - i.e. `builder.with_shared_lists(some_gl_window.context());` +/// +/// `allow_sharing_context` should only be true for the root window - so that +/// we can be sure the shared context can't be re-shared by the created window. Only +/// the root window (via `FakeDisplay`) is allowed to manage the OpenGL context. +fn create_context_builder<'a>( + vsync: bool, + srgb: bool, + shared_context: Option<&'a Context>, +) -> ContextBuilder<'a> { + + // See #33 - specifying a specific OpenGL version + // makes winit crash on older Intel drivers, which is why we + // don't specify a specific OpenGL version here + let mut builder = ContextBuilder::new(); + + if let Some(shared_context) = shared_context { + builder = builder.with_shared_lists(shared_context); + } + + // #[cfg(debug_assertions)] { + // builder = builder.with_gl_debug_flag(true); + // } + + // #[cfg(not(debug_assertions))] { + builder = builder.with_gl_debug_flag(false); + // } + + if vsync { + builder = builder.with_vsync(true); + } + + if srgb { + builder = builder.with_srgb(true); + } + + builder +} + +// This exists because RendererOptions isn't Clone-able +fn get_renderer_opts(native: bool, device_pixel_ratio: f32) -> RendererOptions { + + use webrender::ProgramCache; + + // pre-caching shaders means to compile all shaders on startup + // this can take significant time and should be only used for testing the shaders + const PRECACHE_SHADER_FLAGS: ShaderPrecacheFlags = ShaderPrecacheFlags::EMPTY; + + // NOTE: If the clear_color is None, this may lead to "black screens" + // (because black is the default color) - so instead, white should be the default + // However, if the clear color is specified, then it's hard creating transparent windows + // (because of bugs in webrender / handling multi-window background colors). + // Therefore the background color has to be set before render() is invoked. + + RendererOptions { + resource_override_path: None, + precache_flags: PRECACHE_SHADER_FLAGS, + device_pixel_ratio, + enable_subpixel_aa: true, + enable_aa: true, + cached_programs: Some(ProgramCache::new(None)), + renderer_kind: if native { + RendererKind::Native + } else { + RendererKind::OSMesa + }, + .. RendererOptions::default() + } +} + +fn create_renderer( + gl: Rc, + notifier: Box, + renderer_type: RendererType, + device_pixel_ratio: f64, +) -> Result<(Renderer, RenderApi), WindowCreateError> { + + use self::RendererType::*; + + let opts_native = get_renderer_opts(true, device_pixel_ratio as f32); + let opts_osmesa = get_renderer_opts(false, device_pixel_ratio as f32); + + let (renderer, sender) = match renderer_type { + Hardware => { + // force hardware renderer + Renderer::new(gl, notifier, opts_native, WR_SHADER_CACHE).unwrap() + }, + Software => { + // force software renderer + Renderer::new(gl, notifier, opts_osmesa, WR_SHADER_CACHE).unwrap() + }, + Default => { + // try hardware first, fall back to software + match Renderer::new(gl.clone(), notifier.clone(), opts_native, WR_SHADER_CACHE) { + Ok(r) => r, + Err(_) => Renderer::new(gl, notifier, opts_osmesa, WR_SHADER_CACHE).unwrap() + } + } + }; + + let api = sender.create_api(); + + Ok((renderer, api)) +} + +pub(crate) fn get_gl_context(display: &Display) -> Result, WindowCreateError> { + match display.gl_window().get_api() { + glutin::Api::OpenGl => Ok(unsafe { + gl::GlFns::load_with(|symbol| display.gl_window().get_proc_address(symbol) as *const _) + }), + glutin::Api::OpenGlEs => Ok(unsafe { + gl::GlesFns::load_with(|symbol| display.gl_window().get_proc_address(symbol) as *const _) + }), + glutin::Api::WebGl => Err(WindowCreateError::WebGlNotSupported), + } +} + +#[cfg(target_os = "linux")] +fn get_xft_dpi() -> Option{ + // TODO! + /* + #include + #include + #include + + double _glfwPlatformGetMonitorDPI(_GLFWmonitor* monitor) + { + char *resourceString = XResourceManagerString(_glfw.x11.display); + XrmDatabase db; + XrmValue value; + char *type = NULL; + double dpi = 0.0; + + XrmInitialize(); /* Need to initialize the DB before calling Xrm* functions */ + + db = XrmGetStringDatabase(resourceString); + + if (resourceString) { + printf("Entire DB:\n%s\n", resourceString); + if (XrmGetResource(db, "Xft.dpi", "String", &type, &value) == True) { + if (value.addr) { + dpi = atof(value.addr); + } + } + } + + printf("DPI: %f\n", dpi); + return dpi; + } + */ + None +} + +/// Return the DPI on X11 systems +#[cfg(target_os = "linux")] +fn linux_get_hidpi_factor(monitor: &MonitorId, events_loop: &EventsLoop) -> f64 { + + use std::env; + use std::process::Command; + use glium::glutin::os::unix::EventsLoopExt; + + let winit_dpi = monitor.get_hidpi_factor(); + let winit_hidpi_factor = env::var("WINIT_HIDPI_FACTOR").ok().and_then(|hidpi_factor| hidpi_factor.parse::().ok()); + let qt_font_dpi = env::var("QT_FONT_DPI").ok().and_then(|font_dpi| font_dpi.parse::().ok()); + + // Execute "gsettings get org.gnome.desktop.interface text-scaling-factor" and parse the output + let gsettings_dpi_factor = + Command::new("gsettings") + .arg("get") + .arg("org.gnome.desktop.interface") + .arg("text-scaling-factor") + .output().ok() + .map(|output| output.stdout) + .and_then(|stdout_bytes| String::from_utf8(stdout_bytes).ok()) + .map(|stdout_string| stdout_string.lines().collect::()) + .and_then(|gsettings_output| gsettings_output.parse::().ok()); + + // Wayland: Ignore Xft.dpi + let xft_dpi = if events_loop.is_x11() { get_xft_dpi() } else { None }; + + let options = [winit_hidpi_factor, qt_font_dpi, gsettings_dpi_factor, xft_dpi]; + options.into_iter().filter_map(|x| *x).next().unwrap_or(winit_dpi) +} \ No newline at end of file diff --git a/azul/src/window_state.rs b/azul/src/window_state.rs new file mode 100644 index 000000000..60ec360ee --- /dev/null +++ b/azul/src/window_state.rs @@ -0,0 +1,1032 @@ +use std::{ + collections::{HashSet, BTreeMap}, + path::PathBuf, + fmt, +}; +use glium::glutin::{ + Window, WindowEvent, KeyboardInput, ScanCode, ElementState, + MouseCursor, VirtualKeyCode, MouseScrollDelta, AxisId, + ModifiersState, dpi::{LogicalPosition, LogicalSize}, +}; +use webrender::api::HitTestItem; +use { + app::FrameEventInfo, + dom::{EventFilter, NotEventFilter, HoverEventFilter, FocusEventFilter, WindowEventFilter}, + callbacks:: {CallbackInfo, Callback, DefaultCallbackId, UpdateScreen}, + id_tree::NodeId, + ui_state::UiState, + callbacks::FocusTarget, + app::AppState, +}; + +const DEFAULT_TITLE: &str = "Azul App"; +const DEFAULT_WIDTH: f64 = 800.0; +const DEFAULT_HEIGHT: f64 = 600.0; + +/// Determines which keys are pressed currently (modifiers, etc.) +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct KeyboardState { + + // Modifier keys that are currently actively pressed during this frame + // + // Note: These are tracked separately by glium to prevent missing state changes + // when the window isn't focused + + /// Shift key + pub shift_down: bool, + /// Ctrl key + pub ctrl_down: bool, + /// Alt key + pub alt_down: bool, + /// `Super / Windows / Command` key + pub super_down: bool, + /// Currently pressed key, already converted to characters + pub current_char: Option, + /// Holds the key that was pressed last if there is Some. Holds None otherwise. + pub latest_virtual_keycode: Option, + /// Currently pressed virtual keycodes - this is essentially an "extension" + /// of `current_keys` - `current_keys` stores the characters, but what if the + /// pressed key is not a character (such as `ArrowRight` or `PgUp`)? + /// + /// Note that this can have an overlap, so pressing "a" on the keyboard will insert + /// both a `VirtualKeyCode::A` into `current_virtual_keycodes` and an `"a"` as a char into `current_keys`. + pub current_virtual_keycodes: HashSet, + /// Same as `current_virtual_keycodes`, but the scancode identifies the physical key pressed. + /// + /// This should not change if the user adjusts the host's keyboard map. + /// Use when the physical location of the key is more important than the key's host GUI semantics, + /// such as for movement controls in a first-person game (German keyboard: Z key, UK keyboard: Y key, etc.) + pub current_scancodes: HashSet, +} + +impl KeyboardState { + + fn update_from_modifier_state(&mut self, state: ModifiersState) { + self.shift_down = state.shift; + self.ctrl_down = state.ctrl; + self.alt_down = state.alt; + self.super_down = state.logo; + } +} + +/// Mouse position on the screen +#[derive(Debug, Copy, Clone)] +pub struct MouseState +{ + /// Current mouse cursor type + pub mouse_cursor_type: MouseCursor, + /// Where is the mouse cursor currently? Set to `None` if the window is not focused + pub cursor_pos: Option, + /// Is the left mouse button down? + pub left_down: bool, + /// Is the right mouse button down? + pub right_down: bool, + /// Is the middle mouse button down? + pub middle_down: bool, + /// Scroll amount in pixels in the horizontal direction. Gets reset to 0 after every frame + pub scroll_x: f64, + /// Scroll amount in pixels in the vertical direction. Gets reset to 0 after every frame + pub scroll_y: f64, +} + +impl MouseState { + /// Returns whether any mouse button (left, right or center) is currently held down + pub fn mouse_down(&self) -> bool { + self.right_down || self.left_down || self.middle_down + } +} + +impl Default for MouseState { + /// Creates a new mouse state + fn default() -> Self { + Self { + mouse_cursor_type: MouseCursor::Default, + cursor_pos: None, + left_down: false, + right_down: false, + middle_down: false, + scroll_x: 0.0, + scroll_y: 0.0, + } + } +} + +/// Toggles webrender debug flags (will make stuff appear on +/// the screen that you might not want to - used for debugging purposes) +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct DebugState { + /// Toggles `webrender::DebugFlags::PROFILER_DBG` + pub profiler_dbg: bool, + /// Toggles `webrender::DebugFlags::RENDER_TARGET_DBG` + pub render_target_dbg: bool, + /// Toggles `webrender::DebugFlags::TEXTURE_CACHE_DBG` + pub texture_cache_dbg: bool, + /// Toggles `webrender::DebugFlags::GPU_TIME_QUERIES` + pub gpu_time_queries: bool, + /// Toggles `webrender::DebugFlags::GPU_SAMPLE_QUERIES` + pub gpu_sample_queries: bool, + /// Toggles `webrender::DebugFlags::DISABLE_BATCHING` + pub disable_batching: bool, + /// Toggles `webrender::DebugFlags::EPOCHS` + pub epochs: bool, + /// Toggles `webrender::DebugFlags::COMPACT_PROFILER` + pub compact_profiler: bool, + /// Toggles `webrender::DebugFlags::ECHO_DRIVER_MESSAGES` + pub echo_driver_messages: bool, + /// Toggles `webrender::DebugFlags::NEW_FRAME_INDICATOR` + pub new_frame_indicator: bool, + /// Toggles `webrender::DebugFlags::NEW_SCENE_INDICATOR` + pub new_scene_indicator: bool, + /// Toggles `webrender::DebugFlags::SHOW_OVERDRAW` + pub show_overdraw: bool, + /// Toggles `webrender::DebugFlags::GPU_CACHE_DBG` + pub gpu_cache_dbg: bool, +} + +impl Default for DebugState { + fn default() -> Self { + Self { + profiler_dbg: false, + render_target_dbg: false, + texture_cache_dbg: false, + gpu_time_queries: false, + gpu_sample_queries: false, + disable_batching: false, + epochs: false, + compact_profiler: false, + echo_driver_messages: false, + new_frame_indicator: false, + new_scene_indicator: false, + show_overdraw: false, + gpu_cache_dbg: false, + } + } +} + +#[derive(Debug, Clone)] +pub(crate) struct CrateInternalWindowState { + /// The state of the keyboard for this frame + pub(crate) keyboard_state: KeyboardState, + /// The state of the mouse, read-only + pub(crate) mouse_state: MouseState, + /// Whether there is a file currently hovering over the window + pub(crate) hovered_file: Option, + /// What node is currently hovered over, default to None. Only necessary internal + /// to the crate, for emitting `On::FocusReceived` and `On::FocusLost` events, + /// as well as styling `:focus` elements + pub(crate) focused_node: Option, + /// Currently hovered nodes, default to an empty Vec. Important for + /// styling `:hover` elements. + pub(crate) hovered_nodes: BTreeMap, + /// Previous window state, used for determining mouseout, etc. events + pub(crate) previous_window_state: Option>, + /// Whether there is a focus field overwrite from the last callback calls. + pub(crate) pending_focus_target: Option, + /// What the last motion was in case a controller was used. + pub(crate) last_motion: Option<(AxisId, f64)>, +} + +impl Default for CrateInternalWindowState { + fn default() -> Self { + CrateInternalWindowState { + keyboard_state: KeyboardState::default(), + mouse_state: MouseState::default(), + focused_node: None, + hovered_nodes: BTreeMap::new(), + hovered_file: None, + previous_window_state: None, + pending_focus_target: None, + last_motion: None, + } + } +} + +/// State, size, etc of the window, for comparing to the last frame +#[derive(Debug, Clone)] +pub struct WindowState { + /// Internal, read-only state (TODO: move this out of here!) + pub(crate) internal: CrateInternalWindowState, + /// Mostly used for debugging, shows WebRender-builtin graphs on the screen. + /// Used for performance monitoring and displaying frame times (rendering-only). + pub debug_state: DebugState, + /// Size of the window + max width / max height: 800 x 600 by default + pub size: WindowSize, + /// Current title of the window + pub title: String, + /// The x and y position, or None to let the WM decide where to put the window (default) + pub position: Option, + /// Is the window currently maximized + pub is_maximized: bool, + /// Is the window currently fullscreened? + pub is_fullscreen: bool, + /// Does the window have decorations (close, minimize, maximize, title bar)? + pub has_decorations: bool, + /// Is the window currently visible? + pub is_visible: bool, + /// Is the window always on top? + pub is_always_on_top: bool, +} + +#[derive(Debug, Copy, Clone)] +pub struct WindowSize { + /// Width and height of the window, in logical + /// units (may not correspond to the physical on-screen size) + pub dimensions: LogicalSize, + /// DPI factor of the window + pub hidpi_factor: f64, + /// (Internal only, unused): winit HiDPI factor + pub winit_hidpi_factor: f64, + /// Minimum dimensions of the window + pub min_dimensions: Option, + /// Maximum dimensions of the window + pub max_dimensions: Option, +} + +impl WindowSize { + pub fn get_inner_logical_size(&self) -> LogicalSize { + LogicalSize::new( + self.dimensions.width / self.winit_hidpi_factor * self.hidpi_factor, + self.dimensions.height / self.winit_hidpi_factor * self.hidpi_factor + ) + } + + pub fn get_reverse_logical_size(&self) -> LogicalSize { + LogicalSize::new( + self.dimensions.width / self.hidpi_factor * self.winit_hidpi_factor, + self.dimensions.height / self.hidpi_factor * self.winit_hidpi_factor, + ) + } +} + +impl Default for WindowSize { + fn default() -> Self { + Self { + dimensions: LogicalSize::new(DEFAULT_WIDTH, DEFAULT_HEIGHT), + hidpi_factor: 1.0, + winit_hidpi_factor: 1.0, + min_dimensions: None, + max_dimensions: None, + } + } +} + +impl Default for WindowState { + fn default() -> Self { + Self { + internal: CrateInternalWindowState::default(), + title: DEFAULT_TITLE.into(), + position: None, + size: WindowSize::default(), + is_maximized: false, + is_fullscreen: false, + has_decorations: true, + is_visible: true, + is_always_on_top: false, + debug_state: DebugState::default(), + } + } +} + +pub(crate) struct DetermineCallbackResult { + pub(crate) hit_test_item: Option, + pub(crate) default_callbacks: BTreeMap, + pub(crate) normal_callbacks: BTreeMap>, +} + +impl Default for DetermineCallbackResult { + fn default() -> Self { + DetermineCallbackResult { + hit_test_item: None, + default_callbacks: BTreeMap::new(), + normal_callbacks: BTreeMap::new(), + } + } +} + +impl Clone for DetermineCallbackResult { + fn clone(&self) -> Self { + DetermineCallbackResult { + hit_test_item: self.hit_test_item.clone(), + default_callbacks: self.default_callbacks.clone(), + normal_callbacks: self.normal_callbacks.clone(), + } + } +} + +pub(crate) struct CallbacksOfHitTest { + /// A BTreeMap where each item is already filtered by the proper hit-testing type, + /// meaning in order to get the proper callbacks, you simply have to iterate through + /// all node IDs + pub nodes_with_callbacks: BTreeMap>, + /// Whether the screen should be redrawn even if no Callback returns an `UpdateScreen::Redraw`. + /// This is necessary for `:hover` and `:active` mouseovers - otherwise the screen would + /// only update on the next resize. + pub needs_redraw_anyways: bool, + /// Same as `needs_redraw_anyways`, but for reusing the layout from the previous frame. + /// Each `:hover` and `:active` group stores whether it modifies the layout, as + /// a performance optimization. + pub needs_relayout_anyways: bool, +} + +impl fmt::Debug for DetermineCallbackResult { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}, {:?}, {:?}", self.hit_test_item, self.default_callbacks, self.normal_callbacks) + } +} + +impl Default for CallbacksOfHitTest { + fn default() -> Self { + Self { + nodes_with_callbacks: BTreeMap::new(), + needs_redraw_anyways: false, + needs_relayout_anyways: false, + } + } +} + +impl WindowState +{ + pub fn get_mouse_state(&self) -> &MouseState { + &self.internal.mouse_state + } + + pub fn get_keyboard_state(&self) -> &KeyboardState { + &self.internal.keyboard_state + } + + pub fn get_hovered_file(&self) -> Option<&PathBuf> { + self.internal.hovered_file.as_ref() + } + + pub fn get_last_motion(&self) -> Option<(AxisId, f64)> { + self.internal.last_motion + } + + /// Returns the window state of the previous frame, useful for calculating + /// metrics for dragging motions. Note that you can't call this function + /// recursively - calling `get_previous_window_state()` on the returned + /// `WindowState` will yield a `None` value. + pub fn get_previous_window_state(&self) -> Option<&Box> { + self.internal.previous_window_state.as_ref() + } + + /// Determine which event / which callback(s) should be called and in which order + /// + /// This function also updates / mutates the current window state, so that + /// the window state is updated for the next frame + pub(crate) fn determine_callbacks( + &mut self, + hit_test_items: &[HitTestItem], + event: &WindowEvent, + ui_state: &UiState + ) -> CallbacksOfHitTest + { + use std::collections::BTreeSet; + + // Store the current window state so we can set it in this.previous_window_state later on + let mut previous_state = Box::new(self.clone()); + previous_state.internal.previous_window_state = None; + + let mut needs_hover_redraw = false; + let mut needs_hover_relayout = false; + + // BTreeMap> + let mut nodes_with_callbacks: BTreeMap> = BTreeMap::new(); + + let current_window_events = get_window_events(self, event); + let current_hover_events = get_hover_events(¤t_window_events); + let current_focus_events = get_focus_events(¤t_hover_events); + + let event_was_mouse_down = if let WindowEvent::MouseInput { state: ElementState::Pressed, .. } = event { true } else { false }; + let event_was_mouse_release = if let WindowEvent::MouseInput { state: ElementState::Released, .. } = event { true } else { false }; + let event_was_mouse_enter = if let WindowEvent::CursorEntered { .. } = event { true } else { false }; + let event_was_mouse_leave = if let WindowEvent::CursorLeft { .. } = event { true } else { false }; + + // TODO: If the current mouse is down, but the event + // wasn't a click, that means it was a drag + + // Figure out what the hovered NodeIds are + let mut new_hit_node_ids: BTreeMap = hit_test_items.iter().filter_map(|hit_test_item| { + ui_state.tag_ids_to_node_ids + .get(&hit_test_item.tag.0) + .map(|node_id| (*node_id, hit_test_item.clone())) + }).collect(); + + if event_was_mouse_leave { + new_hit_node_ids = BTreeMap::new(); + } + + // Figure out what the current focused NodeId is + if event_was_mouse_down || event_was_mouse_release { + + // Find the first (closest to cursor in hierarchy) item that has a tabindex + let closest_focus_node = hit_test_items.iter().rev() + .find_map(|item| ui_state.tab_index_tags.get(&item.tag.0)) + .cloned(); + + // Even if the focused node is None, we still have to update self.focused_node! + self.internal.focused_node = closest_focus_node.map(|(node_id, _tab_idx)| node_id); + } + + macro_rules! insert_only_non_empty_callbacks { + ($node_id:expr, $hit_test_item:expr, $normal_hover_callbacks:expr, $default_hover_callbacks:expr) => ({ + if !($normal_hover_callbacks.is_empty() && $default_hover_callbacks.is_empty()) { + let mut callback_result = nodes_with_callbacks.entry(*$node_id) + .or_insert_with(|| DetermineCallbackResult::default()); + + let item: Option = $hit_test_item; + if let Some(hit_test_item) = item { + callback_result.hit_test_item = Some(hit_test_item); + } + callback_result.normal_callbacks.extend($normal_hover_callbacks.into_iter()); + callback_result.default_callbacks.extend($default_hover_callbacks.into_iter()); + } + }) + } + + // Inserts the events from a given NodeId and an Option into the nodes_with_callbacks + macro_rules! insert_callbacks {( + $node_id:expr, + $hit_test_item:expr, + $hover_callbacks:ident, + $hover_default_callbacks:ident, + $current_hover_events:ident, + $event_filter:ident + ) => ({ + // BTreeMap> + let mut normal_hover_callbacks = BTreeMap::new(); + + // Insert all normal Hover events + if let Some(ui_state_hover_event_filters) = ui_state.$hover_callbacks.get($node_id) { + for current_hover_event in &$current_hover_events { + if let Some(callback) = ui_state_hover_event_filters.get(current_hover_event) { + normal_hover_callbacks.insert(EventFilter::$event_filter(*current_hover_event), *callback); + } + } + } + + // BTreeMap + let mut default_hover_callbacks = BTreeMap::new(); + + // Insert all default Hover events + if let Some(ui_state_hover_default_event_filters) = ui_state.$hover_default_callbacks.get($node_id) { + for current_hover_event in &$current_hover_events { + if let Some(callback_id) = ui_state_hover_default_event_filters.get(current_hover_event) { + default_hover_callbacks.insert(EventFilter::$event_filter(*current_hover_event), *callback_id); + } + } + } + + insert_only_non_empty_callbacks!($node_id, $hit_test_item, normal_hover_callbacks, default_hover_callbacks); + }) + } + + // Insert all normal window events + for (window_node_id, window_callbacks) in &ui_state.window_callbacks { + let normal_window_callbacks = window_callbacks.iter() + .filter(|(current_window_event, _)| current_window_events.contains(current_window_event)) + .map(|(current_window_event, callback)| (EventFilter::Window(*current_window_event), *callback)) + .collect::>(); + let default_window_callbacks = BTreeMap::::new(); + insert_only_non_empty_callbacks!(window_node_id, None, normal_window_callbacks, default_window_callbacks); + } + + // Insert all default window events + for (window_node_id, window_callbacks) in &ui_state.window_default_callbacks { + let normal_window_callbacks = BTreeMap::>::new(); + let default_window_callbacks = window_callbacks.iter() + .filter(|(current_window_event, _)| current_window_events.contains(current_window_event)) + .map(|(current_window_event, callback)| (EventFilter::Window(*current_window_event), *callback)) + .collect::>(); + insert_only_non_empty_callbacks!(window_node_id, None, normal_window_callbacks, default_window_callbacks); + } + + // Insert (normal + default) hover events + for (hover_node_id, hit_test_item) in &new_hit_node_ids { + insert_callbacks!(hover_node_id, Some(hit_test_item.clone()), hover_callbacks, hover_default_callbacks, current_hover_events, Hover); + } + + // Insert (normal + default) focus events + if let Some(current_focused_node) = &self.internal.focused_node { + insert_callbacks!(current_focused_node, None, focus_callbacks, focus_default_callbacks, current_focus_events, Focus); + } + + // If the last focused node and the current focused node aren't the same, + // submit a FocusLost for the last node and a FocusReceived for the current one. + let mut focus_received_lost_events: BTreeMap = BTreeMap::new(); + match (self.internal.focused_node, previous_state.internal.focused_node) { + (Some(cur), None) => { + focus_received_lost_events.insert(cur, FocusEventFilter::FocusReceived); + }, + (None, Some(prev)) => { + focus_received_lost_events.insert(prev, FocusEventFilter::FocusLost); + }, + (Some(cur), Some(prev)) => { + if cur != prev { + focus_received_lost_events.insert(cur, FocusEventFilter::FocusReceived); + focus_received_lost_events.insert(prev, FocusEventFilter::FocusLost); + } + } + (None, None) => { }, + } + + // Insert FocusReceived / FocusLost + for (node_id, focus_event) in &focus_received_lost_events { + let current_focus_leave_events = [focus_event.clone()]; + insert_callbacks!(node_id, None, focus_callbacks, focus_default_callbacks, current_focus_leave_events, Focus); + } + + macro_rules! mouse_enter { + ($node_id:expr, $hit_test_item:expr, $event_filter:ident) => ({ + + let node_is_focused = self.internal.focused_node == Some($node_id); + + // BTreeMap> + let mut normal_callbacks = BTreeMap::new(); + + // Insert all normal Hover(MouseEnter) events + if let Some(ui_state_hover_event_filters) = ui_state.hover_callbacks.get(&$node_id) { + if let Some(callback) = ui_state_hover_event_filters.get(&HoverEventFilter::$event_filter) { + normal_callbacks.insert(EventFilter::Hover(HoverEventFilter::$event_filter), *callback); + } + } + + // Insert all normal Focus(MouseEnter) events + if node_is_focused { + if let Some(ui_state_focus_event_filters) = ui_state.focus_callbacks.get(&$node_id) { + if let Some(callback) = ui_state_focus_event_filters.get(&FocusEventFilter::$event_filter) { + normal_callbacks.insert(EventFilter::Focus(FocusEventFilter::$event_filter), *callback); + } + } + } + + // BTreeMap + let mut default_callbacks = BTreeMap::new(); + + // Insert all default Hover(MouseEnter) events + if let Some(ui_state_hover_default_event_filters) = ui_state.hover_default_callbacks.get(&$node_id) { + if let Some(callback_id) = ui_state_hover_default_event_filters.get(&HoverEventFilter::$event_filter) { + default_callbacks.insert(EventFilter::Hover(HoverEventFilter::$event_filter), *callback_id); + } + } + + // Insert all default Focus(MouseEnter) events + if node_is_focused { + if let Some(ui_state_focus_default_event_filters) = ui_state.focus_default_callbacks.get(&$node_id) { + if let Some(callback_id) = ui_state_focus_default_event_filters.get(&FocusEventFilter::$event_filter) { + default_callbacks.insert(EventFilter::Focus(FocusEventFilter::$event_filter), *callback_id); + } + } + } + + if !(default_callbacks.is_empty() && normal_callbacks.is_empty()) { + + let mut callback_result = nodes_with_callbacks.entry($node_id) + .or_insert_with(|| DetermineCallbackResult::default()); + + callback_result.hit_test_item = Some($hit_test_item); + callback_result.normal_callbacks.extend(normal_callbacks.into_iter()); + callback_result.default_callbacks.extend(default_callbacks.into_iter()); + } + + if let Some((_, hover_group)) = ui_state.node_ids_to_tag_ids.get(&$node_id).and_then(|tag_for_this_node| { + ui_state.tag_ids_to_hover_active_states.get(&tag_for_this_node) + }) { + // We definitely need to redraw (on any :hover) change + needs_hover_redraw = true; + // Only set this to true if the :hover group actually affects the layout + if hover_group.affects_layout { + needs_hover_relayout = true; + } + } + }) + } + + // Collect all On::MouseEnter nodes (for both hover and focus events) + let onmouseenter_nodes: BTreeMap = new_hit_node_ids.iter() + .filter(|(current_node_id, _)| previous_state.internal.hovered_nodes.get(current_node_id).is_none()) + .map(|(x, y)| (*x, y.clone())) + .collect(); + + let onmouseenter_empty = onmouseenter_nodes.is_empty(); + + // Insert Focus(MouseEnter) and Hover(MouseEnter) + for (node_id, hit_test_item) in onmouseenter_nodes { + mouse_enter!(node_id, hit_test_item, MouseEnter); + } + + // Collect all On::MouseLeave nodes (for both hover and focus events) + let onmouseleave_nodes: BTreeMap = previous_state.internal.hovered_nodes.iter() + .filter(|(prev_node_id, _)| new_hit_node_ids.get(prev_node_id).is_none()) + .map(|(x, y)| (*x, y.clone())) + .collect(); + + let onmouseleave_empty = onmouseleave_nodes.is_empty(); + + // Insert Focus(MouseEnter) and Hover(MouseEnter) + for (node_id, hit_test_item) in onmouseleave_nodes { + mouse_enter!(node_id, hit_test_item, MouseLeave); + } + + // If the mouse is down, but was up previously or vice versa, that means + // that a :hover or :active state may be invalidated. In that case we need + // to redraw the screen anyways. Setting relayout to true here in order to + let event_is_click_or_release = self.internal.mouse_state.mouse_down() != previous_state.internal.mouse_state.mouse_down(); + if event_is_click_or_release || event_was_mouse_enter || event_was_mouse_leave || !onmouseenter_empty || !onmouseleave_empty { + needs_hover_redraw = true; + needs_hover_relayout = true; + } + + // Insert all Not-callbacks, we need to filter out all Hover and Focus callbacks + // and then look at what callbacks were currently + + // In order to create the Not Events we have to record which events were fired and on what nodes + // Then we need to go through the events and fire them if the event was present, but the NodeID was not + let mut reverse_event_hover_normal_list = BTreeMap::>::new(); + let mut reverse_event_focus_normal_list = BTreeMap::>::new(); + let mut reverse_event_hover_default_list = BTreeMap::>::new(); + let mut reverse_event_focus_default_list = BTreeMap::>::new(); + + for (node_id, DetermineCallbackResult { default_callbacks, normal_callbacks, .. }) in &nodes_with_callbacks { + for event_filter in normal_callbacks.keys() { + match event_filter { + EventFilter::Hover(h) => { + reverse_event_hover_normal_list.entry(*h).or_insert_with(|| BTreeSet::new()).insert(*node_id); + }, + EventFilter::Focus(f) => { + reverse_event_focus_normal_list.entry(*f).or_insert_with(|| BTreeSet::new()).insert(*node_id); + }, + _ => { }, + } + } + for event_filter in default_callbacks.keys() { + match event_filter { + EventFilter::Hover(h) => { + reverse_event_hover_default_list.entry(*h).or_insert_with(|| BTreeSet::new()).insert(*node_id); + }, + EventFilter::Focus(f) => { + reverse_event_focus_default_list.entry(*f).or_insert_with(|| BTreeSet::new()).insert(*node_id); + }, + _ => { }, + } + } + } + + // Insert NotEventFilter callbacks + for (node_id, not_event_filter_callback_list) in &ui_state.not_callbacks { + for (event_filter, event_callback) in not_event_filter_callback_list { + // If we have the event filter, but we don't have the NodeID, then insert the callback + match event_filter { + NotEventFilter::Hover(h) => { + if let Some(on_node_ids) = reverse_event_hover_normal_list.get(&h) { + if !on_node_ids.contains(node_id) { + nodes_with_callbacks.entry(*node_id) + .or_insert_with(|| DetermineCallbackResult::default()) + .normal_callbacks.insert(EventFilter::Not(*event_filter), *event_callback); + } + } + // TODO: Same thing for default callbacks here + }, + NotEventFilter::Focus(f) => { + // TODO: Same thing for focus + } + } + } + } + + self.internal.hovered_nodes = new_hit_node_ids; + self.internal.previous_window_state = Some(previous_state); + + CallbacksOfHitTest { + needs_redraw_anyways: needs_hover_redraw, + needs_relayout_anyways: needs_hover_relayout, + nodes_with_callbacks, + } + } + + // Returns the frame events + if the window should close + pub(crate) fn update_window_state(&mut self, events: &[WindowEvent]) -> (FrameEventInfo, bool) { + let mut frame_event_info = FrameEventInfo::default(); + let mut should_window_close = false; + + for event in events { + if window_should_close(event, &mut frame_event_info) { + should_window_close = true; + } + self.update_mouse_cursor_position(event); + self.update_scroll_state(event); + self.update_keyboard_modifiers(event); + self.update_keyboard_pressed_chars(event); + self.update_misc_events(event); + } + + (frame_event_info, should_window_close) + } + + fn update_keyboard_modifiers(&mut self, event: &WindowEvent) { + let modifiers = match event { + WindowEvent::KeyboardInput { input: KeyboardInput { modifiers, .. }, .. } | + WindowEvent::CursorMoved { modifiers, .. } | + WindowEvent::MouseWheel { modifiers, .. } | + WindowEvent::MouseInput { modifiers, .. } => { + Some(modifiers) + }, + _ => None, + }; + + if let Some(modifiers) = modifiers { + self.internal.keyboard_state.update_from_modifier_state(*modifiers); + } + } + + /// After the initial events are filtered, this will update the mouse + /// cursor position, if the event is a `CursorMoved` and set it to `None` + /// if the cursor has left the window + fn update_mouse_cursor_position(&mut self, event: &WindowEvent) { + match event { + WindowEvent::CursorMoved { position, .. } => { + let world_pos_x = position.x / self.size.hidpi_factor * self.size.winit_hidpi_factor; + let world_pos_y = position.y / self.size.hidpi_factor * self.size.winit_hidpi_factor; + self.internal.mouse_state.cursor_pos = Some(LogicalPosition::new(world_pos_x, world_pos_y)); + }, + WindowEvent::CursorLeft { .. } => { + self.internal.mouse_state.cursor_pos = None; + }, + WindowEvent::CursorEntered { .. } => { + self.internal.mouse_state.cursor_pos = Some(LogicalPosition::new(0.0, 0.0)) + }, + _ => { } + } + } + + fn update_scroll_state(&mut self, event: &WindowEvent) { + match event { + WindowEvent::MouseWheel { delta, .. } => { + const LINE_DELTA: f64 = 38.0; + + let (scroll_x_px, scroll_y_px) = match delta { + MouseScrollDelta::PixelDelta(LogicalPosition { x, y }) => (*x, *y), + MouseScrollDelta::LineDelta(x, y) => (*x as f64 * LINE_DELTA, *y as f64 * LINE_DELTA), + }; + self.internal.mouse_state.scroll_x = -scroll_x_px; + self.internal.mouse_state.scroll_y = -scroll_y_px; // TODO: "natural scrolling"? + }, + _ => { }, + } + } + + /// Updates self.keyboard_state to reflect what characters are currently held down + fn update_keyboard_pressed_chars(&mut self, event: &WindowEvent) { + + match event { + WindowEvent::KeyboardInput { + input: KeyboardInput { state: ElementState::Pressed, virtual_keycode, scancode, .. }, .. + } => { + if let Some(vk) = virtual_keycode { + self.internal.keyboard_state.current_virtual_keycodes.insert(*vk); + self.internal.keyboard_state.latest_virtual_keycode = Some(*vk); + } + self.internal.keyboard_state.current_scancodes.insert(*scancode); + }, + // The char event is sliced inbetween a keydown and a keyup event + // so the keyup has to clear the character again + WindowEvent::ReceivedCharacter(c) => { + self.internal.keyboard_state.current_char = Some(*c); + }, + WindowEvent::KeyboardInput { + input: KeyboardInput { state: ElementState::Released, virtual_keycode, scancode, .. }, .. + } => { + if let Some(vk) = virtual_keycode { + self.internal.keyboard_state.current_virtual_keycodes.remove(vk); + self.internal.keyboard_state.latest_virtual_keycode = None; + } + self.internal.keyboard_state.current_scancodes.remove(scancode); + }, + WindowEvent::Focused(false) => { + self.internal.keyboard_state.current_char = None; + self.internal.keyboard_state.current_virtual_keycodes.clear(); + self.internal.keyboard_state.latest_virtual_keycode = None; + self.internal.keyboard_state.current_scancodes.clear(); + }, + _ => { }, + } + } + + fn update_misc_events(&mut self, event: &WindowEvent) { + match event { + WindowEvent::HoveredFile(path) => { + self.internal.hovered_file = Some(path.clone()); + }, + WindowEvent::DroppedFile(path) => { + self.internal.hovered_file = Some(path.clone()); + }, + WindowEvent::HoveredFileCancelled => { + self.internal.hovered_file = None; + }, + _ => { }, + } + } +} + +fn get_window_events(window_state: &mut WindowState, event: &WindowEvent) -> HashSet { + + use glium::glutin::MouseButton::*; + + let mut events_vec = HashSet::::new(); + + match event { + WindowEvent::MouseInput { state: ElementState::Pressed, button, .. } => { + events_vec.insert(WindowEventFilter::MouseDown); + match button { + Left => { + events_vec.insert(WindowEventFilter::LeftMouseDown); + window_state.internal.mouse_state.left_down = true; + }, + Right => { + events_vec.insert(WindowEventFilter::RightMouseDown); + window_state.internal.mouse_state.right_down = true; + }, + Middle => { + events_vec.insert(WindowEventFilter::MiddleMouseDown); + window_state.internal.mouse_state.middle_down = true; + }, + _ => { } + } + }, + WindowEvent::MouseInput { state: ElementState::Released, button, .. } => { + events_vec.insert(WindowEventFilter::MouseUp); + match button { + Left => { + events_vec.insert(WindowEventFilter::LeftMouseUp); + window_state.internal.mouse_state.left_down = false; + }, + Right => { + events_vec.insert(WindowEventFilter::RightMouseUp); + window_state.internal.mouse_state.right_down = false; + }, + Middle => { + events_vec.insert(WindowEventFilter::MiddleMouseUp); + window_state.internal.mouse_state.middle_down = false; + }, + _ => { } + } + }, + WindowEvent::MouseWheel { .. } => { + events_vec.insert(WindowEventFilter::Scroll); + }, + WindowEvent::KeyboardInput { + input: KeyboardInput { state: ElementState::Pressed, virtual_keycode: Some(_), .. }, .. + } => { + events_vec.insert(WindowEventFilter::VirtualKeyDown); + }, + WindowEvent::ReceivedCharacter(c) => { + if !c.is_control() { + events_vec.insert(WindowEventFilter::TextInput); + } + }, + WindowEvent::KeyboardInput { + input: KeyboardInput { state: ElementState::Released, virtual_keycode: Some(_), .. }, .. + } => { + events_vec.insert(WindowEventFilter::VirtualKeyUp); + }, + WindowEvent::HoveredFile(_) => { + events_vec.insert(WindowEventFilter::HoveredFile); + }, + WindowEvent::DroppedFile(_) => { + events_vec.insert(WindowEventFilter::DroppedFile); + }, + WindowEvent::HoveredFileCancelled => { + events_vec.insert(WindowEventFilter::HoveredFileCancelled); + }, + WindowEvent::CursorMoved { .. } => { + events_vec.insert(WindowEventFilter::MouseOver); + }, + WindowEvent::CursorEntered { .. } => { + events_vec.insert(WindowEventFilter::MouseEnter); + }, + WindowEvent::CursorLeft { .. } => { + events_vec.insert(WindowEventFilter::MouseLeave); + }, + _ => { } + } + events_vec +} + +fn get_hover_events(input: &HashSet) -> HashSet { + input.iter().filter_map(|window_event| window_event.to_hover_event_filter()).collect() +} + +fn get_focus_events(input: &HashSet) -> HashSet { + input.iter().filter_map(|hover_event| hover_event.to_focus_event_filter()).collect() +} + +/// Pre-filters any events that are not handled by the framework yet, since it would be wasteful +/// to process them. Modifies the `frame_event_info` so that the +/// +/// `awakened_task` is a special field that should be set to true if the `Task` +/// system fired a `WindowEvent::Awakened`. +pub(crate) fn window_should_close(event: &WindowEvent, frame_event_info: &mut FrameEventInfo) +-> bool +{ + // use glium::glutin::WindowEvent; + + match event { + WindowEvent::CursorMoved { position, .. } => { + frame_event_info.should_hittest = true; + frame_event_info.cur_cursor_pos = *position; + }, + WindowEvent::Resized(wh) => { + frame_event_info.new_window_size = Some(*wh); + frame_event_info.is_resize_event = true; + frame_event_info.should_redraw_window = true; + }, + WindowEvent::HiDpiFactorChanged(dpi) => { + frame_event_info.new_dpi_factor = Some(*dpi); + frame_event_info.should_redraw_window = true; + }, + WindowEvent::CloseRequested | WindowEvent::Destroyed => { + // TODO: Callback the windows onclose method + // (ex. for implementing a "do you really want to close" dialog) + return true; + }, + WindowEvent::KeyboardInput { .. } | + WindowEvent::ReceivedCharacter(_) | + WindowEvent::MouseWheel { .. } | + WindowEvent::MouseInput { .. } | + WindowEvent::Touch(_) => { + frame_event_info.should_hittest = true; + }, + _ => { }, + } + + // TODO: Event::Awakened is never invoked, since that is handled + // by force_redraw_cache anyways + + false +} + +fn update_mouse_cursor(window: &Window, old: &MouseCursor, new: &MouseCursor) { + if *old != *new { + window.set_cursor(*new); + } +} + +/// Utility function for easier creation of a keymap - i.e. `[vec![Ctrl, S], my_function]` +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum AcceleratorKey { + Ctrl, + Alt, + Shift, + Key(VirtualKeyCode), +} + +impl AcceleratorKey { + /// Checks if the current keyboard state contains the given char or modifier, + /// i.e. if the keyboard state currently has the shift key pressed and the + /// accelerator key is `Shift`, evaluates to true, otherwise to false. + pub fn matches(&self, keyboard_state: &KeyboardState) -> bool { + use self::AcceleratorKey::*; + match self { + Ctrl => keyboard_state.ctrl_down, + Alt => keyboard_state.alt_down, + Shift => keyboard_state.shift_down, + Key(k) => keyboard_state.current_virtual_keycodes.contains(k), + } + } +} + +/// Utility function that, given the current keyboard state and a list of +/// keyboard accelerators + callbacks, checks what callback can be invoked +/// and the first matching callback. This leads to very readable +/// (but still type checked) code like this: +/// +/// ```no_run,ignore +/// use azul::prelude::{AcceleratorKey::*, VirtualKeyCode::*}; +/// +/// fn my_callback(app_state: &mut AppState, event: &mut CallbackInfo) -> UpdateScreen { +/// keymap(app_state, event, &[ +/// [vec![Ctrl, S], save_document], +/// [vec![Ctrl, N], create_new_document], +/// [vec![Ctrl, O], open_new_file], +/// [vec![Ctrl, Shift, N], create_new_window], +/// ]) +/// } +/// ``` +pub fn keymap( + app_state: &mut AppState, + event: &mut CallbackInfo, + events: &[(Vec, fn(&mut AppState, &mut CallbackInfo) -> UpdateScreen)] +) -> UpdateScreen { + + let keyboard_state = app_state.windows[event.window_id].get_keyboard_state().clone(); + + events + .iter() + .filter(|(keymap_character, _)| { + keymap_character + .iter() + .all(|keymap_char| keymap_char.matches(&keyboard_state)) + }) + .next() + .and_then(|(_, callback)| (callback)(app_state, event)) +} \ No newline at end of file diff --git a/azul/src/xml.rs b/azul/src/xml.rs new file mode 100644 index 000000000..444626edf --- /dev/null +++ b/azul/src/xml.rs @@ -0,0 +1,1160 @@ +#![allow(unused_variables)] + +use std::{fmt, collections::BTreeMap}; +use { + callbacks::Callback, + dom::Dom, +}; +use xmlparser::Tokenizer; +pub use xmlparser::{Error as XmlError, TokenType, TextPos, StreamError}; + +/// Error that can happen during hot-reload - +/// stringified, since it is only used for printing and is not exposed in the public API +pub type SyntaxError = String; +/// Error that can happen from the translation from XML code to Rust code - +/// stringified, since it is only used for printing and is not exposed in the public API +pub type CompileError = String; + +/// Tag of an XML node, such as the "button" in ``. +pub type XmlTagName = String; +/// Key of an attribute, such as the "color" in ``. +pub type XmlAttributeKey = String; +/// Value of an attribute, such as the "blue" in ``. +pub type XmlAttributeValue = String; +/// (Unparsed) text content of an XML node, such as the "Hello" in ``. +pub type XmlTextContent = Option; +/// Attributes of an XML node, such as `["color" => "blue"]` in ` +/// +/// ``` +/// +/// ... will turn into the following (generated) Rust code: +/// +/// ```rust,no_run,ignore +/// struct TestRendererArgs<'a> { +/// a: &'a String, +/// b: &'a bool, +/// c: &'a HashMap, +/// } +/// +/// fn render_component_test<'a, T>(args: &TestRendererArgs<'a>) -> Dom { +/// Button::with_label(format!("Is this true? Scientists say: {:?}", args.b)).with_class(format!("test_{}", args.a)) +/// } +/// ``` +/// +/// For this to work, a component has to note all its arguments and types that it can take. +/// If a type is not `str` or `String`, it will be formatted using the `{:?}` formatter +/// in the generated source code, otherwise the compiler will use the `{}` formatter. +pub type ComponentArguments = BTreeMap; + +type ComponentName = String; +type CompiledComponent = String; + +/// Specifies a component that reacts to a parsed XML node +pub trait XmlComponent { + + /// Should return all arguments that this component can take - for example if you have a + /// component called `Calendar`, which can take a `selectedDate` argument: + /// + /// ```xml,no_run,ignore + /// + /// ``` + /// ... then the `ComponentArguments` returned by this function should look something like this: + /// + /// ```rust,no_run,ignore + /// impl XmlComponent for CalendarRenderer { + /// fn get_available_arguments(&self) -> ComponentArguments { + /// btreemap![ + /// "selected_date" => "DateTime", + /// "minimum_date" => "DateTime", + /// "maximum_date" => "DateTime", + /// "first_day_of_week" => "WeekDay", + /// "grid_visible" => "bool", + /// /* ... */ + /// ] + /// } + /// } + /// ``` + /// + /// If a user instantiates a component with an invalid argument (i.e. ``), + /// the user will get an error that the component can't handle this argument. The types are not checked, + /// but they are necessary for the XML-to-Rust compiler. + /// + /// When the XML is then compiled to Rust, the generated Rust code will look like this: + /// + /// ```rust,no_run,ignore + /// render_component_calendar(&CalendarRendererArgs { + /// selected_date: DateTime::from("01.01.2018") + /// minimum_date: DateTime::from("01.01.2018") + /// maximum_date: DateTime::from("01.01.2018") + /// first_day_of_week: WeekDay::from("sunday") + /// grid_visible: false, + /// .. Default::default() + /// }) + /// ``` + /// + /// Of course, the code generation isn't perfect: For non-builtin types, the compiler will use + /// `Type::from` to make the conversion. You can then take that generated Rust code and clean it up, + /// put it somewhere else and create another component out of it - XML should only be seen as a + /// high-level prototyping tool (to get around the problem of compile times), not as the final + /// data format. + fn get_available_arguments(&self) -> ComponentArguments; + /// Given a root node and a list of possible arguments, returns a DOM or a syntax error + fn render_dom(&self, components: &XmlComponentMap, arguments: &FilteredComponentArguments, content: &XmlTextContent) -> Result, RenderDomError>; + /// Used to compile the XML component to Rust code - input + fn compile_to_rust_code(&self, components: &XmlComponentMap, attributes: &FilteredComponentArguments, content: &XmlTextContent) -> Result; +} + +/// Component that was created from a XML node (instead of being registered from Rust code). +/// Necessary to +struct DynamicXmlComponent { + /// What the name of this component is, i.e. "test" for `` + name: String, + /// Whether this component has any `args="a: String"` arguments + arguments: Option, + /// Root XML node of this component (the `` Node) + root: XmlNode, +} + +impl DynamicXmlComponent { + /// Parses a `component` from an XML node + pub fn new(root: XmlNode) -> Result { + let name = root.attributes.get("name").cloned().ok_or(ComponentParseError::NotAComponent)?; + let arguments = match root.attributes.get("args") { + Some(s) => Some(parse_component_arguments(s)?), + None => None, + }; + + Ok(Self { + name: normalize_casing(&name), + arguments, + root, + }) + } +} + +impl XmlComponent for DynamicXmlComponent { + + fn get_available_arguments(&self) -> ComponentArguments { + self.arguments.clone().unwrap_or_default() + } + + fn render_dom( + &self, + components: &XmlComponentMap, + arguments: &FilteredComponentArguments, + content: &XmlTextContent, + ) -> Result, RenderDomError> { + + let mut dom = Dom::div(); + for child_node in &self.root.children { + dom.add_child(render_dom_from_app_node_inner(child_node, components, arguments)?); + } + + Ok(dom) + } + + fn compile_to_rust_code( + &self, + components: &XmlComponentMap, + attributes: &FilteredComponentArguments, + content: &XmlTextContent, + ) -> Result { + Ok("Dom::div()".into()) + } +} + +/// Represents one XML node tag +#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct XmlNode { + /// Type of the node + pub node_type: XmlTagName, + /// Attributes of an XML node (note: not yet filtered and / or broken into function arguments!) + pub attributes: XmlAttributeMap, + /// Direct children of this node + pub children: Vec, + /// String content of the node, i.e the "Hello" in `

Hello

` + pub text: XmlTextContent, +} + +impl XmlNode { + + pub fn new>(node_type: S) -> Self { + Self { + node_type: node_type.into(), + .. Default::default() + } + } + + pub fn with_attribute>(mut self, key: S, value: S) -> Self { + self.attributes.insert(key.into(), value.into()); + self + } + + pub fn with_children(mut self, children: Vec) -> Self { + self.children = children; + self + } + + pub fn with_text>(mut self, text: S) -> Self { + self.text = Some(text.into()); + self + } +} + +/// Holds all XML components - builtin components +pub struct XmlComponentMap { + /// Stores all known components that can be used during DOM rendering + /// + whether this component should inherit variables from the parent scope + components: BTreeMap>, bool)>, + /// Stores "onclick='do_this'" mappings from the string `do_this` to the actual function pointer + callbacks: BTreeMap>, +} + +impl Default for XmlComponentMap { + fn default() -> Self { + let mut map = Self { components: BTreeMap::new(), callbacks: BTreeMap::new() }; + map.register_component("div", Box::new(DivRenderer { }), true); + map.register_component("p", Box::new(TextRenderer { }), true); + map + } +} + +impl XmlComponentMap { + pub fn register_component>(&mut self, id: S, component: Box>, inherit_variables: bool) { + self.components.insert(normalize_casing(id.as_ref()), (component, inherit_variables)); + } + pub fn register_callback>(&mut self, id: S, callback: Callback) { + self.callbacks.insert(normalize_casing(id.as_ref()), callback); + } +} + +pub enum XmlParseError { + /// No `` root component present + NoRootComponent, + /// The DOM can only have one root component, not multiple. + MultipleRootComponents, + /// **Note**: Sadly, the error type can only be a string because xmlparser + /// returns all errors as strings. There is an open PR to fix + /// this deficiency, but since the XML parsing is only needed for + /// hot-reloading and compiling, it doesn't matter that much. + ParseError(XmlError), + /// Invalid hierarchy close tags, i.e `

` + MalformedHierarchy(String, String), + /// A component raised an error while rendering the DOM - holds the component name + error string + RenderDom(RenderDomError), + /// Something went wrong while parsing an XML component + Component(ComponentParseError), +} + +#[derive(Clone, PartialOrd, PartialEq, Ord, Eq)] +pub enum RenderDomError { + /// While instantiating a component, a function argument was encountered that the component won't use or react to. + UselessFunctionArgument(String, String, Vec), + /// A certain node type can't be rendered, because the renderer isn't available + UnknownComponent(String), +} + +#[derive(Clone, PartialOrd, PartialEq, Ord, Eq)] +pub enum ComponentParseError { + /// Given XmlNode is not a `` node. + NotAComponent, + /// A `` node does not have a `name` attribute. + UnnamedComponent, + /// Argument at position `usize` is either empty or has no name + MissingName(usize), + /// Argument at position `usize` with the name `String` doesn't have a `: type` + MissingType(usize, String), + /// Component name may not contain a whitespace (probably missing a `:` between the name and the type) + WhiteSpaceInComponentName(usize, String), + /// Component type may not contain a whitespace (probably missing a `,` between the type and the next name) + WhiteSpaceInComponentType(usize, String, String), +} + +impl_from!{ ComponentParseError, XmlParseError::Component } +impl_from!{ RenderDomError, XmlParseError::RenderDom } + +impl fmt::Debug for XmlParseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self) + } +} + +impl fmt::Display for XmlParseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use self::XmlParseError::*; + match self { + NoRootComponent => write!(f, "No component present - empty DOM"), + MultipleRootComponents => write!(f, "Multiple components present, only one root node is allowed"), + ParseError(e) => write!(f, "XML parsing error: {}", e), + MalformedHierarchy(got, expected) => write!(f, "Invalid tag: expected ", got, expected), + RenderDom(e) => write!(f, "Error while rendering DOM: \"{}\"", e), + Component(c) => write!(f, "Error while parsing XML component: \"{}\"", c), + } + } +} + +impl fmt::Debug for ComponentParseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self) + } +} + +impl fmt::Display for ComponentParseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use self::ComponentParseError::*; + match self { + NotAComponent => write!(f, "Expected node, found no such node"), + UnnamedComponent => write!(f, "Found tag with out a \"name\" attribute, component must have a name"), + MissingName(arg_pos) => write!(f, "Argument at position {} is either empty or has no name", arg_pos), + MissingType(arg_pos, arg_name) => write!(f, "Argument \"{}\" at position {} doesn't have a `: type`", arg_pos, arg_name), + WhiteSpaceInComponentName(arg_pos, arg_name_unparsed) => { + write!(f, "Missing `:` between the name and the type in argument {} (around \"{}\")", arg_pos, arg_name_unparsed) + }, + WhiteSpaceInComponentType(arg_pos, arg_name, arg_type_unparsed) => { + write!(f, + "Missing `,` between two arguments (in argument {}, position {}, around \"{}\")", + arg_name, arg_pos, arg_type_unparsed + ) + }, + } + } +} + +impl fmt::Debug for RenderDomError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self) + } +} + +impl fmt::Display for RenderDomError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use self::RenderDomError::*; + match self { + UselessFunctionArgument(k, v, available_args) => { + write!(f, "Useless component argument \"{}\": \"{}\" - available args are: {:#?}", k, v, available_args) + }, + UnknownComponent(name) => write!(f, "Unknown component: \"{}\"", name), + } + } +} + +/// Parses the XML string into an XML tree, returns +/// the root `` node, with the children attached to it. +/// +/// Since the XML allows multiple root nodes, this function returns +/// a `Vec` - which are the "root" nodes, containing all their +/// children recursively. +/// +/// # Example +/// +/// ```rust +/// # use azul::xml::{XmlNode, parse_xml_string}; +/// assert_eq!( +/// parse_xml_string("

").unwrap(), +/// vec![ +/// XmlNode::new("app").with_children(vec![ +/// XmlNode::new("p"), +/// XmlNode::new("div").with_attribute("id", "thing"), +/// ]) +/// ] +/// ) +/// ``` +pub fn parse_xml_string(xml: &str) -> Result, XmlParseError> { + + use xmlparser::Token::*; + use xmlparser::ElementEnd::*; + use self::XmlParseError::*; + + let mut root_node = XmlNode::default(); + + let mut tokenizer = Tokenizer::from(xml); + tokenizer.enable_fragment_mode(); + + // In order to insert where the item is, let's say + // [0 -> 1st element, 5th-element -> node] + // we need to trach the index of the item in the parent. + let mut current_hierarchy: Vec = Vec::new(); + + for token in tokenizer { + + let token = token.map_err(|e| ParseError(e))?; + match token { + ElementStart(_, open_value) => { + if let Some(current_parent) = get_item(¤t_hierarchy, &mut root_node) { + let children_len = current_parent.children.len(); + current_parent.children.push(XmlNode { + node_type: normalize_casing(open_value.to_str()), + attributes: BTreeMap::new(), + children: Vec::new(), + text: None, + }); + current_hierarchy.push(children_len); + } + }, + ElementEnd(Empty) => { + current_hierarchy.pop(); + }, + ElementEnd(Close(_, close_value)) => { + let close_value = normalize_casing(close_value.to_str()); + if let Some(last) = get_item(¤t_hierarchy, &mut root_node) { + if last.node_type != close_value { + return Err(MalformedHierarchy(close_value, last.node_type.clone())); + } + } + current_hierarchy.pop(); + }, + Attribute((_, key), value) => { + if let Some(last) = get_item(¤t_hierarchy, &mut root_node) { + // NOTE: Only lowercase the key, not the value! + last.attributes.insert(normalize_casing(key.to_str()), value.to_str().to_string()); + } + }, + Text(t) => { + if let Some(last) = get_item(¤t_hierarchy, &mut root_node) { + if let Some(s) = last.text.as_mut() { + s.push_str(t.to_str()); + } + if last.text.is_none() { + last.text = Some(t.to_str().into()); + } + } + } + _ => { }, + } + } + + Ok(root_node.children) +} + +/// Given a root node, traverses along the hierarchy, and returns a +/// mutable reference to a child of the node if +fn get_item<'a>(hierarchy: &[usize], root_node: &'a mut XmlNode) -> Option<&'a mut XmlNode> { + let mut current_node = &*root_node; + let mut iter = hierarchy.iter(); + + while let Some(item) = iter.next() { + current_node = current_node.children.get(*item).as_ref()?; + } + + // Safe because we only ever have one mutable reference, but + // the borrow checker doesn't allow recursive mutable borrowing + let node_ptr = current_node as *const XmlNode; + let mut_node_ptr = node_ptr as *mut XmlNode; + Some(unsafe { &mut *mut_node_ptr }) +} + +/// Compiles a XML `args="a: String, b: bool"` into a `["a" => "String", "b" => "bool"]` map +fn parse_component_arguments(input: &str) -> Result { + + use self::ComponentParseError::*; + + let mut args = ComponentArguments::default(); + + for (arg_idx, arg) in input.split(",").enumerate() { + + let mut colon_iterator = arg.split(":"); + + let arg_name = colon_iterator.next().ok_or(MissingName(arg_idx))?; + let arg_name = arg_name.trim(); + if arg_name.is_empty() { + return Err(MissingName(arg_idx)); + } + if arg_name.chars().any(char::is_whitespace) { + return Err(WhiteSpaceInComponentName(arg_idx, arg_name.into())); + } + + let arg_type = colon_iterator.next().ok_or(MissingType(arg_idx, arg_name.into()))?; + let arg_type = arg_type.trim(); + if arg_type.is_empty() { + return Err(MissingType(arg_idx, arg_name.into())); + } + if arg_type.chars().any(char::is_whitespace) { + return Err(WhiteSpaceInComponentType(arg_idx, arg_name.into(), arg_type.into())); + } + + args.insert(normalize_casing(arg_name), arg_type.to_string()); + } + + Ok(args) +} + +pub type FilteredComponentArguments = ComponentArguments; + +/// Filters the XML attributes of a component given XmlAttributeMap +fn validate_and_filter_component_args(xml_attributes: &XmlAttributeMap, valid_args: &FilteredComponentArguments) +-> Result { + + const DEFAULT_ARGS: [&str;5] = ["id", "class", "tabindex", "draggable", "focusable"]; + + let mut map = FilteredComponentArguments::default(); + + for (xml_attribute_name, xml_attribute_value) in xml_attributes.iter() { + + let arg_value = match valid_args.get(xml_attribute_name) { + Some(s) => Some(s), + None => { + if DEFAULT_ARGS.contains(&xml_attribute_name.as_str()) { + None // no error, but don't insert the attribute name + } else { + let keys = valid_args.keys().cloned().collect(); + return Err(RenderDomError::UselessFunctionArgument(xml_attribute_name.clone(), xml_attribute_value.clone(), keys)); + } + } + }; + + if let Some(value) = arg_value { + map.insert(xml_attribute_name.clone(), value.clone()); + } + } + + Ok(map) +} + +/// Normalizes input such as `abcDef`, `AbcDef`, `abc-def` to the normalized form of `abc_def` +fn normalize_casing(input: &str) -> String { + + let mut words: Vec = Vec::new(); + let mut cur_str = Vec::new(); + + for ch in input.chars() { + if ch.is_uppercase() || ch == '_' || ch == '-' { + if !cur_str.is_empty() { + words.push(cur_str.iter().collect()); + cur_str.clear(); + } + if ch.is_uppercase() { + cur_str.extend(ch.to_lowercase()); + } + } else { + cur_str.extend(ch.to_lowercase()); + } + } + + if !cur_str.is_empty() { + words.push(cur_str.iter().collect()); + cur_str.clear(); + } + + words.join("_") +} + +/// Find the one and only node, return error if +/// there is no app node or there are multiple app nodes +fn get_app_node(root_nodes: &[XmlNode]) -> Result { + + let mut app_node_iterator = root_nodes.iter().filter(|node| { + let node_type_normalized = normalize_casing(&node.node_type); + &node_type_normalized == "app" + }).cloned(); + + let app_node = app_node_iterator.next().ok_or(XmlParseError::NoRootComponent)?; + if app_node_iterator.next().is_some() { + Err(XmlParseError::MultipleRootComponents) + } else { + Ok(app_node) + } +} + +/// Filter all `` nodes and insert them into the `components` node +fn get_xml_components(root_nodes: &[XmlNode], components: &mut XmlComponentMap) -> Result<(), ComponentParseError> { + + for node in root_nodes { + match DynamicXmlComponent::new(node.clone()) { + Ok(node) => { components.register_component(node.name.clone(), Box::new(node), false); }, + Err(ComponentParseError::NotAComponent) => { }, // not a node, ignore + Err(e) => return Err(e), // Error during parsing the XML component, bail + } + } + + Ok(()) +} + +/// Parses an XML string and returns a `Dom` with the components instantiated in the `` +pub fn str_to_dom(xml: &str, component_map: &mut XmlComponentMap) -> Result, XmlParseError> { + let root_nodes = parse_xml_string(xml)?; + get_xml_components(&root_nodes, component_map)?; + let app_node = get_app_node(&root_nodes)?; + render_dom_from_app_node(&app_node, component_map).map_err(|e| e.into()) +} + +/// Parses an XML string and returns a `String`, which contains the Rust source code +/// (i.e. it compiles the XML to valid Rust) +pub fn str_to_rust_code( + xml: &str, + imports: &str, + component_map: &mut XmlComponentMap, +) -> Result { + + const HEADER_WARNING: &str = "/// Auto-generated UI source code"; + + let root_nodes = parse_xml_string(xml).map_err(|e| format!("XML parse error: {}", e))?; + get_xml_components(&root_nodes, component_map).map_err(|e| format!("Error parsing component: {}", e))?; + let app_node = get_app_node(&root_nodes).map_err(|e| format!("Could not find node: {}", e))?; + let components_source = compile_components_to_rust_code(&component_map)?; + let app_source = compile_app_node_to_rust_code(&app_node, &component_map)?; + + Ok( + format!("{}\r\n{}\r\n{}\r\n{}", + HEADER_WARNING, + imports, + compile_components(components_source), + app_source, + ) + ) +} + +fn format_component_args(component_args: &FilteredComponentArguments) -> String { + let mut args = Vec::new(); + for (arg_name, arg_type) in component_args { + args.push(format!("{}: {}", arg_name, arg_type)); + } + args.join(" ") +} + +fn compile_components(components: BTreeMap) -> String { + components.iter().map(|(name, (function_body, function_args))| { + compile_component(name, function_args, function_body) + }).collect::>().join("\r\n") +} + +fn compile_component(component_name: &str, component_args: &FilteredComponentArguments, component_function_body: &str) -> String { + format!( + "fn render_component_{}({}) {{\r\n{}\r\n}}", + normalize_casing(component_name), + format_component_args(component_args), + component_function_body, + ) +} + +fn render_dom_from_app_node( + app_node: &XmlNode, + component_map: &XmlComponentMap +) -> Result, RenderDomError> { + + // Don't actually render the node itself + let mut dom = Dom::div(); + for child_node in &app_node.children { + dom.add_child(render_dom_from_app_node_inner(child_node, component_map, &FilteredComponentArguments::default())?); + } + Ok(dom) +} + +/// Takes a single (expanded) app node and renders the DOM or returns an error +fn render_dom_from_app_node_inner( + xml_node: &XmlNode, + component_map: &XmlComponentMap, + parent_xml_attributes: &FilteredComponentArguments, +) -> Result, RenderDomError> { + + let component_name = normalize_casing(&xml_node.node_type); + + let (renderer, inherit_variables) = component_map.components.get(&component_name) + .ok_or(RenderDomError::UnknownComponent(component_name.clone()))?; + + // Arguments of the current node + let available_function_args = renderer.get_available_arguments(); + let mut filtered_xml_attributes = validate_and_filter_component_args(&xml_node.attributes, &available_function_args)?; + + if *inherit_variables { + // Append all variables that are in scope for the parent node + filtered_xml_attributes.extend(parent_xml_attributes.clone().into_iter()); + } + + // Instantiate the parent arguments in the current child arguments + for v in filtered_xml_attributes.values_mut() { + *v = format_args_dynamic(v, &parent_xml_attributes); + } + + let text = xml_node.text.as_ref().map(|t| format_args_dynamic(t, &filtered_xml_attributes)); + + let mut dom = renderer.render_dom(component_map, &filtered_xml_attributes, &text)?; + set_attributes(&mut dom, &xml_node.attributes, &filtered_xml_attributes); + + for child_node in &xml_node.children { + dom.add_child(render_dom_from_app_node_inner(child_node, component_map, &filtered_xml_attributes)?); + } + + Ok(dom) +} + +fn set_attributes(dom: &mut Dom, xml_attributes: &XmlAttributeMap, filtered_xml_attributes: &FilteredComponentArguments) { + + use dom::{TabIndex, DomString}; + + if let Some(ids) = xml_attributes.get("id") { + for id in ids.split_whitespace() { + dom.add_id(DomString::Heap(format_args_dynamic(id, &filtered_xml_attributes))); + } + } + + if let Some(classes) = xml_attributes.get("class") { + for class in classes.split_whitespace() { + dom.add_class(DomString::Heap(format_args_dynamic(class, &filtered_xml_attributes))); + } + } + + if let Some(drag) = xml_attributes.get("draggable") + .map(|d| format_args_dynamic(d, &filtered_xml_attributes)) + .and_then(|d| parse_bool(&d)) + { + dom.set_draggable(drag); + } + + if let Some(focusable) = xml_attributes.get("focusable") + .map(|f| format_args_dynamic(f, &filtered_xml_attributes)) + .and_then(|f| parse_bool(&f)) + { + match focusable { + true => dom.set_tab_index(TabIndex::Auto), + false => dom.set_tab_index(TabIndex::Auto), // TODO + } + } + + if let Some(tab_index) = xml_attributes.get("tabindex") + .map(|val| format_args_dynamic(val, &filtered_xml_attributes)) + .and_then(|val| val.parse::().ok()) + { + match tab_index { + 0 => dom.set_tab_index(TabIndex::Auto), + i if i > 0 => dom.set_tab_index(TabIndex::OverrideInParent(i as usize)), + _ => dom.set_tab_index(TabIndex::NoKeyboardFocus), + } + } +} + +/// Given a string and a key => value mapping, replaces parts of the string with the value, i.e.: +/// +/// ```rust,no_run,ignore +/// let variables = btreemap!{ "a" => "value1", "b" => "value2" }; +/// let initial = "hello {a}, {b}{{ {c} }}"; +/// let expected = "hello value1, value2{ {c} }"; +/// assert_eq!(format_args_dynamic(initial, &variables), expected.to_string()); +/// ``` +pub fn format_args_dynamic(input: &str, variables: &FilteredComponentArguments) -> String { + + let mut opening_braces = Vec::new(); + let mut final_str = String::new(); + let input: Vec = input.chars().collect(); + + for (ch_idx, ch) in input.iter().enumerate() { + match ch { + '{' => { + if input.get(ch_idx + 1) == Some(&'{') { + final_str.push('{'); + } else if ch_idx != 0 && input.get(ch_idx - 1) == Some(&'{') { + // second "{", do nothing + } else { + // idx + 1 is not a "{" + opening_braces.push(ch_idx); + } + }, + '}' => { + if input.get(ch_idx + 1) == Some(&'}') { + final_str.push('}'); + } else if ch_idx != 0 && input.get(ch_idx - 1) == Some(&'}') { + // second "}", do nothing + } else { + // idx + 1 is not a "}" + match opening_braces.pop() { + Some(last_open) => { + let variable_name: String = input[(last_open + 1)..ch_idx].iter().collect(); + let variable_name = normalize_casing(variable_name.trim()); + match variables.get(&variable_name) { + Some(s) => final_str.push_str(s), + None => { + final_str.push('{'); + final_str.push_str(&variable_name); + final_str.push('}'); + }, + } + }, + None => { + final_str.push('}'); + }, + } + } + }, + _ => { + if opening_braces.last().is_none() { + final_str.push(*ch); + } + }, + } + } + + final_str +} + +/// Parses a string ("true" or "false") +fn parse_bool(input: &str) -> Option { + match input { + "true" => Some(true), + "false" => Some(false), + _ => None, + } +} + +/// Takes all components and generates the source code function from them +fn compile_components_to_rust_code(components: &XmlComponentMap) +-> Result, CompileError> +{ + let mut map = BTreeMap::new(); + + for (name, (component, should_inherit_variables)) in components.components.iter() { + let mut rust_source_code = String::from("Dom::div()"); // TODO + // let mut rust_source_code = component.compile_to_rust_code()?; + let args = component.get_available_arguments(); + + // components: &XmlComponentMap, attributes: &FilteredComponentArguments, content: &XmlTextContent + // warning: args = ComponentArguments, not yet filtered + // fn get_available_arguments(&self) -> ComponentArguments; + + // let dom = component.render_dom(&components, &xml_node.args, &xml_node.text_content); + // render_single_dom_node_to_string(&dom, &mut rust_string); + + map.insert(name.clone(), (rust_source_code, args)); + } + + Ok(map) +} + +fn compile_app_node_to_rust_code(app_node: &XmlNode, component_map: &XmlComponentMap) -> Result { + compile_app_node_to_rust_code_inner(app_node, component_map) +} + +fn compile_app_node_to_rust_code_inner(app_node: &XmlNode, component_map: &XmlComponentMap) -> Result { + // TODO! + Err("unimplemented".into()) +} + +/// Takes a DOM node and appends the necessary `.with_id().with_class()`, etc. to the DOMs HEAD +fn render_single_dom_node_to_string(dom: &Dom, existing_str: &mut String) { + + let head = dom.get_head_node(); + + for id in &head.ids { + existing_str.push_str(&format!(".with_id({})", id)); + } + + for class in &head.classes { + existing_str.push_str(&format!(".with_class({})", class)); + } + + if let Some(tab_index) = &head.tab_index { + use dom::TabIndex::*; + existing_str.push_str(&format!(".with_tab_index({})", match tab_index { + Auto => format!("TabIndex::Auto"), + OverrideInParent(u) => format!("TabIndex::OverrideInParent({})", u), + NoKeyboardFocus => format!("TabIndex::NoKeyboardFocus"), + })); + } + + if head.is_draggable { + *existing_str += ".is_draggable(true)"; + } +} + +#[test] +fn test_compile_dom_1() { + + struct Dummy; + + // Test the output of a certain component + fn test_component_source_code(input: &str, component_name: &str, expected: &str) { + let mut component_map = XmlComponentMap::::default(); + let root_nodes = parse_xml_string(input).unwrap(); + get_xml_components(&root_nodes, &mut component_map).unwrap(); + let app_node = get_app_node(&root_nodes).unwrap(); + let components = compile_components_to_rust_code(&component_map).unwrap(); + let (searched_component_source, searched_component_args) = components.get(component_name).unwrap(); + let component_string = compile_component(component_name, searched_component_args, searched_component_source); + + // TODO! + // assert_eq!(component_string, expected); + } + + fn test_app_source_code(input: &str, expected: &str) { + let mut component_map = XmlComponentMap::::default(); + let root_nodes = parse_xml_string(input).unwrap(); + get_xml_components(&root_nodes, &mut component_map).unwrap(); + let app_node = get_app_node(&root_nodes).unwrap(); + let app_source = compile_app_node_to_rust_code(&app_node, &component_map).unwrap(); + + // TODO! + // assert_eq!(app_source, expected); + } + + let s1 = r#" + +
+
+ + + + + "#; + let s1_expected = r#" + fn render_component_test() -> Dom { + Dom::div().with_id("a").with_class("b").is_draggable(true) + } + "#; + + test_component_source_code(&s1, "test", &s1_expected); +} + +// --- Renderers for various built-in types + +/// Render for a `div` component +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DivRenderer { } + +impl XmlComponent for DivRenderer { + + fn get_available_arguments(&self) -> ComponentArguments { + ComponentArguments::new() + } + + fn render_dom(&self, _: &XmlComponentMap, _: &FilteredComponentArguments, _: &XmlTextContent) -> Result, RenderDomError> { + Ok(Dom::div()) + } + + fn compile_to_rust_code(&self, _: &XmlComponentMap, _: &FilteredComponentArguments, _: &XmlTextContent) -> Result { + Ok("Dom::div()".into()) + } +} + +/// Render for a `p` component +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct TextRenderer { } + +impl XmlComponent for TextRenderer { + + fn get_available_arguments(&self) -> ComponentArguments { + ComponentArguments::new() + } + + fn render_dom(&self, _: &XmlComponentMap, _: &FilteredComponentArguments, content: &XmlTextContent) -> Result, RenderDomError> { + let content = content.as_ref().map(|s| prepare_string(&s)).unwrap_or_default(); + Ok(Dom::label(content)) + } + + fn compile_to_rust_code(&self, _: &XmlComponentMap, args: &FilteredComponentArguments, content: &XmlTextContent) -> Result { + Ok(match content { + Some(c) => format!("Dom::label(format!(\"{}\", {}))", c, args.keys().map(|s| s.as_str()).collect::>().join(", ")), + None => format!("Dom::label(\"\")"), + }) + } +} + +// NOTE: Two sequential returns count as a single return, while single returns get ignored. +fn prepare_string(input: &str) -> String { + + const SPACE: &str = " "; + const RETURN: &str = "\n"; + + let input = input.trim(); + + if input.is_empty() { + return String::new(); + } + + let input = input.replace("<", "<"); + let input = input.replace(">", ">"); + + let input_len = input.len(); + let mut final_lines: Vec = Vec::new(); + let mut last_line_was_empty = false; + + for line in input.lines() { + + let line = line.trim(); + let current_line_is_empty = line.is_empty(); + + if !current_line_is_empty { + if last_line_was_empty { + final_lines.push(format!("{}{}", RETURN, line)); + } else { + final_lines.push(line.to_string()); + } + } + + last_line_was_empty = current_line_is_empty; + } + + let line_len = final_lines.len(); + let mut target = String::with_capacity(input_len); + for (line_idx, line) in final_lines.iter().enumerate() { + if !(line.starts_with(RETURN) || line_idx == 0 || line_idx == line_len.saturating_sub(1)) { + target.push_str(SPACE); + } + target.push_str(line); + } + target +} + +#[test] +fn test_format_args_dynamic() { + let mut variables = FilteredComponentArguments::new(); + variables.insert("a".to_string(), "value1".to_string()); + variables.insert("b".to_string(), "value2".to_string()); + assert_eq!( + format_args_dynamic("hello {a}, {b}{{ {c} }}", &variables), + String::from("hello value1, value2{ {c} }"), + ); + assert_eq!( + format_args_dynamic("hello {{a}, {b}{{ {c} }}", &variables), + String::from("hello {a}, value2{ {c} }"), + ); + assert_eq!( + format_args_dynamic("hello {{{{{{{ a }}, {b}{{ {c} }}", &variables), + String::from("hello {{{{{{ a }, value2{ {c} }"), + ); +} + +#[test] +fn test_normalize_casing() { + assert_eq!(normalize_casing("abcDef"), String::from("abc_def")); + assert_eq!(normalize_casing("abc_Def"), String::from("abc_def")); + assert_eq!(normalize_casing("abc-Def"), String::from("abc_def")); + assert_eq!(normalize_casing("abc-def"), String::from("abc_def")); + assert_eq!(normalize_casing("AbcDef"), String::from("abc_def")); + assert_eq!(normalize_casing("Abc-Def"), String::from("abc_def")); + assert_eq!(normalize_casing("Abc_Def"), String::from("abc_def")); + assert_eq!(normalize_casing("aBc_Def"), String::from("a_bc_def")); // wrong, but whatever + assert_eq!(normalize_casing("StartScreen"), String::from("start_screen")); +} + +#[test] +fn test_parse_component_arguments() { + + let mut args_1_expected = ComponentArguments::new(); + args_1_expected.insert("selected_date".to_string(), "DateTime".to_string()); + args_1_expected.insert("minimum_date".to_string(), "DateTime".to_string()); + args_1_expected.insert("grid_visible".to_string(), "bool".to_string()); + + // Everything OK + assert_eq!( + parse_component_arguments("gridVisible: bool, selectedDate: DateTime, minimumDate: DateTime"), + Ok(args_1_expected) + ); + + // Missing type for selectedDate + assert_eq!( + parse_component_arguments("gridVisible: bool, selectedDate: , minimumDate: DateTime"), + Err(ComponentParseError::MissingType(1, "selectedDate".to_string())) + ); + + // Missing name for first argument + assert_eq!( + parse_component_arguments(": bool, selectedDate: DateTime, minimumDate: DateTime"), + Err(ComponentParseError::MissingName(0)) + ); + + // Missing comma after DateTime + assert_eq!( + parse_component_arguments("gridVisible: bool, selectedDate: DateTime minimumDate: DateTime"), + Err(ComponentParseError::WhiteSpaceInComponentType(1, "selectedDate".to_string(), "DateTime minimumDate".to_string())) + ); + + // Missing colon after gridVisible + assert_eq!( + parse_component_arguments("gridVisible: bool, selectedDate DateTime, minimumDate: DateTime"), + Err(ComponentParseError::WhiteSpaceInComponentName(1, "selectedDate DateTime".to_string())) + ); +} + +#[test] +fn test_xml_get_item() { + + // + // + // + // + // + // + // + // + // + // + // + // + // + + let mut tree = XmlNode::new("component") + .with_children(vec![ + XmlNode::new("a") + .with_children(vec![ + XmlNode::new("b"), + XmlNode::new("c"), + XmlNode::new("d"), + XmlNode::new("e"), + ]), + XmlNode::new("f") + .with_children(vec![ + XmlNode::new("g") + .with_children(vec![XmlNode::new("h")]), + XmlNode::new("i"), + ]), + XmlNode::new("j"), + ]); + + assert_eq!(&get_item(&[], &mut tree).unwrap().node_type, "component"); + assert_eq!(&get_item(&[0], &mut tree).unwrap().node_type, "a"); + assert_eq!(&get_item(&[0, 0], &mut tree).unwrap().node_type, "b"); + assert_eq!(&get_item(&[0, 1], &mut tree).unwrap().node_type, "c"); + assert_eq!(&get_item(&[0, 2], &mut tree).unwrap().node_type, "d"); + assert_eq!(&get_item(&[0, 3], &mut tree).unwrap().node_type, "e"); + assert_eq!(&get_item(&[1], &mut tree).unwrap().node_type, "f"); + assert_eq!(&get_item(&[1, 0], &mut tree).unwrap().node_type, "g"); + assert_eq!(&get_item(&[1, 0, 0], &mut tree).unwrap().node_type, "h"); + assert_eq!(&get_item(&[1, 1], &mut tree).unwrap().node_type, "i"); + assert_eq!(&get_item(&[2], &mut tree).unwrap().node_type, "j"); + + assert_eq!(get_item(&[123213], &mut tree), None); + assert_eq!(get_item(&[0, 1, 2], &mut tree), None); +} + +#[test] +fn test_prepare_string_1() { + let input1 = r#"Test"#; + let output = prepare_string(input1); + assert_eq!(output, String::from("Test")); +} + +#[test] +fn test_prepare_string_2() { + let input1 = r#" + Hello, + 123 + + + Test Test2 + + Test3 + + + + + Test4 + "#; + + let output = prepare_string(input1); + assert_eq!(output, String::from("Hello, 123\nTest Test2\nTest3\nTest4")); +} \ No newline at end of file diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 000000000..e7bb8d289 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,26 @@ +codecov: + notify: + require_ci_to_pass: yes + +coverage: + precision: 2 + round: down + range: "70...100" + + status: + project: yes + patch: yes + changes: no + +parsers: + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no + +comment: + layout: "header, diff" + behavior: default + require_changes: no \ No newline at end of file diff --git a/doc/azul_calculator.PNG b/doc/azul_calculator.PNG new file mode 100644 index 000000000..ebefa4254 Binary files /dev/null and b/doc/azul_calculator.PNG differ diff --git a/doc/azul_default_callbacks_1.png b/doc/azul_default_callbacks_1.png new file mode 100644 index 000000000..469e52774 Binary files /dev/null and b/doc/azul_default_callbacks_1.png differ diff --git a/doc/azul_default_callbacks_2.png b/doc/azul_default_callbacks_2.png new file mode 100644 index 000000000..b5f389187 Binary files /dev/null and b/doc/azul_default_callbacks_2.png differ diff --git a/doc/azul_default_callbacks_3.png b/doc/azul_default_callbacks_3.png new file mode 100644 index 000000000..d41686ce1 Binary files /dev/null and b/doc/azul_default_callbacks_3.png differ diff --git a/doc/azul_event_model.png b/doc/azul_event_model.png new file mode 100644 index 000000000..9d82388f4 Binary files /dev/null and b/doc/azul_event_model.png differ diff --git a/doc/azul_hello_world_button.png b/doc/azul_hello_world_button.png new file mode 100644 index 000000000..0f7da77ea Binary files /dev/null and b/doc/azul_hello_world_button.png differ diff --git a/doc/azul_memory_model.svg b/doc/azul_memory_model.svg new file mode 100644 index 000000000..a0b593629 --- /dev/null +++ b/doc/azul_memory_model.svg @@ -0,0 +1,1257 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + stack memory lifetime: controlled by azul,as long as app.run() is running heap memory lifetime: ??? - callbacks can free memory - not controlled by framework + + struct DataModel { } + + text: TextInputState + table: TableViewState + checkbox: CheckBoxState + custom: MyCustomBoxState + + columns: [ 0: TableColumn, 1: TableColumn, 2: TableColumn,] struct Worksheet { } + scrolled: [50 px, 100px] + + + + + stack + struct DataModel { } + + text: TextInputState + table: TableViewState + checkbox: CheckBoxState + custom: MyCustomBoxState + + struct AzulCallbackList { } + [ &TextInputState as *const (), my_callback_fn] window.push_default_callback() memory ownedby azul + + + + + + + + struct AzulCallbackList { } + [ &TableViewState as *const (), my_callback_fn] memory ownedby azul + + struct DataModel { } + + text: TextInputState + table: TableViewState + checkbox: CheckBoxState + custom: MyCustomBoxState + + columns: [ 0: TableColumn, 1: TableColumn, 2: TableColumn,] struct Worksheet { } + scrolled: [50 px, 100px] + + stack heap Mutex mu { } + + // Unique ownership of pointermu.lock()(my_callback_fn)(*const () as &mut TableViewState)mu.unlock() DOM item was hit + + callback can safely modify stack data(and access heap data through using that)because azul knows that the pointer is valid until the app.run() method has completed + + + diff --git a/doc/azul_svg_tiger.png b/doc/azul_svg_tiger.png new file mode 100644 index 000000000..d2bb09bf5 Binary files /dev/null and b/doc/azul_svg_tiger.png differ diff --git a/doc/azul_table.PNG b/doc/azul_table.PNG new file mode 100644 index 000000000..e23870534 Binary files /dev/null and b/doc/azul_table.PNG differ diff --git a/doc/azul_tutorial_empty_window.png b/doc/azul_tutorial_empty_window.png new file mode 100644 index 000000000..1148dbf89 Binary files /dev/null and b/doc/azul_tutorial_empty_window.png differ diff --git a/doc/pic_azul_wip.jpg b/doc/pic_azul_wip.jpg new file mode 100644 index 000000000..a189077e7 Binary files /dev/null and b/doc/pic_azul_wip.jpg differ diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 000000000..144e5f206 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,82 @@ +# Examples + +## `async` + +- Shows the use of `async::Task` to create a background thread to run a long-running task. + In this case the background thread just sleeps / blocks for 10 seconds. +- While the background thread is running, the main thread has a timer that updates the window, + which could be used for showing +- Note that the window can be resized while the background thread is running +- After the background thread is finished, the UI is updated again to show that the + thread has finished running. The thread is joined without blocking the UI. + +## `calculator` + +- Shows how function composition can be used to reorganize code for the calculator rows, + by compositing `numpad_btn` (to render a single button of a calculator) and `render_row` + (to render a row of buttons) +- Also shows how to handle window-global events (to listen for key input without + requiring the user to hover or focus over any element). + +## `game_of_life` + +- Shows how to use timers in order to update the game board every 200ms. +- Performance demo, performs the layout for 5600 rectangles (TODO: Should be replaced by an image). + +## `hello_world` + +- Hello World "counter" example, used for checking that the README and the wiki aren't out-of-date. + +## `hot_reload` + +- Shows a window where the CSS can be hot-reloaded (you can run the demo, then edit the ) +- In release mode, the CSS isn't hot-reloadable, but only parsed once at startup. + +## `list` + +- Shows how to use iterators to build a DOM by using an iterator over an array of strings + +## `opengl` + +- Shows how to render an OpenGL texture as an image via a `GlTextureCallback` + +## `slider` + +- Shows how to use CSS variables that can be changed at runtime by user input +- Shows how to center an absolute-positioned element + +## `svg` + +- Shows how to spawn a file dialog (to ask for an input SVG file) and load an SVG file + by using the SVG renderer (which, internally is based on drawing to an OpenGL texture, + so it's similar to the `opengl` demo). +- Requires the features `svg` and `svg_parsing`. + +## `table` + +- Shows the use of iframes to render infinite data structures +- Note that cells that are not visible are not rendered in the DOM +- The table is scrollable, the `IFrameCallback` is called again after a certain scroll threshold +- Performance demo, performs the layout for about 6000 rectangles + +## `text_editor` + +- TODO: Should show a text editor + +## `text_input` + +- Shows a simple `TextInput` widget that demonstrates two-way data binding + +## `text_shaping` + +- Shows the use of harfbuzz to show kerning, +- Has an XML file attached that can be edited at runtime. Good for testing fonts and + +## `transparent_window` + +- TODO: Should show a window without standard window decorations with a half-transparent background + +## `xml` + +- Shows the XML hot-reload system and the XML-to-Rust compiler +- XML can be live edited and gets compiled to Rust code in release mode \ No newline at end of file diff --git a/examples/async/async.rs b/examples/async/async.rs new file mode 100644 index 000000000..ba5bae0b7 --- /dev/null +++ b/examples/async/async.rs @@ -0,0 +1,96 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +extern crate azul; + +use azul::{ + prelude::*, + widgets::{button::Button, label::Label}, +}; +use std::{ + thread, + time::{Duration, Instant}, + sync::{Arc, Mutex}, +}; + +#[derive(Debug, PartialEq)] +enum ConnectionStatus { + NotConnected, + Connected, + InProgress(Instant, Duration), +} + +struct MyDataModel { + connection_status: ConnectionStatus, +} + +impl Layout for MyDataModel { + + fn layout(&self, _info: LayoutInfo) -> Dom { + + use self::ConnectionStatus::*; + + println!("layout!"); + + let status = match &self.connection_status { + ConnectionStatus::NotConnected => format!("Not connected!"), + ConnectionStatus::Connected => format!("You are connected!"), + ConnectionStatus::InProgress(_, d) => format!("Loading... {}.{:02}s", d.as_secs(), d.subsec_millis()), + }; + + let mut dom = Dom::div() + .with_child(Label::new(status.clone()).dom()); + + match &self.connection_status { + NotConnected => { + dom.add_child( + Button::with_label("Connect to database...").dom() + .with_callback(On::MouseUp, Callback(start_connection)) + ); + }, + Connected => { + dom.add_child( + Button::with_label(format!("{}\nRetry?", status)).dom() + .with_callback(On::MouseUp, Callback(reset_connection)) + ); + } + InProgress(_, _) => { }, + } + + dom + } +} + +fn reset_connection(app_state: &mut AppState, _event: &mut CallbackInfo) -> UpdateScreen { + app_state.data.modify(|state| state.connection_status = ConnectionStatus::NotConnected)?; + Redraw +} + +fn start_connection(app_state: &mut AppState, _event: &mut CallbackInfo) -> UpdateScreen { + let status = ConnectionStatus::InProgress(Instant::now(), Duration::from_secs(0)); + app_state.data.modify(|state| state.connection_status = status)?; + let task = Task::new(&app_state.data, connect_to_db_async); + app_state.add_task(task); + app_state.add_timer(TimerId::new(), Timer::new(timer_timer)); + Redraw +} + +fn timer_timer(state: &mut MyDataModel, _resources: &mut AppResources) -> (UpdateScreen, TerminateTimer) { + if let ConnectionStatus::InProgress(start, duration) = &mut state.connection_status { + *duration = Instant::now() - *start; + (Redraw, TerminateTimer::Continue) + } else { + (DontRedraw, TerminateTimer::Terminate) + } +} + +fn connect_to_db_async(app_data: Arc>, _: DropCheck) { + thread::sleep(Duration::from_secs(10)); // simulate slow load + app_data.modify(|state| state.connection_status = ConnectionStatus::Connected); +} + +fn main() { + let model = MyDataModel { connection_status: ConnectionStatus::NotConnected }; + let mut app = App::new(model, AppConfig::default()).unwrap(); + let window = app.create_window(WindowCreateOptions::default(), css::native()).unwrap(); + app.run(window).unwrap(); +} diff --git a/examples/calculator/calculator.css b/examples/calculator/calculator.css new file mode 100644 index 000000000..5c1f715da --- /dev/null +++ b/examples/calculator/calculator.css @@ -0,0 +1,55 @@ +#expression { + max-height: 50pt; + background-color: #444; + color: white; + flex-direction: row; + text-align: right; + padding-right: 40pt; + justify-content: flex-end; +} + +#result { + max-height: 81pt; + background: linear-gradient(to top, #111, #444); + color: white; + flex-direction: row; + text-align: right; + padding-right: 16pt; + justify-content: flex-end; + font-size: 60px; +} + +#numpad-container { + background-color: #d6d6d6; +} + +.numpad-button { + border-right: 1px solid #8d8d8d; +} + +.row { + flex-direction: row; + border-bottom: 1px solid #8d8d8d; + height: 78px; +} + +.orange { + background: linear-gradient(to bottom, #f69135, #f37335); + color: white; + border-bottom: 1px solid #8d8d8d; + width: 98px; +} + +.orange:focus { + border: 3px solid blue; +} + +#zero { + flex-grow: 2; + border-bottom: none; +} + +* { + font-size: 27px; + font-family: "KoHo-Light"; +} \ No newline at end of file diff --git a/examples/calculator/calculator.rs b/examples/calculator/calculator.rs new file mode 100644 index 000000000..016b765b0 --- /dev/null +++ b/examples/calculator/calculator.rs @@ -0,0 +1,411 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +extern crate azul; + +use azul::prelude::*; + +macro_rules! CSS_PATH {() => { concat!(env!("CARGO_MANIFEST_DIR"), "/../examples/calculator/calculator.css")};} +macro_rules! FONT_PATH {() => { concat!(env!("CARGO_MANIFEST_DIR"), "/../assets/fonts/KoHo-Light.ttf")};} + +const FONT: &[u8] = include_bytes!(FONT_PATH!()); + +#[derive(Clone, Debug)] +enum Event { + Clear, + InvertSign, + Percent, + Divide, + Multiply, + Subtract, + Plus, + EqualSign, + Dot, + Number(u8), +} + +#[derive(Default)] +struct Calculator { + current_operator: Option, + current_operand_stack: OperandStack, + division_by_zero: bool, + expression: String, + last_event: Option, +} + +impl Layout for Calculator { + fn layout(&self, _info: LayoutInfo) -> Dom { + fn numpad_btn(label: &'static str, class: &'static str) -> Dom { + Dom::label(label) + .with_class(class) + .with_tab_index(TabIndex::Auto) + .with_callback(On::MouseUp, Callback(handle_mouseclick_numpad_btn)) + } + + fn render_row(labels: &[&'static str; 4]) -> Dom { + Dom::div() + .with_class("row") + .with_child(numpad_btn(labels[0], "numpad-button")) + .with_child(numpad_btn(labels[1], "numpad-button")) + .with_child(numpad_btn(labels[2], "numpad-button")) + .with_child(numpad_btn(labels[3], "orange")) + } + + let result = if self.division_by_zero { + String::from("Cannot divide by zero.") + } else { + self.current_operand_stack.get_display() + }; + + Dom::div() + .with_child(Dom::label(self.expression.to_string()).with_id("expression")) + .with_child(Dom::label(result).with_id("result")) + .with_child( + Dom::div() + .with_id("numpad-container") + .with_child(render_row(&["C", "+/-", "%", "/"])) + .with_child(render_row(&["7", "8", "9", "x"])) + .with_child(render_row(&["4", "5", "6", "-"])) + .with_child(render_row(&["1", "2", "3", "+"])) + .with_child( + Dom::div() + .with_class("row") + .with_child(numpad_btn("0", "numpad-button").with_id("zero")) + .with_child(numpad_btn(".", "numpad-button")) + .with_child(numpad_btn("=", "orange")), + ), + ) + .with_callback(EventFilter::Window(WindowEventFilter::TextInput), Callback(handle_text_input)) + .with_callback(EventFilter::Window(WindowEventFilter::VirtualKeyDown), Callback(handle_virtual_key_input)) + } +} + +#[derive(Debug, Clone, Default)] +struct OperandStack { + stack: Vec, + negative_number: bool, +} + +impl From for OperandStack { + fn from(value: f32) -> OperandStack { + let mut result = OperandStack::default(); + for c in value.to_string().chars() { + if c == '-' { + result.negative_number = true; + } else if c == '.' { + result.stack.push(Number::Dot); + } else { + result.stack.push(Number::Value((c as u8 - 48) as u8)) + } + } + result + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum Number { + Value(u8), + Dot, +} + +impl OperandStack { + /// Returns the displayable string, i.e for: + /// `[3, 4, Dot, 5]` => `"34.5"` + pub fn get_display(&self) -> String { + let mut display_string = String::new(); + + if self.negative_number { + display_string.push('-'); + } + + if self.stack.is_empty() { + display_string.push('0'); + } else { + // If we get a dot at the end of the stack, i.e. "35." - store it, + // but don't display it + let mut first_dot_found = false; + for num in &self.stack { + match num { + Number::Value(v) => display_string.push((v + 48) as char), + Number::Dot => { + if !first_dot_found { + display_string.push('.'); + first_dot_found = true; + } + } + } + } + } + + display_string + } + + /// Returns the number which you can use to calculate things with + pub fn get_number(&self) -> f32 { + let stack_size = self.stack.len(); + if stack_size == 0 { + return 0.0; + } + + // Iterate the stack until the first Dot is found + let first_dot_position = self.stack.iter().position(|x| *x == Number::Dot).and_then(|x| Some(x - 1)).unwrap_or(stack_size - 1) as i32; + + let mut final_number = 0.0; + + for (number_position, number) in self.stack.iter().filter_map(|x| match x { + Number::Dot => None, + Number::Value(v) => Some(v), + }) + .enumerate() + { + // i.e. the 5 in 5432.1 has a distance of 3 to the first dot (meaning 3 zeros) + let diff_to_first_dot = first_dot_position - number_position as i32; + final_number += (*number as f32) * 10.0_f32.powi(diff_to_first_dot); + } + + if self.negative_number { + final_number = -final_number; + } + final_number + } +} + +fn handle_mouseclick_numpad_btn(app_state: &mut AppState, event: &mut CallbackInfo) -> UpdateScreen { + + let mut row_iter = event.parent_nodes(); + row_iter.next()?; + + // Figure out which row and column was clicked... + let clicked_col_idx = event.target_index_in_parent()?; + let clicked_row_idx = row_iter.current_index_in_parent()?; + + // Figure out what button was clicked from the given row and column, filter bad events + let event = match (clicked_row_idx, clicked_col_idx) { + (0, 0) => Event::Clear, + (0, 1) => Event::InvertSign, + (0, 2) => Event::Percent, + (0, 3) => Event::Divide, + + (1, 0) => Event::Number(7), + (1, 1) => Event::Number(8), + (1, 2) => Event::Number(9), + (1, 3) => Event::Multiply, + + (2, 0) => Event::Number(4), + (2, 1) => Event::Number(5), + (2, 2) => Event::Number(6), + (2, 3) => Event::Subtract, + + (3, 0) => Event::Number(1), + (3, 1) => Event::Number(2), + (3, 2) => Event::Number(3), + (3, 3) => Event::Plus, + + (4, 0) => Event::Number(0), + (4, 1) => Event::Dot, + (4, 2) => Event::EqualSign, + + _ => return DontRedraw, // invalid item + }; + + println!("Got event via mouse input: {:?}", event); + process_event(app_state, event) +} + +fn handle_text_input(app_state: &mut AppState, event: &mut CallbackInfo) -> UpdateScreen { + let current_key = app_state.windows[event.window_id].state.get_keyboard_state().current_char?; + let event = match current_key { + '0' => Event::Number(0), + '1' => Event::Number(1), + '2' => Event::Number(2), + '3' => Event::Number(3), + '4' => Event::Number(4), + '5' => Event::Number(5), + '6' => Event::Number(6), + '7' => Event::Number(7), + '8' => Event::Number(8), + '9' => Event::Number(9), + '*' => Event::Multiply, + '-' => Event::Subtract, + '+' => Event::Plus, + '/' => Event::Divide, + '%' => Event::Percent, + '.' | ',' => Event::Dot, + _ => return DontRedraw, + }; + + println!("Got event via keyboard input: {:?}", event); + process_event(app_state, event) +} + +fn handle_virtual_key_input(app_state: &mut AppState, event: &mut CallbackInfo) -> UpdateScreen { + let current_key = app_state.windows[event.window_id].state.get_keyboard_state().latest_virtual_keycode?; + let event = match current_key { + VirtualKeyCode::Return => Event::EqualSign, + VirtualKeyCode::Back => Event::Clear, + _ => return DontRedraw, + }; + process_event(app_state, event) +} + +fn process_event(app_state: &mut AppState, event: Event) -> UpdateScreen { + + // Act on the event accordingly + match event { + Event::Clear => { + app_state.data.modify(|state| { + *state = Calculator::default(); + })?; + Redraw + } + Event::InvertSign => { + app_state.data.modify(|state| { + if !state.division_by_zero { + state.current_operand_stack.negative_number = !state.current_operand_stack.negative_number; + } + })?; + + Redraw + } + Event::Percent => { + app_state.data.modify(|state| { + if state.division_by_zero { + return; + } + if let Some(operation) = &state.last_event.clone() { + if let Some(operand) = state.current_operator.clone() { + let num = state.current_operand_stack.get_number(); + let op = operand.get_number(); + let result = match operation { + Event::Plus | Event::Subtract => op / 100.0 * num, + Event::Multiply | Event::Divide => num / 100.0, + _ => unreachable!(), + }; + state.current_operand_stack = OperandStack::from(result); + } + } + })?; + Redraw + } + Event::EqualSign => { + app_state.data.modify(|state| { + if state.division_by_zero { + return; + } + if let Some(Event::EqualSign) = state.last_event { + state.expression = format!("{} =", state.current_operand_stack.get_display()); + } + else { + state.expression.push_str(&format!("{} =", state.current_operand_stack.get_display())); + if let Some(operation) = &state.last_event.clone() { + if let Some(operand) = state.current_operator.clone() { + let num = state.current_operand_stack.get_number(); + let op = operand.get_number(); + match perform_operation(op, &operation, num) { + Some(r) => state.current_operand_stack = OperandStack::from(r), + None => state.division_by_zero = true, + } + } + } + } + state.current_operator = None; + state.last_event = Some(Event::EqualSign); + })?; + Redraw + } + Event::Dot => { + app_state.data.modify(|state| { + if state.division_by_zero { + return; + } + if state.current_operand_stack.stack.iter().position(|x| *x == Number::Dot).is_none() { + if state.current_operand_stack.stack.len() == 0 { + state.current_operand_stack.stack.push(Number::Value(0)); + } + state.current_operand_stack.stack.push(Number::Dot); + } + })?; + Redraw + } + Event::Number(v) => { + app_state.data.modify(|state| { + if let Some(Event::EqualSign) = state.last_event { + *state = Calculator::default(); + } + state.current_operand_stack.stack.push(Number::Value(v)); + }); + Redraw + } + operation => { + app_state.data.modify(|state| { + if state.division_by_zero { + return; + } + if let Some(Event::EqualSign) = state.last_event { + state.expression = String::new(); + } + state.expression.push_str(&state.current_operand_stack.get_display()); + if let Some(Event::EqualSign) = state.last_event { + state.current_operator = Some(state.current_operand_stack.clone()); + } + else if let Some(last_operation) = &state.last_event.clone() { + if let Some(operand) = state.current_operator.clone() { + let num = state.current_operand_stack.get_number(); + let op = operand.get_number(); + match perform_operation(op, last_operation, num) { + Some(r) => state.current_operator = Some(OperandStack::from(r)), + None => state.division_by_zero = true, + } + } + } + else { + state.current_operator = Some(state.current_operand_stack.clone()); + } + state.current_operand_stack = OperandStack::default(); + state.expression.push_str(match operation { + Event::Plus => " + ", + Event::Subtract => " - ", + Event::Multiply => " x ", + Event::Divide => " / ", + _ => unreachable!(), + }); + state.last_event = Some(operation); + })?; + + Redraw + } + } +} + +/// Performs an arithmetic operation. Returns None when trying to divide by zero. +fn perform_operation(left_operand: f32, operation: &Event, right_operand: f32) -> Option { + match operation { + Event::Multiply => Some(left_operand * right_operand), + Event::Subtract => Some(left_operand - right_operand), + Event::Plus => Some(left_operand + right_operand), + Event::Divide => if right_operand == 0.0 { + None + } + else { + Some(left_operand / right_operand) + }, + _ => unreachable!(), + } +} + +fn main() { + + let css = css::override_native(include_str!(CSS_PATH!())).unwrap(); + + let mut app = App::new(Calculator::default(), AppConfig::default()).unwrap(); + let font_id = app.add_css_font_id("KoHo-Light"); + app.add_font(font_id, FontSource::Embedded(FONT)); + + let window = app.create_window(WindowCreateOptions::default(), css.clone()).unwrap(); + let window2 = app.create_window(WindowCreateOptions::default(), css.clone()).unwrap(); + let window3 = app.create_window(WindowCreateOptions::default(), css.clone()).unwrap(); + let window4 = app.create_window(WindowCreateOptions::default(), css.clone()).unwrap(); + app.add_window(window2); + app.add_window(window3); + app.add_window(window4); + app.run(window).unwrap(); +} diff --git a/examples/debug.rs b/examples/debug.rs deleted file mode 100644 index 7d9c3725b..000000000 --- a/examples/debug.rs +++ /dev/null @@ -1,47 +0,0 @@ -extern crate azul; - -use azul::prelude::*; - -const TEST_CSS: &str = include_str!("test_content.css"); - -#[derive(Debug)] -pub struct MyAppData { - // Your app data goes here - pub my_data: u32, -} - -impl LayoutScreen for MyAppData { - - fn get_dom(&self, _window_id: WindowId) -> Dom { - let mut dom = Dom::new(NodeType::Div); - dom.class("__azul-native-button"); - dom.event(On::MouseUp, Callback::Sync(my_button_click_handler)); - - for i in 0..self.my_data { - dom.add_sibling(Dom::new(NodeType::Label { - text: format!("{}", i), - })); - } - - dom - } -} - -fn my_button_click_handler(app_state: &mut AppState) -> UpdateScreen { - app_state.data.my_data += 1; - UpdateScreen::Redraw -} - -fn main() { - let css = Css::new_from_string(TEST_CSS).unwrap(); - - let my_app_data = MyAppData { - my_data: 0, - }; - - let mut app = App::new(my_app_data); - // TODO: Multi-window apps currently crash - // Need to re-factor the event loop for that - app.create_window(WindowCreateOptions::default(), css).unwrap(); - app.start_render_loop(); -} diff --git a/examples/game_of_life/game_of_life.css b/examples/game_of_life/game_of_life.css new file mode 100644 index 000000000..dc2caa9ae --- /dev/null +++ b/examples/game_of_life/game_of_life.css @@ -0,0 +1,62 @@ +#header { + background-color: #3aadff; + box-shadow: -2px 2px 6px 4px #00000080; + height: 65px; + text-align: left; +} + +#title { + background-color: #007ede; + border-radius: 5px; + color: #FFFFFF; + font-size: 25px; + padding: 10px 0px 10px 10px; + margin: 10px 15px; + width: 164px; + height: 40px; +} + +#alive_count { + font-size: 15px; + margin: -47px 0px 0px 200px; +} + +#dead_count { + font-size: 15px; + margin: 8px 0px 0px 200px; +} + +#universe { + margin-top: 5px; +} + +#start_btn { + background-color: #007ede; + border: none; + color: #FFFFFF; + font-size: 25px; + height: 40px; + width: 90px; + position: absolute; + top: 10px; + right: 15px; + text-align: center; +} + +.row { + flex-direction: row; + height: 7.02px; +} + +.alive_cell { + background-color: #000000; + border: 1px solid #444444; + width: 7.02px; + height: 7.02px; +} + +.dead_cell { + border: 1px solid #444444; + width: 7.02px; + height: 7.02px; +} \ No newline at end of file diff --git a/examples/game_of_life/game_of_life.rs b/examples/game_of_life/game_of_life.rs new file mode 100644 index 000000000..85cc99c90 --- /dev/null +++ b/examples/game_of_life/game_of_life.rs @@ -0,0 +1,204 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +extern crate azul; + +use azul::{prelude::*, widgets::button::Button}; +use std::sync::atomic::{AtomicUsize, Ordering}; + +macro_rules! CSS_PATH {() => { concat!(env!("CARGO_MANIFEST_DIR"), "/../examples/game_of_life/game_of_life.css")};} + +const CSS: &str = include_str!(CSS_PATH!()); +const INITIAL_UNIVERSE_WIDTH: usize = 75; +const INITIAL_UNIVERSE_HEIGHT: usize = 75; + +static RAND_SEED: AtomicUsize = AtomicUsize::new(2100); + +/// Simple rand() function (32-bit) +fn rand_xorshift() -> usize { + let mut x = RAND_SEED.fetch_add(21, Ordering::SeqCst); + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + x +} + +#[derive(Debug, Clone, PartialEq)] +enum Cell { + Dead, + Alive, +} + +impl Cell { + pub fn is_alive(&self) -> bool { *self == Cell::Alive } +} + +#[derive(Debug, PartialEq, Clone)] +struct Universe { + board: Board, + game_is_running: bool, +} + +#[derive(Debug, PartialEq, Clone)] +struct Board { + vertical_cells: usize, + horizontal_cells: usize, + cells: Vec>, +} + +impl Layout for Universe { + + fn layout(&self, _info: LayoutInfo) -> Dom { + + let (dead_cells, alive_cells) = count_dead_and_alive_cells(&self.board.cells); + + let header = Dom::div() + .with_id("header") + .with_child(Dom::label("Game of Life").with_id("title")) + .with_child(Dom::label(format!("{} Alive Cells", alive_cells)).with_id("alive_count")) + .with_child(Dom::label(format!("{} Dead Cells", dead_cells)).with_id("dead_count")) + .with_child( + Button::with_label(if !self.game_is_running { "Start" } else { "Restart" }).dom() + .with_id("start_btn") + .with_callback(On::MouseUp, Callback(start_stop_game)) + ); + + Dom::new(NodeType::Div) + .with_child(header) + .with_child(self.board.dom()) + } +} + +/// Returns the number of (dead, alive) cells +fn count_dead_and_alive_cells(cells: &[Vec]) -> (usize, usize) { + let total_cells: usize = cells.iter().map(|row| row.len()).sum(); + let alive_cells = cells.iter().map(|row| row.iter().filter(|c| c.is_alive()).count()).sum(); + let dead_cells = total_cells - alive_cells; + (dead_cells, alive_cells) +} + +impl Board { + + pub fn empty(board_width: usize, board_height: usize) -> Self { + Self { + cells: vec![vec![Cell::Dead; board_width]; board_height], + vertical_cells: board_height, + horizontal_cells: board_width, + } + } + + pub fn new_random(board_width: usize, board_height: usize) -> Self { + + let cells = (0..board_height).map(|_| { + (0..board_width) + // Initial cell has 1 in 4 chance of being alive + .map(|_| rand_xorshift() % 4 == 0) + .map(|alive| if alive { Cell::Alive } else { Cell::Dead }) + .collect() + }).collect(); + + Self { + cells, + vertical_cells: board_height, + horizontal_cells: board_width, + } + } + + /// Render the board in a table-like grid structure + pub fn dom(&self) -> Dom { + self.cells.iter().map(|row| { + row.iter().map(|c| NodeData { + node_type: NodeType::Div, + classes: vec![match c { + Cell::Alive => "alive_cell".into(), + Cell::Dead => "dead_cell".into(), + }], + .. Default::default() + }).collect::>() + .with_class("row") + }).collect() + } +} + +// Update the cell state +fn tick(state: &mut Universe, _: &mut AppResources) -> (UpdateScreen, TerminateTimer) { + state.board = next_iteration(&state.board); + (Redraw, TerminateTimer::Continue) +} + +fn next_iteration(input: &Board) -> Board { + + let mut new_board = input.clone(); + + for (row_idx, row) in new_board.cells.iter_mut().enumerate() { + + let upper_r = if row_idx == 0 { input.vertical_cells - 1 } else { row_idx - 1 }; + let lower_r = if row_idx == input.vertical_cells - 1 { 0 } else { row_idx + 1 }; + + for (cell_idx, cell) in row.iter_mut().enumerate() { + + // Select all neighbours of the current cell (the 8 cells surrounding the current cell) + let left_c = if cell_idx == 0 { input.horizontal_cells - 1 } else { cell_idx - 1 }; + let right_c = if cell_idx == input.horizontal_cells - 1 { 0 } else { cell_idx + 1 }; + + let neighbors = [ + &input.cells[upper_r][left_c], + &input.cells[upper_r][cell_idx], + &input.cells[upper_r][right_c], + &input.cells[row_idx][left_c], + &input.cells[row_idx][right_c], + &input.cells[lower_r][left_c], + &input.cells[lower_r][cell_idx], + &input.cells[lower_r][right_c] + ]; + + let alive_neighbors = neighbors.iter().filter(|c| c.is_alive()).count(); + let is_cell_alive = match cell { + Cell::Alive => !(alive_neighbors < 2 || alive_neighbors > 3), + Cell::Dead => alive_neighbors == 3, + }; + + *cell = if is_cell_alive { Cell::Alive } else { Cell::Dead }; + } + } + + new_board +} + +/// Callback that starts the main +fn start_stop_game(app_state: &mut AppState, _: &mut CallbackInfo) -> UpdateScreen { + + use std::time::Duration; + + if let Some(timer) = { + let state = &mut app_state.data.lock().ok()?; + state.board = Board::new_random(INITIAL_UNIVERSE_WIDTH, INITIAL_UNIVERSE_HEIGHT); + + if state.game_is_running { + None + } else { + let timer = Timer::new(tick).with_interval(Duration::from_millis(200)); + + state.game_is_running = true; + Some(timer) + } + }{ + app_state.add_timer(TimerId::new(), timer); + } + + Redraw +} + +fn main() { + + let mut app = App::new(Universe { + board: Board::empty(INITIAL_UNIVERSE_WIDTH, INITIAL_UNIVERSE_HEIGHT), + game_is_running: false, + }, AppConfig::default()).unwrap(); + + let mut window_options = WindowCreateOptions::default(); + window_options.state.title = "Game of Life".into(); + + let css = css::override_native(CSS).unwrap(); + let window = app.create_window(window_options, css).unwrap(); + app.run(window).unwrap(); +} diff --git a/examples/hello_world/hello_world.rs b/examples/hello_world/hello_world.rs new file mode 100644 index 000000000..eb8c81706 --- /dev/null +++ b/examples/hello_world/hello_world.rs @@ -0,0 +1,30 @@ +extern crate azul; + +use azul::{prelude::*, widgets::{label::Label, button::Button}}; + +struct DataModel { + counter: usize, +} + +impl Layout for DataModel { + fn layout(&self, _info: LayoutInfo) -> Dom { + let label = Label::new(format!("{}", self.counter)).dom(); + let button = Button::with_label("Update counter").dom() + .with_callback(On::MouseUp, Callback(update_counter)); + + Dom::div() + .with_child(label) + .with_child(button) + } +} + +fn update_counter(app_state: &mut AppState, _: &mut CallbackInfo) -> UpdateScreen { + app_state.data.modify(|state| state.counter += 1)?; + Redraw +} + +fn main() { + let mut app = App::new(DataModel { counter: 0 }, AppConfig::default()).unwrap(); + let window = app.create_window(WindowCreateOptions::default(), css::native()).unwrap(); + app.run(window).unwrap(); +} \ No newline at end of file diff --git a/examples/hot_reload/hot_reload.css b/examples/hot_reload/hot_reload.css new file mode 100644 index 000000000..eea62dcf2 --- /dev/null +++ b/examples/hot_reload/hot_reload.css @@ -0,0 +1,76 @@ +#wrapper { + background: linear-gradient(15deg, red 0%, blue 100%); + flex-direction: row; + justify-content: flex-start; +} + +#red { + background-color: #BF0C2B; + color: white; + width: 400px; + font-size: 39px; + font-family: Arial; + text-align: right; + flex-direction: column; + align-items: flex-end; +} + +#sub-wrapper { + flex-direction: column; + width: 200px; + box-shadow: 0px 0px 50px black; + position: relative; + top: 40px; + left: 20px; +} + +#yellow:hover { + background-color: yellow; +} + +#yellow:active { + background-color: red; +} + +#yellow { + background-color: #F5900E; + height: 70px; + flex-direction: column-reverse; + align-items: flex-end; + box-shadow: 0px 0px 50px black; + position: absolute; + right: -25px; + top: 0px; + width: 50px; + height: 50px; +} + +#grey { + background-color: #44ccee33; +} + +#orange-1 { + background-color: #F14C13; + box-shadow: 2px 2px 10px black; +} + +#orange-2 { + background-color: green; + box-shadow: 2px 2px 10px black; + width: 10px; + height: 10px; +} + +#cat { + max-width: 400px; + box-shadow-bottom: 0px 0px 50px black inset; + box-shadow-top: 0px 0px 50px black inset; +} + +#rows { + background-color: green; +} + +#rows:nth-child(even) { + background-color: red; +} \ No newline at end of file diff --git a/examples/hot_reload/hot_reload.rs b/examples/hot_reload/hot_reload.rs new file mode 100644 index 000000000..1bf272f89 --- /dev/null +++ b/examples/hot_reload/hot_reload.rs @@ -0,0 +1,50 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +extern crate azul; + +use azul::prelude::*; +#[cfg(debug_assertions)] +use std::time::Duration; + +const TEST_IMAGE: &[u8] = include_bytes!("../assets/images/cat_image.jpg"); + +struct MyDataModel; + +impl Layout for MyDataModel { + fn layout(&self, info: LayoutInfo) -> Dom { + Dom::div().with_id("wrapper") + .with_child(Dom::label("Hello123").with_id("red")) + .with_child(Dom::div().with_id("sub-wrapper") + .with_child(Dom::div().with_id("yellow") + .with_child(Dom::div().with_id("orange-1")) + .with_child(Dom::div().with_id("orange-2")) + ) + .with_child(Dom::div().with_id("grey")) + ) + .with_child(Dom::image(*info.resources.get_css_image_id("Cat01").unwrap()).with_id("cat")) + .with_child((0..50).map(|i| Dom::label(format!("{}", i))).collect::>().with_id("rows")) + } +} + +fn main() { + + macro_rules! CSS_PATH { () => (concat!(env!("CARGO_MANIFEST_DIR"), "/../examples/hot_reload.css")) } + + let mut app = App::new(MyDataModel, AppConfig::default()).unwrap(); + let image_id = app.add_css_image_id("Cat01"); + app.add_image(image_id, ImageSource::Embedded(TEST_IMAGE)); + + #[cfg(debug_assertions)] + let window = { + let hot_reloader = css::hot_reload(CSS_PATH!(), Duration::from_millis(500)); + app.create_hot_reload_window(WindowCreateOptions::default(), hot_reloader).unwrap() + }; + + #[cfg(not(debug_assertions))] + let window = { + let css = css::from_str(include_str!(CSS_PATH!())).unwrap(); + app.create_window(WindowCreateOptions::default(), css).unwrap() + }; + + app.run(window).unwrap(); +} diff --git a/examples/list/list.rs b/examples/list/list.rs new file mode 100644 index 000000000..0820c675d --- /dev/null +++ b/examples/list/list.rs @@ -0,0 +1,62 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +extern crate azul; + +use azul::prelude::*; + +struct List { + items: Vec<&'static str>, + selected: Option, +} + +const CUSTOM_CSS: &str = ".selected { background-color: black; color: white; }"; + +impl Layout for List { + fn layout(&self, _: LayoutInfo) -> Dom { + self.items.iter().enumerate().map(|(idx, item)| { + NodeData { + node_type: NodeType::Label(DomString::Static(item)), + classes: if self.selected == Some(idx) { vec!["selected".into()] } else { vec![] }, + callbacks: vec![(On::MouseDown.into(), Callback(print_which_item_was_selected))], + .. Default::default() + } + }).collect::>() + } +} + +fn print_which_item_was_selected(app_state: &mut AppState, event: &mut CallbackInfo) -> UpdateScreen { + + let selected = event.target_index_in_parent(); + + let mut state = app_state.data.lock().ok()?; + let should_redraw = if selected != state.selected { + state.selected = selected; + Redraw + } else { + DontRedraw + }; + + println!("selected item: {:?}", state.selected); + + should_redraw +} + +fn main() { + let data = List { + items: vec![ + "Hello", + "World", + "my", + "name", + "is", + "Lorem", + "Ipsum", + ], + selected: None, + }; + + let mut app = App::new(data, AppConfig::default()).unwrap(); + let css = css::override_native(CUSTOM_CSS).unwrap(); + let window = app.create_window(WindowCreateOptions::default(), css).unwrap(); + app.run(window).unwrap(); +} diff --git a/examples/opengl/opengl.rs b/examples/opengl/opengl.rs new file mode 100644 index 000000000..81b3ad794 --- /dev/null +++ b/examples/opengl/opengl.rs @@ -0,0 +1,37 @@ +extern crate azul; + +use azul::prelude::*; +use azul::azul_dependencies::glium::Surface; + +struct OpenGlAppState { } + +impl Layout for OpenGlAppState { + fn layout(&self, _info: LayoutInfo) -> Dom { + // println!("Pause"); + // let mut input = String::new(); + // let _ = std::io::stdin().read_line(&mut input); + Dom::gl_texture(GlTextureCallback(render_my_texture), StackCheckedPointer::new(self, self).unwrap()) + } +} + +fn render_my_texture( + _state: &StackCheckedPointer, + info: LayoutInfo, + hi_dpi_bounds: HidpiAdjustedBounds) +-> Option +{ + let physical_size = hi_dpi_bounds.get_physical_size(); + let texture = info.window.read_only_window().create_texture( + physical_size.width as u32, + physical_size.height as u32 + ); + + texture.as_surface().clear_color(0.0, 1.0, 0.0, 1.0); + Some(texture) +} + +fn main() { + let mut app = App::new(OpenGlAppState { }, AppConfig::default()).unwrap(); + let window = app.create_window(WindowCreateOptions::default(), css::native()).unwrap(); + app.run(window).unwrap(); +} \ No newline at end of file diff --git a/examples/slider/slider.css b/examples/slider/slider.css new file mode 100644 index 000000000..01d861645 --- /dev/null +++ b/examples/slider/slider.css @@ -0,0 +1,38 @@ +#container { + flex-direction: row; +} + +#blue { + background-color: #011C41; + width: [[ drag_width | auto ]]; +} + +#orange { + background-color: #DA2A04; +} + +#dragger { + width: 3px; + background-color: blue; + position: relative; + flex-direction: column; + justify-content: center; +} + +#dragger_handle_container, #dragger_handle { + width: 50px; + height: 50px; +} + +#dragger_handle_container { + position: relative; +} + +#dragger_handle { + /* justify-content: center does work if the item is not position:absolute */ + position: absolute; + background-color: #F5A219; + left: -25px; + border-radius: 3px; + box-shadow: 0px 4px 10px black; +} \ No newline at end of file diff --git a/examples/slider/slider.rs b/examples/slider/slider.rs new file mode 100644 index 000000000..51b0e753a --- /dev/null +++ b/examples/slider/slider.rs @@ -0,0 +1,98 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +extern crate azul; + +use azul::prelude::*; +use std::time::Duration; + +#[derive(Default)] +struct DragMeApp { + width: Option, + is_dragging: bool, +} + +type Event<'a> = CallbackInfo<'a, DragMeApp>; +type State = AppState; + +impl Layout for DragMeApp { + fn layout(&self, _: LayoutInfo) -> Dom { + + let mut left = Dom::new(NodeType::Div).with_id("blue"); + + // Set the width of the dragger on the red element + if let Some(w) = self.width { + left.add_css_override("drag_width", CssProperty::Width(LayoutWidth::px(w))); + } + + let right = Dom::new(NodeType::Div).with_id("orange"); + + // The dragger is 0px wide, but has an absolutely positioned rectangle + // inside of it, which can be dragged + let dragger = + Dom::div() + .with_id("dragger") + .with_child( + Dom::div() + .with_id("dragger_handle_container") + .with_child( + Dom::div() + .with_id("dragger_handle") + .with_callback(On::MouseDown, Callback(start_drag)) + .with_callback(EventFilter::Not(NotEventFilter::Hover(HoverEventFilter::MouseDown)), Callback(click_outside_drag)) + ) + ); + + Dom::new(NodeType::Div).with_id("container") + .with_callback(On::MouseOver, Callback(update_drag)) + .with_callback(On::MouseUp, Callback(stop_drag)) + .with_child(left) + .with_child(dragger) + .with_child(right) + } +} + +fn click_outside_drag(_state: &mut State, _event: &mut Event) -> UpdateScreen { + println!("click outside drag!"); + DontRedraw +} + +fn start_drag(state: &mut State, _event: &mut Event) -> UpdateScreen { + state.data.modify(|data| data.is_dragging = true)?; + DontRedraw +} + +fn stop_drag(state: &mut State, _event: &mut Event) -> UpdateScreen { + state.data.modify(|data| data.is_dragging = false)?; + Redraw +} + +fn update_drag(state: &mut State, event: &mut Event) -> UpdateScreen { + let mouse_state = state.windows.get(event.window_id)?.state.get_mouse_state(); + if state.data.lock().unwrap().is_dragging { + let cursor_pos = mouse_state.cursor_pos.unwrap_or(LogicalPosition::new(0.0, 0.0)); + state.data.modify(|data| data.width = Some(cursor_pos.x as f32)); + Redraw + } else { + DontRedraw + } +} + +fn main() { + macro_rules! CSS_PATH { () => (concat!(env!("CARGO_MANIFEST_DIR"), "/../examples/slider/slider.css")) } + + let mut app = App::new(DragMeApp::default(), AppConfig::default()).unwrap(); + + #[cfg(debug_assertions)] + let window = { + let hot_reloader = css::hot_reload(CSS_PATH!(), Duration::from_millis(500)); + app.create_hot_reload_window(WindowCreateOptions::default(), hot_reloader).unwrap() + }; + + #[cfg(not(debug_assertions))] + let window = { + let css = css::from_str(include_str!(CSS_PATH!())).unwrap(); + app.create_window(WindowCreateOptions::default(), css).unwrap() + }; + + app.run(window).unwrap(); +} diff --git a/examples/svg/svg.css b/examples/svg/svg.css new file mode 100644 index 000000000..9f20779ba --- /dev/null +++ b/examples/svg/svg.css @@ -0,0 +1,21 @@ +#parent-wrapper { + background-color: red; + flex-direction: row; +} + +#child-1 { + background-color: green; + max-width: 200px; +} + +#child-2 { + width: 1000px; + height: 1000px; +} + +* { + font-size: 14.66px; + font-family: sans-serif; + color: #000; + background-color: #f0f0f0; /* Windows Background color, rgb(240, 240, 240) */ +} \ No newline at end of file diff --git a/examples/svg/svg.rs b/examples/svg/svg.rs new file mode 100644 index 000000000..22b1ba252 --- /dev/null +++ b/examples/svg/svg.rs @@ -0,0 +1,250 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +extern crate azul; + +use azul::{ + prelude::*, + widgets::{button::Button, svg::*}, + dialogs::*, +}; + +use std::{ + fs, + collections::HashMap, + sync::atomic::{AtomicUsize, Ordering}, +}; + +static TEXT_ID: AtomicUsize = AtomicUsize::new(0); + +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct TextId(usize); + +fn new_text_id() -> TextId { + TextId(TEXT_ID.fetch_add(1, Ordering::SeqCst)) +} + +#[derive(Debug)] +pub struct MyAppData { + pub map: Option, +} + +#[derive(Debug)] +pub struct Map { + pub cache: SvgCache, + pub layers: Vec<(SvgLayerId, SvgStyle)>, + pub font_cache: VectorizedFontCache, + pub texts: HashMap, + pub hovered_text: Option, + pub zoom: f64, + pub pan_horz: f64, + pub pan_vert: f64, +} + +impl Layout for MyAppData { + fn layout(&self, _info: LayoutInfo) + -> Dom + { + if let Some(map) = &self.map { + Dom::new(NodeType::Div).with_id("parent-wrapper") + .with_child(Dom::new(NodeType::Div).with_id("child-1")) + .with_child(gl_texture_dom(&map, &self).with_id("child-2")) + } else { + // TODO: If this is changed to Label::new(), the text is cut off at the top + // because of the (offset_top / 2.0) - see text_layout.rs file + Button::with_label("Load SVG file...").dom() + .with_callback(On::LeftMouseUp, Callback(my_button_click_handler)) + } + } +} + +fn gl_texture_dom(map: &Map, data: &MyAppData) -> Dom { + Dom::new(NodeType::GlTexture((GlTextureCallback(render_map_callback), StackCheckedPointer::new(data, map).unwrap()) )) + .with_callback(On::Scroll, Callback(scroll_map_contents)) + .with_callback(On::MouseOver, Callback(check_hovered_font)) +} + +fn render_map_callback(ptr: &StackCheckedPointer, window_info: LayoutInfo, dimensions: HidpiAdjustedBounds) -> Option { + unsafe { ptr.invoke_mut_texture(render_map, window_info, dimensions) } +} + +fn render_map(map: &mut Map, info: LayoutInfo, dimensions: HidpiAdjustedBounds) -> Option { + let physical_size = dimensions.get_physical_size(); + Some( + Svg::with_layers(build_layers(&map.layers, &map.texts, &map.hovered_text, &map.font_cache, &info.resources)) + .with_pan(map.pan_horz as f32, map.pan_vert as f32) + .with_zoom(map.zoom as f32) + .render_svg( + &map.cache, &info.window, + physical_size.width as usize, + physical_size.height as usize, + ) + ) +} + +fn build_layers( + existing_layers: &[(SvgLayerId, SvgStyle)], + texts: &HashMap, + hovered_text: &Option, + vector_font_cache: &VectorizedFontCache, + resources: &AppResources) +-> Vec +{ + let mut layers: Vec = existing_layers.iter().map(|e| SvgLayerResource::Reference(*e)).collect(); + + layers.extend(texts.values().map(|text| SvgLayerResource::Direct(text.to_svg_layer(vector_font_cache, resources)))); + layers.extend(texts.values().map(|text| SvgLayerResource::Direct(text.get_bbox().draw_lines(ColorU { r: 0, g: 0, b: 0, a: 255 }, 1.0)))); +/* + if let Some(active) = hovered_text { + layers.push(texts[active].get_bbox().draw_lines()); + } +*/ + // layers.push(curve.draw_lines()); + // layers.push(curve.draw_control_handles()); + + layers +} + +// Check what text was hovered over +fn check_hovered_font(app_state: &mut AppState, event: &mut CallbackInfo) -> UpdateScreen { + let (cursor_x, cursor_y) = event.cursor_relative_to_item?; + + let mut data = app_state.data.lock().ok()?; + let map = data.map.as_mut()?; + + let mut should_redraw = DontRedraw; + + for (k, v) in map.texts.iter() { + if v.get_bbox().contains_point(cursor_x, cursor_y) { + map.hovered_text = Some(*k); + should_redraw = Redraw; + break; + } + } + + should_redraw +} + +fn scroll_map_contents(app_state: &mut AppState, event: &mut CallbackInfo) -> UpdateScreen { + + let mut data = app_state.data.lock().ok()?; + let map = data.map.as_mut()?; + + let mouse_state = app_state.windows.get(event.window_id)?.get_mouse_state(); + let keyboard_state = app_state.windows.get(event.window_id)?.get_keyboard_state(); + + if keyboard_state.shift_down { + map.pan_horz += mouse_state.scroll_y; + } else if keyboard_state.ctrl_down { + if mouse_state.scroll_y.is_sign_positive() { + map.zoom /= 2.0; + } else { + map.zoom *= 2.0; + } + } else { + map.pan_vert += mouse_state.scroll_y; + } + + Redraw +} + +fn my_button_click_handler(app_state: &mut AppState, _event: &mut CallbackInfo) -> UpdateScreen { + + let font_size = 10.0; + let font_id = app_state.resources.get_css_font_id("sans-serif")?; + let font_bytes = app_state.resources.get_font_bytes(&font_id)?.ok()?; + + let text_style = SvgStyle { + fill: Some(ColorU { r: 0, g: 0, b: 0, a: 255 }), + transform: SvgTransform { + translation: Some(SvgTranslation { x: 50.0, y: 50.0 }), + .. Default::default() + }, + .. Default::default() + }; + + // Texts only for testing + let texts = [ + SvgText { + font_size_px: font_size, + font_id: font_id.clone(), + text_layout: SvgTextLayout::from_str( + "On Curve!!!!", + &font_bytes.0, + font_bytes.1 as u32, + &TextLayoutOptions::default(), + StyleTextAlignmentHorz::default(), + ), + style: text_style, + placement: SvgTextPlacement::OnCubicBezierCurve(SampledBezierCurve::from_curve(&[ + BezierControlPoint { x: 0.0, y: 0.0 }, + BezierControlPoint { x: 40.0, y: 120.0 }, + BezierControlPoint { x: 80.0, y: 120.0 }, + BezierControlPoint { x: 120.0, y: 0.0 }, + ])), + }, + SvgText { + font_size_px: font_size, + font_id: font_id.clone(), + text_layout: SvgTextLayout::from_str( + "Rotated", + &font_bytes.0, + font_bytes.1 as u32, + &TextLayoutOptions::default(), + StyleTextAlignmentHorz::default(), + ), + style: text_style, + placement: SvgTextPlacement::Rotated(-30.0), + }, + SvgText { + font_size_px: font_size, + font_id: font_id.clone(), + text_layout: SvgTextLayout::from_str( + "Unmodified\nCool", + &font_bytes.0, + font_bytes.1 as u32, + &TextLayoutOptions::default(), + StyleTextAlignmentHorz::default(), + ), + style: text_style, + placement: SvgTextPlacement::Unmodified, + }, + ]; + + let mut cached_texts = HashMap::::new(); + for t in texts.into_iter() { + let id = new_text_id(); + cached_texts.insert(id, t.clone()); + } + + open_file_dialog(None, None) + .and_then(|path| fs::read_to_string(path.clone()).ok()) + .and_then(|contents| { + + let mut svg_cache = SvgCache::empty(); + let svg_layers = svg_cache.add_svg(&contents).ok()?; + + app_state.data.modify(|data| data.map = Some(Map { + cache: svg_cache, + font_cache: VectorizedFontCache::new(), + hovered_text: None, + texts: cached_texts, + layers: svg_layers, + zoom: 1.0, + pan_horz: 0.0, + pan_vert: 0.0, + })); + + Some(Redraw) + }) + .unwrap_or(DontRedraw) +} + +fn main() { + + macro_rules! CSS_PATH { () => (concat!(env!("CARGO_MANIFEST_DIR"), "/../examples/debug.css")) } + + let css = css::override_native(include_str!(CSS_PATH!())).unwrap(); + let mut app = App::new(MyAppData { map: None }, AppConfig::default()).unwrap(); + let window = app.create_window(WindowCreateOptions::default(), css).unwrap(); + app.run(window).unwrap(); +} diff --git a/examples/table/table.rs b/examples/table/table.rs new file mode 100644 index 000000000..c171d5fff --- /dev/null +++ b/examples/table/table.rs @@ -0,0 +1,25 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +extern crate azul; + +use azul::{prelude::*, widgets::table_view::*}; + +struct TableDemo { + table_state: TableViewState, +} + +impl Layout for TableDemo { + fn layout(&self, info: LayoutInfo) -> Dom { + TableView::new().dom(&self.table_state, &self, info.window) + } +} + +fn main() { + + let mut table_state = TableViewState::default(); + table_state.work_sheet.set_cell(3, 4, "Hello World"); + + let mut app = App::new(TableDemo { table_state }, AppConfig::default()).unwrap(); + let window = app.create_window(WindowCreateOptions::default(), css::native()).unwrap(); + app.run(window).unwrap(); +} diff --git a/examples/test_content.css b/examples/test_content.css deleted file mode 100644 index 56dbc3b97..000000000 --- a/examples/test_content.css +++ /dev/null @@ -1,18 +0,0 @@ -.__azul-native-button { - background-color: #fcfcfc; - color: #000000; - border: 1px solid #b7b7b7; - border-radius: 4px; - /*box-shadow: 0px 0px 3px #c5c5c5ad;*/ - box-shadow: 0px 0px 3px red; - background: linear-gradient(#fcfcfc, #efefef); - width: 200px; - height: 200px; - min-height: 400px; - min-width: 400px; - flex-direction: row; - flex-wrap: nowrap; - justify-content: space-around; - align-items: center; - align-content: center; -} \ No newline at end of file diff --git a/examples/text_input/text_input.rs b/examples/text_input/text_input.rs new file mode 100644 index 000000000..35427633c --- /dev/null +++ b/examples/text_input/text_input.rs @@ -0,0 +1,55 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +extern crate azul; + +use azul::prelude::*; +use azul::widgets::text_input::*; + +const CSS: &str = " +#text_input_1 { + border-radius: 2px; + width: 200px; + height: 20px; + border: 2px solid transparent; + background-color: #ccc; + position: absolute; + top: 40px; + left: 40px; +} + +#text_input_1 p { + font-size: 10px; +} + +#text_input_1:focus { + border: 2px solid #80ff80; +} +"; + +struct TestCrudApp { + text_input: TextInputState, +} + +impl Default for TestCrudApp { + fn default() -> Self { + Self { + text_input: TextInputState::new("Hover mouse over rectangle and press keys") + } + } +} + +impl Layout for TestCrudApp { + fn layout(&self, info: LayoutInfo) -> Dom { + TextInput::new() + .bind(info.window, &self.text_input, &self) + .dom(&self.text_input) + .with_id("text_input_1") + } +} + +fn main() { + let mut app = App::new(TestCrudApp::default(), AppConfig::default()).unwrap(); + let css = css::override_native(CSS).unwrap(); + let window = app.create_window(WindowCreateOptions::default(), css).unwrap(); + app.run(window).unwrap(); +} diff --git a/examples/text_shaping/text_shaping.css b/examples/text_shaping/text_shaping.css new file mode 100644 index 000000000..d2e093255 --- /dev/null +++ b/examples/text_shaping/text_shaping.css @@ -0,0 +1,25 @@ +* { + font-size: 14px; + text-align: left; +} + +#align-default { + flex-direction: column; +} + +#hebrew { + font-size: 35px; + background-color: grey; + color: white; + line-height: 3.8; + word-spacing: 50px; + height: 50px; +} + +#lorem_ipsum { + +} + +#devangari { + /* font-family: MingLiU, NSimSun; -- panics for some reason in dwrote */ +} \ No newline at end of file diff --git a/examples/text_shaping/text_shaping.rs b/examples/text_shaping/text_shaping.rs new file mode 100644 index 000000000..95c1f3a7b --- /dev/null +++ b/examples/text_shaping/text_shaping.rs @@ -0,0 +1,34 @@ +extern crate azul; + +use azul::prelude::*; +use std::time::Duration; + +macro_rules! XML_PATH { () => (concat!(env!("CARGO_MANIFEST_DIR"), "/../examples/text_shaping/text_shaping.xml")) } +macro_rules! CSS_PATH { () => (concat!(env!("CARGO_MANIFEST_DIR"), "/../examples/text_shaping/text_shaping.css")) } + +struct DataModel { } + +impl Layout for DataModel { + fn layout(&self, _: LayoutInfo) -> Dom { + Dom::from_file(XML_PATH!(), &mut XmlComponentMap::default()) + } +} + +fn main() { + + let mut app = App::new(DataModel { }, AppConfig::default()).unwrap(); + + #[cfg(debug_assertions)] + let window = { + let hot_reloader = css::hot_reload_override_native(CSS_PATH!(), Duration::from_millis(500)); + app.create_hot_reload_window(WindowCreateOptions::default(), hot_reloader).unwrap() + }; + + #[cfg(not(debug_assertions))] + let window = { + let css = css::override_native(include_str!(CSS_PATH!())).unwrap(); + app.create_window(WindowCreateOptions::default(), css).unwrap() + }; + + app.run(window).unwrap(); +} \ No newline at end of file diff --git a/examples/text_shaping/text_shaping.xml b/examples/text_shaping/text_shaping.xml new file mode 100644 index 000000000..748253aeb --- /dev/null +++ b/examples/text_shaping/text_shaping.xml @@ -0,0 +1,19 @@ + +
+
+

+ Lorem ipsum dolor sit amet, + consetetur sadipscing elitr, + sed diam nonumy eirmod tempor invidunt + ut labore et dolore magna aliquyam erat, + sed diam voluptua. +

+

ficellé fffffi. VAV.

+

يتكلّم

+

וְר֣וּחַ שרה

+

Дуо вёжи дёжжэнтиюнт ут

+

緳 踥踕

+

हालाँकि प्रचलित रूप पूजा

+
+
+
\ No newline at end of file diff --git a/examples/transparent_window/transparent_window.rs b/examples/transparent_window/transparent_window.rs new file mode 100644 index 000000000..93aa9eaf5 --- /dev/null +++ b/examples/transparent_window/transparent_window.rs @@ -0,0 +1,18 @@ +extern crate azul; + +use azul::prelude::*; +use azul::widgets::button::Button; + +struct MyDataModel { } + +impl Layout for MyDataModel { + fn layout(&self, _: LayoutInfo) -> Dom { + Button::with_label("Update counter").dom() + } +} + +fn main() { + let mut app = App::new(MyDataModel { }, AppConfig::default()).unwrap(); + let window = app.create_window(WindowCreateOptions::default(), css::native()).unwrap(); + app.run(window).unwrap(); +} \ No newline at end of file diff --git a/examples/xml/ui.xml b/examples/xml/ui.xml new file mode 100644 index 000000000..1191a56b0 --- /dev/null +++ b/examples/xml/ui.xml @@ -0,0 +1,70 @@ + + +
+
+

+ You can hot-reload this UI, see /examples/ui.xml. + + If you want to insert a return in this text, you"ll have to use two line breaks, + a single linebreak is ignored (and rendered as a space instead). +

+
+

+

+

Open project

+
+
+
+
+ +
+
+ + + + +

{selectedDate} Test

+
+ + +
+

<

+

+

>

+
+
+ + + + + + \ No newline at end of file diff --git a/examples/xml/xml.css b/examples/xml/xml.css new file mode 100644 index 000000000..a1491b981 --- /dev/null +++ b/examples/xml/xml.css @@ -0,0 +1,44 @@ +* { + color: white; + background-color: #222; + font-size: 16px; + border: 1px solid green; +} + +#start_screen { + flex-direction: row; +} + +#new_project_btn { + color: white; + border-radius: 4px; + background: linear-gradient(to right, #da4453, #89216b); + box-shadow: 0px 5px 10px black; + font-size: 30px; +} + +#new_project_btn:hover { + background-color: green; +} + +#new_project_btn:active { + background-color: blue; +} + +#project_btn_container { + flex-direction: row; + height: 70px; +} + +#last_projects_column { + max-width: 300px; + margin: 10px; +} + +#map_preview_container { + padding: 20px; +} + +#map_preview { + background: linear-gradient(300deg, #03001e, #7303c0, #ec38bc, #fdeff9); +} \ No newline at end of file diff --git a/examples/xml/xml.rs b/examples/xml/xml.rs new file mode 100644 index 000000000..ec7c41f89 --- /dev/null +++ b/examples/xml/xml.rs @@ -0,0 +1,34 @@ +extern crate azul; + +use azul::prelude::*; +use std::time::Duration; + +macro_rules! XML_PATH { () => (concat!(env!("CARGO_MANIFEST_DIR"), "/../examples/xml/ui.xml")) } +macro_rules! CSS_PATH { () => (concat!(env!("CARGO_MANIFEST_DIR"), "/../examples/xml/xml.css")) } + +struct DataModel { } + +impl Layout for DataModel { + fn layout(&self, _: LayoutInfo) -> Dom { + Dom::from_file(XML_PATH!(), &mut XmlComponentMap::default()) + } +} + +fn main() { + + let mut app = App::new(DataModel { }, AppConfig::default()).unwrap(); + + #[cfg(debug_assertions)] + let window = { + let hot_reloader = css::hot_reload_override_native(CSS_PATH!(), Duration::from_millis(500)); + app.create_hot_reload_window(WindowCreateOptions::default(), hot_reloader).unwrap() + }; + + #[cfg(not(debug_assertions))] + let window = { + let css = css::override_native(include_str!(CSS_PATH!())).unwrap(); + app.create_window(WindowCreateOptions::default(), css).unwrap() + }; + + app.run(window).unwrap(); +} \ No newline at end of file diff --git a/src/app.rs b/src/app.rs deleted file mode 100644 index 8c81a4aaf..000000000 --- a/src/app.rs +++ /dev/null @@ -1,280 +0,0 @@ -use css::Css; -use app_state::AppState; -use traits::LayoutScreen; -use input::hit_test_ui; -use ui_state::UiState; -use ui_description::UiDescription; - -use std::sync::{Arc, Mutex}; -use window::{Window, WindowCreateOptions, WindowCreateError, WindowId}; -use glium::glutin::Event; -use euclid::TypedScale; - -/// Graphical application that maintains some kind of application state -pub struct App { - /// The graphical windows, indexed by ID - windows: Vec>, - /// The global application state - pub app_state: Arc>>, -} - -pub(crate) struct FrameEventInfo { - pub(crate) should_redraw_window: bool, - pub(crate) should_swap_window: bool, - pub(crate) should_hittest: bool, - pub(crate) cur_cursor_pos: (f64, f64), - pub(crate) new_window_size: Option<(u32, u32)>, - pub(crate) new_dpi_factor: Option, -} - -impl Default for FrameEventInfo { - fn default() -> Self { - Self { - should_redraw_window: false, - should_swap_window: false, - should_hittest: false, - cur_cursor_pos: (0.0, 0.0), - new_window_size: None, - new_dpi_factor: None, - } - } -} - -impl App { - - /// Create a new, empty application (note: doesn't create a window!) - pub fn new(initial_data: T) -> Self { - Self { - windows: Vec::new(), - app_state: Arc::new(Mutex::new(AppState::new(initial_data))), - } - } - - /// Spawn a new window on the screen - pub fn create_window(&mut self, options: WindowCreateOptions, css: Css) -> Result<(), WindowCreateError> { - self.windows.push(Window::new(options, css)?); - Ok(()) - } - - /// Start the rendering loop for the currently open windows - pub fn start_render_loop(&mut self) - { - let mut ui_state_cache = Vec::with_capacity(self.windows.len()); - let mut ui_description_cache = vec![UiDescription::default(); self.windows.len()]; - - // first redraw, initialize cache - { - let app_state = self.app_state.lock().unwrap(); - for (idx, _) in self.windows.iter().enumerate() { - ui_state_cache.push(UiState::from_app_state(&*app_state, WindowId { id: idx })); - } - - // First repaint, otherwise the window would be black on startup - for (idx, window) in self.windows.iter_mut().enumerate() { - ui_description_cache[idx] = UiDescription::from_ui_state(&ui_state_cache[idx], &mut window.css); - render(window, &WindowId { id: idx, }, &ui_description_cache[idx], true); - window.display.swap_buffers().unwrap(); - } - } - - - 'render_loop: loop { - - let mut closed_windows = Vec::::new(); - - let time_start = ::std::time::Instant::now(); - let mut debug_has_repainted = None; - - // TODO: Use threads on a per-window basis. - // Currently, events in one window will block all others - for (idx, ref mut window) in self.windows.iter_mut().enumerate() { - - let current_window_id = WindowId { id: idx }; - - let mut frame_event_info = FrameEventInfo::default(); - - window.events_loop.poll_events(|event| { - let should_close = process_event(event, &mut frame_event_info); - if should_close { - closed_windows.push(idx); - } - }); - - // update the state - if frame_event_info.should_swap_window { - window.display.swap_buffers().unwrap(); - } - - if frame_event_info.should_hittest { - - use webrender::api::WorldPoint; - use dom::UpdateScreen; - - let point = WorldPoint::new(frame_event_info.cur_cursor_pos.0 as f32, frame_event_info.cur_cursor_pos.1 as f32); - let hit_test_results = hit_test_ui(&window.internal.api, window.internal.document_id, Some(window.internal.pipeline_id), point); - - let mut should_update_screen = UpdateScreen::DontRedraw; - - for item in hit_test_results.items { - if let Some(callback_list) = ui_state_cache[idx].node_ids_to_callbacks_list.get(&item.tag.0) { - // TODO: filter by `On` type (On::MouseOver, On::MouseLeave, etc.) - // currently, just invoke all actions - for callback_id in callback_list.values() { - use dom::Callback::*; - let update = match ui_state_cache[idx].callback_list[callback_id] { - Sync(callback) => { (callback)(&mut *self.app_state.lock().unwrap()) }, - Async(callback) => { (callback)(self.app_state.clone()) }, - }; - if update == UpdateScreen::Redraw { - should_update_screen = UpdateScreen::Redraw; - } - } - } - } - - if should_update_screen == UpdateScreen::Redraw { - frame_event_info.should_redraw_window = true; - } - - } - - let mut app_state = self.app_state.lock().unwrap(); - ui_state_cache[idx] = UiState::from_app_state(&*app_state, WindowId { id: idx }); - - if window.css.is_dirty { - frame_event_info.should_redraw_window = true; - } - - if let Some((w, h)) = frame_event_info.new_window_size { - use webrender::api::{DeviceUintSize, DeviceUintPoint, DeviceUintRect, LayoutSize, Transaction}; - window.internal.layout_size = LayoutSize::new(w as f32, h as f32); - window.internal.framebuffer_size = DeviceUintSize::new(w, h); - let mut txn = Transaction::new(); - let bounds = DeviceUintRect::new(DeviceUintPoint::new(0, 0), window.internal.framebuffer_size); - txn.set_window_parameters(window.internal.framebuffer_size, bounds, window.internal.hidpi_factor); - window.internal.api.send_transaction(window.internal.document_id, txn); - render(window, ¤t_window_id, &ui_description_cache[idx], true); - - let time_end = ::std::time::Instant::now(); - debug_has_repainted = Some(time_end - time_start); - continue; - } - - if let Some(dpi) = frame_event_info.new_dpi_factor { - use webrender::api::{DeviceUintPoint, DeviceUintRect, Transaction}; - window.internal.hidpi_factor = dpi; - let mut txn = Transaction::new(); - let bounds = DeviceUintRect::new(DeviceUintPoint::new(0, 0), window.internal.framebuffer_size); - txn.set_window_parameters(window.internal.framebuffer_size, bounds, window.internal.hidpi_factor); - window.internal.api.send_transaction(window.internal.document_id, txn); - render(window, ¤t_window_id, &ui_description_cache[idx], true); - let time_end = ::std::time::Instant::now(); - debug_has_repainted = Some(time_end - time_start); - continue; - } - - if frame_event_info.should_redraw_window { - ui_description_cache[idx] = UiDescription::from_ui_state(&ui_state_cache[idx], &mut window.css); - render(window, ¤t_window_id, &ui_description_cache[idx], frame_event_info.new_window_size.is_some()); - let time_end = ::std::time::Instant::now(); - debug_has_repainted = Some(time_end - time_start); - } - } - - // close windows if necessary - for closed_window_id in closed_windows { - let closed_window_id = closed_window_id; - ui_state_cache.remove(closed_window_id); - ui_description_cache.remove(closed_window_id); - self.windows.remove(closed_window_id); - } - - if self.windows.is_empty() { - break; - } else { - if let Some(restate_time) = debug_has_repainted { - println!("frame time: {:?} ms", restate_time.subsec_nanos() as f32 / 1_000_000.0); - } - ::std::thread::sleep(::std::time::Duration::from_millis(16)); - } - } - } -} - -fn process_event(event: Event, frame_event_info: &mut FrameEventInfo) -> bool { - use glium::glutin::WindowEvent; - match event { - Event::WindowEvent { - window_id, - event - } => { - match event { - WindowEvent::CursorMoved { - device_id, - position, - modifiers, - } => { - frame_event_info.should_hittest = true; - frame_event_info.cur_cursor_pos = position; - - let _ = window_id; - let _ = device_id; - let _ = modifiers; - }, - WindowEvent::Resized(w, h) => { - frame_event_info.new_window_size = Some((w, h)); - }, - WindowEvent::Refresh => { - frame_event_info.should_redraw_window = true; - }, - WindowEvent::HiDPIFactorChanged(dpi) => { - frame_event_info.new_dpi_factor = Some(dpi); - }, - WindowEvent::Closed => { - return true; - } - _ => { }, - } - }, - Event::Awakened => { - frame_event_info.should_swap_window = true; - }, - _ => { }, - } - - false -} - -fn render(window: &mut Window, _window_id: &WindowId, ui_description: &UiDescription, has_window_size_changed: bool) -{ - use webrender::api::*; - use display_list::DisplayList; - - let display_list = DisplayList::new_from_ui_description(ui_description); - let builder = display_list.into_display_list_builder(window.internal.pipeline_id, &mut window.solver, &mut window.css, has_window_size_changed); - - if let Some(new_builder) = builder { - // only finalize the list if we actually need to. Otherwise just redraw the last display list - window.internal.last_display_list_builder = new_builder.finalize().2; - } - - let resources = ResourceUpdates::new(); - let mut txn = Transaction::new(); - - // TODO: something is wrong, the redraw times increase, even if the same display list is redrawn - txn.set_display_list( - window.internal.epoch, - None, - window.internal.layout_size, - (window.internal.pipeline_id, window.solver.window_dimensions.layout_size, window.internal.last_display_list_builder.clone()), - true, - ); - - txn.update_resources(resources); - txn.set_root_pipeline(window.internal.pipeline_id); - txn.generate_frame(); - window.internal.api.send_transaction(window.internal.document_id, txn); - - window.renderer.as_mut().unwrap().update(); - window.renderer.as_mut().unwrap().render(window.internal.framebuffer_size).unwrap(); -} \ No newline at end of file diff --git a/src/app_state.rs b/src/app_state.rs deleted file mode 100644 index c3e7ff5d3..000000000 --- a/src/app_state.rs +++ /dev/null @@ -1,32 +0,0 @@ -use traits::LayoutScreen; -use resources::{AppResources, FontId, ImageId}; - -/// Wrapper for your application data. In order to be layout-able, -/// you need to satisfy the `LayoutScreen` trait (how the application -/// should be laid out) -pub struct AppState { - /// Your data (the global struct which all callbacks will have access to) - pub data: T, - /// Fonts and images that are currently loaded into the app - resources: AppResources, -} - -impl AppState { - - /// Creates a new `AppState` - pub fn new(initial_data: T) -> Self { - Self { - data: initial_data, - resources: AppResources::default(), - } - } -/* - pub(crate) fn add_font() -> Result { - - } - - pub(crate) fn add_image() -> Result { - - } -*/ -} diff --git a/src/cache.rs b/src/cache.rs deleted file mode 100644 index fbae8522c..000000000 --- a/src/cache.rs +++ /dev/null @@ -1,307 +0,0 @@ -//! DOM cache for library-internal use -//! -//! # Diffing the DOM -//! -//! Changes in the DOM can happen in three ways: -//! -//! - An element changes its content An element is pushed as a child The order / childs of an element -//! - are restructured -//! -//! In order for the caching to be effective, we need to solve the problem of only adding -//! EditVariable-s if needed. In order to do that, we need two elements for each DOM node: -//! -//! - The self-hash (the hash of the current DOM node, including hashing the content) -//! - The hashes of the individual children (like a `Vec`), in their correct order -//! -//! For detecting these changes, we build an `Arena` (empty on startup) and a -//! `HashMap<(DomHash, bool) -> LayoutRect>`. The latter stores all active EditVariables. Whenever we -//! insert or remove from the HashMap, we also remove the variables from the solver -//! -//! When a re-layout is required, we hash the nodes from the UiDescription, starting from the root. Each -//! time we go to the next sibling / next child, this change is also reflected by going through the -//! `Arena`. For each node, we calculate the self-hash of the node and compare it with the hash -//! in that position in the `Arena`. If the hash does not exist in the `Arena`, we -//! insert it in the `HashMap<(DomHash, bool)`, create a new LayoutRect and add the variables to the -//! solver. We set the `bool` to true to indicate, that this hash is currently active in the DOM and -//! should not be removed. Then we add the hash to the `Arena`. -//! -//! If there is a hash, but the hashes differ, this means that either the order of the current siblings -//! were changed or the actual contents of the node were changed. So we look up the hash in the -//! `HashMap<(DomHash, bool)>`. If we can find it, this means that we already have EditVariables in the -//! solver corresponding to the node and the node was simply reordered. -//! If we can't find it, it's either a completely new DOM element or the contents of the node have changed. -//! -//! Lastly, we go through the `HashMap<(DomHash, bool)>` and remove the edit variables if the `bool` is false, -//! meaning that the variable was not present in the current DOM tree, so leaving the variables in the solver -//! would be garbage. - -use std::collections::BTreeMap; -use constraints::DisplayRect; -use cassowary::Solver; -use id_tree::{NodeId, Arena}; -use traits::LayoutScreen; -use dom::NodeData; -use std::ops::Deref; - -/// We keep the tree from the previous re-layout. Then, when a re-layout is required, -/// we re-hash all the nodes, insert the -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub(crate) struct DomTreeCache { - pub(crate) previous_layout: HashedDomTree, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct DomChangeSet { - // todo: calculate the constraints that have to be updated - pub(crate) added_nodes: BTreeMap, -} - -impl DomChangeSet { - pub(crate) fn empty() -> Self { - Self { - added_nodes: BTreeMap::new(), - } - } -} - -impl Deref for DomChangeSet { - type Target = BTreeMap; - - fn deref(&self) -> &Self::Target { - &self.added_nodes - } -} - -impl DomTreeCache { - - pub(crate) fn empty() -> Self { - Self { - previous_layout: HashedDomTree { - arena: Arena::::new(), - root: None, - }, - } - } - - pub(crate) fn update(&mut self, new_root: NodeId, new_nodes_arena: &Arena>) -> DomChangeSet { - - use std::hash::Hash; - - if let Some(previous_root) = self.previous_layout.root { - // let mut changeset = DomChangeSet::empty(); - let new_tree = new_nodes_arena.transform(|data| data.calculate_node_data_hash()); - // Self::update_tree_inner(previous_root, &self.previous_layout.arena, new_root, &new_nodes_arena, &mut changeset); - let changeset = Self::update_tree_inner_2(&self.previous_layout.arena, &new_tree); - self.previous_layout.arena = new_tree; - changeset - } else { - // initialize arena - use std::iter::FromIterator; - self.previous_layout.arena = new_nodes_arena.transform(|data| data.calculate_node_data_hash()); - self.previous_layout.root = Some(new_root); - DomChangeSet { - added_nodes: self.previous_layout.arena.get_all_node_ids(), - } - } - } - - fn update_tree_inner_2(previous_arena: &Arena, next_arena: &Arena) -> DomChangeSet { - - let mut previous_iter = previous_arena.nodes.iter(); - let mut next_iter = next_arena.nodes.iter().enumerate(); - let mut changeset = DomChangeSet::empty(); - - while let Some((next_idx, next_hash)) = next_iter.next() { - if let Some(old_hash) = previous_iter.next() { - if old_hash.data != next_hash.data { - changeset.added_nodes.insert(NodeId { index: next_idx }, next_hash.data); - } - } else { - // println!("chrildren: no old hash, but subtree has to be added: {:?}!", new_next_id); - changeset.added_nodes.insert(NodeId { index: next_idx }, next_hash.data); - } - } -/* - loop { - match (previous_iter.next(), next_iter.next().enumerate()) { - (None, None) => { - // println!("chrildren: old has no children, new has no children!"); - break; - }, - (Some(_), None) => { - prev = previous_iter.next(); - }, - (None, Some(next_hash)) => { - // println!("chrildren: no old hash, but subtree has to be added: {:?}!", new_next_id); - // TODO: add subtree - changeset.added_nodes.insert(NodeId { index: next_idx }, next_hash.data); - next = next_iter.next(); - next_idx += 1; - }, - (Some(old_hash), Some(next_hash)) => { - if old_hash.data != next_hash.data { - changeset.added_nodes.insert(NodeId { index: next_idx }, next_hash.data); - } - next = next_iter.next(); - next_idx += 1; - } - } - } -*/ - changeset - } - - fn update_tree_inner(previous_root: NodeId, - previous_hash_arena: &Arena, - current_root: NodeId, - current_dom_arena: &Arena>, - changeset: &mut DomChangeSet) - where T: LayoutScreen - { - let mut old_child_iterator = previous_root.children(previous_hash_arena); - let mut new_child_iterator = current_root.children(previous_hash_arena); - - // children first - loop { - // skip the root node itself, although it wouldn't be necessary here - // old_child_iterator.next(); - // new_child_iterator.next(); - let old_child_next = old_child_iterator.next(); - let new_child_next = new_child_iterator.next(); - - match (old_child_next, new_child_next) { - (None, None) => { - // println!("chrildren: old has no children, new has no children!"); - break; - }, - (Some(old_hash), None) => { - // meaning, the whole subtree should be removed - // println!("chrildren: old has children at id: {:?}, new has children at id:", old_hash); - }, - (None, Some(new_next_id)) => { - // println!("chrildren: no old hash, but subtree has to be added: {:?}!", new_next_id); - // TODO: add subtree - }, - (Some(old_hash_id), Some(new_next_node_id)) => { - let old_hash = previous_hash_arena[old_hash_id].data; - let new_hash = current_dom_arena[new_next_node_id].data.calculate_node_data_hash(); - - if old_hash == new_hash { - // println!("chrildren: children are the same!"); - } else { - // hashes differ - // println!("chrildren: children are different!"); - // changeset.added_nodes.insert(new_next_node_id, new_hash); - } - } - } - } - - let mut old_iterator = previous_root.following_siblings(previous_hash_arena); - let mut new_iterator = current_root.following_siblings(current_dom_arena); - - // now iterate over siblings - loop { - let old_next = old_iterator.next(); - let new_next = new_iterator.next(); - - match (old_next, new_next) { - (None, None) => { - // both old and new node have the same length - break; - }, - (None, Some(new_next_node_id)) => { - // new node was pushed as a child - let new_hash = current_dom_arena[new_next_node_id].data.calculate_node_data_hash(); - changeset.added_nodes.insert(new_next_node_id, new_hash); - // println!("siblings: node was added as a child!"); - }, - (Some(old_hash_id), None) => { - // node was removed as a child - // mark node as inactive - let old_hash = previous_hash_arena[old_hash_id].data; - // println!("siblings: node was removed as a child: {:?}", old_hash); - }, - (Some(old_hash_id), Some(new_next_node_id)) => { - let old_hash = previous_hash_arena[old_hash_id].data; - let new_hash = current_dom_arena[new_next_node_id].data.calculate_node_data_hash(); - - if old_hash == new_hash { - // println!("siblings: hashes are the same!"); - } else { - // hashes differ - // println!("siblings: hashes differ"); - changeset.added_nodes.insert(new_next_node_id, new_hash); - } - } - } - } - } -} - -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub(crate) struct HashedDomTree { - pub(crate) arena: Arena, - pub(crate) root: Option, -} - -#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, Ord, PartialOrd)] -pub(crate) struct DomHash(pub u64); - -#[derive(Debug, Clone, Hash, PartialEq, Eq, Ord, PartialOrd)] -pub(crate) struct DomNodeHash { - /// The hash of the node itself - pub(crate) self_hash: DomHash, - /// The self_hash-es of the children, in their correct order - pub(crate) children_hash: Vec, -} - -#[derive(Debug)] -pub(crate) struct EditVariableCache { - pub(crate) map: BTreeMap -} - -impl EditVariableCache { - pub(crate) fn empty() -> Self { - Self { - map: BTreeMap::new(), - } - } - - pub(crate) fn initialize_new_rectangles(&mut self, solver: &mut Solver, rects: &DomChangeSet) { - use std::collections::btree_map::Entry::*; - - for dom_hash in rects.added_nodes.values() { - - let map_entry = self.map.entry(*dom_hash); - match map_entry { - Occupied(e) => { - e.into_mut().0 = true; - }, - Vacant(e) => { - let rect = DisplayRect::default(); - rect.add_to_solver(solver); - e.insert((true, rect)); - } - } - } - } - - /// Last step of the caching algorithm: - /// Remove all edit variables where the `bool` is set to false - pub(crate) fn remove_unused_variables(&mut self, solver: &mut Solver) { - - let mut to_be_removed = Vec::::new(); - - for (key, &(active, variable_rect)) in &self.map { - if !active { - variable_rect.remove_from_solver(solver); - to_be_removed.push(*key); - } - } - - for hash in &to_be_removed { - self.map.remove(hash); - } - } -} \ No newline at end of file diff --git a/src/constraints.rs b/src/constraints.rs deleted file mode 100644 index f58fe0bd4..000000000 --- a/src/constraints.rs +++ /dev/null @@ -1,254 +0,0 @@ -//! Constraint building (mostly taken from `limn_layout`) - -use cassowary::{Solver, Variable, Constraint}; -use cassowary::WeightedRelation::{EQ, GE}; -use cassowary::strength::{WEAK, REQUIRED}; -use euclid::{Point2D, Size2D}; - -pub type Size = Size2D; -pub type Point = Point2D; - -/// A set of cassowary `Variable`s representing the -/// bounding rectangle of a layout. -#[derive(Debug, Copy, Clone)] -pub(crate) struct DisplayRect { - pub left: Variable, - pub top: Variable, - pub right: Variable, - pub bottom: Variable, - pub width: Variable, - pub height: Variable, -} - -impl Default for DisplayRect { - fn default() -> Self { - Self { - left: Variable::new(), - top: Variable::new(), - right: Variable::new(), - bottom: Variable::new(), - width: Variable::new(), - height: Variable::new(), - } - } -} - -impl DisplayRect { - - pub fn add_to_solver(&self, solver: &mut Solver) { - solver.add_edit_variable(self.left, WEAK).unwrap_or_else(|_e| { }); - solver.add_edit_variable(self.top, WEAK).unwrap_or_else(|_e| { }); - solver.add_edit_variable(self.right, WEAK).unwrap_or_else(|_e| { }); - solver.add_edit_variable(self.bottom, WEAK).unwrap_or_else(|_e| { }); - solver.add_edit_variable(self.width, WEAK).unwrap_or_else(|_e| { }); - solver.add_edit_variable(self.height, WEAK).unwrap_or_else(|_e| { }); - } - - pub fn remove_from_solver(&self, solver: &mut Solver) { - solver.remove_edit_variable(self.left).unwrap_or_else(|_e| { }); - solver.remove_edit_variable(self.top).unwrap_or_else(|_e| { }); - solver.remove_edit_variable(self.right).unwrap_or_else(|_e| { }); - solver.remove_edit_variable(self.bottom).unwrap_or_else(|_e| { }); - solver.remove_edit_variable(self.width).unwrap_or_else(|_e| { }); - solver.remove_edit_variable(self.height).unwrap_or_else(|_e| { }); - } - -} - -#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)] -pub struct Strength(pub f64); - -#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)] -pub struct Padding(pub f32); - -#[derive(Debug, Copy, Clone)] -pub(crate) enum CssConstraint { - Size((SizeConstraint, Strength)), - Padding((PaddingConstraint, Strength, Padding)) -} - -#[derive(Debug, Copy, Clone)] -pub(crate) enum SizeConstraint { - Width(f32), - Height(f32), - MinWidth(f32), - MinHeight(f32), - Size(Size), - MinSize(Size), - AspectRatio(f32), - Shrink, - ShrinkHorizontal, - ShrinkVertical, - TopLeft(Point), - Center(DisplayRect), - CenterHorizontal(Variable, Variable), - CenterVertical(Variable, Variable), -} - -#[derive(Debug, Copy, Clone)] -pub(crate) enum PaddingConstraint { - AlignTop(Variable), - AlignBottom(Variable), - AlignLeft(Variable), - AlignRight(Variable), - AlignAbove(Variable), - AlignBelow(Variable), - AlignToLeftOf(Variable), - AlignToRightOf(Variable), - Above(Variable), - Below(Variable), - ToLeftOf(Variable), - ToRightOf(Variable), - BoundLeft(Variable), - BoundTop(Variable), - BoundRight(Variable), - BoundBottom(Variable), - BoundBy(DisplayRect), - MatchLayout(DisplayRect), - MatchWidth(Variable), - MatchHeight(Variable), -} - -impl SizeConstraint { - pub(crate) fn build(&self, rect: &DisplayRect, strength: f64) -> Vec { - use self::SizeConstraint::*; - - match *self { - Width(width) => { - vec![ rect.width | EQ(strength) | width ] - }, - Height(height) => { - vec![ rect.height | EQ(strength) | height ] - }, - MinWidth(width) => { - vec![ rect.width | GE(strength) | width ] - }, - MinHeight(height) => { - vec![ rect.height | GE(strength) | height ] - }, - Size(size) => { - vec![ - rect.width | EQ(strength) | size.width, - rect.height | EQ(strength) | size.height, - ] - }, - MinSize(size) => { - vec![ - rect.width | GE(strength) | size.width, - rect.height | GE(strength) | size.height, - ] - }, - AspectRatio(aspect_ratio) => { - vec![ aspect_ratio * rect.width | EQ(strength) | rect.height ] - }, - Shrink => { - vec![ - rect.width | EQ(strength) | 0.0, - rect.height | EQ(strength) | 0.0, - ] - }, - ShrinkHorizontal => { - vec![ rect.width | EQ(strength) | 0.0 ] - }, - ShrinkVertical => { - vec![ rect.height | EQ(strength) | 0.0 ] - }, - TopLeft(point) => { - vec![ - rect.left | EQ(strength) | point.x, - rect.top | EQ(strength) | point.y, - ] - }, - Center(other) => { - vec![ - rect.left - other.left | EQ(REQUIRED) | other.right - rect.right, - rect.top - other.top | EQ(REQUIRED) | other.bottom - rect.bottom, - ] - }, - CenterHorizontal(left, right) => { - vec![ rect.left - left | EQ(REQUIRED) | right - rect.right ] - }, - CenterVertical(top, bottom) => { - vec![ rect.top - top | EQ(REQUIRED) | bottom - rect.bottom ] - }, - } - } -} - -impl PaddingConstraint { - pub(crate) fn build(&self, rect: &DisplayRect, strength: f64, padding: f32) -> Vec { - use self::PaddingConstraint::*; - match *self { - AlignTop(top) => { - vec![ rect.top - top | EQ(strength) | padding ] - }, - AlignBottom(bottom) => { - vec![ bottom - rect.bottom | EQ(strength) | padding ] - }, - AlignLeft(left) => { - vec![ rect.left - left | EQ(strength) | padding ] - }, - AlignRight(right) => { - vec![ right - rect.right | EQ(strength) | padding ] - }, - AlignAbove(top) => { - vec![ top - rect.bottom | EQ(strength) | padding ] - }, - AlignBelow(bottom) => { - vec![ rect.top - bottom | EQ(strength) | padding ] - }, - AlignToLeftOf(left) => { - vec![ left - rect.right | EQ(strength) | padding ] - }, - AlignToRightOf(right) => { - vec![ rect.left - right | EQ(strength) | padding ] - }, - Above(top) => { - vec![ top - rect.bottom | GE(strength) | padding ] - }, - Below(bottom) => { - vec![ rect.top - bottom | GE(strength) | padding ] - }, - ToLeftOf(left) => { - vec![ left - rect.right | GE(strength) | padding ] - }, - ToRightOf(right) => { - vec![ rect.left - right | GE(strength) | padding ] - }, - BoundLeft(left) => { - vec![ rect.left - left | GE(strength) | padding ] - }, - BoundTop(top) => { - vec![ rect.top - top | GE(strength) | padding ] - }, - BoundRight(right) => { - vec![ right - rect.right | GE(strength) | padding ] - }, - BoundBottom(bottom) => { - vec![ bottom - rect.bottom | GE(strength) | padding ] - }, - BoundBy(other) => { - vec![ - rect.left - other.left | GE(strength) | padding, - rect.top - other.top | GE(strength) | padding, - other.right - rect.right | GE(strength) | padding, - other.bottom - rect.bottom | GE(strength) | padding, - ] - }, - MatchLayout(other) => { - vec![ - rect.left - other.left | EQ(strength) | padding, - rect.top - other.top | EQ(strength) | padding, - other.right - rect.right | EQ(strength) | padding, - other.bottom - rect.bottom | EQ(strength) | padding, - ] - }, - MatchWidth(width) => { - vec![ width - rect.width | EQ(strength) | padding ] - }, - MatchHeight(height) => { - vec![ height - rect.height | EQ(strength) | padding ] - }, - } - } -} \ No newline at end of file diff --git a/src/css.rs b/src/css.rs deleted file mode 100644 index 5290cb1f0..000000000 --- a/src/css.rs +++ /dev/null @@ -1,225 +0,0 @@ -//! CSS parsing and styling -use std::ops::Add; - -#[cfg(target_os="windows")] -const NATIVE_CSS_WINDOWS: &str = include_str!("../assets/native_windows.css"); -#[cfg(target_os="linux")] -const NATIVE_CSS_LINUX: &str = include_str!("../assets/native_linux.css"); -#[cfg(target_os="macos")] -const NATIVE_CSS_MACOS: &str = include_str!("../assets/native_macos.css"); - -/// All the keys that, when changed, can trigger a re-layout -const RELAYOUT_RULES: [&str;11] = [ - "border", "width", "height", "min-width", "min-height", - "direction", "wrap", "justify-content", "align-items", "align-content", - "order" -]; - -/// Wrapper for a `Vec`. Fields are private, because the `Css` -/// struct does caching - each time you add / subtract a `Css`, it is checked -/// if the added / removed CSS rules change the actual layout. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Css { - // NOTE: Each time the rules are modified, the `dirty` flag - // has to be set accordingly for the CSS to update! - pub(crate) rules: Vec, - /// - pub(crate) is_dirty: bool, - /// Has the CSS changed in a way where it needs a re-layout? - /// - /// Ex. if only a background color has changed, we need to redraw, but we don't need to re-layout the frame - pub(crate) needs_relayout: bool, -} - -/// Error that can happen during the parsing of a CSS value -#[derive(Debug, Clone)] -pub enum CssParseError { - /// A hard error in the CSS syntax - ParseError(::simplecss::Error), - /// Braces are not balanced properly - UnclosedBlock, - /// Invalid syntax, such as `#div { #div: "my-value" }` - MalformedCss, -} - -/// Rule that applies to some "path" in the CSS, i.e. -/// `div#myid.myclass -> ("justify-content", "center")` -/// -/// The CSS rule is currently not cascaded, use `Css::new_from_string()` -/// to do the cascading. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub struct CssRule { - /// `div` (`*` by default) - pub html_type: String, - /// `#myid` (`None` by default) - pub id: Option, - /// `.myclass .myotherclass` (vec![] by default) - pub classes: Vec, - /// `("justify-content", "center")` - pub declaration: (String, String), -} - -impl CssRule { - pub fn needs_relayout(&self) -> bool { - RELAYOUT_RULES.iter().any(|r| self.declaration.0 == *r) - } -} - -impl Css { - - /// Creates an empty set of CSS rules - pub fn empty() -> Self { - Self { - rules: Vec::new(), - is_dirty: false, - needs_relayout: false, - } - } - - /// Parses a CSS string (single-threaded) and returns the parsed rules - pub fn new_from_string(css_string: &str) -> Result { - use simplecss::{Tokenizer, Token}; - use std::collections::HashSet; - - let mut tokenizer = Tokenizer::new(css_string); - - let mut block_nesting = 0_usize; - let mut css_rules = Vec::::new(); - - // TODO: For now, rules may not be nested, otherwise, this won't work - // TODO: This could be more efficient. We don't even need to clone the - // strings, but this is just a quick-n-dirty CSS parser - // This will also use up a lot of memory, since the strings get duplicated - - let mut parser_in_block = false; - let mut current_type = "*"; - let mut current_id = None; - let mut current_classes = HashSet::<&str>::new(); - - 'css_parse_loop: loop { - let tokenize_result = tokenizer.parse_next(); - match tokenize_result { - Ok(token) => { - match token { - Token::EndOfStream => { - break 'css_parse_loop; - }, - Token::BlockStart => { - parser_in_block = true; - block_nesting += 1; - }, - Token::BlockEnd => { - block_nesting -= 1; - parser_in_block = false; - current_type = "*"; - current_id = None; - current_classes = HashSet::<&str>::new(); - }, - Token::TypeSelector(div_type) => { - if parser_in_block { - return Err(CssParseError::MalformedCss); - } - current_type = div_type; - }, - Token::IdSelector(id) => { - if parser_in_block { - return Err(CssParseError::MalformedCss); - } - current_id = Some(id.to_string()); - } - Token::ClassSelector(class) => { - if parser_in_block { - return Err(CssParseError::MalformedCss); - } - current_classes.insert(class); - } - Token::Declaration(key, val) => { - if !parser_in_block { - return Err(CssParseError::MalformedCss); - } - let mut css_rule = CssRule { - html_type: current_type.to_string(), - id: current_id.clone(), - classes: current_classes.iter().map(|e| e.to_string()).collect::>(), - declaration: (key.to_string(), val.to_string()), - }; - // IMPORTANT! - css_rule.classes.sort(); - css_rules.push(css_rule); - }, - _ => { } - } - }, - Err(e) => { - return Err(CssParseError::ParseError(e)); - } - } - } - - // non-even number of blocks - if block_nesting != 0 { - return Err(CssParseError::UnclosedBlock); - } - - Ok(Self { - rules: css_rules, - // force repaint for the first frame - is_dirty: true, - // force re-layout for the first frame - needs_relayout: true, - }) - } - - /// Adds a CSS rule - pub fn add_rule(&mut self, css_rule: CssRule) { - self.needs_relayout = css_rule.needs_relayout(); - self.rules.push(css_rule); - self.is_dirty = true; - } - - /// Removes a rule from the current stylesheet - pub fn remove_rule(&mut self, css_rule: &CssRule) { - if let Some(pos) = self.rules.iter().position(|x| *x == *css_rule) { - self.needs_relayout = css_rule.needs_relayout(); - self.rules.remove(pos); - self.is_dirty = true; - } - } - - /// Returns the native style for the OS - #[cfg(target_os="windows")] - pub fn native() -> Self { - Self::new_from_string(NATIVE_CSS_WINDOWS).unwrap() - } - - /// Returns the native style for the OS - #[cfg(target_os="linux")] - pub fn native() -> Self { - Self::new_from_string(NATIVE_CSS_LINUX).unwrap() - } - - /// Returns the native style for the OS - #[cfg(target_os="macos")] - pub fn native() -> Self { - Self::new_from_string(NATIVE_CSS_MACOS).unwrap() - } -} - -impl Add for Css { - type Output = Css; - - fn add(mut self, mut other: Css) -> Css { - let needs_relayout = if !other.needs_relayout { - other.rules.iter().any(|r| r.needs_relayout()) - } else { - other.needs_relayout - }; - - self.rules.append(&mut other.rules); - Css { - rules: self.rules, - is_dirty: true, - needs_relayout: needs_relayout, - } - } -} \ No newline at end of file diff --git a/src/css_parser.rs b/src/css_parser.rs deleted file mode 100644 index c31094d06..000000000 --- a/src/css_parser.rs +++ /dev/null @@ -1,1582 +0,0 @@ -//! Contains utilities to convert strings (CSS strings) to servo types - -use webrender::api::{ColorU, BorderRadius, LayoutVector2D, LayoutPoint, - ColorF, BoxShadowClipMode, LayoutSize, BorderStyle, - BorderDetails, BorderSide, NormalBorder, BorderWidths, - ExtendMode, LayoutRect, LayerPixel}; -use std::num::{ParseIntError, ParseFloatError}; -use euclid::{TypedRotation2D, Angle, TypedPoint2D}; - -pub const EM_HEIGHT: f32 = 16.0; - -macro_rules! impl_from { - ($a:ident, $b:ident, $enum_type:ident) => ( - impl<'a> From<$a<'a>> for $b<'a> { - fn from(e: $a<'a>) -> Self { - $b::$enum_type(e) - } - } - ) -} - -/// A parser that can accept a list of items and mappings -macro_rules! multi_type_parser { - ($fn:ident, $return:ident, $([$identifier_string:expr, $enum_type:ident]),+) => ( - pub fn $fn<'a>(input: &'a str) - -> Result<$return, InvalidValueErr<'a>> - { - match input { - $( - $identifier_string => Ok($return::$enum_type), - )+ - _ => Err(InvalidValueErr(input)), - } - } - ) -} - -macro_rules! typed_pixel_value_parser { - ($fn:ident, $return:ident) => ( - pub fn $fn<'a>(input: &'a str) - -> Result<$return, PixelParseError<'a>> - { - parse_pixel_value(input).and_then(|e| Ok($return(e))) - } - ) -} - -/// Simple "invalid value" error, used for -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub struct InvalidValueErr<'a>(pub &'a str); - -#[derive(Debug, PartialEq, Copy, Clone)] -pub struct PixelValue { - metric: CssMetric, - number: f32, -} - -#[derive(Debug, PartialEq, Clone, Copy)] -pub enum CssMetric { - Px, - Em, -} - -impl PixelValue { - pub fn to_pixels(&self) -> f32 { - match self.metric { - CssMetric::Px => { self.number }, - CssMetric::Em => { self.number * EM_HEIGHT }, - } - } -} - -#[derive(Debug, PartialEq, Eq)] -pub enum CssBorderRadiusParseError<'a> { - TooManyValues(&'a str), - PixelParseError(PixelParseError<'a>), -} - -impl_from!(PixelParseError, CssBorderRadiusParseError, PixelParseError); - -#[derive(Debug, PartialEq)] -pub enum CssColorParseError<'a> { - InvalidColor(&'a str), - InvalidColorComponent(u8), - ValueParseErr(ParseIntError), -} - -#[derive(Debug, PartialEq)] -pub enum CssBorderParseError<'a> { - InvalidBorderStyle(InvalidValueErr<'a>), - InvalidBorderDeclaration(&'a str), - ThicknessParseError(PixelParseError<'a>), - ColorParseError(CssColorParseError<'a>), -} - -#[derive(Debug, PartialEq)] -pub enum CssShadowParseError<'a> { - InvalidSingleStatement(&'a str), - TooManyComponents(&'a str), - ValueParseErr(PixelParseError<'a>), - ColorParseError(CssColorParseError<'a>), -} - -impl_from!(PixelParseError, CssShadowParseError, ValueParseErr); -impl_from!(CssColorParseError, CssShadowParseError, ColorParseError); - -/// parse the border-radius like "5px 10px" or "5px 10px 6px 10px" -pub fn parse_css_border_radius<'a>(input: &'a str) --> Result> -{ - let mut components = input.split_whitespace(); - let len = components.clone().count(); - - match len { - 1 => { - // One value - border-radius: 15px; - // (the value applies to all four corners, which are rounded equally: - - let uniform_radius = parse_pixel_value(components.next().unwrap())?.to_pixels(); - Ok(BorderRadius::uniform(uniform_radius)) - }, - 2 => { - // Two values - border-radius: 15px 50px; - // (first value applies to top-left and bottom-right corners, - // and the second value applies to top-right and bottom-left corners): - - let top_left_bottom_right = parse_pixel_value(components.next().unwrap())?.to_pixels(); - let top_right_bottom_left = parse_pixel_value(components.next().unwrap())?.to_pixels(); - - Ok(BorderRadius{ - top_left: LayoutSize::new(top_left_bottom_right, top_left_bottom_right), - bottom_right: LayoutSize::new(top_left_bottom_right, top_left_bottom_right), - top_right: LayoutSize::new(top_right_bottom_left, top_right_bottom_left), - bottom_left: LayoutSize::new(top_right_bottom_left, top_right_bottom_left), - }) - }, - 3 => { - // Three values - border-radius: 15px 50px 30px; - // (first value applies to top-left corner, - // second value applies to top-right and bottom-left corners, - // and third value applies to bottom-right corner): - let top_left = parse_pixel_value(components.next().unwrap())?.to_pixels(); - let top_right_bottom_left = parse_pixel_value(components.next().unwrap())?.to_pixels(); - let bottom_right = parse_pixel_value(components.next().unwrap())?.to_pixels(); - - Ok(BorderRadius{ - top_left: LayoutSize::new(top_left, top_left), - bottom_right: LayoutSize::new(bottom_right, bottom_right), - top_right: LayoutSize::new(top_right_bottom_left, top_right_bottom_left), - bottom_left: LayoutSize::new(top_right_bottom_left, top_right_bottom_left), - }) - } - 4 => { - // Four values - border-radius: 15px 50px 30px 5px; - // (first value applies to top-left corner, - // second value applies to top-right corner, - // third value applies to bottom-right corner, - // fourth value applies to bottom-left corner) - let top_left = parse_pixel_value(components.next().unwrap())?.to_pixels(); - let top_right = parse_pixel_value(components.next().unwrap())?.to_pixels(); - let bottom_right = parse_pixel_value(components.next().unwrap())?.to_pixels(); - let bottom_left = parse_pixel_value(components.next().unwrap())?.to_pixels(); - - Ok(BorderRadius{ - top_left: LayoutSize::new(top_left, top_left), - bottom_right: LayoutSize::new(bottom_right, bottom_right), - top_right: LayoutSize::new(top_right, top_right), - bottom_left: LayoutSize::new(bottom_left, bottom_left), - }) - }, - _ => { - Err(CssBorderRadiusParseError::TooManyValues(input)) - } - } -} - -#[derive(Debug, PartialEq, Eq)] -pub enum PixelParseError<'a> { - InvalidComponent(&'a str), - ValueParseErr(ParseFloatError), -} - -/// parse a single value such as "15px" -pub fn parse_pixel_value<'a>(input: &'a str) --> Result> -{ - let mut split_pos = 0; - for (idx, ch) in input.char_indices() { - if ch.is_numeric() || ch == '.' { - split_pos = idx; - } - } - - split_pos += 1; - - let unit = &input[split_pos..]; - let unit = match unit { - "px" => CssMetric::Px, - "em" => CssMetric::Em, - _ => { return Err(PixelParseError::InvalidComponent(&input[(split_pos - 1)..])); } - }; - - let number = input[..split_pos].parse::().map_err(|e| PixelParseError::ValueParseErr(e))?; - - Ok(PixelValue { - metric: unit, - number: number, - }) -} - -/// Parse any valid CSS color, INCLUDING THE HASH -/// -/// "blue" -> "00FF00" -> ColorF { r: 0, g: 255, b: 0 }) -/// "#00FF00" -> ColorF { r: 0, g: 255, b: 0 }) -pub fn parse_css_color<'a>(input: &'a str) --> Result> -{ - if input.starts_with('#') { - parse_color_no_hash(&input[1..]) - } else { - parse_color_builtin(input) - } -} - -/// Parse a built-in background color -/// -/// "blue" -> "00FF00" -> ColorF { r: 0, g: 255, b: 0 }) -fn parse_color_builtin<'a>(input: &'a str) --> Result> -{ - let color = match input { - "AliceBlue" | "alice-blue" => "F0F8FF", - "AntiqueWhite" | "antique-white" => "FAEBD7", - "Aqua" | "aqua" => "00FFFF", - "Aquamarine" | "aquamarine" => "7FFFD4", - "Azure" | "azure" => "F0FFFF", - "Beige" | "beige" => "F5F5DC", - "Bisque" | "bisque" => "FFE4C4", - "Black" | "black" => "000000", - "BlanchedAlmond" | "blanched-almond" => "FFEBCD", - "Blue" | "blue" => "0000FF", - "BlueViolet" | "blue-violet" => "8A2BE2", - "Brown" | "brown" => "A52A2A", - "BurlyWood" | "burly-wood" => "DEB887", - "CadetBlue" | "cadet-blue" => "5F9EA0", - "Chartreuse" | "chartreuse" => "7FFF00", - "Chocolate" | "chocolate" => "D2691E", - "Coral" | "coral" => "FF7F50", - "CornflowerBlue" | "cornflower-blue" => "6495ED", - "Cornsilk" | "cornsilk" => "FFF8DC", - "Crimson" | "crimson" => "DC143C", - "Cyan" | "cyan" => "00FFFF", - "DarkBlue" | "dark-blue" => "00008B", - "DarkCyan" | "dark-cyan" => "008B8B", - "DarkGoldenRod" | "dark-golden-rod" => "B8860B", - "DarkGray" | "dark-gray" => "A9A9A9", - "DarkGrey" | "dark-grey" => "A9A9A9", - "DarkGreen" | "dark-green" => "006400", - "DarkKhaki" | "dark-khaki" => "BDB76B", - "DarkMagenta" | "dark-magenta" => "8B008B", - "DarkOliveGreen" | "dark-olive-green" => "556B2F", - "DarkOrange" | "dark-orange" => "FF8C00", - "DarkOrchid" | "dark-orchid" => "9932CC", - "DarkRed" | "dark-red" => "8B0000", - "DarkSalmon" | "dark-salmon" => "E9967A", - "DarkSeaGreen" | "dark-sea-green" => "8FBC8F", - "DarkSlateBlue" | "dark-slate-blue" => "483D8B", - "DarkSlateGray" | "dark-slate-gray" => "2F4F4F", - "DarkSlateGrey" | "dark-slate-grey" => "2F4F4F", - "DarkTurquoise" | "dark-turquoise" => "00CED1", - "DarkViolet" | "dark-violet" => "9400D3", - "DeepPink" | "deep-pink" => "FF1493", - "DeepSkyBlue" | "deep-sky-blue" => "00BFFF", - "DimGray" | "dim-gray" => "696969", - "DimGrey" | "dim-grey" => "696969", - "DodgerBlue" | "dodger-blue" => "1E90FF", - "FireBrick" | "fire-brick" => "B22222", - "FloralWhite" | "floral-white" => "FFFAF0", - "ForestGreen" | "forest-green" => "228B22", - "Fuchsia" | "fuchsia" => "FF00FF", - "Gainsboro" | "gainsboro" => "DCDCDC", - "GhostWhite" | "ghost-white" => "F8F8FF", - "Gold" | "gold" => "FFD700", - "GoldenRod" | "golden-rod" => "DAA520", - "Gray" | "gray" => "808080", - "Grey" | "grey" => "808080", - "Green" | "green" => "008000", - "GreenYellow" | "green-yellow" => "ADFF2F", - "HoneyDew" | "honey-dew" => "F0FFF0", - "HotPink" | "hot-pink" => "FF69B4", - "IndianRed" | "indian-red" => "CD5C5C", - "Indigo" | "indigo" => "4B0082", - "Ivory" | "ivory" => "FFFFF0", - "Khaki" | "khaki" => "F0E68C", - "Lavender" | "lavender" => "E6E6FA", - "LavenderBlush" | "lavender-blush" => "FFF0F5", - "LawnGreen" | "lawn-green" => "7CFC00", - "LemonChiffon" | "lemon-chiffon" => "FFFACD", - "LightBlue" | "light-blue" => "ADD8E6", - "LightCoral" | "light-coral" => "F08080", - "LightCyan" | "light-cyan" => "E0FFFF", - "LightGoldenRodYellow" | "light-golden-rod-yellow" => "FAFAD2", - "LightGray" | "light-gray" => "D3D3D3", - "LightGrey" | "light-grey" => "D3D3D3", - "LightGreen" | "light-green" => "90EE90", - "LightPink" | "light-pink" => "FFB6C1", - "LightSalmon" | "light-salmon" => "FFA07A", - "LightSeaGreen" | "light-sea-green" => "20B2AA", - "LightSkyBlue" | "light-sky-blue" => "87CEFA", - "LightSlateGray" | "light-slate-gray" => "778899", - "LightSlateGrey" | "light-slate-grey" => "778899", - "LightSteelBlue" | "light-steel-blue" => "B0C4DE", - "LightYellow" | "light-yellow" => "FFFFE0", - "Lime" | "lime" => "00FF00", - "LimeGreen" | "lime-green" => "32CD32", - "Linen" | "linen" => "FAF0E6", - "Magenta" | "magenta" => "FF00FF", - "Maroon" | "maroon" => "800000", - "MediumAquaMarine" | "medium-aqua-marine" => "66CDAA", - "MediumBlue" | "medium-blue" => "0000CD", - "MediumOrchid" | "medium-orchid" => "BA55D3", - "MediumPurple" | "medium-purple" => "9370DB", - "MediumSeaGreen" | "medium-sea-green" => "3CB371", - "MediumSlateBlue" | "medium-slate-blue" => "7B68EE", - "MediumSpringGreen" | "medium-spring-green" => "00FA9A", - "MediumTurquoise" | "medium-turquoise" => "48D1CC", - "MediumVioletRed" | "medium-violet-red" => "C71585", - "MidnightBlue" | "midnight-blue" => "191970", - "MintCream" | "mint-cream" => "F5FFFA", - "MistyRose" | "misty-rose" => "FFE4E1", - "Moccasin" | "moccasin" => "FFE4B5", - "NavajoWhite" | "navajo-white" => "FFDEAD", - "Navy" | "navy" => "000080", - "OldLace" | "old-lace" => "FDF5E6", - "Olive" | "olive" => "808000", - "OliveDrab" | "olive-drab" => "6B8E23", - "Orange" | "orange" => "FFA500", - "OrangeRed" | "orange-red" => "FF4500", - "Orchid" | "orchid" => "DA70D6", - "PaleGoldenRod" | "pale-golden-rod" => "EEE8AA", - "PaleGreen" | "pale-green" => "98FB98", - "PaleTurquoise" | "pale-turquoise" => "AFEEEE", - "PaleVioletRed" | "pale-violet-red" => "DB7093", - "PapayaWhip" | "papaya-whip" => "FFEFD5", - "PeachPuff" | "peach-puff" => "FFDAB9", - "Peru" | "peru" => "CD853F", - "Pink" | "pink" => "FFC0CB", - "Plum" | "plum" => "DDA0DD", - "PowderBlue" | "powder-blue" => "B0E0E6", - "Purple" | "purple" => "800080", - "RebeccaPurple" | "rebecca-purple" => "663399", - "Red" | "red" => "FF0000", - "RosyBrown" | "rosy-brown" => "BC8F8F", - "RoyalBlue" | "royal-blue" => "4169E1", - "SaddleBrown" | "saddle-brown" => "8B4513", - "Salmon" | "salmon" => "FA8072", - "SandyBrown" | "sandy-brown" => "F4A460", - "SeaGreen" | "sea-green" => "2E8B57", - "SeaShell" | "sea-shell" => "FFF5EE", - "Sienna" | "sienna" => "A0522D", - "Silver" | "silver" => "C0C0C0", - "SkyBlue" | "sky-blue" => "87CEEB", - "SlateBlue" | "slate-blue" => "6A5ACD", - "SlateGray" | "slate-gray" => "708090", - "SlateGrey" | "slate-grey" => "708090", - "Snow" | "snow" => "FFFAFA", - "SpringGreen" | "spring-green" => "00FF7F", - "SteelBlue" | "steel-blue" => "4682B4", - "Tan" | "tan" => "D2B48C", - "Teal" | "teal" => "008080", - "Thistle" | "thistle" => "D8BFD8", - "Tomato" | "tomato" => "FF6347", - "Turquoise" | "turquoise" => "40E0D0", - "Violet" | "violet" => "EE82EE", - "Wheat" | "wheat" => "F5DEB3", - "White" | "white" => "FFFFFF", - "WhiteSmoke" | "white-smoke" => "F5F5F5", - "Yellow" | "yellow" => "FFFF00", - "YellowGreen" | "yellow-green" => "9ACD32", - "Transparent" | "transparent" => "FFFFFFFF", - _ => { return Err(CssColorParseError::InvalidColor(input)); } - }; - parse_color_no_hash(color) -} - -/// Parse a background color, WITHOUT THE HASH -/// -/// "00FFFF" -> ColorF { r: 0, g: 255, b: 255}) -fn parse_color_no_hash<'a>(input: &'a str) --> Result> -{ - #[inline] - fn from_hex<'a>(c: u8) -> Result> { - match c { - b'0' ... b'9' => Ok(c - b'0'), - b'a' ... b'f' => Ok(c - b'a' + 10), - b'A' ... b'F' => Ok(c - b'A' + 10), - _ => Err(CssColorParseError::InvalidColorComponent(c)) - } - } - - match input.len() { - 3 => { - let mut input_iter = input.chars(); - - let r = input_iter.next().unwrap() as u8; - let g = input_iter.next().unwrap() as u8; - let b = input_iter.next().unwrap() as u8; - - let r = from_hex(r)? * 16 + from_hex(r)?; - let g = from_hex(g)? * 16 + from_hex(g)?; - let b = from_hex(b)? * 16 + from_hex(b)?; - - Ok(ColorU { - r: r, - g: g, - b: b, - a: 255, - }) - }, - 4 => { - let mut input_iter = input.chars(); - - let r = input_iter.next().unwrap() as u8; - let g = input_iter.next().unwrap() as u8; - let b = input_iter.next().unwrap() as u8; - let a = input_iter.next().unwrap() as u8; - - let r = from_hex(r)? * 16 + from_hex(r)?; - let g = from_hex(g)? * 16 + from_hex(g)?; - let b = from_hex(b)? * 16 + from_hex(b)?; - let a = from_hex(a)? * 16 + from_hex(a)?; - - Ok(ColorU { - r: r, - g: g, - b: b, - a: a, - }) - }, - 6 => { - let input = u32::from_str_radix(input, 16).map_err(|e| CssColorParseError::ValueParseErr(e))?; - Ok(ColorU { - r: ((input >> 16) & 255) as u8, - g: ((input >> 8) & 255) as u8, - b: (input & 255) as u8, - a: 255, - }) - }, - 8 => { - let input = u32::from_str_radix(input, 16).map_err(|e| CssColorParseError::ValueParseErr(e))?; - Ok(ColorU { - r: ((input >> 24) & 255) as u8, - g: ((input >> 16) & 255) as u8, - b: ((input >> 8) & 255) as u8, - a: (input & 255) as u8, - }) - }, - _ => { Err(CssColorParseError::InvalidColor(input)) } - } -} - -/// Parse a CSS border such as -/// -/// "5px solid red" -pub fn parse_css_border<'a>(input: &'a str) --> Result<(BorderWidths, BorderDetails), CssBorderParseError<'a>> -{ - let mut input_iter = input.split_whitespace(); - - let (thickness, style, color); - - match input_iter.clone().count() { - 1 => { - style = parse_border_style(input_iter.next().unwrap()) - .map_err(|e| CssBorderParseError::InvalidBorderStyle(e))?; - thickness = 1.0; - color = ColorU { r: 0, g: 0, b: 0, a: 255 }; - }, - 3 => { - thickness = parse_pixel_value(input_iter.next().unwrap()) - .map_err(|e| CssBorderParseError::ThicknessParseError(e))?.to_pixels(); - style = parse_border_style(input_iter.next().unwrap()) - .map_err(|e| CssBorderParseError::InvalidBorderStyle(e))?; - color = parse_css_color(input_iter.next().unwrap()) - .map_err(|e| CssBorderParseError::ColorParseError(e))?; - }, - _ => { - return Err(CssBorderParseError::InvalidBorderDeclaration(input)); - } - } - - let border_widths = BorderWidths { - top: thickness, - left: thickness, - right: thickness, - bottom: thickness, - }; - - let border_side = BorderSide { - color: color.into(), - style: style, - }; - - let border_details = BorderDetails::Normal(NormalBorder { - top: border_side, - left: border_side, - right: border_side, - bottom: border_side, - radius: BorderRadius::zero(), - }); - - Ok((border_widths, border_details)) -} - -/// Parse a border style such as "none", "dotted", etc. -/// -/// "solid", "none", etc. -multi_type_parser!(parse_border_style, BorderStyle, - ["none", None], - ["solid", Solid], - ["double", Double], - ["dotted", Dotted], - ["dashed", Dashed], - ["hidden", Hidden], - ["groove", Groove], - ["ridge", Ridge], - ["inset", Inset], - ["outset", Outset]); - -// missing BorderRadius & LayoutRect -#[derive(Debug, Copy, Clone, PartialEq)] -pub struct BoxShadowPreDisplayItem { - pub offset: LayoutVector2D, - pub color: ColorF, - pub blur_radius: f32, - pub spread_radius: f32, - pub clip_mode: BoxShadowClipMode, -} - -/// Parses a CSS box-shadow -pub fn parse_css_box_shadow<'a>(input: &'a str) --> Result, CssShadowParseError<'a>> -{ - let mut input_iter = input.split_whitespace(); - let count = input_iter.clone().count(); - - let mut box_shadow = BoxShadowPreDisplayItem { - offset: LayoutVector2D::zero(), - color: ColorF { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, - blur_radius: 0.0, - spread_radius: 0.0, - clip_mode: BoxShadowClipMode::Outset, - }; - - let last_val = input_iter.clone().rev().next(); - let is_inset = last_val == Some("inset") || last_val == Some("outset"); - - if count > 2 && is_inset { - let l_val = last_val.unwrap(); - if l_val == "outset" { - box_shadow.clip_mode = BoxShadowClipMode::Outset; - } else if l_val == "inset" { - box_shadow.clip_mode = BoxShadowClipMode::Inset; - } - } - - match count { - 1 => { - // box-shadow: none; - match input_iter.next().unwrap() { - "none" => return Ok(None), - _ => return Err(CssShadowParseError::InvalidSingleStatement(input)), - } - }, - 2 => { - // box-shadow: 5px 10px; (h_offset, v_offset) - let h_offset = parse_pixel_value(input_iter.next().unwrap())?.to_pixels(); - let v_offset = parse_pixel_value(input_iter.next().unwrap())?.to_pixels(); - box_shadow.offset.x = h_offset; - box_shadow.offset.y = v_offset; - }, - 3 => { - // box-shadow: 5px 10px inset; (h_offset, v_offset, inset) - let h_offset = parse_pixel_value(input_iter.next().unwrap())?.to_pixels(); - let v_offset = parse_pixel_value(input_iter.next().unwrap())?.to_pixels(); - box_shadow.offset.x = h_offset; - box_shadow.offset.y = v_offset; - - if !is_inset { - // box-shadow: 5px 10px #888888; (h_offset, v_offset, color) - let color = parse_css_color(input_iter.next().unwrap())?; - box_shadow.color = ColorF::from(color); - } - }, - 4 => { - let h_offset = parse_pixel_value(input_iter.next().unwrap())?.to_pixels(); - let v_offset = parse_pixel_value(input_iter.next().unwrap())?.to_pixels(); - box_shadow.offset.x = h_offset; - box_shadow.offset.y = v_offset; - - if !is_inset { - let blur = parse_pixel_value(input_iter.next().unwrap())?.to_pixels(); - box_shadow.blur_radius = blur.into(); - } - - let color = parse_css_color(input_iter.next().unwrap())?; - box_shadow.color = ColorF::from(color); - }, - 5 => { - // box-shadow: 5px 10px 5px 10px #888888; (h_offset, v_offset, blur, spread, color) - // box-shadow: 5px 10px 5px #888888 inset; (h_offset, v_offset, blur, color, inset) - let h_offset = parse_pixel_value(input_iter.next().unwrap())?.to_pixels(); - let v_offset = parse_pixel_value(input_iter.next().unwrap())?.to_pixels(); - box_shadow.offset.x = h_offset; - box_shadow.offset.y = v_offset; - - let blur = parse_pixel_value(input_iter.next().unwrap())?.to_pixels(); - box_shadow.blur_radius = blur.into(); - - if !is_inset { - let spread = parse_pixel_value(input_iter.next().unwrap())?.to_pixels(); - box_shadow.spread_radius = spread.into(); - } - - let color = parse_css_color(input_iter.next().unwrap())?; - box_shadow.color = ColorF::from(color); - }, - 6 => { - // box-shadow: 5px 10px 5px 10px #888888 inset; (h_offset, v_offset, blur, spread, color, inset) - let h_offset = parse_pixel_value(input_iter.next().unwrap())?.to_pixels(); - let v_offset = parse_pixel_value(input_iter.next().unwrap())?.to_pixels(); - box_shadow.offset.x = h_offset; - box_shadow.offset.y = v_offset; - - let blur = parse_pixel_value(input_iter.next().unwrap())?.to_pixels(); - box_shadow.blur_radius = blur.into(); - - let spread = parse_pixel_value(input_iter.next().unwrap())?.to_pixels(); - box_shadow.spread_radius = spread.into(); - - let color = parse_css_color(input_iter.next().unwrap())?; - box_shadow.color = ColorF::from(color); - } - _ => { - return Err(CssShadowParseError::TooManyComponents(input)); - } - } - - Ok(Some(box_shadow)) -} - -#[derive(Debug, PartialEq)] -pub enum CssBackgroundParseError<'a> { - Error(&'a str), - InvalidBackground(&'a str), - UnclosedGradient(&'a str), - NoDirection(&'a str), - TooFewGradientStops(&'a str), - DirectionParseError(CssDirectionParseError<'a>), - GradientParseError(CssGradientStopParseError<'a>), - ShapeParseError(CssShapeParseError<'a>), -} - -impl_from!(CssDirectionParseError, CssBackgroundParseError, DirectionParseError); -impl_from!(CssGradientStopParseError, CssBackgroundParseError, GradientParseError); -impl_from!(CssShapeParseError, CssBackgroundParseError, ShapeParseError); - -#[derive(Debug, Clone, PartialEq)] -pub enum ParsedGradient { - LinearGradient(LinearGradientPreInfo), - RadialGradient(RadialGradientPreInfo), -} - -#[derive(Debug, Clone, PartialEq)] -pub struct LinearGradientPreInfo { - pub direction: Direction, - pub extend_mode: ExtendMode, - pub stops: Vec, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct RadialGradientPreInfo { - pub shape: Shape, - pub extend_mode: ExtendMode, - pub stops: Vec, -} - -#[derive(Debug, Copy, Clone, PartialEq)] -pub enum Direction { - Angle(f32), - FromTo(DirectionCorner, DirectionCorner), -} - -impl Direction { - /// Calculates the point for the bounds - pub fn to_points(&self, rect: &LayoutRect) - -> (LayoutPoint, LayoutPoint) - { - match *self { - Direction::Angle(ref deg) => { - // todo!! - let mut point: LayoutPoint = TypedPoint2D::new(rect.size.width, rect.size.height); - let rot = TypedRotation2D::new(Angle::radians(deg.to_radians())); - (LayoutPoint::zero(), rot.transform_point(&point)) - }, - Direction::FromTo(ref from, ref to) => { - (from.to_point(rect), to.to_point(rect)) - } - } - } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum Shape { - Ellipse, - Circle, -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum DirectionCorner { - Right, - Left, - Top, - Bottom, - TopRight, - TopLeft, - BottomRight, - BottomLeft, -} - -impl DirectionCorner { - - pub fn opposite(&self) -> Self { - use self::DirectionCorner::*; - match *self { - Right => Left, - Left => Right, - Top => Bottom, - Bottom => Top, - TopRight => BottomLeft, - BottomLeft => TopRight, - TopLeft => BottomRight, - BottomRight => TopLeft, - } - } - - pub fn combine(&self, other: &Self) -> Option { - use self::DirectionCorner::*; - match (*self, *other) { - (Right, Top) | (Top, Right) => Some(TopRight), - (Left, Top) | (Top, Left) => Some(TopLeft), - (Right, Bottom) | (Bottom, Right) => Some(BottomRight), - (Left, Bottom) | (Bottom, Left) => Some(BottomLeft), - _ => { None } - } - } - - pub fn to_point(&self, rect: &LayoutRect) -> TypedPoint2D - { - use self::DirectionCorner::*; - match *self { - Right => TypedPoint2D::new(rect.size.width, (rect.size.height / 2.0)), - Left => TypedPoint2D::new(0.0, (rect.size.height / 2.0)), - Top => TypedPoint2D::new((rect.size.width / 2.0), 0.0), - Bottom => TypedPoint2D::new((rect.size.width / 2.0), rect.size.height), - TopRight => TypedPoint2D::new(rect.size.width, 0.0), - TopLeft => TypedPoint2D::new(0.0, 0.0), - BottomRight => TypedPoint2D::new(rect.size.width, rect.size.height), - BottomLeft => TypedPoint2D::new(0.0, rect.size.height), - } - } -} - -// parses a background, such as "linear-gradient(red, green)" -pub fn parse_css_background<'a>(input: &'a str) --> Result> -{ - #[derive(PartialEq)] - enum GradientType { - LinearGradient, - RepeatingLinearGradient, - RadialGradient, - RepeatingRadialGradient, - } - - let mut input_iter = input.splitn(2, "("); - let first_item = input_iter.next(); - - let gradient_type = match first_item { - Some("linear-gradient") => GradientType::LinearGradient, - Some("repeating-linear-gradient") => GradientType::RepeatingLinearGradient, - Some("radial-gradient") => GradientType::RadialGradient, - Some("repeating-radial-gradient") => GradientType::RepeatingRadialGradient, - _ => { return Err(CssBackgroundParseError::InvalidBackground(first_item.unwrap())); } // failure here - }; - - let next_item = match input_iter.next() { - Some(s) => { s }, - None => return Err(CssBackgroundParseError::InvalidBackground(input)), - }; - - let mut brace_iter = next_item.rsplitn(2, ')'); - brace_iter.next(); // important - let brace_contents = brace_iter.clone().next(); - - if brace_contents.is_none() { - // invalid or empty brace - return Err(CssBackgroundParseError::UnclosedGradient(input)); - } - - // brace_contents contains "red, yellow, etc" - let brace_contents = brace_contents.unwrap(); - let mut brace_iterator = brace_contents.split(','); - - let mut gradient_stop_count = brace_iterator.clone().count(); - - // "50deg", "to right bottom", etc. - let first_brace_item = match brace_iterator.next() { - Some(s) => s, - None => return Err(CssBackgroundParseError::NoDirection(input)), - }; - - // default shape: ellipse - let mut shape = Shape::Ellipse; - // default gradient: from top to bottom - let mut direction = Direction::FromTo(DirectionCorner::Top, DirectionCorner::Bottom); - - let mut first_is_direction = false; - let mut first_is_shape = false; - let is_linear_gradient = gradient_type == GradientType::LinearGradient || gradient_type == GradientType::RepeatingLinearGradient; - let is_radial_gradient = gradient_type == GradientType::RadialGradient || gradient_type == GradientType::RepeatingRadialGradient; - - if is_linear_gradient { - if let Ok(dir) = parse_direction(first_brace_item) { - direction = dir; - first_is_direction = true; - } - } - - if is_radial_gradient { - if let Ok(sh) = parse_shape(first_brace_item) { - shape = sh; - first_is_shape = true; - } - } - - let mut first_item_doesnt_count = false; - if (is_linear_gradient && first_is_direction) || (is_radial_gradient && first_is_shape) { - gradient_stop_count -= 1; // first item is not a gradient stop - first_item_doesnt_count = true; - } - - if gradient_stop_count < 2 { - return Err(CssBackgroundParseError::TooFewGradientStops(input)); - } - - let mut color_stops = Vec::::with_capacity(gradient_stop_count); - if !first_item_doesnt_count { - color_stops.push(parse_gradient_stop(first_brace_item)?); - } - - for stop in brace_iterator { - color_stops.push(parse_gradient_stop(stop)?); - } - - // correct percentages - let mut last_stop = 0.0_f32; - let mut increase_stop_cnt: Option = None; - - let color_stop_len = color_stops.len(); - 'outer: for i in 0..color_stop_len { - let offset = color_stops[i].offset; - match offset { - Some(s) => { - last_stop = s; - increase_stop_cnt = None; - }, - None => { - let (_, next) = color_stops.split_at_mut(i); - - if let Some(increase_stop_cnt) = increase_stop_cnt { - last_stop += increase_stop_cnt; - next[0].offset = Some(last_stop); - continue 'outer; - } - - let mut next_count: u32 = 0; - let mut next_value = None; - - // iterate until we find a value where the offset isn't none - { - let mut next_iter = next.iter(); - next_iter.next(); - 'inner: for next_stop in next_iter { - if let Some(off) = next_stop.offset { - next_value = Some(off); - break 'inner; - } else { - next_count += 1; - } - } - } - - let next_value = next_value.unwrap_or(1.0_f32); - let increase = (next_value - last_stop) / (next_count as f32); - increase_stop_cnt = Some(increase); - if next_count == 1 && (color_stop_len - i) == 1 { - next[0].offset = Some(last_stop); - } else { - if i == 0 { - next[0].offset = Some(0.0); - } else { - next[0].offset = Some(last_stop); - // last_stop += increase; - } - } - } - } - } - - match gradient_type { - GradientType::LinearGradient => { - Ok(ParsedGradient::LinearGradient(LinearGradientPreInfo { - direction: direction, - extend_mode: ExtendMode::Clamp, - stops: color_stops, - })) - }, - GradientType::RepeatingLinearGradient => { - Ok(ParsedGradient::LinearGradient(LinearGradientPreInfo { - direction: direction, - extend_mode: ExtendMode::Repeat, - stops: color_stops, - })) - }, - GradientType::RadialGradient => { - Ok(ParsedGradient::RadialGradient(RadialGradientPreInfo { - shape: shape, - extend_mode: ExtendMode::Clamp, - stops: color_stops, - })) - }, - GradientType::RepeatingRadialGradient => { - Ok(ParsedGradient::RadialGradient(RadialGradientPreInfo { - shape: shape, - extend_mode: ExtendMode::Repeat, - stops: color_stops, - })) - } - } -} - -#[derive(Debug, PartialEq)] -pub enum CssGradientStopParseError<'a> { - Error(&'a str), - ColorParseError(CssColorParseError<'a>), -} - -#[derive(Debug, Clone, PartialEq)] -pub struct GradientStopPre { - pub offset: Option, // this is set to None if there was no offset that could be parsed - pub color: ColorF, -} - -// parses "red" , "red 5%" -fn parse_gradient_stop<'a>(input: &'a str) --> Result> -{ - let mut input_iter = input.split_whitespace(); - let first_item = input_iter.next().ok_or(CssGradientStopParseError::Error(input))?; - let color = ColorF::from(parse_css_color(first_item).map_err(|e| CssGradientStopParseError::ColorParseError(e))?); - let second_item = match input_iter.next() { - None => return Ok(GradientStopPre { offset: None, color: color }), - Some(s) => s, - }; - let percentage = parse_percentage(second_item); - Ok(GradientStopPre { offset: percentage, color: color }) -} - -// parses "5%" -> 5 -fn parse_percentage(input: &str) --> Option -{ - let mut input_iter = input.rsplitn(2, '%'); - let perc = input_iter.next(); - if perc.is_none() { - None - } else { - input_iter.next()?.parse::().ok() - } -} - -#[derive(Debug, PartialEq)] -pub enum CssDirectionParseError<'a> { - Error(&'a str), - InvalidArguments(&'a str), - ParseFloat(ParseFloatError), - CornerError(CssDirectionCornerParseError<'a>), -} - -impl<'a> From for CssDirectionParseError<'a> { - fn from(e: ParseFloatError) -> Self { - CssDirectionParseError::ParseFloat(e) - } -} - -impl<'a> From> for CssDirectionParseError<'a> { - fn from(e: CssDirectionCornerParseError<'a>) -> Self { - CssDirectionParseError::CornerError(e) - } -} - -// parses "50deg", "to right bottom" -fn parse_direction<'a>(input: &'a str) --> Result> -{ - use std::f32::consts::PI; - - let input_iter = input.split_whitespace(); - let count = input_iter.clone().count(); - let mut first_input_iter = input_iter.clone(); - // "50deg" | "to" | "right" - let first_input = first_input_iter.next().ok_or(CssDirectionParseError::Error(input))?; - - enum AngleType { - Deg, - Rad, - Gon, - } - - let angle = { - if first_input.ends_with("deg") { Some(AngleType::Deg) } - else if first_input.ends_with("rad") { Some(AngleType::Rad) } - else if first_input.ends_with("grad") { Some(AngleType::Gon) } - else { None } - }; - - if let Some(angle_type) = angle { - match angle_type { - AngleType::Deg => { return Ok(Direction::Angle(first_input.split("deg").next().unwrap().parse::()?)); } - AngleType::Rad => { return Ok(Direction::Angle(first_input.split("rad").next().unwrap().parse::()? * 180.0 * PI)); } - AngleType::Gon => { return Ok(Direction::Angle(first_input.split("grad").next().unwrap().parse::()? / 400.0 * 360.0)); } - } - } - - // if we get here, the input is definitely not an angle - - if first_input != "to" { - return Err(CssDirectionParseError::InvalidArguments(input)); - } - - let second_input = first_input_iter.next().ok_or(CssDirectionParseError::Error(input))?; - let end = parse_direction_corner(second_input)?; - - match count { - 2 => { - // "to right" - let start = end.opposite(); - Ok(Direction::FromTo(start, end)) - }, - 3 => { - // "to bottom right" - let beginning = end; - let third_input = first_input_iter.next().ok_or(CssDirectionParseError::Error(input))?; - let new_end = parse_direction_corner(third_input)?; - // "Bottom, Right" -> "BottomRight" - let new_end = beginning.combine(&new_end).ok_or(CssDirectionParseError::Error(input))?; - let start = new_end.opposite(); - Ok(Direction::FromTo(start, new_end)) - }, - _ => { Err(CssDirectionParseError::InvalidArguments(input)) } - } -} - -#[derive(Debug, PartialEq)] -pub enum CssDirectionCornerParseError<'a> { - InvalidDirection(&'a str), -} - -fn parse_direction_corner<'a>(input: &'a str) --> Result> -{ - match input { - "right" => Ok(DirectionCorner::Right), - "left" => Ok(DirectionCorner::Left), - "top" => Ok(DirectionCorner::Top), - "bottom" => Ok(DirectionCorner::Bottom), - _ => { Err(CssDirectionCornerParseError::InvalidDirection(input))} - } -} - -#[derive(Debug, PartialEq)] -pub enum CssShapeParseError<'a> { - ShapeErr(InvalidValueErr<'a>), -} - -#[derive(Debug, PartialEq, Copy, Clone)] -pub struct LayoutWidth(pub PixelValue); -#[derive(Debug, PartialEq, Copy, Clone)] -pub struct LayoutMinWidth(pub PixelValue); -#[derive(Debug, PartialEq, Copy, Clone)] -pub struct LayoutHeight(pub PixelValue); -#[derive(Debug, PartialEq, Copy, Clone)] -pub struct LayoutMinHeight(pub PixelValue); - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum LayoutDirection { - Horizontal, - Vertical, -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum LayoutWrap { - Wrap, - NoWrap, -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum LayoutJustifyContent { - /// Default value. Items are positioned at the beginning of the container - Start, - /// Items are positioned at the end of the container - End, - /// Items are positioned at the center of the container - Center, - /// Items are positioned with space between the lines - SpaceBetween, - /// Items are positioned with space before, between, and after the lines - SpaceAround, -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum LayoutAlignItems { - /// Items are stretched to fit the container - Stretch, - /// Items are positioned at the center of the container - Center, - /// Items are positioned at the beginning of the container - Start, - /// Items are positioned at the end of the container - End, -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum LayoutAlignContent { - /// Default value. Lines stretch to take up the remaining space - Stretch, - /// Lines are packed toward the center of the flex container - Center, - /// Lines are packed toward the start of the flex container - Start, - /// Lines are packed toward the end of the flex container - End, - /// Lines are evenly distributed in the flex container - SpaceBetween, - /// Lines are evenly distributed in the flex container, with half-size spaces on either end - SpaceAround, -} - -#[derive(Default, Debug, Clone, PartialEq)] -pub(crate) struct RectStyle { - /// Background color of this rectangle - pub(crate) background_color: Option, - /// Shadow color - pub(crate) box_shadow: Option, - /// Gradient (location) + stops - pub(crate) background: Option, - /// Border - pub(crate) border: Option<(BorderWidths, BorderDetails)>, - /// border radius - pub(crate) border_radius: Option, -} - -// Layout constraints for a given rectangle, such as "" -#[derive(Default, Debug, Copy, Clone, PartialEq)] -pub struct RectLayout { - pub width: Option, - pub height: Option, - pub min_width: Option, - pub min_height: Option, - pub direction: Option, - pub wrap: Option, - pub justify_content: Option, - pub align_items: Option, - pub align_content: Option, -} - -typed_pixel_value_parser!(parse_layout_width, LayoutWidth); -typed_pixel_value_parser!(parse_layout_height, LayoutHeight); -typed_pixel_value_parser!(parse_layout_min_height, LayoutMinHeight); -typed_pixel_value_parser!(parse_layout_min_width, LayoutMinWidth); - -multi_type_parser!(parse_layout_direction, LayoutDirection, - ["row", Horizontal], - ["column", Vertical]); - -multi_type_parser!(parse_layout_wrap, LayoutWrap, - ["wrap", Wrap], - ["nowrap", NoWrap]); - -multi_type_parser!(parse_layout_justify_content, LayoutJustifyContent, - ["start", Start], - ["end", End], - ["center", Center], - ["space-between", SpaceBetween], - ["space-around", SpaceAround]); - -multi_type_parser!(parse_layout_align_items, LayoutAlignItems, - ["stretch", Stretch], - ["start", Start], - ["end", End], - ["center", Center]); - -multi_type_parser!(parse_layout_align_content, LayoutAlignContent, - ["stretch", Stretch], - ["start", Start], - ["end", End], - ["center", Center], - ["space-between", SpaceBetween], - ["space-around", SpaceAround]); - -multi_type_parser!(parse_shape, Shape, - ["circle", Circle], - ["ellipse", Ellipse]); - -#[test] -fn test_parse_box_shadow_1() { - assert_eq!(parse_css_box_shadow("none"), Ok(None)); -} - -#[test] -fn test_parse_box_shadow_2() { - assert_eq!(parse_css_box_shadow("5px 10px"), Ok(Some(BoxShadowPreDisplayItem { - offset: LayoutVector2D::new(5.0, 10.0), - color: ColorF { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, - blur_radius: 0.0, - spread_radius: 0.0, - clip_mode: BoxShadowClipMode::Outset, - }))); -} - -#[test] -fn test_parse_box_shadow_3() { - assert_eq!(parse_css_box_shadow("5px 10px #888888"), Ok(Some(BoxShadowPreDisplayItem { - offset: LayoutVector2D::new(5.0, 10.0), - color: ColorF { r: 0.53333336, g: 0.53333336, b: 0.53333336, a: 1.0 }, - blur_radius: 0.0, - spread_radius: 0.0, - clip_mode: BoxShadowClipMode::Outset, - }))); -} - -#[test] -fn test_parse_box_shadow_4() { - assert_eq!(parse_css_box_shadow("5px 10px inset"), Ok(Some(BoxShadowPreDisplayItem { - offset: LayoutVector2D::new(5.0, 10.0), - color: ColorF { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, - blur_radius: 0.0, - spread_radius: 0.0, - clip_mode: BoxShadowClipMode::Inset, - }))); -} - -#[test] -fn test_parse_box_shadow_5() { - assert_eq!(parse_css_box_shadow("5px 10px outset"), Ok(Some(BoxShadowPreDisplayItem { - offset: LayoutVector2D::new(5.0, 10.0), - color: ColorF { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, - blur_radius: 0.0, - spread_radius: 0.0, - clip_mode: BoxShadowClipMode::Outset, - }))); -} - -#[test] -fn test_parse_box_shadow_6() { - assert_eq!(parse_css_box_shadow("5px 10px 5px #888888"), Ok(Some(BoxShadowPreDisplayItem { - offset: LayoutVector2D::new(5.0, 10.0), - color: ColorF { r: 0.53333336, g: 0.53333336, b: 0.53333336, a: 1.0 }, - blur_radius: 5.0, - spread_radius: 0.0, - clip_mode: BoxShadowClipMode::Outset, - }))); -} - -#[test] -fn test_parse_box_shadow_7() { - assert_eq!(parse_css_box_shadow("5px 10px #888888 inset"), Ok(Some(BoxShadowPreDisplayItem { - offset: LayoutVector2D::new(5.0, 10.0), - color: ColorF { r: 0.53333336, g: 0.53333336, b: 0.53333336, a: 1.0 }, - blur_radius: 0.0, - spread_radius: 0.0, - clip_mode: BoxShadowClipMode::Inset, - }))); -} - -#[test] -fn test_parse_box_shadow_8() { - assert_eq!(parse_css_box_shadow("5px 10px 5px #888888 inset"), Ok(Some(BoxShadowPreDisplayItem { - offset: LayoutVector2D::new(5.0, 10.0), - color: ColorF { r: 0.53333336, g: 0.53333336, b: 0.53333336, a: 1.0 }, - blur_radius: 5.0, - spread_radius: 0.0, - clip_mode: BoxShadowClipMode::Inset, - }))); -} - -#[test] -fn test_parse_box_shadow_9() { - assert_eq!(parse_css_box_shadow("5px 10px 5px 10px #888888"), Ok(Some(BoxShadowPreDisplayItem { - offset: LayoutVector2D::new(5.0, 10.0), - color: ColorF { r: 0.53333336, g: 0.53333336, b: 0.53333336, a: 1.0 }, - blur_radius: 5.0, - spread_radius: 10.0, - clip_mode: BoxShadowClipMode::Outset, - }))); -} - -#[test] -fn test_parse_box_shadow_10() { - assert_eq!(parse_css_box_shadow("5px 10px 5px 10px #888888 inset"), Ok(Some(BoxShadowPreDisplayItem { - offset: LayoutVector2D::new(5.0, 10.0), - color: ColorF { r: 0.53333336, g: 0.53333336, b: 0.53333336, a: 1.0 }, - blur_radius: 5.0, - spread_radius: 10.0, - clip_mode: BoxShadowClipMode::Inset, - }))); -} - -#[test] -fn test_parse_css_border_1() { - assert_eq!(parse_css_border("5px solid red"), Ok((BorderWidths { - top: 5.0, - bottom: 5.0, - left: 5.0, - right: 5.0, - }, BorderDetails::Normal(NormalBorder { - left: BorderSide { - color: ColorF { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, - style: BorderStyle::Solid, - }, - right: BorderSide { - color: ColorF { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, - style: BorderStyle::Solid, - }, - bottom: BorderSide { - color: ColorF { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, - style: BorderStyle::Solid, - }, - top: BorderSide { - color: ColorF { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, - style: BorderStyle::Solid, - }, - radius: BorderRadius::zero(), - })))); -} - -#[test] -fn test_parse_css_border_2() { - assert_eq!(parse_css_border("double"), Ok((BorderWidths { - top: 1.0, - bottom: 1.0, - left: 1.0, - right: 1.0, - }, BorderDetails::Normal(NormalBorder { - left: BorderSide { - color: ColorF { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, - style: BorderStyle::Double, - }, - right: BorderSide { - color: ColorF { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, - style: BorderStyle::Double, - }, - bottom: BorderSide { - color: ColorF { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, - style: BorderStyle::Double, - }, - top: BorderSide { - color: ColorF { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, - style: BorderStyle::Double, - }, - radius: BorderRadius::zero(), - })))); -} - -#[test] -fn test_parse_linear_gradient_1() { - assert_eq!(parse_css_background("linear-gradient(red, yellow)"), - Ok(ParsedGradient::LinearGradient(LinearGradientPreInfo { - direction: Direction::FromTo(DirectionCorner::Top, DirectionCorner::Bottom), - extend_mode: ExtendMode::Clamp, - stops: vec![GradientStopPre { - offset: Some(0.0), - color: ColorF { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, - }, - GradientStopPre { - offset: Some(1.0), - color: ColorF { r: 1.0, g: 1.0, b: 0.0, a: 1.0 }, - }], - }))); -} - -#[test] -fn test_parse_linear_gradient_2() { - assert_eq!(parse_css_background("linear-gradient(red, lime, blue, yellow)"), - Ok(ParsedGradient::LinearGradient(LinearGradientPreInfo { - direction: Direction::FromTo(DirectionCorner::Top, DirectionCorner::Bottom), - extend_mode: ExtendMode::Clamp, - stops: vec![GradientStopPre { - offset: Some(0.0), - color: ColorF { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, - }, - GradientStopPre { - offset: Some(0.33333334), - color: ColorF { r: 0.0, g: 1.0, b: 0.0, a: 1.0 }, - }, - GradientStopPre { - offset: Some(0.66666667), - color: ColorF { r: 0.0, g: 0.0, b: 1.0, a: 1.0 }, - }, - GradientStopPre { - offset: Some(1.0), - color: ColorF { r: 1.0, g: 1.0, b: 0.0, a: 1.0 }, - }], - }))); -} - -#[test] -fn test_parse_linear_gradient_3() { - assert_eq!(parse_css_background("repeating-linear-gradient(50deg, blue, yellow, #00FF00)"), - Ok(ParsedGradient::LinearGradient(LinearGradientPreInfo { - direction: Direction::Angle(50.0), - extend_mode: ExtendMode::Repeat, - stops: vec![ - GradientStopPre { - offset: Some(0.0), - color: ColorF { r: 0.0, g: 0.0, b: 1.0, a: 1.0 }, - }, - GradientStopPre { - offset: Some(0.5), - color: ColorF { r: 1.0, g: 1.0, b: 0.0, a: 1.0 }, - }, - GradientStopPre { - offset: Some(1.0), - color: ColorF { r: 0.0, g: 1.0, b: 0.0, a: 1.0 }, - }], - }))); -} - -#[test] -fn test_parse_linear_gradient_4() { - assert_eq!(parse_css_background("linear-gradient(to bottom right, red, yellow)"), - Ok(ParsedGradient::LinearGradient(LinearGradientPreInfo { - direction: Direction::FromTo(DirectionCorner::TopLeft, DirectionCorner::BottomRight), - extend_mode: ExtendMode::Clamp, - stops: vec![GradientStopPre { - offset: Some(0.0), - color: ColorF { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, - }, - GradientStopPre { - offset: Some(1.0), - color: ColorF { r: 1.0, g: 1.0, b: 0.0, a: 1.0 }, - }], - }))); -} - -#[test] -fn test_parse_radial_gradient_1() { - assert_eq!(parse_css_background("radial-gradient(circle, lime, blue, yellow)"), - Ok(ParsedGradient::RadialGradient(RadialGradientPreInfo { - shape: Shape::Circle, - extend_mode: ExtendMode::Clamp, - stops: vec![ - GradientStopPre { - offset: Some(0.0), - color: ColorF { r: 0.0, g: 1.0, b: 0.0, a: 1.0 }, - }, - GradientStopPre { - offset: Some(0.5), - color: ColorF { r: 0.0, g: 0.0, b: 1.0, a: 1.0 }, - }, - GradientStopPre { - offset: Some(1.0), - color: ColorF { r: 1.0, g: 1.0, b: 0.0, a: 1.0 }, - }], - }))); -} - -// This test currently fails, but it's not that important to fix right now -/* -#[test] -fn test_parse_radial_gradient_2() { - assert_eq!(parse_css_background("repeating-radial-gradient(circle, red 10%, blue 50%, lime, yellow)"), - Ok(ParsedGradient::RadialGradient(RadialGradientPreInfo { - shape: Shape::Circle, - extend_mode: ExtendMode::Repeat, - stops: vec![ - GradientStopPre { - offset: Some(0.1), - color: ColorF { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, - }, - GradientStopPre { - offset: Some(0.5), - color: ColorF { r: 0.0, g: 0.0, b: 1.0, a: 1.0 }, - }, - GradientStopPre { - offset: Some(0.75), - color: ColorF { r: 0.0, g: 1.0, b: 0.0, a: 1.0 }, - }, - GradientStopPre { - offset: Some(1.0), - color: ColorF { r: 1.0, g: 1.0, b: 0.0, a: 1.0 }, - }], - }))); -} -*/ - -#[test] -fn test_parse_css_color_1() { - assert_eq!(parse_css_color("#F0F8FF"), Ok(ColorU { r: 240, g: 248, b: 255, a: 255 })); -} - -#[test] -fn test_parse_css_color_2() { - assert_eq!(parse_css_color("#F0F8FF00"), Ok(ColorU { r: 240, g: 248, b: 255, a: 0 })); -} - -#[test] -fn test_parse_css_color_3() { - assert_eq!(parse_css_color("#EEE"), Ok(ColorU { r: 238, g: 238, b: 238, a: 255 })); -} - -#[test] -fn test_parse_pixel_value_1() { - assert_eq!(parse_pixel_value("15px"), Ok(PixelValue { metric: CssMetric::Px, number: 15.0 })); -} - -#[test] -fn test_parse_pixel_value_2() { - assert_eq!(parse_pixel_value("1.2em"), Ok(PixelValue { metric: CssMetric::Em, number: 1.2 })); -} - -#[test] -fn test_parse_pixel_value_3() { - assert_eq!(parse_pixel_value("aslkfdjasdflk"), Err(PixelParseError::InvalidComponent("aslkfdjasdflk"))); -} - -#[test] -fn test_parse_css_border_radius_1() { - assert_eq!(parse_css_border_radius("15px"), Ok(BorderRadius::uniform(15.0))); -} - -#[test] -fn test_parse_css_border_radius_2() { - assert_eq!(parse_css_border_radius("15px 50px"), Ok(BorderRadius { - top_left: LayoutSize::new(15.0, 15.0), - bottom_right: LayoutSize::new(15.0, 15.0), - top_right: LayoutSize::new(50.0, 50.0), - bottom_left: LayoutSize::new(50.0, 50.0), - })); -} - -#[test] -fn test_parse_css_border_radius_3() { - assert_eq!(parse_css_border_radius("15px 50px 30px"), Ok(BorderRadius { - top_left: LayoutSize::new(15.0, 15.0), - bottom_right: LayoutSize::new(30.0, 30.0), - top_right: LayoutSize::new(50.0, 50.0), - bottom_left: LayoutSize::new(50.0, 50.0), - })); -} - -#[test] -fn test_parse_css_border_radius_4() { - assert_eq!(parse_css_border_radius("15px 50px 30px 5px"), Ok(BorderRadius { - top_left: LayoutSize::new(15.0, 15.0), - bottom_right: LayoutSize::new(30.0, 30.0), - top_right: LayoutSize::new(50.0, 50.0), - bottom_left: LayoutSize::new(5.0, 5.0), - })); -} \ No newline at end of file diff --git a/src/display_list.rs b/src/display_list.rs deleted file mode 100644 index dab03e0d4..000000000 --- a/src/display_list.rs +++ /dev/null @@ -1,327 +0,0 @@ -#![allow(unused_variables)] -#![allow(unused_macros)] - -use webrender::api::*; -use traits::LayoutScreen; -use constraints::{DisplayRect, CssConstraint}; -use ui_description::{UiDescription, StyledNode}; -use cassowary::{Constraint, Solver, Variable}; -use window::{WindowDimensions, UiSolver}; -use id_tree::{Arena, NodeId}; -use css_parser::*; -use dom::NodeData; -use css::Css; -use std::collections::BTreeMap; -use FastHashMap; -use cache::DomChangeSet; -use std::sync::atomic::{Ordering, AtomicUsize}; - -pub(crate) struct DisplayList<'a, T: LayoutScreen + 'a> { - pub(crate) ui_descr: &'a UiDescription, - pub(crate) rectangles: BTreeMap> -} - -#[derive(Debug)] -pub(crate) struct DisplayRectangle<'a> { - /// `Some(id)` if this rectangle has a callback attached to it - /// Note: this is not the same as the `NodeId`! - /// These two are completely seperate numbers! - pub tag: Option, - /// The original styled node - pub(crate) styled_node: &'a StyledNode, - /// The style properties of the node, parsed - pub(crate) style: RectStyle, - /// The layout properties of the node, parsed - pub(crate) layout: RectLayout, -} - -/// It is not very efficient to re-create constraints on every call, the difference -/// in performance can be huge. Without re-creating constraints, solving can take 0.3 ms, -/// with re-creation it can take up to 9 ms. So the goal is to not re-create constraints -/// if their contents haven't changed. -#[derive(Default)] -pub(crate) struct SolvedLayout { - // List of previously solved constraints - pub(crate) solved_constraints: FastHashMap>, -} - -impl SolvedLayout { - pub fn empty() -> Self { - Self { - solved_constraints: FastHashMap::default(), - } - } -} - -impl<'a> DisplayRectangle<'a> { - #[inline] - pub fn new(tag: Option, styled_node: &'a StyledNode) -> Self { - Self { - tag: tag, - styled_node: styled_node, - style: RectStyle::default(), - layout: RectLayout::default(), - } - } -} - -impl<'a, T: LayoutScreen> DisplayList<'a, T> { - - /// NOTE: This function assumes that the UiDescription has an initialized arena - /// - /// This only looks at the user-facing styles of the `UiDescription`, not the actual - /// layout. The layout is done only in the `into_display_list_builder` step. - pub fn new_from_ui_description(ui_description: &'a UiDescription) -> Self { - - let arena = &ui_description.ui_descr_arena; - - let mut rect_btree = BTreeMap::new(); - - for node in &ui_description.styled_nodes { - let mut rect = DisplayRectangle::new(arena.borrow()[node.id].data.tag, &node); - parse_css_style_properties(&mut rect); - parse_css_layout_properties(&mut rect); - rect_btree.insert(node.id, rect); - } - - Self { - ui_descr: ui_description, - rectangles: rect_btree, - } - } - - pub fn into_display_list_builder(&self, pipeline_id: PipelineId, ui_solver: &mut UiSolver, css: &mut Css, mut has_window_size_changed: bool) - -> Option - { - let mut changeset = None; - if let Some(root) = self.ui_descr.ui_descr_root { - let local_changeset = ui_solver.dom_tree_cache.update(root, &*(self.ui_descr.ui_descr_arena.borrow())); - ui_solver.edit_variable_cache.initialize_new_rectangles(&mut ui_solver.solver, &local_changeset); - ui_solver.edit_variable_cache.remove_unused_variables(&mut ui_solver.solver); - changeset = Some(local_changeset); - } - - if css.needs_relayout { -/* - // constraints were added or removed during the last frame - for rect_id in self.rectangles.keys() { - let mut layout_contraints = Vec::::new(); - let arena = &*self.ui_descr.ui_descr_arena.borrow(); - create_layout_constraints(&rect, arena, ui_solver); - let cassowary_constraints = css_constraints_to_cassowary_constraints(rect.rect, &layout_contraints); - ui_solver.solver.add_constraints(&cassowary_constraints).unwrap(); - } -*/ - // if we push or pop constraints that means we also need to re-layout the window - has_window_size_changed = true; - } - - let changeset_is_useless = match changeset { - None => true, - Some(c) => c.is_empty() - }; -/* - // early return if we have nothing - if !css.needs_relayout && changeset_is_useless && !has_window_size_changed { - return None; - } -*/ - - // recalculate the actual layout - if css.needs_relayout || has_window_size_changed { - /* - for change in solver.fetch_changes() { - println!("change: - {:?}", change); - } - */ - } - - css.needs_relayout = false; - - let mut builder = DisplayListBuilder::with_capacity(pipeline_id, ui_solver.window_dimensions.layout_size, self.rectangles.len()); - - for (rect_idx, rect) in self.rectangles.iter() { - - // ask the solver what the bounds of the current rectangle is - // let bounds = ui_solver.query_bounds_of_rect(*rect_idx); - - // debugging - there are currently two rectangles on the screen - // if the rectangle doesn't have a background color, choose the first bound - // - // this means, since the DOM in the debug example has two rectangles, we should - // have two touching rectangles - let mut bounds = if rect.style.background_color.is_some() { - LayoutRect::new( - LayoutPoint::new(0.0, 0.0), - LayoutSize::new(200.0, 200.0), - ) - } else { - LayoutRect::new( - LayoutPoint::new(0.0, 0.0), - LayoutSize::new((*rect_idx).index as f32 * 3.0, 3.0), - ) - }; - - // bug - for some reason, the origin gets scaled by 2.0, - // even if the HiDpi factor is set to 1.0 - // this is a workaround, this seems to be a bug in webrender - bounds.origin.x /= 2.0; - bounds.origin.y /= 2.0; - - let clip_region_id = rect.style.border_radius.and_then(|border_radius| { - let region = ComplexClipRegion { - rect: bounds, - radii: border_radius, - mode: ClipMode::Clip, - }; - Some(builder.define_clip(bounds, vec![region], None)) - }); - - let mut info = LayoutPrimitiveInfo::new(bounds); - info.tag = rect.tag.and_then(|tag| Some((tag, 0))); - - builder.push_stacking_context( - &info, - ScrollPolicy::Scrollable, - None, - TransformStyle::Flat, - None, - MixBlendMode::Normal, - Vec::new(), - ); - - if let Some(id) = clip_region_id { - builder.push_clip_id(id); - } - - builder.push_rect(&info, rect.style.background_color.unwrap_or(ColorU { r: 255, g: 0, b: 0, a: 255 }).into()); - - if clip_region_id.is_some() { - builder.pop_clip_id(); - } - - // red rectangle if we don't have a background color - if let Some(ref pre_shadow) = rect.style.box_shadow { - // The pre_shadow is missing the BorderRadius & LayoutRect - // TODO: do we need to pop the shadows? - let border_radius = rect.style.border_radius.unwrap_or(BorderRadius::zero()); - println!("pushing shadow: \n{:#?}", pre_shadow); - builder.push_box_shadow(&info, bounds, pre_shadow.offset, pre_shadow.color, - pre_shadow.blur_radius, pre_shadow.spread_radius, - border_radius, pre_shadow.clip_mode); - } - - if let Some(ref background) = rect.style.background { - match *background { - ParsedGradient::RadialGradient(ref _gradient) => { - - }, - ParsedGradient::LinearGradient(ref gradient) => { - let mut stops: Vec = gradient.stops.iter().map(|gradient_pre| - GradientStop { - offset: gradient_pre.offset.unwrap(), - color: gradient_pre.color, - }).collect(); - let (begin_pt, end_pt) = gradient.direction.to_points(&bounds); - let gradient = builder.create_gradient(begin_pt, end_pt, stops, gradient.extend_mode); - builder.push_gradient(&info, gradient, bounds.size, LayoutSize::zero()); - } - } - } - - if let Some((border_widths, mut border_details)) = rect.style.border { - if let Some(border_radius) = rect.style.border_radius { - if let BorderDetails::Normal(ref mut n) = border_details { - n.radius = border_radius; - } - } - builder.push_border(&info, border_widths, border_details); - } - - builder.pop_stacking_context(); - } - - Some(builder) - } -} - -macro_rules! parse { - ($constraint_list:ident, $key:expr, $func:tt) => ( - $constraint_list.get($key).and_then(|w| $func(w).map_err(|e| { - #[cfg(debug_assertions)] - println!("ERROR - invalid {:?}: {:?}", e, $key); - e - }).ok()) - ) -} - -/// Populate and parse the CSS style properties -fn parse_css_style_properties(rect: &mut DisplayRectangle) -{ - let constraint_list = &rect.styled_node.css_constraints.list; - - rect.style.border_radius = parse!(constraint_list, "border-radius", parse_css_border_radius); - rect.style.background_color = parse!(constraint_list, "background-color", parse_css_color); - rect.style.border = parse!(constraint_list, "border", parse_css_border); - rect.style.background = parse!(constraint_list, "background", parse_css_background); - let box_shadow_opt = parse!(constraint_list, "box-shadow", parse_css_box_shadow); - if let Some(box_shadow_opt) = box_shadow_opt{ - rect.style.box_shadow = box_shadow_opt; - } -} - -/// Populate and parse the CSS layout properties -fn parse_css_layout_properties(rect: &mut DisplayRectangle) { - - let constraint_list = &rect.styled_node.css_constraints.list; - - rect.layout.width = parse!(constraint_list, "width", parse_layout_width); - rect.layout.height = parse!(constraint_list, "height", parse_layout_height); - rect.layout.min_width = parse!(constraint_list, "min-width", parse_layout_min_width); - rect.layout.min_height = parse!(constraint_list, "min-height", parse_layout_min_height); - - rect.layout.wrap = parse!(constraint_list, "flex-wrap", parse_layout_wrap); - rect.layout.direction = parse!(constraint_list, "flex-direction", parse_layout_direction); - rect.layout.justify_content = parse!(constraint_list, "justify-content", parse_layout_justify_content); - rect.layout.align_items = parse!(constraint_list, "align-items", parse_layout_align_items); - rect.layout.align_content = parse!(constraint_list, "align-content", parse_layout_align_content); -} - -// Adds and removes layout constraints if necessary -fn create_layout_constraints(rect: &DisplayRectangle, - arena: &Arena>, - ui_solver: &mut UiSolver) -where T: LayoutScreen -{ - use css_parser; - // todo: put these to use! - let window_dimensions = &ui_solver.window_dimensions; - let solver = &mut ui_solver.solver; - let previous_layout = &mut ui_solver.solved_layout; - - use cassowary::strength::*; - use constraints::{SizeConstraint, Strength}; - - /* - // centering a rectangle: - center(&root), - bound_by(&root).padding(50.0).strength(WEAK), - */ -} - -fn css_constraints_to_cassowary_constraints(rect: &DisplayRect, css: &Vec) --> Vec -{ - use self::CssConstraint::*; - - css.iter().flat_map(|constraint| - match *constraint { - Size((constraint, strength)) => { - constraint.build(&rect, strength.0) - } - Padding((constraint, strength, padding)) => { - constraint.build(&rect, strength.0, padding.0) - } - } - ).collect() -} \ No newline at end of file diff --git a/src/dom.rs b/src/dom.rs deleted file mode 100644 index d047e6a6e..000000000 --- a/src/dom.rs +++ /dev/null @@ -1,471 +0,0 @@ -use app_state::AppState; -use resources::{ImageId, FontId}; -use traits::LayoutScreen; -use std::collections::BTreeMap; -use id_tree::{NodeId, Arena}; -use std::sync::{Arc, Mutex}; -use std::fmt; -use std::rc::Rc; -use std::cell::RefCell; -use std::hash::{Hash, Hasher}; -use webrender::api::ColorU; - -/// This is only accessed from the main thread, so it's safe to use -pub(crate) static mut NODE_ID: u64 = 0; -pub(crate) static mut CALLBACK_ID: u64 = 0; - -/// A callback function has to return if the screen should -/// be updated after the function has run.PartialEq -/// -/// This is necessary for updating the screen only if it is absolutely necessary. -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub enum UpdateScreen { - /// Redraw the screen - Redraw, - /// Don't redraw the screen - DontRedraw, -} - -/// Stores a function pointer that is executed when the given UI element is hit -/// -/// Must return an `UpdateScreen` that denotes if the screen should be redrawn. -/// The CSS is not affected by this, so if you push to the windows' CSS inside the -/// function, the screen will not be automatically redrawn, unless you return an -/// `UpdateScreen::Redraw` from the function -pub enum Callback { - /// One-off function (for ex. exporting a file) - /// - /// This is best for actions where you don't care if or when they complete. - /// Because you accept a Mutex, you can create a background thread - /// (azul won't create this for you) - Async(fn(Arc>>) -> UpdateScreen), - /// Same as the `FnOnceNonBlocking`, but it blocks the current - /// thread and does not require the type to be `Send`. - Sync(fn(&mut AppState) -> UpdateScreen), -} - -impl fmt::Debug for Callback { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use self::Callback::*; - match *self { - Async(func) => write!(f, "Callback::Async @ {:?}", func as usize), - Sync(func) => write!(f, "Callback::Sync @ {:?}", func as usize), - } - } -} - -impl Clone for Callback -{ - fn clone(&self) -> Self { - match *self { - Callback::Async(ref f) => Callback::Async(f.clone()), - Callback::Sync(ref f) => Callback::Sync(f.clone()), - } - } -} - -/// As a hashing function, we use the function pointer casted to a usize -/// as a unique ID for the function. This way, we can hash and compare DOM nodes -/// (to create diffs between two states). Comparing usizes is more efficient -/// than re-creating the whole DOM and serves as a caching mechanism. -impl Hash for Callback { - fn hash(&self, state: &mut H) where H: Hasher { - use self::Callback::*; - match *self { - Async(f) => { state.write_usize(f as usize); } - Sync(f) => { state.write_usize(f as usize); } - } - } -} - -/// Basically compares the function pointers and types for equality -impl PartialEq for Callback { - fn eq(&self, rhs: &Self) -> bool { - use self::Callback::*; - if let (Async(self_f), Async(other_f)) = (*self, *rhs) { - if self_f as usize == other_f as usize { return true; } - } else if let (Sync(self_f), Sync(other_f)) = (*self, *rhs) { - if self_f as usize == other_f as usize { return true; } - } - false - } -} - -impl Eq for Callback { } - -impl Copy for Callback { } - -/// List of allowed DOM node types that are supported by `azul`. -/// -/// All node types are purely convenience functions around `Div`, -/// `Image` and `Label`. For example a `Ul` is simply a convenience -/// wrapper around a repeated (`Div` + `Label`) clone where the first -/// `Div` is shaped like a circle (for `Ul`). -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub enum NodeType { - /// Regular div - Div, - Image { - id: ImageId, - }, - /// A label that can be (optionally) be selectable with the mouse - Label { - /// Text of the label - text: String, - /// Font ID used for the string - font_id: FontId, - }, - /// Button - Button { - /// The text on the button - label: String, - /// Font ID used for the string - font_id: FontId, - }, - /// Unordered list - Ul, - /// Ordered list - Ol, - /// List item. Only valid if the parent is `NodeType::Ol` or `NodeType::Ul`. - Li, - /// This is more or less like a `GroupBox` in Visual Basic, draws a border - Form { - /// The text of the label - text: Option, - }, - /// Single-line text input - TextInput { - content: String, - placeholder: Option - }, - /// Multi line text input - TextEdit { - content: String, - placeholder: Option, - }, - /// A register-like tab - Tab { - label: String, - }, - /// Checkbox - Checkbox { - /// active - state: CheckboxState, - }, - /// Dropdown item - Dropdown { - items: Vec, - }, - /// Small (default yellow) tooltip for help - ToolTip { - title: String, - content: String, - }, - /// Password input, like the TextInput, but the items are rendered as dots - /// (if `use_dots` is active) - Password { - content: String, - placeholder: Option, - use_dots: bool, - }, -} - -/// State of a checkbox (disabled, checked, etc.) -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] -pub enum CheckboxState { - /// `[■]` - Active, - /// `[✔]` - Checked, - /// Greyed out checkbox - Disabled { - /// Should the checkbox fire on a mouseover / mouseup, etc. event - /// - /// This can be useful for showing warnings / tooltips / help messages - /// as to why this checkbox is disabled - fire_on_click: bool, - }, - /// `[ ]` - Unchecked -} - -impl NodeType { - - /// Get the CSS / HTML identifier "p", "ul", "li", etc. - /// - /// Full list of the types you can use in CSS: - /// - /// ```ignore - /// Div => "div" - /// Image => "img" - /// Button => "button" - /// Ul => "ul" - /// Ol => "ol" - /// Li => "li" - /// Label => "label" - /// Form => "form" - /// TextInput => "text-input" - /// TextEdit => "text-edit" - /// Tab => "tab" - /// Checkbox => "checkbox" - /// Color => "color" - /// Drowdown => "dropdown" - /// ToolTip => "tooltip" - /// Password => "password" - /// ``` - pub fn get_css_identifier(&self) -> &'static str { - use self::NodeType::*; - match *self { - Div => "div", - Image { .. } => "img", - Label { .. } => "label", - Button { .. } => "button", - Ul => "ul", - Ol => "ol", - Li => "li", - Form { .. } => "form", - TextInput { .. } => "text-input", - TextEdit { .. } => "text-edit", - Tab { .. } => "tab", - Checkbox { .. } => "checkbox", - Dropdown { .. } => "dropdown", - ToolTip { .. } => "tooltip", - Password { .. } => "password", - } - } -} - -/// When to call a callback action - `On::MouseOver`, `On::MouseOut`, etc. -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub enum On { - /// Mouse cursor is hovering over the element - MouseOver, - /// Mouse cursor has is over element and is pressed - /// (not good for "click" events - use `MouseUp` instead) - MouseDown, - /// Mouse button has been released while cursor was over the element - MouseUp, - /// Mouse cursor has entered the element - MouseEnter, - /// Mouse cursor has left the element - MouseLeave, -} - -#[derive(PartialEq, Eq)] -pub(crate) struct NodeData { - /// `div` - pub node_type: NodeType, - /// `#main` - pub id: Option, - /// `.myclass .otherclass` - pub classes: Vec, - /// `onclick` -> `my_button_click_handler` - pub events: CallbackList, - /// Tag for hit-testing - pub tag: Option, -} - -impl Hash for NodeData { - fn hash(&self, state: &mut H) { - self.node_type.hash(state); - self.id.hash(state); - for class in &self.classes { - class.hash(state); - } - self.events.hash(state); - } -} - -use cache::DomHash; - -impl NodeData { - pub fn calculate_node_data_hash(&self) -> DomHash { - use std::hash::Hash; - use twox_hash::XxHash; - let mut hasher = XxHash::default(); - self.hash(&mut hasher); - DomHash(hasher.finish()) - } -} - -impl Clone for NodeData { - fn clone(&self) -> Self { - Self { - node_type: self.node_type.clone(), - id: self.id.clone(), - classes: self.classes.clone(), - events: self.events.special_clone(), - tag: self.tag.clone(), - } - } -} - -impl fmt::Debug for NodeData { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "NodeData {{ - node_type: {:?}, - id: {:?}, - classes: {:?}, - events: {:?}, - tag: {:?} -}}", - self.node_type, self.id, self.classes, self.events, self.tag) - } -} - -impl CallbackList { - fn special_clone(&self) -> Self { - Self { - callbacks: self.callbacks.clone(), - } - } -} - -impl NodeData { - /// Creates a new NodeData - pub fn new(node_type: NodeType) -> Self { - Self { - node_type: node_type, - id: None, - classes: Vec::new(), - events: CallbackList::::new(), - tag: None, - } - } - - /// Since `#[derive(Clone)]` requires `T: Clone`, we currently - /// have to make our own version - fn special_clone(&self) -> Self { - Self { - node_type: self.node_type.clone(), - id: self.id.clone(), - classes: self.classes.clone(), - events: self.events.special_clone(), - tag: self.tag.clone(), - } - } -} - -/// The document model, similar to HTML. This is a create-only structure, you don't actually read anything back -#[derive(Clone, PartialEq, Eq)] -pub struct Dom { - pub(crate) arena: Rc>>>, - pub(crate) root: NodeId, - pub(crate) current_root: NodeId, - pub(crate) last: NodeId, -} - -impl fmt::Debug for Dom { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Dom {{ - arena: {:?}, - root: {:?}, - current_root: {:?}, - last: {:?} -}}", - self.arena, - self.root, - self.current_root, - self.last) - } -} - -#[derive(Clone, PartialEq, Eq)] -pub(crate) struct CallbackList { - pub(crate) callbacks: BTreeMap> -} - -impl Hash for CallbackList { - fn hash(&self, state: &mut H) { - for callback in &self.callbacks { - callback.hash(state); - } - } -} - -impl fmt::Debug for CallbackList { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "CallbackList (length: {:?})", self.callbacks.len()) - } -} - -impl CallbackList { - pub fn new() -> Self { - Self { - callbacks: BTreeMap::new(), - } - } -} - -impl Dom { - - /// Creates an empty DOM - #[inline] - pub fn new(node_type: NodeType) -> Self { - let mut arena = Arena::new(); - let root = arena.new_node(NodeData::new(node_type)); - Self { - arena: Rc::new(RefCell::new(arena)), - root: root, - current_root: root, - last: root, - } - } - - /// Adds a child DOM to the current DOM - #[inline] - pub fn add_child(&mut self, child: Self) { - for ch in child.root.children(&*child.arena.borrow()) { - let new_last = (*self.arena.borrow_mut()).new_node((*child.arena.borrow())[ch].data.special_clone()); - self.last.append(new_last, &mut self.arena.borrow_mut()); - self.last = new_last; - } - } - - /// Adds a sibling to the current DOM - #[inline] - pub fn add_sibling(&mut self, sibling: Self) { - for sib in sibling.root.following_siblings(&*sibling.arena.borrow()) { - let sibling_clone = (*sibling.arena.borrow())[sib].data.special_clone(); - let new_sibling = (*self.arena.borrow_mut()).new_node(sibling_clone); - self.current_root.insert_after(new_sibling, &mut self.arena.borrow_mut()); - self.current_root = new_sibling; - } - } - - #[inline] - pub fn id>(&mut self, id: S) { - self.arena.borrow_mut()[self.last].data.id = Some(id.into()); - } - - #[inline] - pub fn class>(&mut self, class: S) { - self.arena.borrow_mut()[self.last].data.classes.push(class.into()); - } - - #[inline] - pub fn event(&mut self, on: On, callback: Callback) { - self.arena.borrow_mut()[self.last].data.events.callbacks.insert(on, callback); - self.arena.borrow_mut()[self.last].data.tag = Some(unsafe { NODE_ID }); - unsafe { NODE_ID += 1; }; - } -} - -impl Dom { - - pub(crate) fn collect_callbacks(&self, callback_list: &mut BTreeMap>, nodes_to_callback_id_list: &mut BTreeMap>) { - for item in self.root.traverse(&*self.arena.borrow()) { - let mut cb_id_list = BTreeMap::::new(); - let item = &self.arena.borrow()[item.inner_value()]; - for (on, callback) in item.data.events.callbacks.iter() { - let callback_id = unsafe { CALLBACK_ID }; - unsafe { CALLBACK_ID += 1; } - callback_list.insert(callback_id, *callback); - cb_id_list.insert(*on, callback_id); - } - if let Some(tag) = item.data.tag { - nodes_to_callback_id_list.insert(tag, cb_id_list); - } - } - } -} \ No newline at end of file diff --git a/src/id_tree.rs b/src/id_tree.rs deleted file mode 100644 index 09ba672d4..000000000 --- a/src/id_tree.rs +++ /dev/null @@ -1,638 +0,0 @@ -//! ID-based node tree - -use std::mem; -use std::ops::{Index, IndexMut}; -use std::fmt; -use std::hash::{Hasher, Hash}; -use std::collections::BTreeMap; - -/// A node identifier within a particular `Arena`. -#[derive(PartialOrd, Ord, PartialEq, Eq, Copy, Clone, Debug, Hash)] -pub struct NodeId { - pub(crate) index: usize, // FIXME: use NonZero to optimize the size of Option -} - -#[derive(Clone, PartialEq)] -pub struct Node { - // Keep these private (with read-only accessors) so that we can keep them consistent. - // E.g. the parent of a node’s child is that node. - parent: Option, - previous_sibling: Option, - next_sibling: Option, - first_child: Option, - last_child: Option, - pub data: T, -} - -// Manual implementation, since `#[derive(Debug)]` requires `T: Debug` -impl fmt::Debug for Node { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Node {{ \ - parent: {:?}, \ - previous_sibling: {:?}, \ - next_sibling: {:?}, \ - first_child: {:?}, \ - last_child: {:?}, \ - data: {:?}, \ - }}", - self.parent, - self.previous_sibling, - self.next_sibling, - self.first_child, - self.last_child, - self.data) - } -} - -#[derive(Debug, Clone)] -pub struct Arena { - pub(crate) nodes: Vec>, -} - -impl PartialEq for Arena { - fn eq(&self, other: &Self) -> bool { - self.nodes == other.nodes - } -} - -impl Eq for Arena { -} - -impl Hash for Arena { - fn hash(&self, state: &mut H) { - for node in &self.nodes { - node.data.hash(state); - } - } -} - -impl Arena { - - /// Transform keeps the relative order of parents / children - /// but transforms an Arena into an Arena, by running the closure on each of the - /// items. The `NodeId` for the root is then valid for the newly created `Arena`, too. - pub fn transform(&self, closure: F) -> Arena where F: Fn(&T) -> U { - Arena { - nodes: self.nodes.iter().map(|node| Node { - parent: node.parent, - previous_sibling: node.previous_sibling, - next_sibling: node.next_sibling, - first_child: node.first_child, - last_child: node.last_child, - data: closure(&node.data) - }).collect() - } - } - - pub fn from_nodes(nodes: Vec>) -> Arena { - Self { - nodes: nodes, - } - } - - pub fn new() -> Arena { - Arena { - nodes: Vec::new(), - } - } - - /// Create a new node from its associated data. - pub fn new_node(&mut self, data: T) -> NodeId { - let next_index = self.nodes.len(); - self.nodes.push(Node { - parent: None, - first_child: None, - last_child: None, - previous_sibling: None, - next_sibling: None, - data: data, - }); - NodeId { - index: next_index, - } - } - - // Useful for debugging - returns how many - // nodes there are in the arena - pub fn nodes_len(&self) -> usize { - self.nodes.len() - } - - pub fn is_empty(&self) -> bool { - self.nodes_len() == 0 - } -} - -impl Arena { - #[inline] - pub fn get_all_node_ids(&self) -> BTreeMap { - use std::iter::FromIterator; - BTreeMap::from_iter(self.nodes.iter().enumerate().map(|(i, node)| - (NodeId { index: i }, node.data) - )) - } -} - -trait GetPairMut { - /// Get mutable references to two distinct nodes - /// - /// Panic - /// ----- - /// - /// Panics if the two given IDs are the same. - fn get_pair_mut(&mut self, a: usize, b: usize, same_index_error_message: &'static str) - -> (&mut T, &mut T); -} - -impl GetPairMut for Vec { - fn get_pair_mut(&mut self, a: usize, b: usize, same_index_error_message: &'static str) - -> (&mut T, &mut T) { - if a == b { - panic!(same_index_error_message) - } - unsafe { - let self2 = mem::transmute_copy::<&mut Vec, &mut Vec>(&self); - (&mut self[a], &mut self2[b]) - } - } -} - -impl Index for Arena { - type Output = Node; - - fn index(&self, node: NodeId) -> &Node { - &self.nodes[node.index] - } -} - -impl IndexMut for Arena { - fn index_mut(&mut self, node: NodeId) -> &mut Node { - &mut self.nodes[node.index] - } -} - - -impl Node { - /// Return the ID of the parent node, unless this node is the root of the tree. - pub fn parent(&self) -> Option { self.parent } - - /// Return the ID of the first child of this node, unless it has no child. - pub fn first_child(&self) -> Option { self.first_child } - - /// Return the ID of the last child of this node, unless it has no child. - pub fn last_child(&self) -> Option { self.last_child } - - /// Return the ID of the previous sibling of this node, unless it is a first child. - pub fn previous_sibling(&self) -> Option { self.previous_sibling } - - /// Return the ID of the previous sibling of this node, unless it is a first child. - pub fn next_sibling(&self) -> Option { self.next_sibling } -} - - -impl NodeId { - /// Return an iterator of references to this node and its ancestors. - /// - /// Call `.next().unwrap()` once on the iterator to skip the node itself. - pub fn ancestors(self, arena: &Arena) -> Ancestors { - Ancestors { - arena: arena, - node: Some(self), - } - } - - /// Return an iterator of references to this node and the siblings before it. - /// - /// Call `.next().unwrap()` once on the iterator to skip the node itself. - pub fn preceding_siblings(self, arena: &Arena) -> PrecedingSiblings { - PrecedingSiblings { - arena: arena, - node: Some(self), - } - } - - /// Return an iterator of references to this node and the siblings after it. - /// - /// Call `.next().unwrap()` once on the iterator to skip the node itself. - pub fn following_siblings(self, arena: &Arena) -> FollowingSiblings { - FollowingSiblings { - arena: arena, - node: Some(self), - } - } - - /// Return an iterator of references to this node’s children. - pub fn children(self, arena: &Arena) -> Children { - Children { - arena: arena, - node: arena[self].first_child, - } - } - - /// Return an iterator of references to this node’s children, in reverse order. - pub fn reverse_children(self, arena: &Arena) -> ReverseChildren { - ReverseChildren { - arena: arena, - node: arena[self].last_child, - } - } - - /// Return an iterator of references to this node and its descendants, in tree order. - /// - /// Parent nodes appear before the descendants. - /// Call `.next().unwrap()` once on the iterator to skip the node itself. - pub fn descendants(self, arena: &Arena) -> Descendants { - Descendants(self.traverse(arena)) - } - - /// Return an iterator of references to this node and its descendants, in tree order. - pub fn traverse(self, arena: &Arena) -> Traverse { - Traverse { - arena: arena, - root: self, - next: Some(NodeEdge::Start(self)), - } - } - - /// Return an iterator of references to this node and its descendants, in tree order. - pub fn reverse_traverse(self, arena: &Arena) -> ReverseTraverse { - ReverseTraverse { - arena: arena, - root: self, - next: Some(NodeEdge::End(self)), - } - } - - /// Detach a node from its parent and siblings. Children are not affected. - pub fn detach(self, arena: &mut Arena) { - let (parent, previous_sibling, next_sibling) = { - let node = &mut arena[self]; - (node.parent.take(), node.previous_sibling.take(), node.next_sibling.take()) - }; - - if let Some(next_sibling) = next_sibling { - arena[next_sibling].previous_sibling = previous_sibling; - } else if let Some(parent) = parent { - arena[parent].last_child = previous_sibling; - } - - if let Some(previous_sibling) = previous_sibling { - arena[previous_sibling].next_sibling = next_sibling; - } else if let Some(parent) = parent { - arena[parent].first_child = next_sibling; - } - } - - /// Append a new child to this node, after existing children. - pub fn append(self, new_child: NodeId, arena: &mut Arena) { - new_child.detach(arena); - let last_child_opt; - { - let (self_borrow, new_child_borrow) = arena.nodes.get_pair_mut( - self.index, new_child.index, "Can not append a node to itself"); - new_child_borrow.parent = Some(self); - last_child_opt = mem::replace(&mut self_borrow.last_child, Some(new_child)); - if let Some(last_child) = last_child_opt { - new_child_borrow.previous_sibling = Some(last_child); - } else { - debug_assert!(self_borrow.first_child.is_none()); - self_borrow.first_child = Some(new_child); - } - } - if let Some(last_child) = last_child_opt { - debug_assert!(arena[last_child].next_sibling.is_none()); - arena[last_child].next_sibling = Some(new_child); - } - } - - /// Prepend a new child to this node, before existing children. - pub fn prepend(self, new_child: NodeId, arena: &mut Arena) { - new_child.detach(arena); - let first_child_opt; - { - let (self_borrow, new_child_borrow) = arena.nodes.get_pair_mut( - self.index, new_child.index, "Can not prepend a node to itself"); - new_child_borrow.parent = Some(self); - first_child_opt = mem::replace(&mut self_borrow.first_child, Some(new_child)); - if let Some(first_child) = first_child_opt { - new_child_borrow.next_sibling = Some(first_child); - } else { - debug_assert!(&self_borrow.first_child.is_none()); - self_borrow.last_child = Some(new_child); - } - } - if let Some(first_child) = first_child_opt { - debug_assert!(arena[first_child].previous_sibling.is_none()); - arena[first_child].previous_sibling = Some(new_child); - } - } - - /// Insert a new sibling after this node. - pub fn insert_after(self, new_sibling: NodeId, arena: &mut Arena) { - new_sibling.detach(arena); - let next_sibling_opt; - let parent_opt; - { - let (self_borrow, new_sibling_borrow) = arena.nodes.get_pair_mut( - self.index, new_sibling.index, "Can not insert a node after itself"); - parent_opt = self_borrow.parent; - new_sibling_borrow.parent = parent_opt; - new_sibling_borrow.previous_sibling = Some(self); - next_sibling_opt = mem::replace(&mut self_borrow.next_sibling, Some(new_sibling)); - if let Some(next_sibling) = next_sibling_opt { - new_sibling_borrow.next_sibling = Some(next_sibling); - } - } - if let Some(next_sibling) = next_sibling_opt { - debug_assert!(arena[next_sibling].previous_sibling.unwrap() == self); - arena[next_sibling].previous_sibling = Some(new_sibling); - } else if let Some(parent) = parent_opt { - debug_assert!(arena[parent].last_child.unwrap() == self); - arena[parent].last_child = Some(new_sibling); - } - } - - /// Insert a new sibling before this node. - pub fn insert_before(self, new_sibling: NodeId, arena: &mut Arena) { - new_sibling.detach(arena); - let previous_sibling_opt; - let parent_opt; - { - let (self_borrow, new_sibling_borrow) = arena.nodes.get_pair_mut( - self.index, new_sibling.index, "Can not insert a node before itself"); - parent_opt = self_borrow.parent; - new_sibling_borrow.parent = parent_opt; - new_sibling_borrow.next_sibling = Some(self); - previous_sibling_opt = mem::replace(&mut self_borrow.previous_sibling, Some(new_sibling)); - if let Some(previous_sibling) = previous_sibling_opt { - new_sibling_borrow.previous_sibling = Some(previous_sibling); - } - } - if let Some(previous_sibling) = previous_sibling_opt { - debug_assert!(arena[previous_sibling].next_sibling.unwrap() == self); - arena[previous_sibling].next_sibling = Some(new_sibling); - } else if let Some(parent) = parent_opt { - debug_assert!(arena[parent].first_child.unwrap() == self); - arena[parent].first_child = Some(new_sibling); - } - } -} - - -macro_rules! impl_node_iterator { - ($name: ident, $next: expr) => { - impl<'a, T> Iterator for $name<'a, T> { - type Item = NodeId; - - fn next(&mut self) -> Option { - match self.node.take() { - Some(node) => { - self.node = $next(&self.arena[node]); - Some(node) - } - None => None - } - } - } - } -} - -/// An iterator of references to the ancestors a given node. -pub struct Ancestors<'a, T: 'a> { - arena: &'a Arena, - node: Option, -} - -impl_node_iterator!(Ancestors, |node: &Node| node.parent); - -/// An iterator of references to the siblings before a given node. -pub struct PrecedingSiblings<'a, T: 'a> { - arena: &'a Arena, - node: Option, -} - -impl_node_iterator!(PrecedingSiblings, |node: &Node| node.previous_sibling); - -/// An iterator of references to the siblings after a given node. -pub struct FollowingSiblings<'a, T: 'a> { - arena: &'a Arena, - node: Option, -} - -impl_node_iterator!(FollowingSiblings, |node: &Node| node.next_sibling); - -/// An iterator of references to the children of a given node. -pub struct Children<'a, T: 'a> { - arena: &'a Arena, - node: Option, -} - -impl_node_iterator!(Children, |node: &Node| node.next_sibling); - -/// An iterator of references to the children of a given node, in reverse order. -pub struct ReverseChildren<'a, T: 'a> { - arena: &'a Arena, - node: Option, -} - -impl_node_iterator!(ReverseChildren, |node: &Node| node.previous_sibling); - - -/// An iterator of references to a given node and its descendants, in tree order. -pub struct Descendants<'a, T: 'a>(Traverse<'a, T>); - -impl<'a, T> Iterator for Descendants<'a, T> { - type Item = NodeId; - - fn next(&mut self) -> Option { - loop { - match self.0.next() { - Some(NodeEdge::Start(node)) => return Some(node), - Some(NodeEdge::End(_)) => {} - None => return None - } - } - } -} - -#[derive(Debug, Clone)] -pub enum NodeEdge { - /// Indicates that start of a node that has children. - /// Yielded by `Traverse::next` before the node’s descendants. - /// In HTML or XML, this corresponds to an opening tag like `
` - Start(T), - - /// Indicates that end of a node that has children. - /// Yielded by `Traverse::next` after the node’s descendants. - /// In HTML or XML, this corresponds to a closing tag like `
` - End(T), -} - -impl NodeEdge { - pub fn inner_value(self) -> T { - use self::NodeEdge::*; - match self { - Start(t) => t, - End(t) => t, - } - } -} - -/// An iterator of references to a given node and its descendants, in tree order. -pub struct Traverse<'a, T: 'a> { - arena: &'a Arena, - root: NodeId, - next: Option>, -} - -impl<'a, T> Iterator for Traverse<'a, T> { - type Item = NodeEdge; - - fn next(&mut self) -> Option> { - match self.next.take() { - Some(item) => { - self.next = match item { - NodeEdge::Start(node) => { - match self.arena[node].first_child { - Some(first_child) => Some(NodeEdge::Start(first_child)), - None => Some(NodeEdge::End(node.clone())) - } - } - NodeEdge::End(node) => { - if node == self.root { - None - } else { - match self.arena[node].next_sibling { - Some(next_sibling) => Some(NodeEdge::Start(next_sibling)), - None => match self.arena[node].parent { - Some(parent) => Some(NodeEdge::End(parent)), - - // `node.parent()` here can only be `None` - // if the tree has been modified during iteration, - // but silently stoping iteration - // seems a more sensible behavior than panicking. - None => None - } - } - } - } - }; - Some(item) - } - None => None - } - } -} - -/// An iterator of references to a given node and its descendants, in reverse tree order. -pub struct ReverseTraverse<'a, T: 'a> { - arena: &'a Arena, - root: NodeId, - next: Option>, -} - -impl<'a, T> Iterator for ReverseTraverse<'a, T> { - type Item = NodeEdge; - - fn next(&mut self) -> Option> { - match self.next.take() { - Some(item) => { - self.next = match item { - NodeEdge::End(node) => { - match self.arena[node].last_child { - Some(last_child) => Some(NodeEdge::End(last_child)), - None => Some(NodeEdge::Start(node.clone())) - } - } - NodeEdge::Start(node) => { - if node == self.root { - None - } else { - match self.arena[node].previous_sibling { - Some(previous_sibling) => Some(NodeEdge::End(previous_sibling)), - None => match self.arena[node].parent { - Some(parent) => Some(NodeEdge::Start(parent)), - - // `node.parent()` here can only be `None` - // if the tree has been modified during iteration, - // but silently stoping iteration - // seems a more sensible behavior than panicking. - None => None - } - } - } - } - }; - Some(item) - } - None => None - } - } -} - -#[test] -fn drop_allocator() { - use std::cell::Cell; - - struct DropTracker<'a>(&'a Cell); - impl<'a> Drop for DropTracker<'a> { - fn drop(&mut self) { - self.0.set(&self.0.get() + 1); - } - } - - let drop_counter = Cell::new(0); - { - let mut new_counter = 0; - let arena = &mut Arena::new(); - macro_rules! new { - () => { - { - new_counter += 1; - arena.new_node((new_counter, DropTracker(&drop_counter))) - } - } - }; - - let a = new!(); // 1 - a.append(new!(), arena); // 2 - a.append(new!(), arena); // 3 - a.prepend(new!(), arena); // 4 - let b = new!(); // 5 - b.append(a, arena); - a.insert_before(new!(), arena); // 6 - a.insert_before(new!(), arena); // 7 - a.insert_after(new!(), arena); // 8 - a.insert_after(new!(), arena); // 9 - let c = new!(); // 10 - b.append(c, arena); - - assert_eq!(drop_counter.get(), 0); - arena[c].previous_sibling().unwrap().detach(arena); - assert_eq!(drop_counter.get(), 0); - - assert_eq!(b.descendants(arena).map(|node| arena[node].data.0).collect::>(), [ - 5, 6, 7, 1, 4, 2, 3, 9, 10 - ]); - } - - assert_eq!(drop_counter.get(), 10); -} - - -#[test] -fn children_ordering() { - - let arena = &mut Arena::new(); - let root = arena.new_node("".to_string()); - - root.append(arena.new_node("b".to_string()), arena); - root.prepend(arena.new_node("a".to_string()), arena); - root.append(arena.new_node("c".to_string()), arena); - - let children = root.children(arena).map(|node| &*arena[node].data).collect::>(); - let reverse_children = root.reverse_children(arena).map(|node| &*arena[node].data).collect::>(); - - assert_eq!(children, vec!["a", "b", "c"]); - assert_eq!(reverse_children, vec!["c", "b", "a"]); -} \ No newline at end of file diff --git a/src/input.rs b/src/input.rs deleted file mode 100644 index accd4896a..000000000 --- a/src/input.rs +++ /dev/null @@ -1,108 +0,0 @@ -use webrender::api::{HitTestResult, PipelineId, DocumentId, HitTestFlags, RenderApi, WorldPoint}; - -pub fn hit_test_ui(api: &RenderApi, document_id: DocumentId, pipeline_id: Option, point: WorldPoint) -> HitTestResult { - api.hit_test(document_id, pipeline_id, point, HitTestFlags::FIND_ALL) -} - -use std::time::{Instant, Duration}; -use glium::glutin::{MouseCursor, VirtualKeyCode}; - -/// Determines which keys are pressed currently (modifiers, etc.) -#[derive(Debug, Clone)] -pub struct KeyboardState -{ - /// Modifier keys that are currently actively pressed during this cycle - pub modifiers: Vec, - /// Hidden keys, such as the "n" in CTRL + n. Always lowercase - pub hidden_keys: Vec, - /// Actual keys pressed during this cycle (i.e. regular text input) - pub keys: Vec, -} - -impl KeyboardState -{ - pub fn new() -> Self - { - Self { - modifiers: Vec::new(), - hidden_keys: Vec::new(), - keys: Vec::new(), - } - } -} - -/// Mouse position on the screen -#[derive(Debug, Copy, Clone)] -pub struct MouseState -{ - /// Current mouse cursor type - pub mouse_cursor_type: MouseCursor, - //// Where the mouse cursor is. None if the window is not focused - pub mouse_cursor: Option<(i32, i32)>, - //// Is the left MB down? - pub left_down: bool, - //// Is the right MB down? - pub right_down: bool, - //// Is the middle MB down? - pub middle_down: bool, - /// How far has the mouse scrolled in x direction? - pub mouse_scroll_x: f32, - /// How far has the mouse scrolled in y direction? - pub mouse_scroll_y: f32, -} - -impl MouseState -{ - /// Creates a new mouse state - /// Input: How fast the scroll (mouse) should be converted into pixels - /// Usually around 10.0 (10 pixels per mouse wheel line) - pub fn new() -> Self - { - MouseState { - mouse_cursor_type: MouseCursor::Default, - mouse_cursor: Some((0, 0)), - left_down: false, - right_down: false, - middle_down: false, - mouse_scroll_x: 0.0, - mouse_scroll_y: 0.0, - } - } -} - -/// State, size, etc of the window, for comparing to the last frame -#[derive(Debug, Clone)] -pub struct WindowState -{ - /// The state of the keyboard - pub(crate) keyboard_state: KeyboardState, - /// The state of the mouse - pub(crate) mouse_state: MouseState, - /// Width of the window - pub width: u32, - /// Height of the window - pub height: u32, - /// Time of the last rendering update, set after the `redraw()` method - pub time_of_last_update: Instant, - /// Minimum frame time - pub min_frame_time: Duration, -} - -impl WindowState -{ - /// Creates a new window state - pub fn new( - width: u32, - height: u32, - ) -> Self - { - Self { - keyboard_state: KeyboardState::new(), - mouse_state: MouseState::new(), - width, - height, - time_of_last_update: Instant::now(), - min_frame_time: Duration::from_millis(16), - } - } -} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index a565a9048..000000000 --- a/src/lib.rs +++ /dev/null @@ -1,85 +0,0 @@ -//! azul is a library for creating graphical user interfaces in Rust. -//! -//! ## How it works -//! -//! azul requires your app data to "serialize" itself into a UI. -//! This is different from how other GUI frameworks work, so it requires a bit of explanation: -//! -//! Your app data is one global struct for your whole application. This is the "model". -//! azul takes your model and requires you to build a DOM tree to translate the model into a view. -//! This (layouting, restyling, constraint solving) is done every 2 milliseconds. However, if your -//! UI doesn't change, nothing is done (in order to not stress the CPU too much). -//! -//! This model makes conditional UI elements and conditional styling very easy. azul takes care -//! of caching for you - your CSS and DOM elements are cached and diffed for changes, in order to -//! maximize performance. A full screen redraw should not take longer than 16 milliseconds -//! (currently the frame time is around 1 - 2 milliseconds). -//! -//! ## Hello world example -//! -//! For more examples, please look in the `/examples` folder. - - -#![deny(unused_must_use)] -#![allow(dead_code)] -#![allow(unused_imports)] -#![allow(unused_variables)] - -extern crate webrender; -extern crate cassowary; -extern crate twox_hash; -extern crate glium; -extern crate gleam; -extern crate euclid; -extern crate simplecss; - -/// Global application (Initialization starts here) -pub mod app; -/// Wrapper for the application data & application state -pub mod app_state; -/// Styling & CSS parsing -pub mod css; -/// DOM / HTML node handling -pub mod dom; -/// The layout traits for creating a layout-able application -pub mod traits; -/// Window handling -pub mod window; -/// Font & image resource handling, lookup and caching -pub mod resources; -/// Input handling (mostly glium) -mod input; -/// UI Description & display list handling (webrender) -mod ui_description; -/// Constraint handling -mod constraints; -/// Converts the UI description (the styled HTML nodes) -/// to an actual display list (+ layout) -mod display_list; -/// CSS parser -mod css_parser; -/// Slab allocator for nodes, based on IDs (replaces kuchiki + markup5ever) -mod id_tree; -/// State handling for user interfaces -mod ui_state; -/// Dom / CSS caching -mod cache; - -/// Faster implementation of a HashMap -type FastHashMap = ::std::collections::HashMap>; -type FastHashSet = ::std::collections::HashSet>; - -/// Quick exports of common types -pub mod prelude { - pub use app::App; - pub use app_state::AppState; - pub use css::{CssRule, Css}; - pub use dom::{Dom, NodeType, Callback, CheckboxState, On, UpdateScreen}; - pub use traits::LayoutScreen; - pub use webrender::api::{ColorF, ColorU}; - pub use window::{MonitorIter, Window, WindowCreateOptions, - WindowId, WindowPlacement}; - pub use window::{MouseMode, UpdateBehaviour, UpdateMode, WindowClass, - WindowCreateError, WindowDecorations, WindowMonitorTarget}; - -} \ No newline at end of file diff --git a/src/resources.rs b/src/resources.rs deleted file mode 100644 index 5c3145b28..000000000 --- a/src/resources.rs +++ /dev/null @@ -1,27 +0,0 @@ -use webrender::api::{ImageKey, FontKey}; -use FastHashMap; - -/// Font and image keys -/// -/// The idea is that azul doesn't know where the resources come from, -/// whether they are loaded from the network or a disk. -/// Fonts and images must be added and removed dynamically. If you have a -/// fonts that should be always accessible, then simply add them before the app -/// starts up. -/// -/// Images and fonts can be references across window contexts -/// (not yet tested, but should work). -#[derive(Debug, Default, Clone)] -pub(crate) struct AppResources { - pub(crate) images: FastHashMap, - pub(crate) fonts: FastHashMap, -} - -/// An `ImageId` is a wrapper around webrenders `ImageKey`. -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct ImageId(usize); - -/// A Font ID is a wrapper around webrenders `FontKey`. -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct FontId(usize); - diff --git a/src/traits.rs b/src/traits.rs deleted file mode 100644 index 6dd3a7f97..000000000 --- a/src/traits.rs +++ /dev/null @@ -1,200 +0,0 @@ -use dom::{NodeData, Dom}; -use ui_description::{StyledNode, CssConstraintList, UiDescription}; -use css::{Css, CssRule}; -use window::WindowId; -use id_tree::{NodeId, Arena}; -use std::rc::Rc; -use std::cell::RefCell; - -pub trait LayoutScreen { - /// Updates the DOM, must be provided by the final application. - /// - /// On each frame, a completely new DOM tree is generated. The final - /// application can cache the DOM tree, but this isn't in the scope of `azul`. - /// - /// The `style_dom` looks through the given DOM rules, applies the style and - /// recalculates the layout. This is done on each frame (except there are shortcuts - /// when the DOM doesn't have to be recalculated). - fn get_dom(&self, window_id: WindowId) -> Dom where Self: Sized; - /// Applies the CSS styles to the nodes calculated from the `layout_screen` - /// function and calculates the final display list that is submitted to the - /// renderer. - fn style_dom(dom: &Dom, css: &mut Css) -> UiDescription where Self: Sized { - css.is_dirty = false; - match_dom_css_selectors(dom.root, &dom.arena, &ParsedCss::from_css(css), css, 0) - } -} - -pub(crate) struct ParsedCss<'a> { - pub(crate) pure_global_rules: Vec<&'a CssRule>, - pub(crate) pure_div_rules: Vec<&'a CssRule>, - pub(crate) pure_class_rules: Vec<&'a CssRule>, - pub(crate) pure_id_rules: Vec<&'a CssRule>, -} - -impl<'a> ParsedCss<'a> { - pub(crate) fn from_css(css: &'a Css) -> Self { - - // Parse the CSS nodes cascading by their importance - // 1. global rules - // 2. div-type ("html { }") specific rules - // 3. class-based rules - // 4. ID-based rules - - /* - CssRule { html_type: "div", id: Some("main"), classes: [], declaration: ("direction", "row") } - CssRule { html_type: "div", id: Some("main"), classes: [], declaration: ("justify-content", "center") } - CssRule { html_type: "div", id: Some("main"), classes: [], declaration: ("align-items", "center") } - CssRule { html_type: "div", id: Some("main"), classes: [], declaration: ("align-content", "center") } - */ - - // note: the following passes can be done in parallel ... - - // Global rules - // * { - // background-color: blue; - // } - let pure_global_rules: Vec<&CssRule> = css.rules.iter().filter(|rule| - rule.html_type == "*" && rule.id.is_none() && rule.classes.is_empty() - ).collect(); - - // Pure-div-type specific rules - // button { - // justify-content: center; - // } - let pure_div_rules: Vec<&CssRule> = css.rules.iter().filter(|rule| - rule.html_type != "*" && rule.id.is_none() && rule.classes.is_empty() - ).collect(); - - // Pure-class rules - // NOTE: These classes are sorted alphabetically and are not duplicated - // - // .something .otherclass { - // text-color: red; - // } - let pure_class_rules: Vec<&CssRule> = css.rules.iter().filter(|rule| - rule.id.is_none() && !rule.classes.is_empty() - ).collect(); - - // Pure-id rules - // #something { - // background-color: red; - // } - let pure_id_rules: Vec<&CssRule> = css.rules.iter().filter(|rule| - rule.id.is_some() && rule.classes.is_empty() - ).collect(); - - Self { - pure_global_rules: pure_global_rules, - pure_div_rules: pure_div_rules, - pure_class_rules: pure_class_rules, - pure_id_rules: pure_id_rules, - } - } -} - -fn match_dom_css_selectors(root: NodeId, arena: &Rc>>>, parsed_css: &ParsedCss, css: &Css, parent_z_level: u32) --> UiDescription -{ - let mut root_constraints = CssConstraintList::empty(); - for global_rule in &parsed_css.pure_global_rules { - push_rule(&mut root_constraints, global_rule); - } - - let arena_borrow = &*(*arena).borrow(); - let mut styled_nodes = Vec::::new(); - let sibling_iterator = root.following_siblings(arena_borrow); - // skip the root node itself, see documentation for `following_siblings` in id_tree.rs - // sibling_iterator.next().unwrap(); - - for sibling in sibling_iterator { - styled_nodes.append(&mut match_dom_css_selectors_inner(sibling, arena_borrow, parsed_css, css, &root_constraints, parent_z_level)); - } - - UiDescription { - // note: this clone is neccessary, otherwise, - // we wouldn't be able to update the UiState - ui_descr_arena: (*arena).clone(), - ui_descr_root: Some(root), - styled_nodes: styled_nodes, - } -} - -fn match_dom_css_selectors_inner(root: NodeId, arena: &Arena>, parsed_css: &ParsedCss, css: &Css, parent_constraints: &CssConstraintList, parent_z_level: u32) --> Vec -{ - let mut styled_nodes = Vec::::new(); - - let mut current_constraints = parent_constraints.clone(); - cascade_constraints(&arena[root].data, &mut current_constraints, parsed_css, css); - - let current_node = StyledNode { - id: root, - z_level: parent_z_level, - css_constraints: current_constraints, - }; - - // DFS tree - for child in root.children(arena) { - styled_nodes.append(&mut match_dom_css_selectors_inner(child, arena, parsed_css, css, ¤t_node.css_constraints, parent_z_level + 1)); - } - - styled_nodes.push(current_node); - styled_nodes -} - -/// Cascade the rules, put them into the list -#[allow(unused_variables)] -fn cascade_constraints(node: &NodeData, list: &mut CssConstraintList, parsed_css: &ParsedCss, css: &Css) { - - for div_rule in &parsed_css.pure_div_rules { - if *node.node_type.get_css_identifier() == div_rule.html_type { - push_rule(list, div_rule); - } - } - - let mut node_classes: Vec<&String> = node.classes.iter().map(|x| x).collect(); - node_classes.sort(); - node_classes.dedup_by(|a, b| *a == *b); - - // for all classes that this node has - for class_rule in &parsed_css.pure_class_rules { - // NOTE: class_rule is sorted and de-duplicated - // If the selector matches, the node classes must be identical - let mut should_insert_rule = true; - if class_rule.classes.len() != node_classes.len() { - should_insert_rule = false; - } else { - for i in 0..class_rule.classes.len() { - // we verified that the length of the two classes is the same - if *node_classes[i] != class_rule.classes[i] { - should_insert_rule = false; - break; - } - } - } - - if should_insert_rule { - push_rule(list, class_rule); - } - } - - // first attribute for "id = something" - let node_id = &node.id; - - if let Some(ref node_id) = *node_id { - // if the node has an ID - for id_rule in &parsed_css.pure_id_rules { - if *id_rule.id.as_ref().unwrap() == *node_id { - push_rule(list, id_rule); - } - } - } - - // TODO: all the mixed rules -} - -#[inline] -fn push_rule(list: &mut CssConstraintList, rule: &CssRule) { - list.list.insert(rule.declaration.0.clone(), rule.declaration.1.clone()); -} \ No newline at end of file diff --git a/src/ui_description.rs b/src/ui_description.rs deleted file mode 100644 index 26abe538e..000000000 --- a/src/ui_description.rs +++ /dev/null @@ -1,64 +0,0 @@ -use FastHashMap; -use id_tree::{Arena, NodeId}; -use traits::LayoutScreen; -use ui_state::UiState; -use css::Css; -use dom::NodeData; -use std::cell::RefCell; -use std::rc::Rc; - -pub struct UiDescription { - pub(crate) ui_descr_arena: Rc>>>, - pub(crate) ui_descr_root: Option, - pub(crate) styled_nodes: Vec, -} - -impl Clone for UiDescription { - fn clone(&self) -> Self { - Self { - ui_descr_arena: self.ui_descr_arena.clone(), - ui_descr_root: self.ui_descr_root.clone(), - styled_nodes: self.styled_nodes.clone(), - } - } -} - -impl Default for UiDescription { - fn default() -> Self { - Self { - ui_descr_arena: Rc::new(RefCell::new(Arena::new())), - ui_descr_root: None, - styled_nodes: Vec::new(), - } - } -} - -impl UiDescription { - pub fn from_ui_state(ui_state: &UiState, style: &mut Css) -> Self - { - T::style_dom(&ui_state.dom, style) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct StyledNode { - /// The current node we are processing (the current HTML element) - pub id: NodeId, - /// The z-index level that we are currently on - pub z_level: u32, - /// The CSS constraints, after the cascading step - pub css_constraints: CssConstraintList -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct CssConstraintList { - pub list: FastHashMap -} - -impl CssConstraintList { - pub fn empty() -> Self { - Self { - list: FastHashMap::default(), - } - } -} \ No newline at end of file diff --git a/src/ui_state.rs b/src/ui_state.rs deleted file mode 100644 index fe33f3540..000000000 --- a/src/ui_state.rs +++ /dev/null @@ -1,45 +0,0 @@ -use traits::LayoutScreen; -use window::WindowId; -use std::collections::BTreeMap; -use dom::{NODE_ID, CALLBACK_ID, Callback, Dom, On}; -use app_state::AppState; -use std::fmt; - -pub struct UiState { - pub dom: Dom, - pub callback_list: BTreeMap>, - pub node_ids_to_callbacks_list: BTreeMap>, -} - -impl fmt::Debug for UiState { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "UiState {{ - dom: {:?}, - callback_list: {:?}, - node_ids_to_callbacks_list: {:?} -}}", - self.dom, - self.callback_list, - self.node_ids_to_callbacks_list) - } -} - -impl UiState { - pub(crate) fn from_app_state(app_state: &AppState, window_id: WindowId) -> Self - { - use dom::{Dom, On}; - - let dom: Dom = app_state.data.get_dom(window_id); - unsafe { NODE_ID = 0 }; - unsafe { CALLBACK_ID = 0 }; - let mut callback_list = BTreeMap::>::new(); - let mut node_ids_to_callbacks_list = BTreeMap::>::new(); - dom.collect_callbacks(&mut callback_list, &mut node_ids_to_callbacks_list); - - UiState { - dom: dom, - callback_list: callback_list, - node_ids_to_callbacks_list: node_ids_to_callbacks_list, - } - } -} \ No newline at end of file diff --git a/src/window.rs b/src/window.rs deleted file mode 100644 index 4af02253a..000000000 --- a/src/window.rs +++ /dev/null @@ -1,573 +0,0 @@ -use webrender::api::*; -use webrender::{Renderer, RendererOptions}; -use glium::{IncompatibleOpenGl, Display}; -use glium::debug::DebugCallbackBehavior; -use glium::glutin::{self, EventsLoop, AvailableMonitorsIter, GlProfile, GlContext, GlWindow, CreationError, - MonitorId, EventsLoopProxy, ContextError, ContextBuilder, WindowBuilder}; -use gleam::gl; -use glium::backend::glutin::DisplayCreationError; -use euclid::TypedScale; -use cassowary::{Variable, Solver}; -use cassowary::strength::*; - -use display_list::SolvedLayout; -use traits::LayoutScreen; -use css::Css; -use cache::{EditVariableCache, DomTreeCache}; -use id_tree::NodeId; - -use std::time::Duration; -use std::fmt; - -const DEFAULT_TITLE: &str = "Azul App"; -const DEFAULT_WIDTH: u32 = 800; -const DEFAULT_HEIGHT: u32 = 600; - -/// azul-internal ID for a window -#[derive(Debug, Copy, Clone, PartialOrd, Ord, PartialEq, Eq)] -pub struct WindowId { - pub id: usize, -} - -impl WindowId { - pub fn new(id: usize) -> Self { Self { id: id } } -} - -/// Options on how to initially create the window -#[derive(Debug, Clone)] -pub struct WindowCreateOptions { - /// Title of the window - pub title: String, - /// OpenGL clear color - pub background: ColorF, - /// Clear the stencil buffer with the given value. If not set, stencil buffer is not cleared - pub clear_stencil: Option, - /// Clear the depth buffer with the given value. If not set, depth buffer is not cleared - pub clear_depth: Option, - /// How should the screen be updated - as fast as possible - /// or retained & energy saving? - pub update_mode: UpdateMode, - /// Which monitor should the window be created on? - pub monitor: WindowMonitorTarget, - /// How precise should the mouse updates be? - pub mouse_mode: MouseMode, - /// Should the window update regardless if the mouse is hovering - /// over the window? (useful for games vs. applications) - pub update_behaviour: UpdateBehaviour, - /// How should the window be decorated? - pub decorations: WindowDecorations, - /// Size and position of the window - pub size: WindowPlacement, - /// What type of window (full screen, popup, normal) - pub class: WindowClass, -} - -impl Default for WindowCreateOptions { - fn default() -> Self { - Self { - title: self::DEFAULT_TITLE.into(), - background: ColorF::new(1.0, 1.0, 1.0, 1.0), - clear_stencil: None, - clear_depth: None, - update_mode: UpdateMode::default(), - monitor: WindowMonitorTarget::default(), - mouse_mode: MouseMode::default(), - update_behaviour: UpdateBehaviour::default(), - decorations: WindowDecorations::default(), - size: WindowPlacement::default(), - class: WindowClass::default(), - } - } -} - -/// How should the window be decorated -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum WindowDecorations { - /// Regular window decorations - Normal, - /// Maximize button disabled - MaximizeDisabled, - /// Minimize button disabled - MinimizeDisabled, - /// Both maximize and minimize button disabled - MaximizeMinimizeDisabled, - /// No decorations (borderless window) - /// - /// Combine this with `WindowClass::FullScreen` - /// to get borderless fullscreen mode - /// (useful for correct Alt+Tab behaviour) - NoDecorations, -} - -impl Default for WindowDecorations { - fn default() -> Self { - WindowDecorations::Normal - } -} - -/// Where the window should be positioned, -/// from the top left corner of the screen -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct WindowPlacement { - pub x: u32, - pub y: u32, - pub width: u32, - pub height: u32, -} - -impl Default for WindowPlacement { - fn default() -> Self { - Self { - x: 0, - y: 0, - width: self::DEFAULT_WIDTH, - height: self::DEFAULT_HEIGHT, - } - } -} - -/// What class the window should have (important for window managers). -/// Currently not in use. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum WindowClass { - /// Regular desktop window - Normal, - /// Popup window (some window managers handle this differently) - Popup, - /// Will open the window in full-screen mode - /// and set it as the top-level window on the given monitor. - /// Window size is ignored - FullScreen, - /// Start the window maximized - Maximized, - /// Start the window minimized - Minimized, - /// Window is hidden at startup. - /// - /// This is useful for background rendering. Many windowing systems - /// do not properly support off-screen rendering (via OSMesa or similar). - /// As a workaround, you can just create a hidden window - Hidden, -} - -impl Default for WindowClass { - fn default() -> Self { - WindowClass::Normal - } -} - -/// Should the window be updated only if the mouse cursor is hovering over it? -#[derive(Debug, PartialEq, Eq, Copy, Clone)] -pub enum UpdateBehaviour { - /// Redraw the window only if the mouse cursor is - /// on top of the window - UpdateOnHover, - /// Always update the screen, regardless of the - /// position of the mouse cursor - UpdateAlways, -} - -impl Default for UpdateBehaviour { - fn default() -> Self { - UpdateBehaviour::UpdateOnHover - } -} - -/// In which intervals should the screen be updated -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum UpdateMode { - /// Retained = the screen is only updated when necessary. - /// Underlying GlImages will be ignored and only updated when the UI changes - Retained, - /// Fixed update every X duration. - FixedUpdate(Duration), - /// Draw the screen as fast as possible. - AsFastAsPossible, -} - -impl Default for UpdateMode { - fn default() -> Self { - UpdateMode::Retained - } -} - -/// Mouse configuration -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum MouseMode { - /// A mouse event is only fired if the cursor has moved at least 1px. - /// More energy saving, but less precision. - Normal, - /// High-precision mouse input (useful for games) - /// - /// This disables acceleration and uses the raw values - /// provided by the mouse. - DirectInput, -} - -impl Default for MouseMode { - fn default() -> Self { - MouseMode::Normal - } -} - -/// Error that could happen during window creation -#[derive(Debug)] -pub enum WindowCreateError { - /// WebGl is not supported by webrender - WebGlNotSupported, - /// Couldn't create the display from the window and the EventsLoop - DisplayCreateError(DisplayCreationError), - /// OpenGL version is either too old or invalid - Gl(IncompatibleOpenGl), - /// Could not create an OpenGL context - Context(ContextError), - /// Could not create a window - CreateError(CreationError), - /// Could not swap the front & back buffers - SwapBuffers(::glium::SwapBuffersError), - /// IO error - Io(::std::io::Error), -} - -impl From<::glium::SwapBuffersError> for WindowCreateError { - fn from(e: ::glium::SwapBuffersError) -> Self { - WindowCreateError::SwapBuffers(e) - } -} - -impl From for WindowCreateError { - fn from(e: CreationError) -> Self { - WindowCreateError::CreateError(e) - } -} - -impl From<::std::io::Error> for WindowCreateError { - fn from(e: ::std::io::Error) -> Self { - WindowCreateError::Io(e) - } -} - -impl From for WindowCreateError { - fn from(e: IncompatibleOpenGl) -> Self { - WindowCreateError::Gl(e) - } -} - -impl From for WindowCreateError { - fn from(e: DisplayCreationError) -> Self { - WindowCreateError::DisplayCreateError(e) - } -} - -impl From for WindowCreateError { - fn from(e: ContextError) -> Self { - WindowCreateError::Context(e) - } -} - -struct Notifier { - events_loop_proxy: EventsLoopProxy, -} - -impl Notifier { - fn new(events_loop_proxy: EventsLoopProxy) -> Notifier { - Notifier { - events_loop_proxy - } - } -} - -impl RenderNotifier for Notifier { - fn clone(&self) -> Box { - Box::new(Notifier { - events_loop_proxy: self.events_loop_proxy.clone(), - }) - } - - fn wake_up(&self) { - #[cfg(not(target_os = "android"))] - self.events_loop_proxy.wakeup().unwrap_or_else(|_| { }); - } - - fn new_document_ready(&self, _: DocumentId, _scrolled: bool, _composite_needed: bool) { - self.wake_up(); - } -} - -/// Iterator over connected monitors (for positioning, etc.) -pub struct MonitorIter { - inner: AvailableMonitorsIter, -} - -impl Iterator for MonitorIter { - type Item = MonitorId; - fn next(&mut self) -> Option { - self.inner.next() - } -} - -/// Select on which monitor the window should pop up. -#[derive(Clone)] -pub enum WindowMonitorTarget { - /// Window should appear on the primary monitor - Primary, - /// Use `Window::get_available_monitors()` to select the correct monitor - Custom(MonitorId) -} - -impl fmt::Debug for WindowMonitorTarget { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use self::WindowMonitorTarget::*; - match *self { - Primary => write!(f, "WindowMonitorTarget::Primary"), - Custom(_) => write!(f, "WindowMonitorTarget::Custom(_)"), - } - } -} - -impl Default for WindowMonitorTarget { - fn default() -> Self { - WindowMonitorTarget::Primary - } -} - -/// Represents one graphical window to be rendered -pub struct Window { - // TODO: technically, having one EventsLoop for all windows is sufficient - pub(crate) events_loop: EventsLoop, - pub(crate) options: WindowCreateOptions, - pub(crate) renderer: Option, - pub(crate) display: Display, - pub(crate) internal: WindowInternal, - /// The solver for the UI, for caching the results of the computations - pub(crate) solver: UiSolver, - // The background thread that is running for this window. - // pub(crate) background_thread: Option>, - /// The css (how the current window is styled) - pub css: Css, -} - -/// Used in the solver, for the root constraint -#[derive(Debug, Copy, Clone, PartialEq)] -pub(crate) struct WindowDimensions { - pub(crate) layout_size: LayoutSize, - pub(crate) width_var: Variable, - pub(crate) height_var: Variable, -} - -impl WindowDimensions { - pub fn new_from_layout_size(layout_size: LayoutSize) -> Self { - Self { - layout_size: layout_size, - width_var: Variable::new(), - height_var: Variable::new(), - } - } - - pub fn width(&self) -> f32 { - self.layout_size.width_typed().get() - } - pub fn height(&self) -> f32 { - self.layout_size.height_typed().get() - } -} - -/// Solver for solving the UI of the current window -pub(crate) struct UiSolver { - /// The actual solver - pub(crate) solver: Solver, - /// Dimensions of the root window - pub(crate) window_dimensions: WindowDimensions, - /// Solved layout from the previous frame (empty by default) - /// This is necessary for caching the constraints of the given layout - pub(crate) solved_layout: SolvedLayout, - /// The list of variables that has been added to the solver - pub(crate) edit_variable_cache: EditVariableCache, - /// The cache of the previous frames DOM tree - pub(crate) dom_tree_cache: DomTreeCache, -} - -impl UiSolver { - pub(crate) fn query_bounds_of_rect(&self, rect_id: NodeId) { - // TODO: After solving the UI, use this function to get the actual coordinates of an item in the UI. - // This function should cache values accordingly - } -} - -pub(crate) struct WindowInternal { - pub(crate) last_display_list_builder: BuiltDisplayList, - pub(crate) layout_size: LayoutSize, - pub(crate) api: RenderApi, - pub(crate) epoch: Epoch, - pub(crate) framebuffer_size: DeviceUintSize, - pub(crate) pipeline_id: PipelineId, - pub(crate) document_id: DocumentId, - pub(crate) hidpi_factor: f32, -} - -impl Window { - - /// Creates a new window - pub fn new(options: WindowCreateOptions, css: Css) -> Result { - - let events_loop = EventsLoop::new(); - - let mut window = WindowBuilder::new() - .with_dimensions(options.size.width, options.size.height) - .with_title(options.title.clone()) - .with_decorations(options.decorations != WindowDecorations::NoDecorations) - .with_maximized(options.class == WindowClass::Maximized); - - if options.class == WindowClass::FullScreen { - let monitor = match options.monitor { - WindowMonitorTarget::Primary => events_loop.get_primary_monitor(), - WindowMonitorTarget::Custom(ref id) => id.clone(), - }; - - window = window.with_fullscreen(Some(monitor)); - } - - fn create_context_builder<'a>(vsync: bool, srgb: bool) -> ContextBuilder<'a> { - let mut builder = ContextBuilder::new() - .with_gl(glutin::GlRequest::GlThenGles { - opengl_version: (3, 2), - opengles_version: (3, 0), - }) - .with_gl_profile(GlProfile::Core) - .with_gl_debug_flag(false); - if vsync { - builder = builder.with_vsync(true); - } - if srgb { - builder = builder.with_srgb(true); - } - builder - } - - // For some reason, there is GL_INVALID_OPERATION stuff going on, - // but the display works fine. TODO: report this to glium - - // Only create a context with VSync and SRGB if the context creation works - let gl_window = GlWindow::new(window.clone(), create_context_builder(true, true), &events_loop) - .or_else(|_| GlWindow::new(window.clone(), create_context_builder(true, false), &events_loop)) - .or_else(|_| GlWindow::new(window, create_context_builder(false, false), &events_loop))?; - - let hidpi_factor = gl_window.hidpi_factor(); - let display = Display::with_debug(gl_window, DebugCallbackBehavior::Ignore)?; - - unsafe { - display.gl_window().make_current()?; - } - - // draw the first frame in the background color - use glium::Surface; - let mut frame = display.draw(); - if let Some(depth) = options.clear_depth { - if let Some(stencil) = options.clear_stencil { - frame.clear_all_srgb((options.background.r, options.background.g, options.background.b, options.background.a), depth, stencil); - } - frame.clear_color_srgb_and_depth((options.background.r, options.background.g, options.background.b, options.background.a), depth); - } else if let Some(stencil) = options.clear_stencil { - frame.clear_color_srgb_and_stencil((options.background.r, options.background.g, options.background.b, options.background.a), stencil); - } - frame.clear_color_srgb(options.background.r, options.background.g, options.background.b, options.background.a); - frame.finish()?; - - let gl = match display.gl_window().get_api() { - glutin::Api::OpenGl => unsafe { - gl::GlFns::load_with(|symbol| - display.gl_window().get_proc_address(symbol) as *const _) - }, - glutin::Api::OpenGlEs => unsafe { - gl::GlesFns::load_with(|symbol| - display.gl_window().get_proc_address(symbol) as *const _) - }, - glutin::Api::WebGl => return Err(WindowCreateError::WebGlNotSupported), - }; - - let device_pixel_ratio = display.gl_window().hidpi_factor(); - - let opts = RendererOptions { - resource_override_path: None, - debug: false, - // pre-caching shaders means to compile all shaders on startup - // this can take significant time and should be only used for testing the shaders - precache_shaders: false, - device_pixel_ratio, - enable_subpixel_aa: true, - enable_aa: true, - clear_color: Some(options.background), - enable_render_on_scroll: false, - // TODO: Fallback to OSMesa if needed! - // renderer_kind: RendererKind::Native, - .. RendererOptions::default() - }; - - let framebuffer_size = { - #[allow(deprecated)] - let (width, height) = display.gl_window().get_inner_size_pixels().unwrap(); - DeviceUintSize::new(width, height) - }; - let notifier = Box::new(Notifier::new(events_loop.create_proxy())); - let (renderer, sender) = Renderer::new(gl.clone(), notifier, opts).unwrap(); - - let api = sender.create_api(); - let document_id = api.add_document(framebuffer_size, 0); - let epoch = Epoch(0); - let pipeline_id = PipelineId(0, 0); - let layout_size = framebuffer_size.to_f32() / TypedScale::new(device_pixel_ratio); -/* - let (sender, receiver) = channel(); - let thread = Builder::new().name(options.title.clone()).spawn(move || Self::handle_event(receiver))?; -*/ - let mut solver = Solver::new(); - - let window_dim = WindowDimensions::new_from_layout_size(layout_size); - - solver.add_edit_variable(window_dim.width_var, STRONG).unwrap(); - solver.add_edit_variable(window_dim.height_var, STRONG).unwrap(); - solver.suggest_value(window_dim.width_var, window_dim.width() as f64).unwrap(); - solver.suggest_value(window_dim.height_var, window_dim.height() as f64).unwrap(); - - let window = Window { - events_loop: events_loop, - options: options, - renderer: Some(renderer), - display: display, - css: css, - internal: WindowInternal { - layout_size: layout_size, - api: api, - epoch: epoch, - framebuffer_size: framebuffer_size, - pipeline_id: pipeline_id, - document_id: document_id, - hidpi_factor: hidpi_factor, - last_display_list_builder: BuiltDisplayList::default(), - }, - solver: UiSolver { - solver: solver, - window_dimensions: window_dim, - solved_layout: SolvedLayout::empty(), - edit_variable_cache: EditVariableCache::empty(), - dom_tree_cache: DomTreeCache::empty(), - } - }; - - Ok(window) - } - - pub fn get_available_monitors() -> MonitorIter { - MonitorIter { - inner: EventsLoop::new().get_available_monitors(), - } - } -} - -impl Drop for Window { - fn drop(&mut self) { - // self.background_thread.take().unwrap().join(); - let renderer = self.renderer.take().unwrap(); - renderer.deinit(); - } -} \ No newline at end of file