├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CONTRIBUTING.MD ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── apps └── readme │ ├── Cargo.toml │ ├── assets │ ├── blitz-markdown-overrides.css │ └── github-markdown.css │ └── src │ ├── main.rs │ ├── markdown.rs │ └── readme_application.rs ├── examples ├── assets │ ├── border.html │ ├── bottom_only.html │ ├── docsrs_header.html │ ├── github_profile_reduced.html │ ├── github_profile_reduced2.html │ ├── github_profile_reduced3.html │ ├── google.html │ ├── google_reduced.html │ ├── gosub.html │ ├── gosub_reduced.html │ ├── input.html │ ├── noscript.html │ ├── object_fit.html │ ├── pseudo.html │ ├── servo-color-negative-no-container.png │ ├── servo.css │ ├── servo.html │ ├── servo_header_reduced.html │ ├── servo_reduced.html │ ├── square.png │ ├── svg.html │ ├── tall.png │ ├── todomvc.css │ └── wide.png ├── box_shadow.rs ├── counter │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── flex.rs ├── form.rs ├── gradient.rs ├── html.rs ├── inline.rs ├── outline.rs ├── output │ └── .gitkeep ├── restyle.rs ├── screenshot.rs ├── todomvc.rs └── url.rs ├── justfile ├── packages ├── anyrender │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── wasm_send_sync.rs ├── anyrender_svg │ ├── Cargo.toml │ └── src │ │ ├── error.rs │ │ ├── lib.rs │ │ ├── render.rs │ │ └── util.rs ├── anyrender_vello │ ├── Cargo.toml │ └── src │ │ ├── image_renderer.rs │ │ ├── lib.rs │ │ ├── scene.rs │ │ └── window_renderer.rs ├── anyrender_vello_cpu │ ├── Cargo.toml │ └── src │ │ ├── image_renderer.rs │ │ ├── lib.rs │ │ ├── scene.rs │ │ └── window_renderer.rs ├── blitz-dom │ ├── Cargo.toml │ ├── assets │ │ ├── default.css │ │ └── moz-bullet-font.otf │ └── src │ │ ├── accessibility.rs │ │ ├── debug.rs │ │ ├── document.rs │ │ ├── events │ │ ├── driver.rs │ │ ├── ime.rs │ │ ├── keyboard.rs │ │ ├── mod.rs │ │ └── mouse.rs │ │ ├── form.rs │ │ ├── layout │ │ ├── construct.rs │ │ ├── inline.rs │ │ ├── mod.rs │ │ ├── replaced.rs │ │ └── table.rs │ │ ├── lib.rs │ │ ├── mutator.rs │ │ ├── net.rs │ │ ├── node.rs │ │ ├── query_selector.rs │ │ ├── stylo.rs │ │ ├── stylo_to_cursor_icon.rs │ │ ├── stylo_to_parley.rs │ │ ├── traversal.rs │ │ └── util.rs ├── blitz-html │ ├── Cargo.toml │ └── src │ │ ├── html_document.rs │ │ ├── html_sink.rs │ │ └── lib.rs ├── blitz-net │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── blitz-paint │ ├── Cargo.toml │ └── src │ │ ├── color.rs │ │ ├── debug_overlay.rs │ │ ├── gradient.rs │ │ ├── layers.rs │ │ ├── lib.rs │ │ ├── multicolor_rounded_rect.rs │ │ ├── non_uniform_rounded_rect.rs │ │ ├── render.rs │ │ ├── render │ │ ├── background.rs │ │ ├── box_shadow.rs │ │ └── form_controls.rs │ │ ├── sizing.rs │ │ └── text.rs ├── blitz-shell │ ├── Cargo.toml │ └── src │ │ ├── accessibility.rs │ │ ├── application.rs │ │ ├── convert_events.rs │ │ ├── event.rs │ │ ├── lib.rs │ │ ├── menu.rs │ │ └── window.rs ├── blitz-traits │ ├── Cargo.toml │ └── src │ │ ├── devtools.rs │ │ ├── events.rs │ │ ├── lib.rs │ │ ├── navigation.rs │ │ ├── net.rs │ │ ├── shell.rs │ │ └── viewport.rs ├── blitz │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── mini-dxn │ ├── Cargo.toml │ └── src │ │ ├── dioxus_document.rs │ │ ├── events.rs │ │ ├── lib.rs │ │ └── mutation_writer.rs └── stylo_taffy │ ├── Cargo.toml │ └── src │ ├── convert.rs │ ├── lib.rs │ └── wrapper.rs ├── tests ├── renders_boxes.rs └── stylo_usage.rs └── wpt └── runner ├── Cargo.toml └── src ├── main.rs ├── net_provider.rs ├── panic_backtrace.rs ├── report.rs └── test_runners ├── attr_test.rs ├── mod.rs └── ref_test.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 11 | cancel-in-progress: true 12 | 13 | env: 14 | RUSTDOCFLAGS: "-D warnings" 15 | CARGO_REGISTRIES_CRATES_IO_PROTOCOL: "sparse" 16 | 17 | jobs: 18 | 19 | # MSRV check. 20 | # Blitz only guarantees "latest stable". However we have this check here to ensure that we advertise 21 | # our MSRV. We also make an effort not to increase MSRV in patch versions of Blitz. 22 | # 23 | # We only run `cargo build` (not `cargo test`) so as to avoid requiring dev-dependencies to build with the MSRV 24 | # version. Building is likely sufficient as runtime errors varying between rust versions is very unlikely. 25 | build-msrv: 26 | name: "MSRV Build [Rust 1.85]" 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v4 30 | - uses: dtolnay/rust-toolchain@master 31 | with: 32 | toolchain: 1.85 33 | - run: perl -pi.bak -e 's/opt-level = 2/opt-level = 0/g' Cargo.toml 34 | - run: sudo apt update; sudo apt install libgtk-3-dev libxdo-dev 35 | - run: cargo build --workspace 36 | 37 | build-features-default: 38 | name: "Build [default features]" 39 | runs-on: ubuntu-latest 40 | steps: 41 | - uses: actions/checkout@v4 42 | - uses: dtolnay/rust-toolchain@stable 43 | - run: perl -pi.bak -e 's/opt-level = 2/opt-level = 0/g' Cargo.toml 44 | - run: sudo apt update; sudo apt install libgtk-3-dev libxdo-dev 45 | - run: cargo build --workspace 46 | 47 | test-features-default: 48 | name: "Test [default features]" 49 | runs-on: ubuntu-latest 50 | steps: 51 | - uses: actions/checkout@v4 52 | - uses: dtolnay/rust-toolchain@stable 53 | - run: perl -pi.bak -e 's/opt-level = 2/opt-level = 0/g' Cargo.toml 54 | - run: sudo apt update; sudo apt install libgtk-3-dev libxdo-dev 55 | - run: cargo test --workspace 56 | 57 | fmt: 58 | name: Rustfmt 59 | runs-on: ubuntu-latest 60 | steps: 61 | - uses: actions/checkout@v4 62 | - uses: dtolnay/rust-toolchain@master 63 | with: 64 | toolchain: stable 65 | components: rustfmt 66 | - run: cargo fmt --all --check 67 | 68 | clippy: 69 | name: Clippy 70 | runs-on: ubuntu-latest 71 | steps: 72 | - uses: actions/checkout@v4 73 | - uses: dtolnay/rust-toolchain@master 74 | with: 75 | toolchain: stable 76 | components: clippy 77 | - run: perl -pi.bak -e 's/opt-level = 2/opt-level = 0/g' Cargo.toml 78 | - run: sudo apt update; sudo apt install libgtk-3-dev libxdo-dev 79 | - run: cargo clippy --workspace -- -D warnings 80 | 81 | doc: 82 | name: Documentation 83 | runs-on: ubuntu-latest 84 | steps: 85 | - uses: actions/checkout@v4 86 | - uses: dtolnay/rust-toolchain@stable 87 | - run: cargo doc 88 | 89 | # just cargo check for now 90 | matrix_test: 91 | runs-on: ${{ matrix.platform.os }} 92 | if: github.event.pull_request.draft == false 93 | env: 94 | RUST_CARGO_COMMAND: ${{ matrix.platform.cross == true && 'cross' || 'cargo' }} 95 | strategy: 96 | matrix: 97 | platform: 98 | - { 99 | name: windows, 100 | target: x86_64-pc-windows-msvc, 101 | os: windows-latest, 102 | cross: false, 103 | command: "test", 104 | args: "--all --tests", 105 | setup: perl -pi.bak -e 's/opt-level = 2/opt-level = 0/g' Cargo.toml 106 | } 107 | - { 108 | name: macos, 109 | target: aarch64-apple-darwin, 110 | os: macos-latest, 111 | cross: false, 112 | command: "test", 113 | args: "--all --tests", 114 | setup: perl -pi.bak -e 's/opt-level = 2/opt-level = 0/g' Cargo.toml 115 | } 116 | - { 117 | name: linux, 118 | target: x86_64-unknown-linux-gnu, 119 | os: ubuntu-latest, 120 | cross: false, 121 | command: "test", 122 | args: "--all --tests", 123 | setup: "sudo apt update; sudo apt install --no-install-recommends \ 124 | libasound2-dev \ 125 | libatk1.0-dev \ 126 | libgtk-3-dev \ 127 | libudev-dev \ 128 | libpango1.0-dev \ 129 | libxdo-dev; 130 | perl -pi.bak -e 's/opt-level = 2/opt-level = 0/g' Cargo.toml" 131 | } 132 | 133 | name: Test (${{ matrix.platform.name }}) 134 | 135 | steps: 136 | - uses: actions/checkout@v4 137 | - name: install stable 138 | uses: dtolnay/rust-toolchain@master 139 | with: 140 | toolchain: stable 141 | targets: ${{ matrix.platform.target }} 142 | components: rustfmt 143 | 144 | - name: Install cross 145 | if: ${{ matrix.platform.cross == true }} 146 | uses: taiki-e/install-action@cross 147 | 148 | - name: Free Disk Space (Ubuntu) 149 | if: ${{ matrix.platform.os == 'ubuntu-latest' }} 150 | uses: jlumbroso/free-disk-space@v1.3.1 151 | with: # speed things up a bit 152 | large-packages: false 153 | docker-images: false 154 | swap-storage: false 155 | 156 | - uses: Swatinem/rust-cache@v2 157 | with: 158 | key: "${{ matrix.platform.target }}" 159 | cache-all-crates: "true" 160 | save-if: ${{ github.ref == 'refs/heads/main' }} 161 | 162 | - name: Setup 163 | run: ${{ matrix.platform.setup }} 164 | shell: bash 165 | 166 | - name: test 167 | run: | 168 | ${{ env.RUST_CARGO_COMMAND }} ${{ matrix.platform.command }} ${{ matrix.platform.args }} --target ${{ matrix.platform.target }} 169 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .DS_Store 3 | /scratch 4 | /.vscode 5 | /examples/output/**/*.png 6 | /examples/output/**/*.jpg 7 | /examples/output/**/*.jpeg 8 | /out 9 | /wpt/output 10 | /apps/wpt/output 11 | /sites -------------------------------------------------------------------------------- /CONTRIBUTING.MD: -------------------------------------------------------------------------------- 1 | # Contributing to Blitz 2 | 3 | Welcome to the Dioxus community! 4 | Blitz is a "native" HTML/CSS renderer built to support the "Dioxus Native" project. It is effectively a lightweight webview except that the JavaScript engine is replaced with a native Rust API which allows Rust reactivity / state management libraries like Dioxus to interface with it directly. 5 | 6 | Talk to us in: the #native channel in the [Dioxus Discord](https://discord.gg/BWTrn6d3) 7 | 8 | ## Development 9 | 10 | ### Windows 11 | Building Blitz requires Python, which can be installed from the Windows app store. 12 | 13 | ### Linux 14 | Requirements: 15 | * asound2 16 | * atk1.0 17 | * gtk-3 18 | * udev 19 | * pango1.0 20 | * xdo 21 | 22 | For example on Ubuntu you can install these by running: 23 | ```sh 24 | sudo apt-get update 25 | sudo apt-get install \ 26 | libasound2-dev \ 27 | libatk1.0-dev \ 28 | libgtk-3-dev \ 29 | libudev-dev \ 30 | libpango1.0-dev \ 31 | libxdo-dev 32 | ``` 33 | 34 | ### VSCode 35 | You can add the following JSON to your `.vscode/settings.json` to automically build Blitz on all supported targets. 36 | ```json 37 | { 38 | "rust-analyzer.check.features": "all", 39 | "rust-analyzer.cargo.features": "all", 40 | "rust-analyzer.check.allTargets": true, 41 | "rust-analyzer.cargo.allTargets": true 42 | } 43 | ``` 44 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "packages/anyrender", 4 | "packages/anyrender_vello", 5 | "packages/anyrender_vello_cpu", 6 | "packages/anyrender_svg", 7 | "packages/blitz-traits", 8 | "packages/blitz-dom", 9 | "packages/blitz-html", 10 | "packages/blitz-net", 11 | "packages/blitz-paint", 12 | "packages/blitz-shell", 13 | "packages/blitz", 14 | "packages/mini-dxn", 15 | "packages/stylo_taffy", 16 | "apps/readme", 17 | "wpt/runner", 18 | "examples/counter", 19 | ] 20 | exclude = ["sites"] 21 | resolver = "2" 22 | 23 | [workspace.package] 24 | license = "MIT OR Apache-2.0" 25 | 26 | [workspace.dependencies] 27 | # Servo dependencies 28 | style = { version = "0.3", package = "stylo" } 29 | style_traits = { version = "0.3", package = "stylo_traits" } 30 | style_config = { version = "0.3", package = "stylo_config" } 31 | style_dom = { version = "0.3", package = "stylo_dom" } 32 | selectors = { version = "0.28", package = "selectors" } 33 | 34 | markup5ever = "0.16.1" # needs to match stylo web_atoms version 35 | html5ever = "0.31" # needs to match stylo web_atoms version 36 | xml5ever = "0.22" # needs to match stylo web_atoms version 37 | euclid = "0.22" 38 | string_cache = "0.8.7" 39 | atomic_refcell = "0.1.13" 40 | app_units = "0.7.5" 41 | smallvec = "1" 42 | 43 | # DioxusLabs dependencies 44 | dioxus = { version = "=0.7.0-alpha.1" } 45 | dioxus-core = { version = "=0.7.0-alpha.1" } 46 | dioxus-html = { version = "=0.7.0-alpha.1" } 47 | dioxus-cli-config = { version = "=0.7.0-alpha.1" } 48 | dioxus-devtools = { version = "=0.7.0-alpha.1" } 49 | taffy = { version = "0.8", default-features = false, features = ["std", "flexbox", "grid", "block_layout", "content_size", "calc"] } 50 | 51 | # Linebender + WGPU + SVG 52 | color = "0.3" 53 | peniko = "0.4" 54 | kurbo = "0.11" 55 | parley = { version = "0.5", default-features = false, features = ["std"] } 56 | wgpu = "24" 57 | softbuffer = "0.4" 58 | vello = { version = "0.5", features = [ "wgpu" ] } 59 | vello_cpu = { version = "0.0.1" } 60 | usvg = "0.44.0" 61 | 62 | # Windowing & Input 63 | raw-window-handle = "0.6.0" 64 | winit = { version = "0.30.2", features = ["rwh_06"] } 65 | accesskit_winit = "0.23" 66 | accesskit = "0.17" 67 | muda = { version = "0.11.5", default-features = false } 68 | arboard = { version = "3.4.1", default-features = false } 69 | keyboard-types = "0.7" 70 | cursor-icon = "1" 71 | 72 | # IO & Networking 73 | url = "2.5.0" 74 | http = "1.1.0" 75 | data-url = "0.3.1" 76 | tokio = "1.42" 77 | reqwest = "0.12" 78 | 79 | # Media & Decoding 80 | image = { version = "0.25", default-features = false } 81 | woff = { version = "0.6", default-features = false } 82 | woff2 = "0.3" 83 | html-escape = "0.2.13" 84 | percent-encoding = "2.3.1" 85 | 86 | # Other dependencies 87 | rustc-hash = "1.1.0" 88 | bytes = "1.7.1" 89 | slab = "0.4.9" 90 | tracing = "0.1.40" 91 | futures-util = "0.3.30" 92 | futures-intrusive = "0.5.0" 93 | pollster = "0.4" 94 | smol_str = "0.2" 95 | bitflags = "2.8.0" 96 | 97 | [profile.production] 98 | inherits = "release" 99 | opt-level = 3 100 | debug = false 101 | lto = true 102 | codegen-units = 1 103 | strip = true 104 | incremental = false 105 | 106 | [profile.p2] 107 | inherits = "production" 108 | opt-level = 2 109 | 110 | [profile.small] 111 | inherits = "production" 112 | opt-level = "z" 113 | panic = "abort" 114 | 115 | # This is a "virtual package" 116 | # It is not meant to be published, but is used so "cargo run --example XYZ" works properly 117 | [package] 118 | name = "blitz-examples" 119 | version = "0.0.1" 120 | authors = ["Jonathan Kelley"] 121 | edition = "2024" 122 | description = "Top level crate for Blitz" 123 | license = "MIT OR Apache-2.0" 124 | keywords = ["dom", "ui", "gui", "react", "wasm"] 125 | rust-version = "1.85.1" 126 | publish = false 127 | 128 | [dev-dependencies] 129 | blitz-dom = { path = "./packages/blitz-dom" } 130 | blitz-html = { path = "./packages/blitz-html" } 131 | blitz-traits = { path = "./packages/blitz-traits" } 132 | blitz-paint = { path = "./packages/blitz-paint" } 133 | anyrender = { path = "./packages/anyrender" } 134 | anyrender_vello = { path = "./packages/anyrender_vello" } 135 | anyrender_vello_cpu = { path = "./packages/anyrender_vello_cpu" } 136 | blitz-shell = { path = "./packages/blitz-shell" } 137 | blitz-net = { path = "./packages/blitz-net" } 138 | blitz = { path = "./packages/blitz", features = ["net"] } 139 | mini-dxn = { path = "./packages/mini-dxn", features = ["tracing", "autofocus"] } 140 | dioxus = { workspace = true } 141 | euclid = { workspace = true } 142 | reqwest = { workspace = true } 143 | tokio = { workspace = true, features = ["macros"] } 144 | image = { workspace = true } 145 | png = "0.17" 146 | env_logger = "0.11" 147 | tracing-subscriber = "0.3" 148 | 149 | # [patch.crates-io] 150 | # [patch."https://github.com/dioxuslabs/taffy"] 151 | # taffy = { path = "../taffy" } 152 | 153 | # [patch."https://github.com/nicoburns/parley"] 154 | # parley = { path = "../parley/parley" } 155 | # fontique = { path = "../parley/fontique" } 156 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /apps/readme/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "readme" 3 | version = "0.1.0" 4 | edition = "2024" 5 | license.workspace = true 6 | 7 | [features] 8 | default = ["gpu"] 9 | gpu = ["dep:anyrender_vello"] 10 | cpu = ["dep:anyrender_vello_cpu"] 11 | avif = ["dep:image", "image?/avif-native"] 12 | 13 | [dependencies] 14 | blitz-traits = { path = "../../packages/blitz-traits" } 15 | blitz-dom = { path = "../../packages/blitz-dom" } 16 | blitz-html = { path = "../../packages/blitz-html" } 17 | blitz-paint = { path = "../../packages/blitz-paint" } 18 | blitz-net = { path = "../../packages/blitz-net", features = ["cookies"] } 19 | blitz-shell = { path = "../../packages/blitz-shell", features = ["tracing"] } 20 | anyrender_vello = { path = "../../packages/anyrender_vello", optional = true } 21 | anyrender_vello_cpu = { path = "../../packages/anyrender_vello_cpu", optional = true } 22 | tokio = { workspace = true, features = ["rt", "rt-multi-thread"] } 23 | reqwest = { workspace = true } 24 | url = { workspace = true } 25 | winit = { workspace = true } 26 | comrak = { version = "0.39", default-features = false } 27 | image = { workspace = true, default-features = false, optional = true } 28 | notify = "8.0.0" -------------------------------------------------------------------------------- /apps/readme/assets/blitz-markdown-overrides.css: -------------------------------------------------------------------------------- 1 | .markdown-body { 2 | max-width: 892px; 3 | padding: 16px 32px; 4 | margin: 0 auto; 5 | } 6 | 7 | .markdown-body table { 8 | display: table; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | html, body { 13 | background-color: #0d1117; 14 | } 15 | 16 | [href$="#gh-light-mode-only"] { 17 | display: none !important; 18 | } 19 | [src$="#gh-light-mode-only"] { 20 | display: none !important; 21 | } 22 | } 23 | 24 | @media (prefers-color-scheme: light) { 25 | [href$="#gh-dark-mode-only"] { 26 | display: none !important; 27 | } 28 | [src$="#gh-dark-mode-only"] { 29 | display: none !important; 30 | } 31 | } -------------------------------------------------------------------------------- /apps/readme/src/markdown.rs: -------------------------------------------------------------------------------- 1 | //! Render the readme.md using the gpu renderer 2 | 3 | use comrak::{ExtensionOptions, Options, Plugins, RenderOptions, markdown_to_html_with_plugins}; 4 | 5 | pub(crate) const GITHUB_MD_STYLES: &str = include_str!("../assets/github-markdown.css"); 6 | pub(crate) const BLITZ_MD_STYLES: &str = include_str!("../assets/blitz-markdown-overrides.css"); 7 | 8 | pub(crate) fn markdown_to_html(contents: String) -> String { 9 | let plugins = Plugins::default(); 10 | // let syntax_highligher = CustomSyntectAdapter(SyntectAdapter::new(Some("InspiredGitHub"))); 11 | // plugins.render.codefence_syntax_highlighter = Some(&syntax_highligher as _); 12 | 13 | let body_html = markdown_to_html_with_plugins( 14 | &contents, 15 | &Options { 16 | extension: ExtensionOptions { 17 | strikethrough: true, 18 | tagfilter: false, 19 | table: true, 20 | autolink: true, 21 | tasklist: true, 22 | superscript: false, 23 | header_ids: None, 24 | footnotes: false, 25 | description_lists: false, 26 | front_matter_delimiter: None, 27 | multiline_block_quotes: false, 28 | alerts: true, 29 | ..ExtensionOptions::default() 30 | }, 31 | render: RenderOptions { 32 | unsafe_: true, 33 | tasklist_classes: true, 34 | ..RenderOptions::default() 35 | }, 36 | ..Options::default() 37 | }, 38 | &plugins, 39 | ); 40 | 41 | // Strip trailing newlines in code blocks 42 | let body_html = body_html.replace("\n 47 | 48 | 49 |
{body_html}
50 | 51 | 52 | "# 53 | ) 54 | } 55 | 56 | // #[allow(unused)] 57 | // mod syntax_highlighter { 58 | // use comrak::adapters::SyntaxHighlighterAdapter; 59 | // use comrak::plugins::syntect::SyntectAdapter; 60 | // use std::collections::HashMap; 61 | 62 | // struct CustomSyntectAdapter(SyntectAdapter); 63 | 64 | // impl SyntaxHighlighterAdapter for CustomSyntectAdapter { 65 | // fn write_highlighted( 66 | // &self, 67 | // output: &mut dyn std::io::Write, 68 | // lang: Option<&str>, 69 | // code: &str, 70 | // ) -> std::io::Result<()> { 71 | // let norm_lang = lang.map(|l| l.split_once(',').map(|(lang, _)| lang).unwrap_or(l)); 72 | // self.0.write_highlighted(output, norm_lang, code) 73 | // } 74 | 75 | // fn write_pre_tag( 76 | // &self, 77 | // output: &mut dyn std::io::Write, 78 | // attributes: HashMap, 79 | // ) -> std::io::Result<()> { 80 | // self.0.write_pre_tag(output, attributes) 81 | // } 82 | 83 | // fn write_code_tag( 84 | // &self, 85 | // output: &mut dyn std::io::Write, 86 | // attributes: HashMap, 87 | // ) -> std::io::Result<()> { 88 | // self.0.write_code_tag(output, attributes) 89 | // } 90 | // } 91 | // } 92 | -------------------------------------------------------------------------------- /examples/assets/border.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 63 | 64 | 65 |
hi
66 |
Dioxus12312312312321
hi
67 |
Dioxus12312312312321
hi
68 |
Dioxus12312312312321
hi
69 |
Dioxus12312312312321
hi
70 |
Dioxus12312312312321
hi
71 | 72 | 73 | -------------------------------------------------------------------------------- /examples/assets/docsrs_header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 |

Crate taffy

12 | 13 |
14 | Settings 15 |
16 |
17 | Help 18 |
19 | 20 |
21 | Source
22 | 23 | 24 | -------------------------------------------------------------------------------- /examples/assets/github_profile_reduced.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 16 | 17 | 18 |
Foo
19 | 20 | 21 | -------------------------------------------------------------------------------- /examples/assets/github_profile_reduced3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | nicoburns (Nico Burns) 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
62 |
63 |
64 |
65 | 68 | DioxusLabs/blitz Public 69 |
70 |
71 | 72 |
73 |
74 | 75 |
76 |
77 | 78 | 79 | -------------------------------------------------------------------------------- /examples/assets/google_reduced.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Google 5 | 6 | 7 |
Foo
8 | 9 | -------------------------------------------------------------------------------- /examples/assets/gosub.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | Gosub - The gateway to optimized searching and browsing 94 | 95 | 96 | 97 |
98 |

Gosub

99 |

The gateway to optimized searching and browsing

100 | 101 | 102 |
103 | Join us on the journey to a new web browser 104 |
105 | 106 | 111 |
112 | 113 | 114 | -------------------------------------------------------------------------------- /examples/assets/gosub_reduced.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |
6 |
GitHub
7 |
Global Discord
8 |
Developer chat - Zudivp
9 |
10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/assets/input.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | 13 | -------------------------------------------------------------------------------- /examples/assets/noscript.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /examples/assets/object_fit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 |

Fill

13 | 14 | 15 | 16 | 17 |

Contain (50px)

18 | 19 | 20 | 21 | 22 |

Cover (50px)

23 | 24 | 25 | 26 | 27 |
28 | 29 |

Contain (100px)

30 | 31 | 32 | 33 | 34 |

Cover (100px)

35 | 36 | 37 | 38 | 39 |

Contain (200px)

40 | 41 | 42 | 43 | 44 |
45 | 46 |

Cover (200px)

47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /examples/assets/pseudo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 69 | 70 | 71 |
Hello
72 | 73 |
Hello
74 | 80 |
  
81 | 82 | -------------------------------------------------------------------------------- /examples/assets/servo-color-negative-no-container.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DioxusLabs/blitz/42aa7f8625c112495edd73d12b622ab97aefbf37/examples/assets/servo-color-negative-no-container.png -------------------------------------------------------------------------------- /examples/assets/servo_header_reduced.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 21 | 22 | -------------------------------------------------------------------------------- /examples/assets/servo_reduced.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |
6 |
WRAP WRAP WRAP WRAP WRAP
7 | 8 |
9 |
10 | 11 | -------------------------------------------------------------------------------- /examples/assets/square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DioxusLabs/blitz/42aa7f8625c112495edd73d12b622ab97aefbf37/examples/assets/square.png -------------------------------------------------------------------------------- /examples/assets/svg.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/assets/tall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DioxusLabs/blitz/42aa7f8625c112495edd73d12b622ab97aefbf37/examples/assets/tall.png -------------------------------------------------------------------------------- /examples/assets/wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DioxusLabs/blitz/42aa7f8625c112495edd73d12b622ab97aefbf37/examples/assets/wide.png -------------------------------------------------------------------------------- /examples/box_shadow.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | 3 | fn main() { 4 | mini_dxn::launch(app); 5 | } 6 | 7 | fn app() -> Element { 8 | rsx! { 9 | div { 10 | style { {CSS} } 11 | div { 12 | id: "box-shadow-1", 13 | class: "box-shadow", 14 | } 15 | div { 16 | id: "box-shadow-2", 17 | class: "box-shadow", 18 | } 19 | div { 20 | id: "box-shadow-3", 21 | class: "box-shadow", 22 | } 23 | } 24 | } 25 | } 26 | 27 | const CSS: &str = r#" 28 | .box-shadow { 29 | width: 200px; 30 | height: 200px; 31 | background-color: red; 32 | margin: 60px; 33 | } 34 | 35 | #box-shadow-1 { 36 | width: 100px; 37 | height: 100px; 38 | box-shadow: 140px 0 blue; 39 | } 40 | 41 | #box-shadow-2 { 42 | box-shadow: 10px 10px 5px 10px rgb(238 255 7), 10px 10px 5px 30px blue; 43 | } 44 | 45 | #box-shadow-3 { 46 | box-shadow: 0 0 10px 20px rgb(238 255 7); 47 | } 48 | "#; 49 | -------------------------------------------------------------------------------- /examples/counter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "counter" 3 | version = "0.1.0" 4 | edition = "2024" 5 | license.workspace = true 6 | 7 | [features] 8 | default = ["system_fonts", "gpu_backend"] 9 | system_fonts = ["blitz-dom/system_fonts"] 10 | gpu_backend = ["mini-dxn/gpu_backend"] 11 | cpu_backend = ["mini-dxn/cpu_backend"] 12 | 13 | [dependencies] 14 | mini-dxn = { path = "../../packages/mini-dxn", default-features = false } 15 | dioxus = { version = "=0.7.0-alpha.1", default-features = false, features = ["html", "hooks", "macro", "signals"] } 16 | 17 | # Control whether system font support is enabled 18 | blitz-dom = { path = "../../packages/blitz-dom", default-features = false } 19 | 20 | # Disable unicode URL support 21 | # See https://github.com/hsivonen/idna_adapter 22 | idna_adapter = "=1.0.0" -------------------------------------------------------------------------------- /examples/counter/src/main.rs: -------------------------------------------------------------------------------- 1 | //! Drive the renderer from Dioxus 2 | use dioxus::prelude::*; 3 | 4 | fn main() { 5 | mini_dxn::launch(app); 6 | } 7 | 8 | fn app() -> Element { 9 | let mut count = use_signal(|| 0); 10 | 11 | rsx! { 12 | div { 13 | class: "container", 14 | style { {CSS} } 15 | h1 { class: "header", "Count: {count}" } 16 | div { class: "buttons", 17 | button { 18 | class: "counter-button btn-green", 19 | onclick: move |_| { count += 1 }, 20 | "Increment" 21 | } 22 | button { 23 | class: "counter-button btn-red", 24 | onclick: move |_| { count -= 1 }, 25 | "Decrement" 26 | } 27 | } 28 | button { 29 | class: "counter-button btn-blue", 30 | onclick: move |_| { count.set(0) }, 31 | "Reset" 32 | } 33 | } 34 | } 35 | } 36 | 37 | const CSS: &str = r#" 38 | .header { 39 | background-color: pink; 40 | padding: 20px; 41 | line-height: 1; 42 | font-family: sans-serif; 43 | } 44 | 45 | .container { 46 | display: flex; 47 | flex-direction: column; 48 | justify-content: center; 49 | align-items: center; 50 | height: 100vh; 51 | width: 100vw; 52 | background: linear-gradient(217deg, rgba(255,0,0,.8), rgba(255,0,0,0) 70.71%), 53 | linear-gradient(127deg, rgba(0,255,0,.8), rgba(0,255,0,0) 70.71%), 54 | linear-gradient(336deg, rgba(0,0,255,.8), rgba(0,0,255,0) 70.71%); 55 | } 56 | 57 | .buttons { 58 | display: flex; 59 | flex-direction: row; 60 | justify-content: center; 61 | align-items: center; 62 | margin: 20px 0; 63 | } 64 | 65 | .counter-button { 66 | margin: 0 10px; 67 | padding: 10px 20px; 68 | border-radius: 5px; 69 | font-size: 1.5rem; 70 | cursor: pointer; 71 | line-height: 1; 72 | font-family: sans-serif; 73 | border-width: 2px; 74 | border-style: solid; 75 | } 76 | .counter-button:focus { 77 | outline: 4px solid black; 78 | } 79 | 80 | .btn-green { 81 | background-color: green; 82 | border-color: green; 83 | color: white; 84 | } 85 | .btn-green:hover { 86 | color: green; 87 | background-color: white; 88 | } 89 | 90 | .btn-red { 91 | background-color: red; 92 | border-color: red; 93 | color: white; 94 | } 95 | .btn-red:hover { 96 | color: red; 97 | background-color: white; 98 | } 99 | 100 | .btn-blue { 101 | background-color: blue; 102 | border-color: blue; 103 | color: white; 104 | } 105 | .btn-blue:hover { 106 | color: blue; 107 | background-color: white; 108 | } 109 | 110 | 111 | "#; 112 | -------------------------------------------------------------------------------- /examples/flex.rs: -------------------------------------------------------------------------------- 1 | /* 2 | Servo doesn't have: 3 | - space-evenly? 4 | - gap 5 | */ 6 | 7 | use dioxus::prelude::*; 8 | 9 | fn main() { 10 | mini_dxn::launch(app); 11 | } 12 | 13 | fn app() -> Element { 14 | rsx! { 15 | div { 16 | style { {CSS} } 17 | div { 18 | h2 { "justify-content" } 19 | for row in ["flex-start", "flex-end", "center", "space-between", "space-around", "space-evenly"] { 20 | h3 { "{row}" } 21 | div { id: "container", justify_content: "{row}", 22 | div { class: "floater", "__1__" } 23 | div { class: "floater", "__2__" } 24 | div { class: "floater", "__3__" } 25 | } 26 | } 27 | } 28 | h3 { "CSS Grid Test"} 29 | div { 30 | id: "grid_container", 31 | for _ in 0..3 { 32 | div { class: "floater", "__1__" } 33 | div { class: "floater", "__2__" } 34 | div { class: "floater", "__3__" } 35 | div { class: "floater", "__4__" } 36 | div { class: "floater", "__5__" } 37 | div { class: "floater", "__6__" } 38 | div { class: "floater", "__7__" } 39 | div { class: "floater", "__8__" } 40 | div { class: "floater", "__9__" } 41 | div { class: "floater", "__0__" } 42 | div { class: "floater", "__A__" } 43 | div { class: "floater", "__B__" } 44 | } 45 | } 46 | } 47 | } 48 | } 49 | 50 | const CSS: &str = r#" 51 | #container { 52 | flex: 1 1 auto; 53 | flex-direction: row; 54 | background-color: gray; 55 | border: 1px solid black; 56 | border-top-color: red; 57 | border-left: 4px solid #000; 58 | border-top: 10px solid #ff0; 59 | border-right: 3px solid #F01; 60 | border-bottom: 9px solid #0f0; 61 | box-shadow: 10px 10px gray; 62 | 63 | 64 | outline-style: solid; 65 | outline-color: blue; 66 | border-radius: 50px 20px; 67 | padding: 10px; 68 | margin: 5px; 69 | display: flex; 70 | gap: 10px; 71 | } 72 | 73 | div { 74 | font-family: sans-serif; 75 | } 76 | 77 | h3 { 78 | font-size: 2em; 79 | } 80 | 81 | #grid_container { 82 | display: grid; 83 | grid-template-columns: 100px 1fr 1fr 100px; 84 | gap: 10px; 85 | padding: 10px; 86 | } 87 | 88 | .floater { 89 | background-color: orange; 90 | border: 3px solid black; 91 | padding: 10px; 92 | border-radius: 5px; 93 | // margin: 0px 10px 0px 10px; 94 | } 95 | "#; 96 | -------------------------------------------------------------------------------- /examples/form.rs: -------------------------------------------------------------------------------- 1 | //! Drive the renderer from Dioxus 2 | 3 | use dioxus::prelude::*; 4 | 5 | fn main() { 6 | mini_dxn::launch(app); 7 | } 8 | 9 | fn app() -> Element { 10 | let mut checkbox_checked = use_signal(|| false); 11 | 12 | rsx! { 13 | div { 14 | class: "container", 15 | style { {CSS} } 16 | form { 17 | div { 18 | input { 19 | type: "checkbox", 20 | id: "check1", 21 | name: "check1", 22 | value: "check1", 23 | checked: checkbox_checked(), 24 | // This works too 25 | // checked: "{checkbox_checked}", 26 | oninput: move |ev| checkbox_checked.set(!ev.checked()), 27 | } 28 | label { 29 | r#for: "check1", 30 | "Checkbox 1 (controlled)" 31 | } 32 | } 33 | div { 34 | input { 35 | type: "checkbox", 36 | id: "check3", 37 | name: "check3", 38 | value: "check3", 39 | } 40 | label { 41 | r#for: "check3", 42 | "Checkbox 1 (uncontrolled with for)" 43 | } 44 | } 45 | div { 46 | label { 47 | input { 48 | type: "checkbox", 49 | name: "check2", 50 | value: "check2", 51 | } 52 | "Checkbox 2 (uncontrolled nested)" 53 | } 54 | } 55 | div { 56 | label { 57 | r#for: "radio1", 58 | id: "radio1label", 59 | input { 60 | type: "radio", 61 | name: "radiobuttons", 62 | id: "radio1", 63 | value: "radiovalue1", 64 | checked: true, 65 | } 66 | "Radio Button 1" 67 | } 68 | } 69 | div { 70 | label { 71 | r#for: "radio2", 72 | id: "radio2label", 73 | input { 74 | type: "radio", 75 | name: "radiobuttons", 76 | id: "radio2", 77 | value: "radiovalue2", 78 | } 79 | "Radio Button 2" 80 | } 81 | } 82 | div { 83 | label { 84 | r#for: "radio3", 85 | id: "radio3label", 86 | input { 87 | type: "radio", 88 | name: "radiobuttons", 89 | id: "radio3", 90 | value: "radiovalue3", 91 | } 92 | "Radio Button 3" 93 | } 94 | } 95 | } 96 | div { "Checkbox 1 checked: {checkbox_checked}" } 97 | } 98 | } 99 | } 100 | 101 | const CSS: &str = r#" 102 | 103 | .container { 104 | display: flex; 105 | flex-direction: column; 106 | justify-content: center; 107 | align-items: center; 108 | height: 100vh; 109 | width: 100vw; 110 | } 111 | 112 | 113 | form { 114 | margin: 12px 0; 115 | display: block; 116 | } 117 | 118 | form > div { 119 | margin: 8px 0; 120 | } 121 | 122 | label { 123 | display: inline-block; 124 | } 125 | 126 | input { 127 | /* Should be accent-color */ 128 | color: #0000cc; 129 | } 130 | 131 | input[type=radio]:checked { 132 | border-color: #0000cc; 133 | } 134 | 135 | "#; 136 | -------------------------------------------------------------------------------- /examples/gradient.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | 3 | fn main() { 4 | mini_dxn::launch(app); 5 | } 6 | 7 | fn app() -> Element { 8 | rsx! { 9 | style { {CSS} } 10 | div { 11 | class: "grid-container", 12 | div { id: "a1" } 13 | div { id: "a2" } 14 | div { id: "a3" } 15 | div { id: "a4" } 16 | 17 | div { id: "b1" } 18 | div { id: "b2" } 19 | div { id: "b3" } 20 | div { id: "b4" } 21 | div { id: "b5" } 22 | 23 | div { id: "c1" } 24 | div { id: "c2" } 25 | div { id: "c3" } 26 | 27 | div { id: "d1" } 28 | div { id: "d2" } 29 | div { id: "d3" } 30 | div { id: "d4" } 31 | div { id: "d5" } 32 | 33 | div { id: "e1" } 34 | div { id: "e2" } 35 | div { id: "e3" } 36 | div { id: "e4" } 37 | div { id: "e5" } 38 | } 39 | } 40 | } 41 | 42 | const CSS: &str = r#" 43 | .grid-container { 44 | display: grid; 45 | grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); 46 | gap: 10px; 47 | width: 95vw; 48 | height: 95vh; 49 | } 50 | 51 | div { 52 | min-width: 100px; 53 | min-height: 100px; 54 | } 55 | 56 | #a1 { background: linear-gradient(#e66465, #9198e5) } 57 | #a2 { background: linear-gradient(0.25turn, #3f87a6, #ebf8e1, #f69d3c) } 58 | #a3 { background: linear-gradient(to left, #333, #333 50%, #eee 75%, #333 75%) } 59 | #a4 { background: linear-gradient(217deg, rgba(255,0,0,.8), rgba(255,0,0,0) 70.71%), 60 | linear-gradient(127deg, rgba(0,255,0,.8), rgba(0,255,0,0) 70.71%), 61 | linear-gradient(336deg, rgba(0,0,255,.8), rgba(0,0,255,0) 70.71%) } 62 | 63 | #b1 { background: linear-gradient(to right, red 0%, 0%, blue 100%) } 64 | #b2 { background: linear-gradient(to right, red 0%, 25%, blue 100%) } 65 | #b3 { background: linear-gradient(to right, red 0%, 50%, blue 100%) } 66 | #b4 { background: linear-gradient(to right, red 0%, 100%, blue 100%) } 67 | #b5 { background: linear-gradient(to right, yellow, red 10%, 10%, blue 100%) } 68 | 69 | #c1 { background: repeating-linear-gradient(#e66465, #e66465 20px, #9198e5 20px, #9198e5 25px) } 70 | #c2 { background: repeating-linear-gradient(45deg, #3f87a6, #ebf8e1 15%, #f69d3c 20%) } 71 | #c3 { background: repeating-linear-gradient(transparent, #4d9f0c 40px), 72 | repeating-linear-gradient(0.25turn, transparent, #3f87a6 20px) } 73 | 74 | #d1 { background: radial-gradient(circle, red 20px, black 21px, blue) } 75 | #d2 { background: radial-gradient(closest-side, #3f87a6, #ebf8e1, #f69d3c) } 76 | #d3 { background: radial-gradient(circle at 100%, #333, #333 50%, #eee 75%, #333 75%) } 77 | #d4 { background: radial-gradient(ellipse at top, #e66465, transparent), 78 | radial-gradient(ellipse at bottom, #4d9f0c, transparent) } 79 | #d5 { background: radial-gradient(closest-corner circle at 20px 30px, red, yellow, green) } 80 | #e1 { background: repeating-conic-gradient(red 0%, yellow 15%, red 33%) } 81 | #e2 { background: repeating-conic-gradient( 82 | from 45deg at 10% 50%, 83 | brown 0deg 10deg, 84 | darkgoldenrod 10deg 20deg, 85 | chocolate 20deg 30deg 86 | ) } 87 | #e3 { background: repeating-radial-gradient(#e66465, #9198e5 20%) } 88 | #e4 { background: repeating-radial-gradient(closest-side, #3f87a6, #ebf8e1, #f69d3c) } 89 | #e5 { background: repeating-radial-gradient(circle at 100%, #333, #333 10px, #eee 10px, #eee 20px) } 90 | "#; 91 | -------------------------------------------------------------------------------- /examples/html.rs: -------------------------------------------------------------------------------- 1 | //! Example of rendering a static html string to a window 2 | 3 | const HTML: &str = r#" 4 | 5 | 6 | 7 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | "#; 46 | 47 | fn main() { 48 | blitz::launch_static_html(HTML); 49 | } 50 | -------------------------------------------------------------------------------- /examples/inline.rs: -------------------------------------------------------------------------------- 1 | //! https://www.w3schools.com/css/tryit.asp?filename=trycss_inline-block_span1 2 | 3 | use dioxus::prelude::*; 4 | 5 | fn main() { 6 | mini_dxn::launch(app); 7 | } 8 | 9 | fn app() -> Element { 10 | rsx! { 11 | head { 12 | style { {CSS} } 13 | } 14 | body { 15 | h1 { "The display Property" } 16 | h2 { "display: inline" } 17 | div { 18 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum consequat scelerisque elit sit amet consequat. Aliquam erat volutpat. " 19 | span { class: "a", "Aliquam" } 20 | span { class: "a", "venenatis" } 21 | " gravida nisl sit amet facilisis. Nullam cursus fermentum velit sed laoreet. " 22 | } 23 | h2 { "display: inline-block" } 24 | div { 25 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum consequat scelerisque elit sit amet consequat. Aliquam erat volutpat. " 26 | span { class: "b", "Aliquam" } 27 | span { class: "b", "venenatis" } 28 | " gravida nisl sit amet facilisis. Nullam cursus fermentum velit sed laoreet. " 29 | } 30 | h2 { "display: block" } 31 | div { 32 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum consequat scelerisque elit sit amet consequat. Aliquam erat volutpat. " 33 | span { class: "c", "Aliquam" } 34 | span { class: "c", "venenatis" } 35 | " gravida nisl sit amet facilisis. Nullam cursus fermentum velit sed laoreet. " 36 | } 37 | div { id: "a", 38 | "Some text" 39 | em { "Another block of text" } 40 | "Should connect no space between" 41 | } 42 | h1 { "ul" } 43 | ul { 44 | li { "Item 1" } 45 | li { "Item 2" } 46 | li { 47 | class: "square", 48 | "Square item" 49 | } 50 | li { 51 | class: "circle", 52 | "Circle item" 53 | } 54 | li { 55 | class: "disclosure-open", 56 | "Disclosure open item" 57 | } 58 | li { 59 | class: "disclosure-closed", 60 | "Disclosure closed item" 61 | } 62 | } 63 | h1 { "ol - decimal" } 64 | ol { 65 | li { "Item 1" } 66 | li { "Item 2" } 67 | li { 68 | ul { 69 | li { "Nested Item 1" } 70 | li { "Nested Item 2" } 71 | } 72 | } 73 | li { "Item 3" } 74 | li { "Item 4" } 75 | ol { 76 | li { "Sub 1" } 77 | li { "Sub 2" } 78 | } 79 | } 80 | h1 { "ol - alpha" } 81 | ol { class: "alpha", 82 | li { "Item 1" } 83 | li { "Item 2" } 84 | li { "Item 3" } 85 | } 86 | } 87 | } 88 | } 89 | 90 | const CSS: &str = r#" 91 | span.a { 92 | display: inline; /* the default for span */ 93 | width: 100px; 94 | height: 100px; 95 | padding: 5px; 96 | border: 1px solid blue; 97 | background-color: yellow; 98 | } 99 | 100 | span.b { 101 | display: inline-block; 102 | width: 100px; 103 | height: 100px; 104 | padding: 5px; 105 | border: 1px solid blue; 106 | background-color: yellow; 107 | } 108 | 109 | span.c { 110 | display: block; 111 | width: 100px; 112 | height: 100px; 113 | padding: 5px; 114 | border: 1px solid blue; 115 | background-color: yellow; 116 | } 117 | 118 | #a { 119 | } 120 | h1 { 121 | font-size: 20px; 122 | } 123 | ol.alpha { 124 | list-style-type: lower-alpha; 125 | } 126 | li.square { 127 | list-style-type: square; 128 | } 129 | li.circle { 130 | list-style-type: circle; 131 | } 132 | li.disclosure-open { 133 | list-style-type: disclosure-open; 134 | } 135 | li.disclosure-closed { 136 | list-style-type: disclosure-closed; 137 | } 138 | 139 | "#; 140 | -------------------------------------------------------------------------------- /examples/outline.rs: -------------------------------------------------------------------------------- 1 | // background: rgb(2,0,36); 2 | // background: linear-gradient(90deg, rgba(2,0,36,1) 0%, rgba(9,9,121,1) 35%, rgba(0,212,255,1) 100%); 3 | 4 | use dioxus::prelude::*; 5 | 6 | fn main() { 7 | mini_dxn::launch(app); 8 | } 9 | 10 | fn app() -> Element { 11 | rsx! { 12 | style { {CSS} } 13 | div { "padd " } 14 | div { "padd " } 15 | div { "padd " } 16 | div { "padd " } 17 | div { "padd " } 18 | div { "padd " } 19 | div { "padd " } 20 | div { "padd " } 21 | div { 22 | class: "colorful", 23 | id: "a", 24 | div { "Dioxus12312312312321" } 25 | div { "Dioxus12312312312321" } 26 | div { "Dioxus12312312312321" } 27 | div { "Dioxus12312312312321" } 28 | div { "Dioxus12312312312321" } 29 | div { "Dioxus12312312312321" } 30 | } 31 | } 32 | } 33 | 34 | const CSS: &str = r#" 35 | .colorful { 36 | border-right-color: #000; 37 | border-left-color: #ff0; 38 | border-top-color: #F01; 39 | border-bottom-color: #0f0; 40 | } 41 | #a { 42 | height:300px; 43 | background-color: gray; 44 | border: 1px solid black; 45 | // border-radius: 50px 20px; 46 | border-top-color: red; 47 | // padding:20px; 48 | // margin:20px; 49 | // border-radius: 10px; 50 | border-radius: 10% 30% 50% 70%; 51 | border-left: 4px solid #000; 52 | border-top: 10px solid #ff0; 53 | border-right: 3px solid #F01; 54 | border-bottom: 9px solid #0f0; 55 | box-shadow: 10px 10px gray; 56 | 57 | outline-width: 50px; 58 | outline-style: solid; 59 | outline-color: blue; 60 | } 61 | "#; 62 | -------------------------------------------------------------------------------- /examples/output/.gitkeep: -------------------------------------------------------------------------------- 1 | .gitkeep -------------------------------------------------------------------------------- /examples/restyle.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | use tokio::time::{Duration, sleep}; 3 | 4 | fn main() { 5 | // Turn on the runtime and enter it 6 | let rt = tokio::runtime::Builder::new_multi_thread() 7 | .enable_all() 8 | .build() 9 | .unwrap(); 10 | let _guard = rt.enter(); 11 | 12 | mini_dxn::launch(app); 13 | } 14 | 15 | #[derive(Copy, Clone)] 16 | enum AnimationState { 17 | Increasing, 18 | Decreasing, 19 | } 20 | 21 | impl std::ops::Not for AnimationState { 22 | type Output = Self; 23 | fn not(self) -> Self::Output { 24 | match self { 25 | AnimationState::Increasing => AnimationState::Decreasing, 26 | AnimationState::Decreasing => AnimationState::Increasing, 27 | } 28 | } 29 | } 30 | 31 | const MIN_SIZE: i32 = 12; 32 | const MAX_SIZE: i32 = 120; 33 | 34 | fn app() -> Element { 35 | let mut size = use_signal(|| 12); 36 | let mut direction = use_signal(|| AnimationState::Increasing); 37 | let mut running = use_signal(|| true); 38 | 39 | // `use_future` will spawn an infinitely direction future that can be started and stopped 40 | use_future(move || async move { 41 | loop { 42 | if running() { 43 | match direction() { 44 | AnimationState::Increasing => size += 1, 45 | AnimationState::Decreasing => size -= 1, 46 | } 47 | 48 | let size = *size.read(); 49 | if size <= MIN_SIZE { 50 | *direction.write() = AnimationState::Increasing; 51 | } 52 | if size >= MAX_SIZE { 53 | *direction.write() = AnimationState::Decreasing; 54 | } 55 | } 56 | 57 | sleep(Duration::from_millis(16)).await; 58 | } 59 | }); 60 | rsx! { 61 | div { 62 | style { { STYLES } } 63 | h1 { "Current size: {size}" } 64 | div { 65 | style: "display: flex", 66 | div { class: "button", onclick: move |_| running.toggle(), "Start/Stop"} 67 | div { class: "button", onclick: move |_| size.set(12), "Reset the size" } 68 | } 69 | p { 70 | style: "font-size: {size}px", 71 | "Animate Font Size" 72 | } 73 | } 74 | } 75 | } 76 | 77 | static STYLES: &str = r#" 78 | .button { 79 | padding: 6px; 80 | border: 1px solid #999; 81 | margin-left: 12px; 82 | cursor: pointer; 83 | } 84 | 85 | .button:hover { 86 | background: #999; 87 | color: white; 88 | } 89 | "#; 90 | -------------------------------------------------------------------------------- /examples/url.rs: -------------------------------------------------------------------------------- 1 | //! Load first CLI argument as a url. Fallback to google.com if no CLI argument is provided. 2 | 3 | fn main() { 4 | let url = std::env::args() 5 | .nth(1) 6 | .unwrap_or_else(|| "https://www.google.com".into()); 7 | blitz::launch_url(&url); 8 | } 9 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | check: 2 | cargo check --workspace 3 | 4 | clippy: 5 | cargo clippy --workspace 6 | 7 | fmt: 8 | cargo fmt --all 9 | 10 | wpt *ARGS: 11 | cargo run --release --package wpt {{ARGS}} 12 | 13 | screenshot *ARGS: 14 | cargo run --release --example screenshot {{ARGS}} 15 | 16 | open *ARGS: 17 | cargo run --release --package readme {{ARGS}} 18 | 19 | todomvc: 20 | cargo run --release --example todomvc 21 | 22 | small: 23 | cargo build --profile small -p counter --no-default-features --features cpu_backend,system_fonts -------------------------------------------------------------------------------- /packages/anyrender/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "anyrender" 3 | description = "2D Canvas abstraction" 4 | version = "0.1.0" 5 | homepage = "https://github.com/dioxuslabs/blitz" 6 | repository = "https://github.com/dioxuslabs/blitz" 7 | documentation = "https://docs.rs/anyrender" 8 | license.workspace = true 9 | edition = "2024" 10 | 11 | [dependencies] 12 | kurbo = { workspace = true } 13 | peniko = { workspace = true } 14 | raw-window-handle = { workspace = true } -------------------------------------------------------------------------------- /packages/anyrender/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use kurbo::{Affine, Rect, Shape, Stroke}; 4 | use peniko::{BlendMode, BrushRef, Color, Fill, Font, Image, StyleRef}; 5 | use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; 6 | 7 | mod wasm_send_sync; 8 | pub use wasm_send_sync::*; 9 | 10 | pub type NormalizedCoord = i16; 11 | 12 | /// A positioned glyph. 13 | #[derive(Copy, Clone, Debug)] 14 | pub struct Glyph { 15 | pub id: u32, 16 | pub x: f32, 17 | pub y: f32, 18 | } 19 | 20 | // #[derive(Copy, Clone, Debug)] 21 | // pub struct Viewport { 22 | // pub width: u32, 23 | // pub height: u32, 24 | // pub scale: f64, 25 | // } 26 | 27 | pub trait WindowHandle: HasWindowHandle + HasDisplayHandle + WasmNotSendSync {} 28 | impl WindowHandle for T {} 29 | 30 | pub trait WindowRenderer { 31 | type Scene: Scene; 32 | fn new(window: Arc) -> Self; 33 | fn resume(&mut self, width: u32, height: u32); 34 | fn suspend(&mut self); 35 | fn is_active(&self) -> bool; 36 | fn set_size(&mut self, width: u32, height: u32); 37 | fn render(&mut self, draw_fn: F); 38 | } 39 | 40 | pub trait ImageRenderer { 41 | type Scene: Scene; 42 | fn new(width: u32, height: u32) -> Self; 43 | fn render(&mut self, draw_fn: F, buffer: &mut Vec); 44 | } 45 | 46 | pub fn render_to_buffer( 47 | draw_fn: F, 48 | width: u32, 49 | height: u32, 50 | ) -> Vec { 51 | let mut buf = Vec::with_capacity((width * height * 4) as usize); 52 | let mut renderer = R::new(width, height); 53 | renderer.render(draw_fn, &mut buf); 54 | 55 | buf 56 | } 57 | 58 | /// The primary drawing abstraction for drawing a single 2D scene 59 | pub trait Scene { 60 | /// The output type. 61 | /// This will usually be either a rendered scene or an encoded set of instructions with which to render a scene. 62 | type Output: 'static; 63 | 64 | /// Removes all content from the scene 65 | fn reset(&mut self); 66 | 67 | /// Pushes a new layer clipped by the specified shape and composed with previous layers using the specified blend mode. 68 | /// Every drawing command after this call will be clipped by the shape until the layer is popped. 69 | /// However, the transforms are not saved or modified by the layer stack. 70 | fn push_layer( 71 | &mut self, 72 | blend: impl Into, 73 | alpha: f32, 74 | transform: Affine, 75 | clip: &impl Shape, 76 | ); 77 | 78 | /// Pops the current layer. 79 | fn pop_layer(&mut self); 80 | 81 | /// Strokes a shape using the specified style and brush. 82 | fn stroke<'a>( 83 | &mut self, 84 | style: &Stroke, 85 | transform: Affine, 86 | brush: impl Into>, 87 | brush_transform: Option, 88 | shape: &impl Shape, 89 | ); 90 | 91 | /// Fills a shape using the specified style and brush. 92 | fn fill<'a>( 93 | &mut self, 94 | style: Fill, 95 | transform: Affine, 96 | brush: impl Into>, 97 | brush_transform: Option, 98 | shape: &impl Shape, 99 | ); 100 | 101 | /// Returns a builder for encoding a glyph run. 102 | #[allow(clippy::too_many_arguments)] 103 | fn draw_glyphs<'a, 's: 'a>( 104 | &'s mut self, 105 | font: &'a Font, 106 | font_size: f32, 107 | hint: bool, 108 | normalized_coords: &'a [NormalizedCoord], 109 | style: impl Into>, 110 | brush: impl Into>, 111 | brush_alpha: f32, 112 | transform: Affine, 113 | glyph_transform: Option, 114 | glyphs: impl Iterator, 115 | ); 116 | 117 | /// Draw a rounded rectangle blurred with a gaussian filter. 118 | fn draw_box_shadow( 119 | &mut self, 120 | transform: Affine, 121 | rect: Rect, 122 | brush: Color, 123 | radius: f64, 124 | std_dev: f64, 125 | ); 126 | 127 | /// Turn the scene into it's output type. 128 | fn finish(self) -> Self::Output; 129 | 130 | // --- Provided methods 131 | 132 | /// Utility method to draw an image at it's natural size. For more advanced image drawing use the `fill` method 133 | fn draw_image(&mut self, image: &Image, transform: Affine) { 134 | self.fill( 135 | Fill::NonZero, 136 | transform, 137 | image, 138 | None, 139 | &Rect::new(0.0, 0.0, image.width as f64, image.height as f64), 140 | ); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /packages/anyrender/src/wasm_send_sync.rs: -------------------------------------------------------------------------------- 1 | pub trait WasmNotSendSync: WasmNotSend + WasmNotSync {} 2 | impl WasmNotSendSync for T {} 3 | 4 | #[cfg(not(target_arch = "wasm32"))] 5 | pub trait WasmNotSend: Send {} 6 | #[cfg(not(target_arch = "wasm32"))] 7 | impl WasmNotSend for T {} 8 | #[cfg(target_arch = "wasm32")] 9 | pub trait WasmNotSend {} 10 | #[cfg(target_arch = "wasm32")] 11 | impl WasmNotSend for T {} 12 | 13 | #[cfg(not(target_arch = "wasm32"))] 14 | pub trait WasmNotSync: Sync {} 15 | #[cfg(not(target_arch = "wasm32"))] 16 | impl WasmNotSync for T {} 17 | #[cfg(target_arch = "wasm32")] 18 | pub trait WasmNotSync {} 19 | #[cfg(target_arch = "wasm32")] 20 | impl WasmNotSync for T {} 21 | -------------------------------------------------------------------------------- /packages/anyrender_svg/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "anyrender_svg" 3 | description = "Render SVGs with anyrender" 4 | version = "0.1.0" 5 | homepage = "https://github.com/dioxuslabs/blitz" 6 | repository = "https://github.com/dioxuslabs/blitz" 7 | documentation = "https://docs.rs/anyrender-svg" 8 | license.workspace = true 9 | edition = "2024" 10 | 11 | [dependencies] 12 | anyrender = { version = "0.1", path = "../anyrender" } 13 | peniko = { workspace = true } 14 | kurbo = { workspace = true } 15 | usvg = { workspace = true } 16 | image = { workspace = true, default-features = false, optional = true } 17 | thiserror = "2" 18 | 19 | [features] 20 | default = ["image_format_png", "image_format_gif", "image_format_jpeg", "image_format_webp"] 21 | image = ["dep:image"] 22 | image_format_png = ["image", "image/png"] 23 | image_format_jpeg = ["image", "image/jpeg"] 24 | image_format_gif = ["image", "image/gif"] 25 | image_format_webp = ["image", "image/webp"] 26 | -------------------------------------------------------------------------------- /packages/anyrender_svg/src/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 the Vello Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | use thiserror::Error; 5 | 6 | /// Triggered when there is an issue parsing user input. 7 | #[derive(Error, Debug)] 8 | #[non_exhaustive] 9 | pub enum Error { 10 | #[error("Error parsing svg: {0}")] 11 | Svg(#[from] usvg::Error), 12 | } 13 | -------------------------------------------------------------------------------- /packages/anyrender_svg/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 the Vello Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | //! Render an SVG document to a Vello [`Scene`](vello::Scene). 5 | //! 6 | //! This currently lacks support for a [number of important](crate#unsupported-features) SVG features. 7 | //! 8 | //! This is also intended to be the preferred integration between Vello and [usvg], so [consider 9 | //! contributing](https://github.com/linebender/vello_svg) if you need a feature which is missing. 10 | //! 11 | //! This crate also re-exports [`usvg`] and [`vello`], so you can easily use the specific versions that are compatible with Vello SVG. 12 | //! 13 | //! # Unsupported features 14 | //! 15 | //! Missing features include: 16 | //! - text 17 | //! - group opacity 18 | //! - mix-blend-modes 19 | //! - clipping 20 | //! - masking 21 | //! - filter effects 22 | //! - group background 23 | //! - path shape-rendering 24 | //! - patterns 25 | 26 | // LINEBENDER LINT SET - lib.rs - v1 27 | // See https://linebender.org/wiki/canonical-lints/ 28 | // These lints aren't included in Cargo.toml because they 29 | // shouldn't apply to examples and tests 30 | #![warn(unused_crate_dependencies)] 31 | #![warn(clippy::print_stdout, clippy::print_stderr)] 32 | // END LINEBENDER LINT SET 33 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 34 | // The following lints are part of the Linebender standard set, 35 | // but resolving them has been deferred for now. 36 | // Feel free to send a PR that solves one or more of these. 37 | #![allow(missing_docs, clippy::shadow_unrelated, clippy::missing_errors_doc)] 38 | #![cfg_attr(test, allow(unused_crate_dependencies))] // Some dev dependencies are only used in tests 39 | 40 | mod render; 41 | 42 | mod error; 43 | pub use error::Error; 44 | 45 | pub mod util; 46 | 47 | /// Re-export usvg. 48 | pub use usvg; 49 | 50 | use anyrender::Scene; 51 | use kurbo::Affine; 52 | 53 | /// Append an SVG to a vello [`Scene`](vello::Scene), with default error handling. 54 | /// 55 | /// This will draw a red box over (some) unsupported elements. 56 | pub fn append(scene: &mut S, svg: &str, transform: Affine) -> Result<(), Error> { 57 | let opt = usvg::Options::default(); 58 | let tree = usvg::Tree::from_str(svg, &opt)?; 59 | append_tree(scene, &tree, transform); 60 | Ok(()) 61 | } 62 | 63 | /// Append an SVG to a vello [`Scene`](vello::Scene), with user-provided error handling logic. 64 | /// 65 | /// See the [module level documentation](crate#unsupported-features) for a list of some unsupported svg features 66 | pub fn append_with( 67 | scene: &mut S, 68 | svg: &str, 69 | transform: Affine, 70 | error_handler: &mut F, 71 | ) -> Result<(), Error> { 72 | let opt = usvg::Options::default(); 73 | let tree = usvg::Tree::from_str(svg, &opt)?; 74 | append_tree_with(scene, &tree, transform, error_handler); 75 | Ok(()) 76 | } 77 | 78 | /// Append an [`usvg::Tree`] to a vello [`Scene`](vello::Scene), with default error handling. 79 | /// 80 | /// This will draw a red box over (some) unsupported elements. 81 | pub fn append_tree(scene: &mut S, svg: &usvg::Tree, transform: Affine) { 82 | append_tree_with(scene, svg, transform, &mut util::default_error_handler); 83 | } 84 | 85 | /// Append an [`usvg::Tree`] to a vello [`Scene`](vello::Scene), with user-provided error handling logic. 86 | /// 87 | /// See the [module level documentation](crate#unsupported-features) for a list of some unsupported svg features 88 | pub fn append_tree_with( 89 | scene: &mut S, 90 | svg: &usvg::Tree, 91 | transform: Affine, 92 | error_handler: &mut F, 93 | ) { 94 | render::render_group( 95 | scene, 96 | svg.root(), 97 | Affine::IDENTITY, 98 | transform, 99 | error_handler, 100 | ); 101 | } 102 | 103 | #[cfg(test)] 104 | mod tests { 105 | // CI will fail unless cargo nextest can execute at least one test per workspace. 106 | // Delete this dummy test once we have an actual real test. 107 | #[test] 108 | fn dummy_test_until_we_have_a_real_test() {} 109 | } 110 | -------------------------------------------------------------------------------- /packages/anyrender_vello/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "anyrender_vello" 3 | description = "Vello backend for anyrender" 4 | version = "0.1.0" 5 | homepage = "https://github.com/dioxuslabs/blitz" 6 | repository = "https://github.com/dioxuslabs/blitz" 7 | documentation = "https://docs.rs/anyrender_vello" 8 | license.workspace = true 9 | edition = "2024" 10 | 11 | [dependencies] 12 | anyrender = { version = "0.1", path = "../anyrender"} 13 | kurbo = { workspace = true } 14 | peniko = { workspace = true } 15 | vello = { workspace = true } 16 | raw-window-handle = { workspace = true } 17 | wgpu = { workspace = true } 18 | futures-intrusive = { workspace = true } 19 | pollster = { workspace = true } -------------------------------------------------------------------------------- /packages/anyrender_vello/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! An Anyrender backend using the vello crate 2 | mod image_renderer; 3 | mod scene; 4 | mod window_renderer; 5 | 6 | pub use image_renderer::VelloImageRenderer; 7 | pub use scene::VelloAnyrenderScene; 8 | pub use window_renderer::VelloWindowRenderer; 9 | 10 | use std::num::NonZeroUsize; 11 | 12 | #[cfg(target_os = "macos")] 13 | const DEFAULT_THREADS: Option = NonZeroUsize::new(1); 14 | #[cfg(not(target_os = "macos"))] 15 | const DEFAULT_THREADS: Option = None; 16 | -------------------------------------------------------------------------------- /packages/anyrender_vello/src/scene.rs: -------------------------------------------------------------------------------- 1 | use anyrender::{NormalizedCoord, Scene}; 2 | use kurbo::{Affine, Rect, Shape, Stroke}; 3 | use peniko::{BlendMode, BrushRef, Color, Fill, Font, StyleRef}; 4 | 5 | pub struct VelloAnyrenderScene(pub vello::Scene); 6 | 7 | impl Scene for VelloAnyrenderScene { 8 | type Output = vello::Scene; 9 | 10 | fn reset(&mut self) { 11 | self.0.reset(); 12 | } 13 | 14 | fn push_layer( 15 | &mut self, 16 | blend: impl Into, 17 | alpha: f32, 18 | transform: Affine, 19 | clip: &impl Shape, 20 | ) { 21 | self.0.push_layer(blend, alpha, transform, clip); 22 | } 23 | 24 | fn pop_layer(&mut self) { 25 | self.0.pop_layer(); 26 | } 27 | 28 | fn stroke<'a>( 29 | &mut self, 30 | style: &Stroke, 31 | transform: Affine, 32 | brush: impl Into>, 33 | brush_transform: Option, 34 | shape: &impl Shape, 35 | ) { 36 | self.0 37 | .stroke(style, transform, brush, brush_transform, shape); 38 | } 39 | 40 | fn fill<'a>( 41 | &mut self, 42 | style: Fill, 43 | transform: Affine, 44 | brush: impl Into>, 45 | brush_transform: Option, 46 | shape: &impl Shape, 47 | ) { 48 | self.0.fill(style, transform, brush, brush_transform, shape); 49 | } 50 | 51 | fn draw_glyphs<'a, 's: 'a>( 52 | &'a mut self, 53 | font: &'a Font, 54 | font_size: f32, 55 | hint: bool, 56 | normalized_coords: &'a [NormalizedCoord], 57 | style: impl Into>, 58 | brush: impl Into>, 59 | brush_alpha: f32, 60 | transform: Affine, 61 | glyph_transform: Option, 62 | glyphs: impl Iterator, 63 | ) { 64 | self.0 65 | .draw_glyphs(font) 66 | .font_size(font_size) 67 | .hint(hint) 68 | .normalized_coords(normalized_coords) 69 | .brush(brush) 70 | .brush_alpha(brush_alpha) 71 | .transform(transform) 72 | .glyph_transform(glyph_transform) 73 | .draw( 74 | style, 75 | glyphs.map(|g: anyrender::Glyph| vello::Glyph { 76 | id: g.id, 77 | x: g.x, 78 | y: g.y, 79 | }), 80 | ); 81 | } 82 | 83 | fn draw_box_shadow( 84 | &mut self, 85 | transform: Affine, 86 | rect: Rect, 87 | brush: Color, 88 | radius: f64, 89 | std_dev: f64, 90 | ) { 91 | self.0 92 | .draw_blurred_rounded_rect(transform, rect, brush, radius, std_dev); 93 | } 94 | 95 | fn finish(self) -> Self::Output { 96 | self.0 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /packages/anyrender_vello/src/window_renderer.rs: -------------------------------------------------------------------------------- 1 | use anyrender::{Scene as _, WindowHandle, WindowRenderer}; 2 | use peniko::Color; 3 | use std::sync::Arc; 4 | use vello::{ 5 | AaSupport, RenderParams, Renderer as VelloRenderer, RendererOptions, Scene, 6 | util::{RenderContext, RenderSurface}, 7 | }; 8 | use wgpu::{CommandEncoderDescriptor, PresentMode, TextureViewDescriptor}; 9 | 10 | use crate::{DEFAULT_THREADS, VelloAnyrenderScene}; 11 | 12 | // Simple struct to hold the state of the renderer 13 | struct ActiveRenderState { 14 | renderer: VelloRenderer, 15 | surface: RenderSurface<'static>, 16 | } 17 | 18 | #[allow(clippy::large_enum_variant)] 19 | enum RenderState { 20 | Active(ActiveRenderState), 21 | Suspended, 22 | } 23 | 24 | pub struct VelloWindowRenderer { 25 | // The fields MUST be in this order, so that the surface is dropped before the window 26 | // Window is cached even when suspended so that it can be reused when the app is resumed after being suspended 27 | render_state: RenderState, 28 | window_handle: Arc, 29 | 30 | // Vello 31 | render_context: RenderContext, 32 | scene: VelloAnyrenderScene, 33 | } 34 | 35 | impl WindowRenderer for VelloWindowRenderer { 36 | type Scene = VelloAnyrenderScene; 37 | 38 | fn new(window: Arc) -> Self { 39 | // 2. Set up Vello specific stuff 40 | let render_context = RenderContext::new(); 41 | 42 | Self { 43 | render_context, 44 | render_state: RenderState::Suspended, 45 | window_handle: window, 46 | scene: VelloAnyrenderScene(Scene::new()), 47 | } 48 | } 49 | 50 | fn is_active(&self) -> bool { 51 | matches!(self.render_state, RenderState::Active(_)) 52 | } 53 | 54 | fn resume(&mut self, width: u32, height: u32) { 55 | let surface = pollster::block_on(self.render_context.create_surface( 56 | self.window_handle.clone(), 57 | width, 58 | height, 59 | PresentMode::AutoVsync, 60 | )) 61 | .expect("Error creating surface"); 62 | 63 | let options = RendererOptions { 64 | antialiasing_support: AaSupport::all(), 65 | use_cpu: false, 66 | num_init_threads: DEFAULT_THREADS, 67 | // TODO: add pipeline cache 68 | pipeline_cache: None, 69 | }; 70 | 71 | let renderer = 72 | VelloRenderer::new(&self.render_context.devices[surface.dev_id].device, options) 73 | .unwrap(); 74 | 75 | self.render_state = RenderState::Active(ActiveRenderState { renderer, surface }); 76 | } 77 | 78 | fn suspend(&mut self) { 79 | self.render_state = RenderState::Suspended; 80 | } 81 | 82 | fn set_size(&mut self, width: u32, height: u32) { 83 | if let RenderState::Active(state) = &mut self.render_state { 84 | self.render_context 85 | .resize_surface(&mut state.surface, width, height); 86 | }; 87 | } 88 | 89 | fn render(&mut self, draw_fn: F) { 90 | let RenderState::Active(state) = &mut self.render_state else { 91 | return; 92 | }; 93 | 94 | let device = &self.render_context.devices[state.surface.dev_id]; 95 | let surface = &state.surface; 96 | 97 | let render_params = RenderParams { 98 | base_color: Color::WHITE, 99 | width: state.surface.config.width, 100 | height: state.surface.config.height, 101 | antialiasing_method: vello::AaConfig::Msaa16, 102 | }; 103 | 104 | // Regenerate the vello scene 105 | draw_fn(&mut self.scene); 106 | 107 | state 108 | .renderer 109 | .render_to_texture( 110 | &device.device, 111 | &device.queue, 112 | &self.scene.0, 113 | &surface.target_view, 114 | &render_params, 115 | ) 116 | .expect("failed to render to texture"); 117 | 118 | // TODO: verify that handling of SurfaceError::Outdated is no longer required 119 | // 120 | // let surface_texture = match state.surface.surface.get_current_texture() { 121 | // Ok(surface) => surface, 122 | // // When resizing too aggresively, the surface can get outdated (another resize) before being rendered into 123 | // Err(SurfaceError::Outdated) => return, 124 | // Err(_) => panic!("failed to get surface texture"), 125 | // }; 126 | 127 | let surface_texture = state 128 | .surface 129 | .surface 130 | .get_current_texture() 131 | .expect("failed to get surface texture"); 132 | 133 | // Perform the copy 134 | // (TODO: Does it improve throughput to acquire the surface after the previous texture render has happened?) 135 | let mut encoder = device 136 | .device 137 | .create_command_encoder(&CommandEncoderDescriptor { 138 | label: Some("Surface Blit"), 139 | }); 140 | 141 | state.surface.blitter.copy( 142 | &device.device, 143 | &mut encoder, 144 | &surface.target_view, 145 | &surface_texture 146 | .texture 147 | .create_view(&TextureViewDescriptor::default()), 148 | ); 149 | device.queue.submit([encoder.finish()]); 150 | surface_texture.present(); 151 | 152 | device.device.poll(wgpu::Maintain::Wait); 153 | 154 | // Empty the Vello scene (memory optimisation) 155 | self.scene.reset(); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /packages/anyrender_vello_cpu/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "anyrender_vello_cpu" 3 | description = "vello_cpu backend for anyrender" 4 | version = "0.1.0" 5 | homepage = "https://github.com/dioxuslabs/blitz" 6 | repository = "https://github.com/dioxuslabs/blitz" 7 | documentation = "https://docs.rs/anyrender_vello_cpu" 8 | license.workspace = true 9 | edition = "2024" 10 | 11 | [dependencies] 12 | anyrender = { version = "0.1", path = "../anyrender"} 13 | kurbo = { workspace = true } 14 | peniko = { workspace = true } 15 | vello_cpu = { workspace = true } 16 | softbuffer = { workspace = true } -------------------------------------------------------------------------------- /packages/anyrender_vello_cpu/src/image_renderer.rs: -------------------------------------------------------------------------------- 1 | use crate::VelloCpuAnyrenderScene; 2 | use anyrender::ImageRenderer; 3 | use vello_cpu::{RenderContext, RenderMode}; 4 | 5 | pub struct VelloCpuImageRenderer { 6 | scene: VelloCpuAnyrenderScene, 7 | } 8 | 9 | impl ImageRenderer for VelloCpuImageRenderer { 10 | type Scene = VelloCpuAnyrenderScene; 11 | 12 | fn new(width: u32, height: u32) -> Self { 13 | Self { 14 | scene: VelloCpuAnyrenderScene(RenderContext::new(width as u16, height as u16)), 15 | } 16 | } 17 | 18 | fn render(&mut self, draw_fn: F, buffer: &mut Vec) { 19 | let width = self.scene.0.width(); 20 | let height = self.scene.0.height(); 21 | draw_fn(&mut self.scene); 22 | buffer.resize(width as usize * height as usize * 4, 0); 23 | self.scene 24 | .0 25 | .render_to_buffer(&mut *buffer, width, height, RenderMode::OptimizeSpeed); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/anyrender_vello_cpu/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! An Anyrender backend using the vello_cpu crate 2 | mod image_renderer; 3 | mod scene; 4 | mod window_renderer; 5 | 6 | pub use image_renderer::VelloCpuImageRenderer; 7 | pub use scene::VelloCpuAnyrenderScene; 8 | pub use window_renderer::VelloCpuWindowRenderer; 9 | -------------------------------------------------------------------------------- /packages/anyrender_vello_cpu/src/window_renderer.rs: -------------------------------------------------------------------------------- 1 | use crate::VelloCpuAnyrenderScene; 2 | use anyrender::{WindowHandle, WindowRenderer}; 3 | use peniko::color::PremulRgba8; 4 | use softbuffer::{Context, Surface}; 5 | use std::{num::NonZero, sync::Arc}; 6 | use vello_cpu::{Pixmap, RenderContext, RenderMode}; 7 | 8 | // Simple struct to hold the state of the renderer 9 | pub struct ActiveRenderState { 10 | _context: Context>, 11 | surface: Surface, Arc>, 12 | } 13 | 14 | #[allow(clippy::large_enum_variant)] 15 | pub enum RenderState { 16 | Active(ActiveRenderState), 17 | Suspended, 18 | } 19 | 20 | pub struct VelloCpuWindowRenderer { 21 | // The fields MUST be in this order, so that the surface is dropped before the window 22 | // Window is cached even when suspended so that it can be reused when the app is resumed after being suspended 23 | render_state: RenderState, 24 | window_handle: Arc, 25 | render_context: VelloCpuAnyrenderScene, 26 | } 27 | 28 | impl WindowRenderer for VelloCpuWindowRenderer { 29 | type Scene = VelloCpuAnyrenderScene; 30 | 31 | fn new(window: Arc) -> Self { 32 | Self { 33 | render_state: RenderState::Suspended, 34 | window_handle: window, 35 | render_context: VelloCpuAnyrenderScene(RenderContext::new(0, 0)), 36 | } 37 | } 38 | 39 | fn is_active(&self) -> bool { 40 | matches!(self.render_state, RenderState::Active(_)) 41 | } 42 | 43 | fn resume(&mut self, width: u32, height: u32) { 44 | let context = Context::new(self.window_handle.clone()).unwrap(); 45 | let surface = Surface::new(&context, self.window_handle.clone()).unwrap(); 46 | self.render_state = RenderState::Active(ActiveRenderState { 47 | _context: context, 48 | surface, 49 | }); 50 | 51 | self.set_size(width, height); 52 | } 53 | 54 | fn suspend(&mut self) { 55 | self.render_state = RenderState::Suspended; 56 | } 57 | 58 | fn set_size(&mut self, physical_width: u32, physical_height: u32) { 59 | if let RenderState::Active(state) = &mut self.render_state { 60 | state 61 | .surface 62 | .resize( 63 | NonZero::new(physical_width.max(1)).unwrap(), 64 | NonZero::new(physical_height.max(1)).unwrap(), 65 | ) 66 | .unwrap(); 67 | self.render_context = VelloCpuAnyrenderScene(RenderContext::new( 68 | physical_width as u16, 69 | physical_height as u16, 70 | )); 71 | }; 72 | } 73 | 74 | fn render(&mut self, draw_fn: F) { 75 | let RenderState::Active(state) = &mut self.render_state else { 76 | return; 77 | }; 78 | let Ok(mut surface_buffer) = state.surface.buffer_mut() else { 79 | return; 80 | }; 81 | 82 | // Paint 83 | let width = self.render_context.0.width(); 84 | let height = self.render_context.0.height(); 85 | let mut pixmap = Pixmap::new(width, height); 86 | draw_fn(&mut self.render_context); 87 | self.render_context 88 | .0 89 | .render_to_pixmap(&mut pixmap, RenderMode::OptimizeSpeed); 90 | 91 | let out = surface_buffer.as_mut(); 92 | assert_eq!(pixmap.data().len(), out.len()); 93 | for (src, dest) in pixmap.data().iter().zip(out.iter_mut()) { 94 | let PremulRgba8 { r, g, b, a } = *src; 95 | if a == 0 { 96 | *dest = u32::MAX; 97 | } else { 98 | *dest = (r as u32) << 16 | (g as u32) << 8 | b as u32; 99 | } 100 | } 101 | 102 | surface_buffer.present().unwrap(); 103 | 104 | // Empty the Vello render context (memory optimisation) 105 | self.render_context.0.reset(); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /packages/blitz-dom/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "blitz-dom" 3 | description = "Blitz DOM implementation" 4 | version = "0.1.0-alpha.2" 5 | homepage = "https://github.com/dioxuslabs/blitz" 6 | repository = "https://github.com/dioxuslabs/blitz" 7 | documentation = "https://docs.rs/blitz-dom" 8 | license.workspace = true 9 | edition = "2024" 10 | 11 | [features] 12 | default = ["tracing", "svg", "woff-c", "clipboard", "accessibility", "system_fonts"] 13 | tracing = ["dep:tracing"] 14 | svg = ["dep:usvg"] 15 | # WOFF decoding using the "woff" crate which binds to C libraries 16 | # ("woff" for woff2) and "sfnt2woff" for woff1). 17 | # Both woff1 and woff2 are supported 18 | woff-c = ["dep:woff"] 19 | # WOFF decoding using the "woff2" crate which is pure Rust 20 | # Only woff2 is supported. Does not work correct with all woff2 fonts 21 | woff-rust = ["dep:woff2"] 22 | clipboard = ["dep:arboard"] 23 | accessibility = ["accesskit"] 24 | system_fonts = ["parley/system"] 25 | autofocus = [] 26 | 27 | [dependencies] 28 | # Blitz dependencies 29 | blitz-traits = { version = "0.1.0-alpha.2", path = "../blitz-traits" } 30 | stylo_taffy = { version = "0.1.0-alpha.2", path = "../stylo_taffy" } 31 | 32 | # Servo dependencies 33 | style = { workspace = true } 34 | selectors = { workspace = true } 35 | style_config = { workspace = true } 36 | style_traits = { workspace = true } 37 | style_dom = { workspace = true } 38 | app_units = { workspace = true } 39 | euclid = { workspace = true, features = ["serde"] } 40 | atomic_refcell = { workspace = true, features = ["serde"] } 41 | string_cache = { workspace = true } 42 | markup5ever = { workspace = true } 43 | smallvec = { workspace = true } 44 | 45 | # DioxusLabs dependencies 46 | taffy = { workspace = true } 47 | 48 | # Linebender dependencies 49 | accesskit = { workspace = true, optional = true } 50 | parley = { workspace = true } 51 | peniko = { workspace = true } 52 | color = { workspace = true } 53 | 54 | # Other dependencies 55 | slab = { workspace = true } 56 | tracing = { workspace = true, optional = true } 57 | 58 | # Media & Decoding 59 | image = { workspace = true } 60 | usvg = { workspace = true, optional = true } 61 | woff = { workspace = true, optional = true, features = ["version2"] } 62 | woff2 = { workspace = true, optional = true } 63 | html-escape = { workspace = true } 64 | percent-encoding = { workspace = true } 65 | 66 | # IO & Networking 67 | url = { workspace = true, features = ["serde"] } 68 | 69 | # Input 70 | keyboard-types = { workspace = true } 71 | cursor-icon = { workspace = true } 72 | 73 | [target.'cfg(any(target_os = "windows",target_os = "macos",target_os = "linux",target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))'.dependencies] 74 | arboard = { workspace = true, optional = true } 75 | 76 | # HACK: Blitz doesn't need to depend on objc2 directly. But this feature flag is necessary 77 | # to prevent debug builds from panicking. 78 | [target.'cfg(any(target_vendor = "apple"))'.dependencies] 79 | objc2 = { version = "0.6", features = ["disable-encoding-assertions"] } -------------------------------------------------------------------------------- /packages/blitz-dom/assets/moz-bullet-font.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DioxusLabs/blitz/42aa7f8625c112495edd73d12b622ab97aefbf37/packages/blitz-dom/assets/moz-bullet-font.otf -------------------------------------------------------------------------------- /packages/blitz-dom/src/accessibility.rs: -------------------------------------------------------------------------------- 1 | use crate::{BaseDocument, Node as BlitzDomNode, local_name}; 2 | use accesskit::{Node as AccessKitNode, NodeId, Role, Tree, TreeUpdate}; 3 | 4 | impl BaseDocument { 5 | pub fn build_accessibility_tree(&self) -> TreeUpdate { 6 | let mut nodes = std::collections::HashMap::new(); 7 | let mut window = AccessKitNode::new(Role::Window); 8 | 9 | self.visit(|node_id, node| { 10 | let parent = node 11 | .parent 12 | .and_then(|parent_id| nodes.get_mut(&parent_id)) 13 | .map(|(_, parent)| parent) 14 | .unwrap_or(&mut window); 15 | let (id, builder) = self.build_accessibility_node(node, parent); 16 | 17 | nodes.insert(node_id, (id, builder)); 18 | }); 19 | 20 | let mut nodes: Vec<_> = nodes 21 | .into_iter() 22 | .map(|(_, (id, node))| (id, node)) 23 | .collect(); 24 | nodes.push((NodeId(u64::MAX), window)); 25 | 26 | let tree = Tree::new(NodeId(u64::MAX)); 27 | TreeUpdate { 28 | nodes, 29 | tree: Some(tree), 30 | focus: NodeId(self.focus_node_id.map(|id| id as u64).unwrap_or(u64::MAX)), 31 | } 32 | } 33 | 34 | fn build_accessibility_node( 35 | &self, 36 | node: &BlitzDomNode, 37 | parent: &mut AccessKitNode, 38 | ) -> (NodeId, AccessKitNode) { 39 | let id = NodeId(node.id as u64); 40 | 41 | let mut builder = AccessKitNode::default(); 42 | if node.id == 0 { 43 | builder.set_role(Role::Window) 44 | } else if let Some(element_data) = node.element_data() { 45 | let name = element_data.name.local.to_string(); 46 | 47 | // TODO match more roles 48 | let role = match &*name { 49 | "button" => Role::Button, 50 | "div" => Role::GenericContainer, 51 | "header" => Role::Header, 52 | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" => Role::Heading, 53 | "p" => Role::Paragraph, 54 | "section" => Role::Section, 55 | "input" => { 56 | let ty = element_data.attr(local_name!("type")).unwrap_or("text"); 57 | match ty { 58 | "number" => Role::NumberInput, 59 | "checkbox" => Role::CheckBox, 60 | _ => Role::TextInput, 61 | } 62 | } 63 | _ => Role::Unknown, 64 | }; 65 | 66 | builder.set_role(role); 67 | builder.set_html_tag(name); 68 | } else if node.is_text_node() { 69 | builder.set_role(Role::TextRun); 70 | builder.set_value(node.text_content()); 71 | parent.push_labelled_by(id) 72 | } 73 | 74 | parent.push_child(id); 75 | 76 | (id, builder) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /packages/blitz-dom/src/debug.rs: -------------------------------------------------------------------------------- 1 | use parley::layout::PositionedLayoutItem; 2 | 3 | use crate::BaseDocument; 4 | 5 | impl BaseDocument { 6 | pub fn print_taffy_tree(&self) { 7 | taffy::print_tree(self, taffy::NodeId::from(0usize)); 8 | } 9 | 10 | pub fn debug_log_node(&self, node_id: usize) { 11 | let node = &self.nodes[node_id]; 12 | 13 | #[cfg(feature = "tracing")] 14 | { 15 | tracing::info!("Layout: {:?}", &node.final_layout); 16 | tracing::info!("Style: {:?}", &node.style); 17 | } 18 | 19 | println!("\nNode {} {}", node.id, node.node_debug_str()); 20 | 21 | println!("Attrs:"); 22 | 23 | for attr in node.attrs().into_iter().flatten() { 24 | println!(" {}: {}", attr.name.local, attr.value); 25 | } 26 | 27 | if node.is_inline_root { 28 | let inline_layout = &node 29 | .data 30 | .downcast_element() 31 | .unwrap() 32 | .inline_layout_data 33 | .as_ref() 34 | .unwrap(); 35 | 36 | println!( 37 | "Size: {}x{}", 38 | inline_layout.layout.width(), 39 | inline_layout.layout.height() 40 | ); 41 | println!("Text content: {:?}", inline_layout.text); 42 | println!("Inline Boxes:"); 43 | for ibox in inline_layout.layout.inline_boxes() { 44 | print!("(id: {}) ", ibox.id); 45 | } 46 | println!(); 47 | println!("Lines:"); 48 | for (i, line) in inline_layout.layout.lines().enumerate() { 49 | println!("Line {i}:"); 50 | for item in line.items() { 51 | print!(" "); 52 | match item { 53 | PositionedLayoutItem::GlyphRun(run) => { 54 | print!( 55 | "RUN (x: {}, w: {}) ", 56 | run.offset().round(), 57 | run.run().advance() 58 | ) 59 | } 60 | PositionedLayoutItem::InlineBox(ibox) => print!( 61 | "BOX (id: {} x: {} y: {} w: {}, h: {})", 62 | ibox.id, 63 | ibox.x.round(), 64 | ibox.y.round(), 65 | ibox.width.round(), 66 | ibox.height.round() 67 | ), 68 | } 69 | println!(); 70 | } 71 | } 72 | } 73 | 74 | let layout = &node.final_layout; 75 | println!("Layout:"); 76 | println!( 77 | " x: {x} y: {y} w: {width} h: {height} content_w: {content_width} content_h: {content_height}", 78 | x = layout.location.x, 79 | y = layout.location.y, 80 | width = layout.size.width, 81 | height = layout.size.height, 82 | content_width = layout.content_size.width, 83 | content_height = layout.content_size.height, 84 | ); 85 | println!( 86 | " border: l:{l} r:{r} t:{t} b:{b}", 87 | l = layout.border.left, 88 | r = layout.border.right, 89 | t = layout.border.top, 90 | b = layout.border.bottom, 91 | ); 92 | println!( 93 | " padding: l:{l} r:{r} t:{t} b:{b}", 94 | l = layout.padding.left, 95 | r = layout.padding.right, 96 | t = layout.padding.top, 97 | b = layout.padding.bottom, 98 | ); 99 | println!( 100 | " margin: l:{l} r:{r} t:{t} b:{b}", 101 | l = layout.margin.left, 102 | r = layout.margin.right, 103 | t = layout.margin.top, 104 | b = layout.margin.bottom, 105 | ); 106 | println!("Parent: {:?}", node.parent); 107 | 108 | let children: Vec<_> = node 109 | .children 110 | .iter() 111 | .map(|id| &self.nodes[*id]) 112 | .map(|node| (node.id, node.order(), node.node_debug_str())) 113 | .collect(); 114 | println!("Children: {children:?}"); 115 | 116 | println!("Layout Parent: {:?}", node.layout_parent.get()); 117 | 118 | let layout_children: Option> = node.layout_children.borrow().as_ref().map(|lc| { 119 | lc.iter() 120 | .map(|id| &self.nodes[*id]) 121 | .map(|node| (node.id, node.order(), node.node_debug_str())) 122 | .collect() 123 | }); 124 | if let Some(layout_children) = layout_children { 125 | println!("Layout Children: {layout_children:?}"); 126 | } 127 | 128 | let paint_children: Option> = node.paint_children.borrow().as_ref().map(|lc| { 129 | lc.iter() 130 | .map(|id| &self.nodes[*id]) 131 | .map(|node| (node.id, node.order(), node.node_debug_str())) 132 | .collect() 133 | }); 134 | if let Some(paint_children) = paint_children { 135 | println!("Paint Children: {paint_children:?}"); 136 | } 137 | // taffy::print_tree(&self.dom, node_id.into()); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /packages/blitz-dom/src/events/driver.rs: -------------------------------------------------------------------------------- 1 | use crate::{BaseDocument, DocumentMutator}; 2 | use blitz_traits::{DomEvent, DomEventData, EventState, events::UiEvent}; 3 | use std::collections::VecDeque; 4 | 5 | pub trait EventHandler { 6 | fn handle_event( 7 | &mut self, 8 | chain: &[usize], 9 | event: &mut DomEvent, 10 | mutr: &mut DocumentMutator<'_>, 11 | event_state: &mut EventState, 12 | ); 13 | } 14 | 15 | pub struct NoopEventHandler; 16 | impl EventHandler for NoopEventHandler { 17 | fn handle_event( 18 | &mut self, 19 | _chain: &[usize], 20 | _event: &mut DomEvent, 21 | _mutr: &mut DocumentMutator<'_>, 22 | _event_state: &mut EventState, 23 | ) { 24 | // Do nothing 25 | } 26 | } 27 | 28 | pub struct EventDriver<'doc, Handler: EventHandler> { 29 | mutr: DocumentMutator<'doc>, 30 | handler: Handler, 31 | } 32 | 33 | impl<'doc, Handler: EventHandler> EventDriver<'doc, Handler> { 34 | fn doc_mut(&mut self) -> &mut BaseDocument { 35 | self.mutr.doc 36 | } 37 | 38 | fn doc(&self) -> &BaseDocument { 39 | &*self.mutr.doc 40 | } 41 | 42 | pub fn new(mutr: DocumentMutator<'doc>, handler: Handler) -> Self { 43 | EventDriver { mutr, handler } 44 | } 45 | 46 | pub fn handle_ui_event(&mut self, event: UiEvent) { 47 | let viewport_scroll = self.doc().viewport_scroll(); 48 | let zoom = self.doc().viewport.zoom(); 49 | 50 | let mut hover_node_id = self.doc().hover_node_id; 51 | let focussed_node_id = self.doc().focus_node_id; 52 | 53 | // Update document input state (hover, focus, active, etc) 54 | match &event { 55 | UiEvent::MouseMove(event) => { 56 | let dom_x = event.x + viewport_scroll.x as f32 / zoom; 57 | let dom_y = event.y + viewport_scroll.y as f32 / zoom; 58 | self.doc_mut().set_hover_to(dom_x, dom_y); 59 | hover_node_id = self.doc().hover_node_id; 60 | } 61 | UiEvent::MouseDown(_) => { 62 | self.doc_mut().active_node(); 63 | self.doc_mut().set_mousedown_node_id(hover_node_id); 64 | } 65 | UiEvent::MouseUp(_) => { 66 | self.doc_mut().unactive_node(); 67 | } 68 | _ => {} 69 | }; 70 | 71 | let target = match event { 72 | UiEvent::MouseMove(_) => hover_node_id, 73 | UiEvent::MouseUp(_) => hover_node_id, 74 | UiEvent::MouseDown(_) => hover_node_id, 75 | UiEvent::KeyUp(_) => focussed_node_id, 76 | UiEvent::KeyDown(_) => focussed_node_id, 77 | UiEvent::Ime(_) => focussed_node_id, 78 | }; 79 | 80 | let data = match event { 81 | UiEvent::MouseMove(data) => DomEventData::MouseMove(data), 82 | UiEvent::MouseUp(data) => DomEventData::MouseUp(data), 83 | UiEvent::MouseDown(data) => DomEventData::MouseDown(data), 84 | UiEvent::KeyUp(data) => DomEventData::KeyUp(data), 85 | UiEvent::KeyDown(data) => DomEventData::KeyDown(data), 86 | UiEvent::Ime(data) => DomEventData::Ime(data), 87 | }; 88 | 89 | let target = target.unwrap_or_else(|| self.doc().root_element().id); 90 | let dom_event = DomEvent::new(target, data); 91 | 92 | self.handle_dom_event(dom_event); 93 | } 94 | 95 | pub fn handle_dom_event(&mut self, event: DomEvent) { 96 | let mut queue = VecDeque::with_capacity(4); 97 | queue.push_back(event); 98 | 99 | while let Some(mut event) = queue.pop_front() { 100 | let chain = if event.bubbles { 101 | self.doc().node_chain(event.target) 102 | } else { 103 | vec![event.target] 104 | }; 105 | 106 | let mut event_state = EventState::default(); 107 | self.handler 108 | .handle_event(&chain, &mut event, &mut self.mutr, &mut event_state); 109 | 110 | if !event_state.is_cancelled() { 111 | self.doc_mut() 112 | .handle_event(&mut event, |new_evt| queue.push_back(new_evt)); 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /packages/blitz-dom/src/events/ime.rs: -------------------------------------------------------------------------------- 1 | use blitz_traits::BlitzImeEvent; 2 | 3 | use crate::BaseDocument; 4 | 5 | pub(crate) fn handle_ime_event(doc: &mut BaseDocument, event: BlitzImeEvent) { 6 | if let Some(node_id) = doc.focus_node_id { 7 | let node = &mut doc.nodes[node_id]; 8 | let text_input_data = node 9 | .data 10 | .downcast_element_mut() 11 | .and_then(|el| el.text_input_data_mut()); 12 | if let Some(input_data) = text_input_data { 13 | let editor = &mut input_data.editor; 14 | let mut driver = editor.driver(&mut doc.font_ctx, &mut doc.layout_ctx); 15 | 16 | match event { 17 | BlitzImeEvent::Enabled => { /* Do nothing */ } 18 | BlitzImeEvent::Disabled => { 19 | driver.clear_compose(); 20 | } 21 | BlitzImeEvent::Commit(text) => { 22 | driver.insert_or_replace_selection(&text); 23 | } 24 | BlitzImeEvent::Preedit(text, cursor) => { 25 | if text.is_empty() { 26 | driver.clear_compose(); 27 | } else { 28 | driver.set_compose(&text, cursor); 29 | } 30 | } 31 | } 32 | println!("Sent ime event to {node_id}"); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/blitz-dom/src/events/mod.rs: -------------------------------------------------------------------------------- 1 | mod driver; 2 | mod ime; 3 | mod keyboard; 4 | mod mouse; 5 | 6 | use blitz_traits::{DomEvent, DomEventData}; 7 | pub use driver::{EventDriver, EventHandler, NoopEventHandler}; 8 | pub(crate) use ime::handle_ime_event; 9 | pub(crate) use keyboard::handle_keypress; 10 | use mouse::handle_mouseup; 11 | pub(crate) use mouse::{handle_click, handle_mousedown, handle_mousemove}; 12 | 13 | use crate::BaseDocument; 14 | 15 | pub(crate) fn handle_event( 16 | doc: &mut BaseDocument, 17 | event: &mut DomEvent, 18 | dispatch_event: F, 19 | ) { 20 | let target_node_id = event.target; 21 | 22 | match &event.data { 23 | DomEventData::MouseMove(mouse_event) => { 24 | let changed = handle_mousemove( 25 | doc, 26 | target_node_id, 27 | mouse_event.x, 28 | mouse_event.y, 29 | mouse_event.buttons, 30 | ); 31 | if changed { 32 | // TODO: request redraw 33 | // event_state.request_redraw(); 34 | } 35 | } 36 | DomEventData::MouseDown(event) => { 37 | handle_mousedown(doc, target_node_id, event.x, event.y); 38 | } 39 | DomEventData::MouseUp(event) => { 40 | handle_mouseup(doc, target_node_id, event, dispatch_event); 41 | } 42 | DomEventData::Click(event) => { 43 | handle_click(doc, target_node_id, event, dispatch_event); 44 | } 45 | DomEventData::KeyDown(event) => { 46 | handle_keypress(doc, target_node_id, event.clone(), dispatch_event); 47 | } 48 | DomEventData::KeyPress(_) => { 49 | // Do nothing (no default action) 50 | } 51 | DomEventData::KeyUp(_) => { 52 | // Do nothing (no default action) 53 | } 54 | DomEventData::Ime(event) => { 55 | handle_ime_event(doc, event.clone()); 56 | } 57 | DomEventData::Input(_) => { 58 | // Do nothing (no default action) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/blitz-dom/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Blitz-dom 2 | //! 3 | //! This crate implements a simple ECS-based DOM, with a focus on performance and ease of use. We don't attach bindings 4 | //! to languages here, simplifying the API and decreasing code size. 5 | //! 6 | //! The goal behind this crate is that any implementor can interact with the DOM and render it out using any renderer 7 | //! they want. 8 | //! 9 | //! ## Feature flags 10 | //! - `default`: Enables the features listed below. 11 | //! - `tracing`: Enables tracing support. 12 | 13 | pub const DEFAULT_CSS: &str = include_str!("../assets/default.css"); 14 | pub(crate) const BULLET_FONT: &[u8] = include_bytes!("../assets/moz-bullet-font.otf"); 15 | 16 | /// The DOM implementation. 17 | /// 18 | /// This is the primary entry point for this crate. 19 | mod document; 20 | 21 | /// The nodes themsleves, and their data. 22 | /// 23 | /// todo: we want this to use ECS, but we're not done with the design yet. 24 | pub mod node; 25 | 26 | mod debug; 27 | mod events; 28 | mod form; 29 | /// Integration of taffy and the DOM. 30 | mod layout; 31 | mod mutator; 32 | mod query_selector; 33 | /// Implementations that interact with servo's style engine 34 | mod stylo; 35 | mod stylo_to_cursor_icon; 36 | mod stylo_to_parley; 37 | mod traversal; 38 | 39 | pub mod net; 40 | pub mod util; 41 | 42 | #[cfg(feature = "accessibility")] 43 | mod accessibility; 44 | 45 | pub use document::{BaseDocument, Document}; 46 | pub use markup5ever::{ 47 | Namespace, NamespaceStaticSet, Prefix, PrefixStaticSet, QualName, local_name, namespace_prefix, 48 | namespace_url, ns, 49 | }; 50 | pub use mutator::DocumentMutator; 51 | pub use node::{Attribute, ElementNodeData, Node, NodeData, TextNodeData}; 52 | pub use parley::FontContext; 53 | pub use string_cache::Atom; 54 | pub use style::invalidation::element::restyle_hints::RestyleHint; 55 | pub type SelectorList = selectors::SelectorList; 56 | pub use events::{EventDriver, EventHandler, NoopEventHandler}; 57 | -------------------------------------------------------------------------------- /packages/blitz-dom/src/query_selector.rs: -------------------------------------------------------------------------------- 1 | use selectors::SelectorList; 2 | use smallvec::SmallVec; 3 | use style::dom_apis::{MayUseInvalidation, QueryAll, QueryFirst, query_selector}; 4 | use style::selector_parser::{SelectorImpl, SelectorParser}; 5 | use style::stylesheets::UrlExtraData; 6 | use style_traits::ParseError; 7 | use url::Url; 8 | 9 | use crate::{BaseDocument, Node}; 10 | 11 | impl BaseDocument { 12 | /// Find the first node that matches the selector specified as a string 13 | /// Returns: 14 | /// - Err(_) if parsing the selector fails 15 | /// - Ok(None) if nothing matches 16 | /// - Ok(Some(node_id)) with the first node ID that matches if one is found 17 | pub fn query_selector<'input>( 18 | &self, 19 | selector: &'input str, 20 | ) -> Result, ParseError<'input>> { 21 | let selector_list = self.try_parse_selector_list(selector)?; 22 | Ok(self.query_selector_raw(&selector_list)) 23 | } 24 | 25 | /// Find the first node that matches the selector(s) specified in selector_list 26 | pub fn query_selector_raw(&self, selector_list: &SelectorList) -> Option { 27 | let root_node = self.root_node(); 28 | let mut result = None; 29 | query_selector::<&Node, QueryFirst>( 30 | root_node, 31 | selector_list, 32 | &mut result, 33 | MayUseInvalidation::Yes, 34 | ); 35 | 36 | result.map(|node| node.id) 37 | } 38 | 39 | /// Find all nodes that match the selector specified as a string 40 | /// Returns: 41 | /// - Err(_) if parsing the selector fails 42 | /// - Ok(SmallVec) with all matching nodes otherwise 43 | pub fn query_selector_all<'input>( 44 | &self, 45 | selector: &'input str, 46 | ) -> Result, ParseError<'input>> { 47 | let selector_list = self.try_parse_selector_list(selector)?; 48 | Ok(self.query_selector_all_raw(&selector_list)) 49 | } 50 | 51 | /// Find all nodes that match the selector(s) specified in selector_list 52 | pub fn query_selector_all_raw( 53 | &self, 54 | selector_list: &SelectorList, 55 | ) -> SmallVec<[usize; 32]> { 56 | let root_node = self.root_node(); 57 | let mut results = SmallVec::new(); 58 | query_selector::<&Node, QueryAll>( 59 | root_node, 60 | selector_list, 61 | &mut results, 62 | MayUseInvalidation::Yes, 63 | ); 64 | 65 | results.iter().map(|node| node.id).collect() 66 | } 67 | 68 | pub fn try_parse_selector_list<'input>( 69 | &self, 70 | input: &'input str, 71 | ) -> Result, ParseError<'input>> { 72 | let url_extra_data = UrlExtraData::from(self.base_url.clone().unwrap_or_else(|| { 73 | "data:text/css;charset=utf-8;base64," 74 | .parse::() 75 | .unwrap() 76 | })); 77 | SelectorParser::parse_author_origin_no_namespace(input, &url_extra_data) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /packages/blitz-dom/src/stylo_to_cursor_icon.rs: -------------------------------------------------------------------------------- 1 | use cursor_icon::CursorIcon; 2 | use style::values::computed::ui::CursorKind as StyloCursorKind; 3 | 4 | pub(crate) fn stylo_to_cursor_icon(cursor: StyloCursorKind) -> CursorIcon { 5 | match cursor { 6 | StyloCursorKind::None => todo!("set the cursor to none"), 7 | StyloCursorKind::Default => CursorIcon::Default, 8 | StyloCursorKind::Pointer => CursorIcon::Pointer, 9 | StyloCursorKind::ContextMenu => CursorIcon::ContextMenu, 10 | StyloCursorKind::Help => CursorIcon::Help, 11 | StyloCursorKind::Progress => CursorIcon::Progress, 12 | StyloCursorKind::Wait => CursorIcon::Wait, 13 | StyloCursorKind::Cell => CursorIcon::Cell, 14 | StyloCursorKind::Crosshair => CursorIcon::Crosshair, 15 | StyloCursorKind::Text => CursorIcon::Text, 16 | StyloCursorKind::VerticalText => CursorIcon::VerticalText, 17 | StyloCursorKind::Alias => CursorIcon::Alias, 18 | StyloCursorKind::Copy => CursorIcon::Copy, 19 | StyloCursorKind::Move => CursorIcon::Move, 20 | StyloCursorKind::NoDrop => CursorIcon::NoDrop, 21 | StyloCursorKind::NotAllowed => CursorIcon::NotAllowed, 22 | StyloCursorKind::Grab => CursorIcon::Grab, 23 | StyloCursorKind::Grabbing => CursorIcon::Grabbing, 24 | StyloCursorKind::EResize => CursorIcon::EResize, 25 | StyloCursorKind::NResize => CursorIcon::NResize, 26 | StyloCursorKind::NeResize => CursorIcon::NeResize, 27 | StyloCursorKind::NwResize => CursorIcon::NwResize, 28 | StyloCursorKind::SResize => CursorIcon::SResize, 29 | StyloCursorKind::SeResize => CursorIcon::SeResize, 30 | StyloCursorKind::SwResize => CursorIcon::SwResize, 31 | StyloCursorKind::WResize => CursorIcon::WResize, 32 | StyloCursorKind::EwResize => CursorIcon::EwResize, 33 | StyloCursorKind::NsResize => CursorIcon::NsResize, 34 | StyloCursorKind::NeswResize => CursorIcon::NeswResize, 35 | StyloCursorKind::NwseResize => CursorIcon::NwseResize, 36 | StyloCursorKind::ColResize => CursorIcon::ColResize, 37 | StyloCursorKind::RowResize => CursorIcon::RowResize, 38 | StyloCursorKind::AllScroll => CursorIcon::AllScroll, 39 | StyloCursorKind::ZoomIn => CursorIcon::ZoomIn, 40 | StyloCursorKind::ZoomOut => CursorIcon::ZoomOut, 41 | StyloCursorKind::Auto => { 42 | // todo: we should be the ones determining this based on the UA? 43 | // https://developer.mozilla.org/en-US/docs/Web/CSS/cursor 44 | 45 | CursorIcon::Default 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/blitz-dom/src/traversal.rs: -------------------------------------------------------------------------------- 1 | use crate::BaseDocument; 2 | 3 | #[derive(Clone)] 4 | /// An pre-order tree traverser for a [BaseDocument](crate::document::BaseDocument). 5 | pub struct TreeTraverser<'a> { 6 | doc: &'a BaseDocument, 7 | stack: Vec, 8 | } 9 | 10 | impl<'a> TreeTraverser<'a> { 11 | /// Creates a new tree traverser for the given document which starts at the root node. 12 | pub fn new(doc: &'a BaseDocument) -> Self { 13 | Self::new_with_root(doc, 0) 14 | } 15 | 16 | /// Creates a new tree traverser for the given document which starts at the specified node. 17 | pub fn new_with_root(doc: &'a BaseDocument, root: usize) -> Self { 18 | let mut stack = Vec::with_capacity(32); 19 | stack.push(root); 20 | TreeTraverser { doc, stack } 21 | } 22 | } 23 | impl Iterator for TreeTraverser<'_> { 24 | type Item = usize; 25 | 26 | fn next(&mut self) -> Option { 27 | let id = self.stack.pop()?; 28 | let node = self.doc.get_node(id)?; 29 | self.stack.extend(node.children.iter().rev()); 30 | Some(id) 31 | } 32 | } 33 | 34 | #[derive(Clone)] 35 | /// An ancestor traverser for a [BaseDocument](crate::document::BaseDocument). 36 | pub struct AncestorTraverser<'a> { 37 | doc: &'a BaseDocument, 38 | current: usize, 39 | } 40 | impl<'a> AncestorTraverser<'a> { 41 | /// Creates a new ancestor traverser for the given document and node ID. 42 | pub fn new(doc: &'a BaseDocument, node_id: usize) -> Self { 43 | AncestorTraverser { 44 | doc, 45 | current: node_id, 46 | } 47 | } 48 | } 49 | impl Iterator for AncestorTraverser<'_> { 50 | type Item = usize; 51 | 52 | fn next(&mut self) -> Option { 53 | let current_node = self.doc.get_node(self.current)?; 54 | self.current = current_node.parent?; 55 | Some(self.current) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/blitz-dom/src/util.rs: -------------------------------------------------------------------------------- 1 | use crate::node::{Node, NodeData}; 2 | use color::{AlphaColor, Srgb}; 3 | use style::color::AbsoluteColor; 4 | 5 | pub type Color = AlphaColor; 6 | 7 | #[cfg(feature = "svg")] 8 | use std::sync::{Arc, LazyLock}; 9 | #[cfg(feature = "svg")] 10 | use usvg::fontdb; 11 | #[cfg(feature = "svg")] 12 | pub(crate) static FONT_DB: LazyLock> = LazyLock::new(|| { 13 | let mut db = fontdb::Database::new(); 14 | db.load_system_fonts(); 15 | Arc::new(db) 16 | }); 17 | 18 | #[derive(Clone, Debug)] 19 | pub enum ImageType { 20 | Image, 21 | Background(usize), 22 | } 23 | 24 | pub(crate) fn resolve_url(base_url: &Option, raw: &str) -> Option { 25 | match base_url { 26 | Some(base_url) => base_url.join(raw), 27 | None => url::Url::parse(raw), 28 | } 29 | .ok() 30 | } 31 | 32 | // Debug print an RcDom 33 | pub fn walk_tree(indent: usize, node: &Node) { 34 | // Skip all-whitespace text nodes entirely 35 | if let NodeData::Text(data) = &node.data { 36 | if data.content.chars().all(|c| c.is_ascii_whitespace()) { 37 | return; 38 | } 39 | } 40 | 41 | print!("{}", " ".repeat(indent)); 42 | let id = node.id; 43 | match &node.data { 44 | NodeData::Document => println!("#Document {id}"), 45 | 46 | NodeData::Text(data) => { 47 | if data.content.chars().all(|c| c.is_ascii_whitespace()) { 48 | println!("{id} #text: "); 49 | } else { 50 | let content = data.content.trim(); 51 | if content.len() > 10 { 52 | println!( 53 | "#text {id}: {}...", 54 | content 55 | .split_at(content.char_indices().take(10).last().unwrap().0) 56 | .0 57 | .escape_default() 58 | ) 59 | } else { 60 | println!("#text {id}: {}", data.content.trim().escape_default()) 61 | } 62 | } 63 | } 64 | 65 | NodeData::Comment => println!(""), 66 | 67 | NodeData::AnonymousBlock(_) => println!("{id} AnonymousBlock"), 68 | 69 | NodeData::Element(data) => { 70 | print!("<{} {id}", data.name.local); 71 | for attr in data.attrs.iter() { 72 | print!(" {}=\"{}\"", attr.name.local, attr.value); 73 | } 74 | if !node.children.is_empty() { 75 | println!(">"); 76 | } else { 77 | println!("/>"); 78 | } 79 | } // NodeData::Doctype { 80 | // ref name, 81 | // ref public_id, 82 | // ref system_id, 83 | // } => println!("", name, public_id, system_id), 84 | // NodeData::ProcessingInstruction { .. } => unreachable!(), 85 | } 86 | 87 | if !node.children.is_empty() { 88 | for child_id in node.children.iter() { 89 | walk_tree(indent + 2, node.with(*child_id)); 90 | } 91 | 92 | if let NodeData::Element(data) = &node.data { 93 | println!("{}", " ".repeat(indent), data.name.local); 94 | } 95 | } 96 | } 97 | 98 | #[cfg(feature = "svg")] 99 | pub(crate) fn parse_svg(source: &[u8]) -> Result { 100 | let options = usvg::Options { 101 | fontdb: Arc::clone(&*FONT_DB), 102 | ..Default::default() 103 | }; 104 | 105 | let tree = usvg::Tree::from_data(source, &options)?; 106 | Ok(tree) 107 | } 108 | 109 | pub trait ToColorColor { 110 | /// Converts a color into the `AlphaColor` type from the `color` crate 111 | fn as_color_color(&self) -> Color; 112 | } 113 | impl ToColorColor for AbsoluteColor { 114 | fn as_color_color(&self) -> Color { 115 | Color::new( 116 | *self 117 | .to_color_space(style::color::ColorSpace::Srgb) 118 | .raw_components(), 119 | ) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /packages/blitz-html/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "blitz-html" 3 | description = "Blitz HTML parser" 4 | version = "0.1.0-alpha.2" 5 | homepage = "https://github.com/dioxuslabs/blitz" 6 | repository = "https://github.com/dioxuslabs/blitz" 7 | documentation = "https://docs.rs/blitz-html" 8 | license.workspace = true 9 | edition = "2024" 10 | 11 | [dependencies] 12 | # Blitz dependencies 13 | blitz-dom = { version = "0.1.0-alpha.2", path = "../blitz-dom", default-features = false } 14 | blitz-traits = { version = "0.1.0-alpha.2", path = "../blitz-traits" } 15 | 16 | # Servo dependencies 17 | html5ever = { workspace = true } 18 | xml5ever = { workspace = true } -------------------------------------------------------------------------------- /packages/blitz-html/src/html_document.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | ops::{Deref, DerefMut}, 3 | sync::Arc, 4 | }; 5 | 6 | use crate::DocumentHtmlParser; 7 | 8 | use blitz_dom::{ 9 | BaseDocument, DEFAULT_CSS, Document, EventDriver, FontContext, NoopEventHandler, net::Resource, 10 | }; 11 | use blitz_traits::{ 12 | ColorScheme, Viewport, events::UiEvent, navigation::NavigationProvider, net::SharedProvider, 13 | }; 14 | 15 | pub struct HtmlDocument { 16 | inner: BaseDocument, 17 | } 18 | 19 | // Implement DocumentLike and required traits for HtmlDocument 20 | impl Deref for HtmlDocument { 21 | type Target = BaseDocument; 22 | fn deref(&self) -> &BaseDocument { 23 | &self.inner 24 | } 25 | } 26 | impl DerefMut for HtmlDocument { 27 | fn deref_mut(&mut self) -> &mut Self::Target { 28 | &mut self.inner 29 | } 30 | } 31 | impl From for BaseDocument { 32 | fn from(doc: HtmlDocument) -> BaseDocument { 33 | doc.inner 34 | } 35 | } 36 | impl Document for HtmlDocument { 37 | fn handle_event(&mut self, event: UiEvent) { 38 | let mut driver = EventDriver::new(self.inner.mutate(), NoopEventHandler); 39 | driver.handle_ui_event(event); 40 | } 41 | 42 | fn id(&self) -> usize { 43 | self.inner.id() 44 | } 45 | 46 | fn as_any_mut(&mut self) -> &mut dyn std::any::Any { 47 | self 48 | } 49 | } 50 | 51 | impl HtmlDocument { 52 | pub fn from_html( 53 | html: &str, 54 | base_url: Option, 55 | stylesheets: Vec, 56 | net_provider: SharedProvider, 57 | font_ctx: Option, 58 | navigation_provider: Arc, 59 | ) -> Self { 60 | // Spin up the virtualdom and include the default stylesheet 61 | let viewport = Viewport::new(0, 0, 1.0, ColorScheme::Light); 62 | let mut doc = match font_ctx { 63 | Some(font_ctx) => BaseDocument::with_font_ctx(viewport, font_ctx), 64 | None => BaseDocument::new(viewport), 65 | }; 66 | 67 | // Set base url if configured 68 | if let Some(url) = &base_url { 69 | doc.set_base_url(url); 70 | } 71 | 72 | // Set the net provider 73 | doc.set_net_provider(net_provider.clone()); 74 | 75 | // Set the navigation provider 76 | doc.set_navigation_provider(navigation_provider.clone()); 77 | 78 | // Include default and user-specified stylesheets 79 | doc.add_user_agent_stylesheet(DEFAULT_CSS); 80 | for ss in &stylesheets { 81 | doc.add_user_agent_stylesheet(ss); 82 | } 83 | 84 | // Parse HTML string into document 85 | DocumentHtmlParser::parse_into_doc(&mut doc, html); 86 | 87 | HtmlDocument { inner: doc } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /packages/blitz-html/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod html_document; 2 | mod html_sink; 3 | 4 | pub use html_document::HtmlDocument; 5 | pub use html_sink::DocumentHtmlParser; 6 | -------------------------------------------------------------------------------- /packages/blitz-net/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "blitz-net" 3 | description = "Blitz networking" 4 | version = "0.1.0-alpha.2" 5 | homepage = "https://github.com/dioxuslabs/blitz" 6 | repository = "https://github.com/dioxuslabs/blitz" 7 | documentation = "https://docs.rs/blitz-net" 8 | license.workspace = true 9 | edition = "2024" 10 | 11 | [features] 12 | cookies = ["reqwest/cookies"] 13 | 14 | [dependencies] 15 | # Blitz dependencies 16 | blitz-traits = { version = "0.1.0-alpha.2", path = "../blitz-traits" } 17 | 18 | # Networking dependencies 19 | tokio = { workspace = true } 20 | reqwest = { workspace = true } 21 | data-url = { workspace = true } 22 | -------------------------------------------------------------------------------- /packages/blitz-paint/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "blitz-paint" 3 | description = "Paint a Blitz Document using anyrender" 4 | version = "0.1.0-alpha.2" 5 | homepage = "https://github.com/dioxuslabs/blitz" 6 | repository = "https://github.com/dioxuslabs/blitz" 7 | documentation = "https://docs.rs/blitz-paint" 8 | license.workspace = true 9 | edition = "2024" 10 | 11 | [features] 12 | default = ["tracing", "svg"] 13 | tracing = ["dep:tracing"] 14 | svg = ["dep:anyrender_svg", "dep:usvg", "blitz-dom/svg"] 15 | 16 | [dependencies] 17 | # Blitz dependencies 18 | anyrender = { version = "0.1", path = "../anyrender" } 19 | anyrender_svg = { version = "0.1", path = "../anyrender_svg", optional = true } 20 | blitz-traits = { version = "0.1.0-alpha.2", path = "../blitz-traits" } 21 | blitz-dom = {version = "0.1.0-alpha.2", path = "../blitz-dom", default-features = false } 22 | 23 | # Servo dependencies 24 | style = { workspace = true } 25 | euclid = { workspace = true } 26 | 27 | # DioxusLabs dependencies 28 | taffy = { workspace = true } 29 | 30 | # Linebender + WGPU dependencies 31 | parley = { workspace = true } 32 | color = { workspace = true } 33 | peniko = { workspace = true } 34 | kurbo = { workspace = true } 35 | usvg = { workspace = true, optional = true } 36 | 37 | # Other dependencies 38 | tracing = { workspace = true, optional = true } 39 | -------------------------------------------------------------------------------- /packages/blitz-paint/src/color.rs: -------------------------------------------------------------------------------- 1 | use color::{AlphaColor, DynamicColor, Srgb}; 2 | use style::color::AbsoluteColor; 3 | 4 | pub type Color = AlphaColor; 5 | 6 | pub trait ToColorColor { 7 | /// Converts a color into the `AlphaColor` type from the `color` crate 8 | fn as_srgb_color(&self) -> Color; 9 | 10 | /// Converts a color into the `DynamicColor` type from the `color` crate 11 | fn as_dynamic_color(&self) -> DynamicColor; 12 | } 13 | impl ToColorColor for AbsoluteColor { 14 | fn as_srgb_color(&self) -> Color { 15 | Color::new( 16 | *self 17 | .to_color_space(style::color::ColorSpace::Srgb) 18 | .raw_components(), 19 | ) 20 | } 21 | 22 | fn as_dynamic_color(&self) -> DynamicColor { 23 | DynamicColor::from_alpha_color(self.as_srgb_color()) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/blitz-paint/src/debug_overlay.rs: -------------------------------------------------------------------------------- 1 | use blitz_dom::BaseDocument; 2 | use kurbo::{Affine, Rect, Vec2}; 3 | 4 | use crate::color::Color; 5 | 6 | /// Renders a layout debugging overlay which visualises the content size, padding and border 7 | /// of the node with a transparent overlay. 8 | pub(crate) fn render_debug_overlay( 9 | scene: &mut impl anyrender::Scene, 10 | dom: &BaseDocument, 11 | node_id: usize, 12 | scale: f64, 13 | ) { 14 | let viewport_scroll = dom.as_ref().viewport_scroll(); 15 | let mut node = &dom.as_ref().tree()[node_id]; 16 | 17 | let taffy::Layout { 18 | size, 19 | border, 20 | padding, 21 | margin, 22 | .. 23 | } = node.final_layout; 24 | let taffy::Size { width, height } = size; 25 | 26 | let padding_border = padding + border; 27 | let scaled_pb = padding_border.map(|v| f64::from(v) * scale); 28 | let scaled_padding = padding.map(|v| f64::from(v) * scale); 29 | let scaled_border = border.map(|v| f64::from(v) * scale); 30 | let scaled_margin = margin.map(|v| f64::from(v) * scale); 31 | 32 | let content_width = width - padding_border.left - padding_border.right; 33 | let content_height = height - padding_border.top - padding_border.bottom; 34 | 35 | let taffy::Point { x, y } = node.final_layout.location; 36 | 37 | let mut abs_x = x; 38 | let mut abs_y = y; 39 | while let Some(parent_id) = node.layout_parent.get() { 40 | node = &dom.as_ref().tree()[parent_id]; 41 | let taffy::Point { x, y } = node.final_layout.location; 42 | abs_x += x; 43 | abs_y += y; 44 | } 45 | 46 | abs_x -= viewport_scroll.x as f32; 47 | abs_y -= viewport_scroll.y as f32; 48 | 49 | // Hack: scale factor 50 | let abs_x = f64::from(abs_x) * scale; 51 | let abs_y = f64::from(abs_y) * scale; 52 | let width = f64::from(width) * scale; 53 | let height = f64::from(height) * scale; 54 | let content_width = f64::from(content_width) * scale; 55 | let content_height = f64::from(content_height) * scale; 56 | 57 | // Fill content box blue 58 | let base_translation = Vec2::new(abs_x, abs_y); 59 | let transform = Affine::translate(base_translation + Vec2::new(scaled_pb.left, scaled_pb.top)); 60 | let rect = Rect::new(0.0, 0.0, content_width, content_height); 61 | let fill_color = Color::from_rgba8(66, 144, 245, 128); // blue 62 | scene.fill(peniko::Fill::NonZero, transform, fill_color, None, &rect); 63 | 64 | let padding_color = Color::from_rgba8(81, 144, 66, 128); // green 65 | draw_cutout_rect( 66 | scene, 67 | base_translation + Vec2::new(scaled_border.left, scaled_border.top), 68 | Vec2::new( 69 | content_width + scaled_padding.left + scaled_padding.right, 70 | content_height + scaled_padding.top + scaled_padding.bottom, 71 | ), 72 | scaled_padding.map(f64::from), 73 | padding_color, 74 | ); 75 | 76 | let border_color = Color::from_rgba8(245, 66, 66, 128); // red 77 | draw_cutout_rect( 78 | scene, 79 | base_translation, 80 | Vec2::new(width, height), 81 | scaled_border.map(f64::from), 82 | border_color, 83 | ); 84 | 85 | let margin_color = Color::from_rgba8(249, 204, 157, 128); // orange 86 | draw_cutout_rect( 87 | scene, 88 | base_translation - Vec2::new(scaled_margin.left, scaled_margin.top), 89 | Vec2::new( 90 | width + scaled_margin.left + scaled_margin.right, 91 | height + scaled_margin.top + scaled_margin.bottom, 92 | ), 93 | scaled_margin.map(f64::from), 94 | margin_color, 95 | ); 96 | } 97 | 98 | fn draw_cutout_rect( 99 | scene: &mut impl anyrender::Scene, 100 | base_translation: Vec2, 101 | size: Vec2, 102 | edge_widths: taffy::Rect, 103 | color: Color, 104 | ) { 105 | let mut fill = |pos: Vec2, width: f64, height: f64| { 106 | scene.fill( 107 | peniko::Fill::NonZero, 108 | Affine::translate(pos), 109 | color, 110 | None, 111 | &Rect::new(0.0, 0.0, width, height), 112 | ); 113 | }; 114 | 115 | let right = size.x - edge_widths.right; 116 | let bottom = size.y - edge_widths.bottom; 117 | let inner_h = size.y - edge_widths.top - edge_widths.bottom; 118 | let inner_w = size.x - edge_widths.left - edge_widths.right; 119 | 120 | let bt = base_translation; 121 | let ew = edge_widths; 122 | 123 | // Corners 124 | fill(bt, ew.left, ew.top); // top-left 125 | fill(bt + Vec2::new(0.0, bottom), ew.left, ew.bottom); // bottom-left 126 | fill(bt + Vec2::new(right, 0.0), ew.right, ew.top); // top-right 127 | fill(bt + Vec2::new(right, bottom), ew.right, ew.bottom); // bottom-right 128 | 129 | // Sides 130 | fill(bt + Vec2::new(0.0, ew.top), ew.left, inner_h); // left 131 | fill(bt + Vec2::new(right, ew.top), ew.right, inner_h); // right 132 | fill(bt + Vec2::new(ew.left, 0.0), inner_w, ew.top); // top 133 | fill(bt + Vec2::new(ew.left, bottom), inner_w, ew.bottom); // bottom 134 | } 135 | -------------------------------------------------------------------------------- /packages/blitz-paint/src/layers.rs: -------------------------------------------------------------------------------- 1 | use anyrender::Scene; 2 | use kurbo::{Affine, Shape}; 3 | use peniko::Mix; 4 | use std::sync::atomic::{AtomicUsize, Ordering}; 5 | 6 | const LAYER_LIMIT: usize = 1024; 7 | 8 | static LAYERS_USED: AtomicUsize = AtomicUsize::new(0); 9 | static LAYER_DEPTH: AtomicUsize = AtomicUsize::new(0); 10 | static LAYER_DEPTH_USED: AtomicUsize = AtomicUsize::new(0); 11 | static LAYERS_WANTED: AtomicUsize = AtomicUsize::new(0); 12 | 13 | pub(crate) fn reset_layer_stats() { 14 | LAYERS_USED.store(0, Ordering::SeqCst); 15 | LAYERS_WANTED.store(0, Ordering::SeqCst); 16 | LAYER_DEPTH.store(0, Ordering::SeqCst); 17 | LAYER_DEPTH_USED.store(0, Ordering::SeqCst); 18 | } 19 | 20 | pub(crate) fn maybe_with_layer( 21 | scene: &mut S, 22 | condition: bool, 23 | opacity: f32, 24 | transform: Affine, 25 | shape: &impl Shape, 26 | paint_layer: F, 27 | ) { 28 | let layer_used = maybe_push_layer(scene, condition, opacity, transform, shape); 29 | paint_layer(scene); 30 | maybe_pop_layer(scene, layer_used); 31 | } 32 | 33 | pub(crate) fn maybe_push_layer( 34 | scene: &mut impl Scene, 35 | condition: bool, 36 | opacity: f32, 37 | transform: Affine, 38 | shape: &impl Shape, 39 | ) -> bool { 40 | if !condition { 41 | return false; 42 | } 43 | LAYERS_WANTED.fetch_add(1, Ordering::SeqCst); 44 | 45 | // Check if clips are above limit 46 | let layers_available = LAYERS_USED.load(Ordering::SeqCst) <= LAYER_LIMIT; 47 | if !layers_available { 48 | return false; 49 | } 50 | 51 | let blend_mode = if opacity == 1.0 { 52 | Mix::Clip 53 | } else { 54 | Mix::Normal 55 | }; 56 | 57 | // Actually push the clip layer 58 | scene.push_layer(blend_mode, opacity, transform, shape); 59 | 60 | // Update accounting 61 | LAYERS_USED.fetch_add(1, Ordering::SeqCst); 62 | let depth = LAYER_DEPTH.fetch_add(1, Ordering::SeqCst) + 1; 63 | LAYER_DEPTH_USED.fetch_max(depth, Ordering::SeqCst); 64 | 65 | true 66 | } 67 | 68 | pub(crate) fn maybe_pop_layer(scene: &mut impl Scene, condition: bool) { 69 | if condition { 70 | scene.pop_layer(); 71 | LAYER_DEPTH.fetch_sub(1, Ordering::SeqCst); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /packages/blitz-paint/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A generic painter for blitz-dom using anyrender 2 | 3 | mod color; 4 | mod debug_overlay; 5 | mod gradient; 6 | mod layers; 7 | mod multicolor_rounded_rect; 8 | mod non_uniform_rounded_rect; 9 | mod render; 10 | mod sizing; 11 | mod text; 12 | 13 | pub use render::{BlitzDomPainter, paint_scene}; 14 | -------------------------------------------------------------------------------- /packages/blitz-paint/src/non_uniform_rounded_rect.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Mul, MulAssign}; 2 | 3 | use kurbo::{Rect, Vec2}; 4 | 5 | /// Radii for each corner of a non-uniform rounded rectangle. 6 | /// 7 | /// The use of `top` as in `top_left` assumes a y-down coordinate space. Piet 8 | /// (and Druid by extension) uses a y-down coordinate space, but Kurbo also 9 | /// supports a y-up coordinate space, in which case `top_left` would actually 10 | /// refer to the bottom-left corner, and vice versa. Top may not always 11 | /// actually be the top, but `top` corners will always have a smaller y-value 12 | /// than `bottom` corners. 13 | #[derive(Clone, Copy, Default, Debug, PartialEq)] 14 | // #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] 15 | // #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 16 | pub struct NonUniformRoundedRectRadii { 17 | /// The radii of the top-left corner. 18 | pub top_left: Vec2, 19 | /// The radii of the top-right corner. 20 | pub top_right: Vec2, 21 | /// The radii of the bottom-right corner. 22 | pub bottom_right: Vec2, 23 | /// The radii of the bottom-left corner. 24 | pub bottom_left: Vec2, 25 | } 26 | 27 | impl NonUniformRoundedRectRadii { 28 | pub fn average(&self) -> f64 { 29 | (self.top_left.x 30 | + self.top_left.y 31 | + self.top_right.x 32 | + self.top_right.y 33 | + self.bottom_left.x 34 | + self.bottom_left.y 35 | + self.bottom_right.x 36 | + self.bottom_right.y) 37 | / 8.0 38 | } 39 | } 40 | 41 | impl Mul for NonUniformRoundedRectRadii { 42 | type Output = Self; 43 | 44 | fn mul(self, rhs: f64) -> Self::Output { 45 | Self { 46 | top_left: self.top_left * rhs, 47 | top_right: self.top_right * rhs, 48 | bottom_right: self.bottom_right * rhs, 49 | bottom_left: self.bottom_left * rhs, 50 | } 51 | } 52 | } 53 | 54 | impl MulAssign for NonUniformRoundedRectRadii { 55 | fn mul_assign(&mut self, rhs: f64) { 56 | self.top_left *= rhs; 57 | self.top_right *= rhs; 58 | self.bottom_left *= rhs; 59 | self.bottom_right *= rhs; 60 | } 61 | } 62 | 63 | /// A rectangle with rounded corners. 64 | /// 65 | /// By construction the rounded rectangle will have 66 | /// non-negative dimensions and radii clamped to half size of the rect. 67 | #[derive(Clone, Copy, Default, Debug, PartialEq)] 68 | // #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] 69 | // #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 70 | pub struct NonUniformRoundedRect { 71 | /// Coordinates of the rectangle. 72 | rect: Rect, 73 | /// Radius of all four corners. 74 | radii: NonUniformRoundedRectRadii, 75 | } 76 | -------------------------------------------------------------------------------- /packages/blitz-paint/src/render/box_shadow.rs: -------------------------------------------------------------------------------- 1 | use super::ElementCx; 2 | use crate::{ 3 | color::{Color, ToColorColor as _}, 4 | layers::maybe_with_layer, 5 | }; 6 | use kurbo::{Rect, Vec2}; 7 | 8 | impl ElementCx<'_> { 9 | pub(super) fn draw_outset_box_shadow(&self, scene: &mut impl anyrender::Scene) { 10 | let box_shadow = &self.style.get_effects().box_shadow.0; 11 | 12 | // TODO: Only apply clip if element has transparency 13 | let has_outset_shadow = box_shadow.iter().any(|s| !s.inset); 14 | if !has_outset_shadow { 15 | return; 16 | } 17 | 18 | let current_color = self.style.clone_color(); 19 | let max_shadow_rect = box_shadow.iter().fold(Rect::ZERO, |prev, shadow| { 20 | let x = shadow.base.horizontal.px() as f64 * self.scale; 21 | let y = shadow.base.vertical.px() as f64 * self.scale; 22 | let blur = shadow.base.blur.px() as f64 * self.scale; 23 | let spread = shadow.spread.px() as f64 * self.scale; 24 | let offset = spread + blur * 2.5; 25 | 26 | let rect = self.frame.border_box.inflate(offset, offset) + Vec2::new(x, y); 27 | 28 | prev.union(rect) 29 | }); 30 | 31 | maybe_with_layer( 32 | scene, 33 | has_outset_shadow, 34 | 1.0, 35 | self.transform, 36 | &self.frame.shadow_clip(max_shadow_rect), 37 | |scene| { 38 | for shadow in box_shadow.iter().filter(|s| !s.inset).rev() { 39 | let shadow_color = shadow 40 | .base 41 | .color 42 | .resolve_to_absolute(¤t_color) 43 | .as_srgb_color(); 44 | 45 | let alpha = shadow_color.components[3]; 46 | if alpha != 0.0 { 47 | let transform = self.transform.then_translate(Vec2 { 48 | x: shadow.base.horizontal.px() as f64 * self.scale, 49 | y: shadow.base.vertical.px() as f64 * self.scale, 50 | }); 51 | 52 | // TODO draw shadows with matching individual radii instead of averaging 53 | let radius = self.frame.border_radii.average(); 54 | 55 | let spread = shadow.spread.px() as f64 * self.scale; 56 | let rect = self.frame.border_box.inflate(spread, spread); 57 | 58 | // Fill the color 59 | scene.draw_box_shadow( 60 | transform, 61 | rect, 62 | shadow_color, 63 | radius, 64 | shadow.base.blur.px() as f64, 65 | ); 66 | } 67 | } 68 | }, 69 | ) 70 | } 71 | 72 | pub(super) fn draw_inset_box_shadow(&self, scene: &mut impl anyrender::Scene) { 73 | let current_color = self.style.clone_color(); 74 | let box_shadow = &self.style.get_effects().box_shadow.0; 75 | let has_inset_shadow = box_shadow.iter().any(|s| s.inset); 76 | if !has_inset_shadow { 77 | return; 78 | } 79 | 80 | maybe_with_layer( 81 | scene, 82 | has_inset_shadow, 83 | 1.0, 84 | self.transform, 85 | &self.frame.padding_box_path(), 86 | |scene| { 87 | for shadow in box_shadow.iter().filter(|s| s.inset) { 88 | let shadow_color = shadow 89 | .base 90 | .color 91 | .resolve_to_absolute(¤t_color) 92 | .as_srgb_color(); 93 | if shadow_color != Color::TRANSPARENT { 94 | let transform = self.transform.then_translate(Vec2 { 95 | x: shadow.base.horizontal.px() as f64, 96 | y: shadow.base.vertical.px() as f64, 97 | }); 98 | 99 | //TODO draw shadows with matching individual radii instead of averaging 100 | let radius = self.frame.border_radii.average(); 101 | 102 | // Fill the color 103 | scene.draw_box_shadow( 104 | transform, 105 | self.frame.border_box, 106 | shadow_color, 107 | radius, 108 | shadow.base.blur.px() as f64 * self.scale, 109 | ); 110 | } 111 | } 112 | }, 113 | ); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /packages/blitz-paint/src/render/form_controls.rs: -------------------------------------------------------------------------------- 1 | use super::ElementCx; 2 | use crate::color::{Color, ToColorColor as _}; 3 | use anyrender::Scene; 4 | use blitz_dom::local_name; 5 | use kurbo::{Affine, BezPath, Cap, Circle, Join, Point, RoundedRect, Stroke, Vec2}; 6 | use peniko::Fill; 7 | use style::dom::TElement as _; 8 | 9 | impl ElementCx<'_> { 10 | pub(super) fn draw_input(&self, scene: &mut impl anyrender::Scene) { 11 | if self.node.local_name() != "input" { 12 | return; 13 | } 14 | let Some(checked) = self.element.checkbox_input_checked() else { 15 | return; 16 | }; 17 | 18 | let type_attr = self.node.attr(local_name!("type")); 19 | let disabled = self.node.attr(local_name!("disabled")).is_some(); 20 | 21 | // TODO this should be coming from css accent-color, but I couldn't find how to retrieve it 22 | let accent_color = if disabled { 23 | Color::from_rgba8(209, 209, 209, 255) 24 | } else { 25 | self.style.clone_color().as_srgb_color() 26 | }; 27 | 28 | let width = self.frame.border_box.width(); 29 | let height = self.frame.border_box.height(); 30 | let min_dimension = width.min(height); 31 | let scale = (min_dimension - 4.0).max(0.0) / 16.0; 32 | 33 | let frame = self.frame.border_box.to_rounded_rect(scale * 2.0); 34 | 35 | match type_attr { 36 | Some("checkbox") => { 37 | draw_checkbox(scene, checked, frame, self.transform, accent_color, scale); 38 | } 39 | Some("radio") => { 40 | let center = frame.center(); 41 | draw_radio_button(scene, checked, center, self.transform, accent_color, scale); 42 | } 43 | _ => {} 44 | } 45 | } 46 | } 47 | 48 | fn draw_checkbox( 49 | scene: &mut impl Scene, 50 | checked: bool, 51 | frame: RoundedRect, 52 | transform: Affine, 53 | accent_color: Color, 54 | scale: f64, 55 | ) { 56 | if checked { 57 | scene.fill(Fill::NonZero, transform, accent_color, None, &frame); 58 | //Tick code derived from masonry 59 | let mut path = BezPath::new(); 60 | path.move_to((2.0, 9.0)); 61 | path.line_to((6.0, 13.0)); 62 | path.line_to((14.0, 2.0)); 63 | 64 | path.apply_affine(Affine::translate(Vec2 { x: 2.0, y: 1.0 }).then_scale(scale)); 65 | 66 | let style = Stroke { 67 | width: 2.0 * scale, 68 | join: Join::Round, 69 | miter_limit: 10.0, 70 | start_cap: Cap::Round, 71 | end_cap: Cap::Round, 72 | dash_pattern: Default::default(), 73 | dash_offset: 0.0, 74 | }; 75 | 76 | scene.stroke(&style, transform, Color::WHITE, None, &path); 77 | } else { 78 | scene.fill(Fill::NonZero, transform, Color::WHITE, None, &frame); 79 | scene.stroke(&Stroke::default(), transform, accent_color, None, &frame); 80 | } 81 | } 82 | 83 | fn draw_radio_button( 84 | scene: &mut impl Scene, 85 | checked: bool, 86 | center: Point, 87 | transform: Affine, 88 | accent_color: Color, 89 | scale: f64, 90 | ) { 91 | let outer_ring = Circle::new(center, 8.0 * scale); 92 | let gap = Circle::new(center, 6.0 * scale); 93 | let inner_circle = Circle::new(center, 4.0 * scale); 94 | if checked { 95 | scene.fill(Fill::NonZero, transform, accent_color, None, &outer_ring); 96 | scene.fill(Fill::NonZero, transform, Color::WHITE, None, &gap); 97 | scene.fill(Fill::NonZero, transform, accent_color, None, &inner_circle); 98 | } else { 99 | const GRAY: Color = color::palette::css::GRAY; 100 | scene.fill(Fill::NonZero, transform, GRAY, None, &outer_ring); 101 | scene.fill(Fill::NonZero, transform, Color::WHITE, None, &gap); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /packages/blitz-paint/src/sizing.rs: -------------------------------------------------------------------------------- 1 | use style::properties::generated::longhands::object_fit::computed_value::T as ObjectFit; 2 | 3 | pub(crate) fn compute_object_fit( 4 | container_size: taffy::Size, 5 | object_size: Option>, 6 | object_fit: ObjectFit, 7 | ) -> taffy::Size { 8 | match object_fit { 9 | ObjectFit::None => object_size.unwrap_or(container_size), 10 | ObjectFit::Fill => container_size, 11 | ObjectFit::Cover => compute_object_fit_cover(container_size, object_size), 12 | ObjectFit::Contain => compute_object_fit_contain(container_size, object_size), 13 | ObjectFit::ScaleDown => { 14 | let contain_size = compute_object_fit_contain(container_size, object_size); 15 | match object_size { 16 | Some(object_size) if object_size.width < contain_size.width => object_size, 17 | _ => contain_size, 18 | } 19 | } 20 | } 21 | } 22 | 23 | fn compute_object_fit_contain( 24 | container_size: taffy::Size, 25 | object_size: Option>, 26 | ) -> taffy::Size { 27 | let Some(object_size) = object_size else { 28 | return container_size; 29 | }; 30 | 31 | let x_ratio = container_size.width / object_size.width; 32 | let y_ratio = container_size.height / object_size.height; 33 | 34 | let ratio = match (x_ratio < 1.0, y_ratio < 1.0) { 35 | (true, true) => x_ratio.min(y_ratio), 36 | (true, false) => x_ratio, 37 | (false, true) => y_ratio, 38 | (false, false) => x_ratio.min(y_ratio), 39 | }; 40 | 41 | object_size.map(|dim| dim * ratio) 42 | } 43 | 44 | fn compute_object_fit_cover( 45 | container_size: taffy::Size, 46 | object_size: Option>, 47 | ) -> taffy::Size { 48 | let Some(object_size) = object_size else { 49 | return container_size; 50 | }; 51 | 52 | let x_ratio = container_size.width / object_size.width; 53 | let y_ratio = container_size.height / object_size.height; 54 | 55 | let ratio = match (x_ratio < 1.0, y_ratio < 1.0) { 56 | (true, true) => x_ratio.max(y_ratio), 57 | (true, false) => y_ratio, 58 | (false, true) => x_ratio, 59 | (false, false) => x_ratio.max(y_ratio), 60 | }; 61 | 62 | object_size.map(|dim| dim * ratio) 63 | } 64 | -------------------------------------------------------------------------------- /packages/blitz-paint/src/text.rs: -------------------------------------------------------------------------------- 1 | use blitz_dom::node::TextBrush; 2 | use kurbo::{Affine, Point, Stroke}; 3 | use parley::{Line, PositionedLayoutItem}; 4 | use peniko::Fill; 5 | 6 | pub(crate) fn stroke_text<'a>( 7 | scale: f64, 8 | scene: &mut impl anyrender::Scene, 9 | lines: impl Iterator>, 10 | pos: Point, 11 | ) { 12 | let transform = Affine::translate((pos.x * scale, pos.y * scale)); 13 | for line in lines { 14 | for item in line.items() { 15 | if let PositionedLayoutItem::GlyphRun(glyph_run) = item { 16 | let mut x = glyph_run.offset(); 17 | let y = glyph_run.baseline(); 18 | 19 | let run = glyph_run.run(); 20 | let font = run.font(); 21 | let font_size = run.font_size(); 22 | let metrics = run.metrics(); 23 | let style = glyph_run.style(); 24 | let synthesis = run.synthesis(); 25 | let glyph_xform = synthesis 26 | .skew() 27 | .map(|angle| Affine::skew(angle.to_radians().tan() as f64, 0.0)); 28 | 29 | scene.draw_glyphs( 30 | font, 31 | font_size, 32 | true, // hint 33 | run.normalized_coords(), 34 | Fill::NonZero, 35 | &style.brush.brush, 36 | 1.0, // alpha 37 | transform, 38 | glyph_xform, 39 | glyph_run.glyphs().map(|glyph| { 40 | let gx = x + glyph.x; 41 | let gy = y - glyph.y; 42 | x += glyph.advance; 43 | 44 | anyrender::Glyph { 45 | id: glyph.id as _, 46 | x: gx, 47 | y: gy, 48 | } 49 | }), 50 | ); 51 | 52 | let mut draw_decoration_line = |offset: f32, size: f32, brush: &TextBrush| { 53 | let x = glyph_run.offset() as f64; 54 | let w = glyph_run.advance() as f64; 55 | let y = (glyph_run.baseline() - offset + size / 2.0) as f64; 56 | let line = kurbo::Line::new((x, y), (x + w, y)); 57 | scene.stroke( 58 | &Stroke::new(size as f64), 59 | transform, 60 | &brush.brush, 61 | None, 62 | &line, 63 | ) 64 | }; 65 | 66 | if let Some(underline) = &style.underline { 67 | let offset = underline.offset.unwrap_or(metrics.underline_offset); 68 | let size = underline.size.unwrap_or(metrics.underline_size); 69 | 70 | // TODO: intercept line when crossing an descending character like "gqy" 71 | draw_decoration_line(offset, size, &underline.brush); 72 | } 73 | if let Some(strikethrough) = &style.strikethrough { 74 | let offset = strikethrough.offset.unwrap_or(metrics.strikethrough_offset); 75 | let size = strikethrough.size.unwrap_or(metrics.strikethrough_size); 76 | 77 | draw_decoration_line(offset, size, &strikethrough.brush); 78 | } 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /packages/blitz-shell/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "blitz-shell" 3 | description = "Blitz application shell" 4 | version = "0.1.0-alpha.2" 5 | homepage = "https://github.com/dioxuslabs/blitz" 6 | repository = "https://github.com/dioxuslabs/blitz" 7 | documentation = "https://docs.rs/blitz-shell" 8 | license.workspace = true 9 | edition = "2024" 10 | 11 | [features] 12 | default = ["accessibility", "menu", "tracing"] 13 | accessibility = ["dep:accesskit", "dep:accesskit_winit", "blitz-dom/accessibility"] 14 | menu = ["dep:muda"] 15 | tracing = ["dep:tracing", "blitz-dom/tracing"] 16 | 17 | [dependencies] 18 | # Blitz dependencies 19 | blitz-traits = { version = "0.1.0-alpha.2", path = "../blitz-traits" } 20 | blitz-dom = { version = "0.1.0-alpha.2", path = "../blitz-dom", default-features = false } 21 | blitz-paint = { version = "0.1.0-alpha.2", path = "../blitz-paint", default-features = false } 22 | anyrender = { version = "0.1", path = "../anyrender", default-features = false } 23 | 24 | # Windowing & Input 25 | winit = { workspace = true } 26 | keyboard-types = { workspace = true } 27 | 28 | accesskit = { workspace = true, optional = true } 29 | accesskit_winit = {workspace = true, optional = true } 30 | 31 | # Other dependencies 32 | tracing = { workspace = true, optional = true } 33 | futures-util = { workspace = true } 34 | 35 | [target.'cfg(any(target_os = "windows",target_os = "macos",target_os = "linux",target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))'.dependencies] 36 | muda = { workspace = true, default-features = false, features = ["serde"], optional = true } 37 | 38 | [target.'cfg(target_os = "android")'.dependencies] 39 | android-activity = { version = "0.6.0", features = ["native-activity"] } 40 | 41 | [package.metadata.docs.rs] 42 | all-features = true 43 | rustdoc-args = ["--cfg", "docsrs"] 44 | -------------------------------------------------------------------------------- /packages/blitz-shell/src/accessibility.rs: -------------------------------------------------------------------------------- 1 | use crate::event::BlitzShellEvent; 2 | use accesskit_winit::Adapter; 3 | use blitz_dom::BaseDocument; 4 | use winit::{event_loop::EventLoopProxy, window::Window}; 5 | 6 | /// State of the accessibility node tree and platform adapter. 7 | pub struct AccessibilityState { 8 | /// Adapter to connect to the [`EventLoop`](`winit::event_loop::EventLoop`). 9 | adapter: accesskit_winit::Adapter, 10 | } 11 | 12 | impl AccessibilityState { 13 | pub fn new(window: &Window, proxy: EventLoopProxy) -> Self { 14 | Self { 15 | adapter: Adapter::with_event_loop_proxy(window, proxy.clone()), 16 | } 17 | } 18 | pub fn build_tree(&mut self, doc: &BaseDocument) { 19 | self.adapter 20 | .update_if_active(|| doc.build_accessibility_tree()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/blitz-shell/src/application.rs: -------------------------------------------------------------------------------- 1 | use crate::event::BlitzShellEvent; 2 | 3 | use anyrender::WindowRenderer; 4 | use std::collections::HashMap; 5 | use winit::application::ApplicationHandler; 6 | use winit::event::WindowEvent; 7 | use winit::event_loop::{ActiveEventLoop, EventLoopProxy}; 8 | use winit::window::WindowId; 9 | 10 | use crate::{View, WindowConfig}; 11 | 12 | pub struct BlitzApplication { 13 | pub windows: HashMap>, 14 | pending_windows: Vec>, 15 | pub proxy: EventLoopProxy, 16 | #[cfg(all(feature = "menu", not(any(target_os = "android", target_os = "ios"))))] 17 | menu_channel: muda::MenuEventReceiver, 18 | } 19 | 20 | impl BlitzApplication { 21 | pub fn new(proxy: EventLoopProxy) -> Self { 22 | BlitzApplication { 23 | windows: HashMap::new(), 24 | pending_windows: Vec::new(), 25 | proxy, 26 | 27 | #[cfg(all(feature = "menu", not(any(target_os = "android", target_os = "ios"))))] 28 | menu_channel: muda::MenuEvent::receiver().clone(), 29 | } 30 | } 31 | 32 | pub fn add_window(&mut self, window_config: WindowConfig) { 33 | self.pending_windows.push(window_config); 34 | } 35 | 36 | fn window_mut_by_doc_id(&mut self, doc_id: usize) -> Option<&mut View> { 37 | self.windows.values_mut().find(|w| w.doc.id() == doc_id) 38 | } 39 | } 40 | 41 | impl ApplicationHandler for BlitzApplication { 42 | fn resumed(&mut self, event_loop: &ActiveEventLoop) { 43 | // Resume existing windows 44 | for (_, view) in self.windows.iter_mut() { 45 | view.resume(); 46 | } 47 | 48 | // Initialise pending windows 49 | for window_config in self.pending_windows.drain(..) { 50 | let mut view = View::init(window_config, event_loop, &self.proxy); 51 | view.resume(); 52 | if !view.renderer.is_active() { 53 | continue; 54 | } 55 | self.windows.insert(view.window_id(), view); 56 | } 57 | } 58 | 59 | fn suspended(&mut self, _event_loop: &ActiveEventLoop) { 60 | for (_, view) in self.windows.iter_mut() { 61 | view.suspend(); 62 | } 63 | } 64 | 65 | fn new_events(&mut self, _event_loop: &ActiveEventLoop, _cause: winit::event::StartCause) { 66 | #[cfg(all(feature = "menu", not(any(target_os = "android", target_os = "ios"))))] 67 | if let Ok(event) = self.menu_channel.try_recv() { 68 | if event.id == muda::MenuId::new("dev.show_layout") { 69 | for (_, view) in self.windows.iter_mut() { 70 | view.doc.as_mut().devtools_mut().toggle_show_layout(); 71 | view.request_redraw(); 72 | } 73 | } 74 | } 75 | } 76 | 77 | fn window_event( 78 | &mut self, 79 | event_loop: &ActiveEventLoop, 80 | window_id: WindowId, 81 | event: WindowEvent, 82 | ) { 83 | // Exit the app when window close is requested. 84 | if matches!(event, WindowEvent::CloseRequested) { 85 | // Drop window before exiting event loop 86 | // See https://github.com/rust-windowing/winit/issues/4135 87 | let window = self.windows.remove(&window_id); 88 | drop(window); 89 | if self.windows.is_empty() { 90 | event_loop.exit(); 91 | } 92 | return; 93 | } 94 | 95 | if let Some(window) = self.windows.get_mut(&window_id) { 96 | window.handle_winit_event(event); 97 | } 98 | 99 | let _ = self.proxy.send_event(BlitzShellEvent::Poll { window_id }); 100 | } 101 | 102 | fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: BlitzShellEvent) { 103 | match event { 104 | BlitzShellEvent::Poll { window_id } => { 105 | if let Some(window) = self.windows.get_mut(&window_id) { 106 | window.poll(); 107 | }; 108 | } 109 | BlitzShellEvent::ResourceLoad { doc_id, data } => { 110 | // TODO: Handle multiple documents per window 111 | if let Some(window) = self.window_mut_by_doc_id(doc_id) { 112 | window.doc.as_mut().load_resource(data); 113 | window.request_redraw(); 114 | } 115 | } 116 | 117 | #[cfg(feature = "accessibility")] 118 | BlitzShellEvent::Accessibility { window_id, data } => { 119 | if let Some(window) = self.windows.get_mut(&window_id) { 120 | match &*data { 121 | accesskit_winit::WindowEvent::InitialTreeRequested => { 122 | window.build_accessibility_tree(); 123 | } 124 | accesskit_winit::WindowEvent::AccessibilityDeactivated => { 125 | // TODO 126 | } 127 | accesskit_winit::WindowEvent::ActionRequested(_req) => { 128 | // TODO 129 | } 130 | } 131 | } 132 | } 133 | 134 | BlitzShellEvent::Embedder(_) => { 135 | // Do nothing. Should be handled by embedders (if required). 136 | } 137 | BlitzShellEvent::Navigate(_opts) => { 138 | // Do nothing. Should be handled by embedders (if required). 139 | } 140 | BlitzShellEvent::NavigationLoad { .. } => { 141 | // Do nothing. Should be handled by embedders (if required). 142 | } 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /packages/blitz-shell/src/event.rs: -------------------------------------------------------------------------------- 1 | use blitz_traits::navigation::NavigationOptions; 2 | use futures_util::task::ArcWake; 3 | use std::{any::Any, sync::Arc}; 4 | use winit::{event_loop::EventLoopProxy, window::WindowId}; 5 | 6 | #[cfg(feature = "accessibility")] 7 | use accesskit_winit::{Event as AccessKitEvent, WindowEvent as AccessKitWindowEvent}; 8 | use blitz_dom::net::Resource; 9 | 10 | #[derive(Debug, Clone)] 11 | pub enum BlitzShellEvent { 12 | Poll { 13 | window_id: WindowId, 14 | }, 15 | 16 | ResourceLoad { 17 | doc_id: usize, 18 | data: Resource, 19 | }, 20 | 21 | /// An accessibility event from `accesskit`. 22 | #[cfg(feature = "accessibility")] 23 | Accessibility { 24 | window_id: WindowId, 25 | data: Arc, 26 | }, 27 | 28 | /// An arbitary event from the Blitz embedder 29 | Embedder(Arc), 30 | 31 | /// Navigate to another URL (triggered by e.g. clicking a link) 32 | Navigate(Box), 33 | 34 | /// Navigate to another URL (triggered by e.g. clicking a link) 35 | NavigationLoad { 36 | url: String, 37 | contents: String, 38 | retain_scroll_position: bool, 39 | is_md: bool, 40 | }, 41 | } 42 | impl BlitzShellEvent { 43 | pub fn embedder_event(value: T) -> Self { 44 | let boxed = Arc::new(value) as Arc; 45 | Self::Embedder(boxed) 46 | } 47 | } 48 | impl From<(usize, Resource)> for BlitzShellEvent { 49 | fn from((doc_id, data): (usize, Resource)) -> Self { 50 | BlitzShellEvent::ResourceLoad { doc_id, data } 51 | } 52 | } 53 | 54 | #[cfg(feature = "accessibility")] 55 | impl From for BlitzShellEvent { 56 | fn from(value: AccessKitEvent) -> Self { 57 | Self::Accessibility { 58 | window_id: value.window_id, 59 | data: Arc::new(value.window_event), 60 | } 61 | } 62 | } 63 | 64 | /// Create a waker that will send a poll event to the event loop. 65 | /// 66 | /// This lets the VirtualDom "come up for air" and process events while the main thread is blocked by the WebView. 67 | /// 68 | /// All other IO lives in the Tokio runtime, 69 | pub fn create_waker(proxy: &EventLoopProxy, id: WindowId) -> std::task::Waker { 70 | struct DomHandle { 71 | proxy: EventLoopProxy, 72 | id: WindowId, 73 | } 74 | 75 | // this should be implemented by most platforms, but ios is missing this until 76 | // https://github.com/tauri-apps/wry/issues/830 is resolved 77 | unsafe impl Send for DomHandle {} 78 | unsafe impl Sync for DomHandle {} 79 | 80 | impl ArcWake for DomHandle { 81 | fn wake_by_ref(arc_self: &Arc) { 82 | _ = arc_self.proxy.send_event(BlitzShellEvent::Poll { 83 | window_id: arc_self.id, 84 | }) 85 | } 86 | } 87 | 88 | futures_util::task::waker(Arc::new(DomHandle { 89 | id, 90 | proxy: proxy.clone(), 91 | })) 92 | } 93 | -------------------------------------------------------------------------------- /packages/blitz-shell/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(docsrs, feature(doc_cfg))] 2 | 3 | //! A native renderer for Dioxus. 4 | //! 5 | //! ## Feature flags 6 | //! - `default`: Enables the features listed below. 7 | //! - `accessibility`: Enables [`accesskit`] accessibility support. 8 | //! - `hot-reload`: Enables hot-reloading of Dioxus RSX. 9 | //! - `menu`: Enables the [`muda`] menubar. 10 | //! - `tracing`: Enables tracing support. 11 | 12 | mod application; 13 | mod convert_events; 14 | mod event; 15 | mod window; 16 | 17 | #[cfg(all(feature = "menu", not(any(target_os = "android", target_os = "ios"))))] 18 | mod menu; 19 | 20 | #[cfg(feature = "accessibility")] 21 | mod accessibility; 22 | 23 | pub use crate::application::BlitzApplication; 24 | pub use crate::event::BlitzShellEvent; 25 | pub use crate::window::{View, WindowConfig}; 26 | 27 | use blitz_dom::net::Resource; 28 | use blitz_traits::net::NetCallback; 29 | use blitz_traits::shell::ShellProvider; 30 | use std::sync::Arc; 31 | use winit::event_loop::{ControlFlow, EventLoop, EventLoopProxy}; 32 | use winit::window::{CursorIcon, Window}; 33 | 34 | #[derive(Default)] 35 | pub struct Config { 36 | pub stylesheets: Vec, 37 | pub base_url: Option, 38 | } 39 | 40 | /// Build an event loop for the application 41 | pub fn create_default_event_loop() -> EventLoop { 42 | let mut ev_builder = EventLoop::::with_user_event(); 43 | #[cfg(target_os = "android")] 44 | { 45 | use winit::platform::android::EventLoopBuilderExtAndroid; 46 | ev_builder.with_android_app(current_android_app()); 47 | } 48 | 49 | let event_loop = ev_builder.build().unwrap(); 50 | event_loop.set_control_flow(ControlFlow::Wait); 51 | 52 | event_loop 53 | } 54 | 55 | #[cfg(target_os = "android")] 56 | static ANDROID_APP: std::sync::OnceLock = std::sync::OnceLock::new(); 57 | 58 | #[cfg(target_os = "android")] 59 | #[cfg_attr(docsrs, doc(cfg(target_os = "android")))] 60 | /// Set the current [`AndroidApp`](android_activity::AndroidApp). 61 | pub fn set_android_app(app: android_activity::AndroidApp) { 62 | ANDROID_APP.set(app).unwrap() 63 | } 64 | 65 | #[cfg(target_os = "android")] 66 | #[cfg_attr(docsrs, doc(cfg(target_os = "android")))] 67 | /// Get the current [`AndroidApp`](android_activity::AndroidApp). 68 | /// This will panic if the android activity has not been setup with [`set_android_app`]. 69 | pub fn current_android_app() -> android_activity::AndroidApp { 70 | ANDROID_APP.get().unwrap().clone() 71 | } 72 | 73 | /// A NetCallback that injects the fetched Resource into our winit event loop 74 | pub struct BlitzShellNetCallback(EventLoopProxy); 75 | 76 | impl BlitzShellNetCallback { 77 | pub fn new(proxy: EventLoopProxy) -> Self { 78 | Self(proxy) 79 | } 80 | 81 | pub fn shared(proxy: EventLoopProxy) -> Arc> { 82 | Arc::new(Self(proxy)) 83 | } 84 | } 85 | impl NetCallback for BlitzShellNetCallback { 86 | fn call(&self, doc_id: usize, result: Result>) { 87 | // TODO: handle error case 88 | if let Ok(data) = result { 89 | self.0 90 | .send_event(BlitzShellEvent::ResourceLoad { doc_id, data }) 91 | .unwrap() 92 | } 93 | } 94 | } 95 | 96 | pub struct BlitzShellProvider { 97 | window: Arc, 98 | } 99 | impl BlitzShellProvider { 100 | pub fn new(window: Arc) -> Self { 101 | Self { window } 102 | } 103 | } 104 | 105 | impl ShellProvider for BlitzShellProvider { 106 | fn request_redraw(&self) { 107 | self.window.request_redraw(); 108 | } 109 | fn set_cursor(&self, icon: CursorIcon) { 110 | self.window.set_cursor(icon); 111 | } 112 | fn set_window_title(&self, title: String) { 113 | self.window.set_title(&title); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /packages/blitz-shell/src/menu.rs: -------------------------------------------------------------------------------- 1 | use winit::window::Window; 2 | 3 | /// Initialize the default menu bar. 4 | pub fn init_menu(window: &Window) -> muda::Menu { 5 | use muda::{AboutMetadata, Menu, MenuId, MenuItem, PredefinedMenuItem, Submenu}; 6 | 7 | let menu = Menu::new(); 8 | 9 | // Build the about section 10 | let about = Submenu::new("About", true); 11 | about 12 | .append_items(&[ 13 | &PredefinedMenuItem::about("Dioxus".into(), Option::from(AboutMetadata::default())), 14 | &MenuItem::with_id(MenuId::new("dev.show_layout"), "Show layout", true, None), 15 | ]) 16 | .unwrap(); 17 | menu.append(&about).unwrap(); 18 | 19 | #[cfg(target_os = "windows")] 20 | { 21 | use winit::raw_window_handle::*; 22 | if let RawWindowHandle::Win32(handle) = window.window_handle().unwrap().as_raw() { 23 | menu.init_for_hwnd(handle.hwnd.get()).unwrap(); 24 | } 25 | } 26 | 27 | // todo: menu on linux 28 | // #[cfg(target_os = "linux")] 29 | // { 30 | // use winit::platform::unix::WindowExtUnix; 31 | // menu.init_for_gtk_window(window.gtk_window(), window.default_vbox()) 32 | // .unwrap(); 33 | // } 34 | 35 | #[cfg(target_os = "macos")] 36 | { 37 | menu.init_for_nsapp(); 38 | } 39 | 40 | // Suppress unused variable warning on non-windows platforms 41 | let _ = window; 42 | 43 | menu 44 | } 45 | -------------------------------------------------------------------------------- /packages/blitz-traits/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "blitz-traits" 3 | description = "Shared traits and types for Blitz" 4 | version = "0.1.0-alpha.2" 5 | homepage = "https://github.com/dioxuslabs/blitz" 6 | repository = "https://github.com/dioxuslabs/blitz" 7 | documentation = "https://docs.rs/blitz-traits" 8 | license.workspace = true 9 | edition = "2024" 10 | 11 | [dependencies] 12 | http = { workspace = true } 13 | url = { workspace = true } 14 | bytes = { workspace = true } 15 | keyboard-types = { workspace = true } 16 | smol_str = { workspace = true } 17 | bitflags = { workspace = true } 18 | cursor-icon = { workspace = true } -------------------------------------------------------------------------------- /packages/blitz-traits/src/devtools.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Default, Clone, Copy)] 2 | pub struct Devtools { 3 | pub show_layout: bool, 4 | pub highlight_hover: bool, 5 | } 6 | 7 | impl Devtools { 8 | pub fn toggle_show_layout(&mut self) { 9 | self.show_layout = !self.show_layout 10 | } 11 | 12 | pub fn toggle_highlight_hover(&mut self) { 13 | self.highlight_hover = !self.highlight_hover 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/blitz-traits/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod events; 2 | pub mod navigation; 3 | pub mod net; 4 | pub mod shell; 5 | 6 | mod devtools; 7 | mod viewport; 8 | 9 | pub use devtools::Devtools; 10 | pub use events::{ 11 | BlitzImeEvent, BlitzKeyEvent, BlitzMouseButtonEvent, DomEvent, DomEventData, EventState, 12 | HitResult, KeyState, MouseEventButton, MouseEventButtons, 13 | }; 14 | pub use viewport::{ColorScheme, Viewport}; 15 | -------------------------------------------------------------------------------- /packages/blitz-traits/src/navigation.rs: -------------------------------------------------------------------------------- 1 | use bytes::Bytes; 2 | use http::{HeaderMap, HeaderValue, Method}; 3 | use url::Url; 4 | 5 | use crate::net::Request; 6 | 7 | /// A provider to enable a document to bubble up navigation events (e.g. clicking a link) 8 | pub trait NavigationProvider: Send + Sync + 'static { 9 | fn navigate_to(&self, options: NavigationOptions); 10 | } 11 | 12 | pub struct DummyNavigationProvider; 13 | 14 | impl NavigationProvider for DummyNavigationProvider { 15 | fn navigate_to(&self, _options: NavigationOptions) { 16 | // Default impl: do nothing 17 | } 18 | } 19 | 20 | #[non_exhaustive] 21 | #[derive(Debug, Clone)] 22 | pub struct NavigationOptions { 23 | /// The URL to navigate to 24 | pub url: Url, 25 | 26 | pub content_type: String, 27 | 28 | /// Source document for the navigation 29 | pub source_document: usize, 30 | 31 | pub document_resource: Option, 32 | } 33 | 34 | impl NavigationOptions { 35 | pub fn new(url: Url, content_type: String, source_document: usize) -> Self { 36 | Self { 37 | url, 38 | content_type, 39 | source_document, 40 | document_resource: None, 41 | } 42 | } 43 | pub fn set_document_resource(mut self, document_resource: Option) -> Self { 44 | self.document_resource = document_resource; 45 | self 46 | } 47 | 48 | pub fn into_request(self) -> Request { 49 | let mut headers = HeaderMap::new(); 50 | headers.insert( 51 | "content-type", 52 | HeaderValue::from_str(&self.content_type).unwrap(), 53 | ); 54 | 55 | if let Some(document_resource) = self.document_resource { 56 | Request { 57 | url: self.url, 58 | method: Method::POST, 59 | headers, 60 | body: document_resource, 61 | } 62 | } else { 63 | Request { 64 | url: self.url, 65 | method: Method::GET, 66 | headers, 67 | body: Bytes::new(), 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/blitz-traits/src/net.rs: -------------------------------------------------------------------------------- 1 | pub use bytes::Bytes; 2 | pub use http::{self, HeaderMap, Method}; 3 | use std::sync::Arc; 4 | pub use url::Url; 5 | 6 | pub type SharedProvider = Arc>; 7 | pub type BoxedHandler = Box>; 8 | pub type SharedCallback = Arc>; 9 | 10 | /// A type that fetches resources for a Document. 11 | /// 12 | /// This may be over the network via http(s), via the filesystem, or some other method. 13 | pub trait NetProvider: Send + Sync + 'static { 14 | fn fetch(&self, doc_id: usize, request: Request, handler: BoxedHandler); 15 | } 16 | 17 | /// A type that parses raw bytes from a network request into a Data and then calls 18 | /// the NetCallack with the result. 19 | pub trait NetHandler: Send + Sync + 'static { 20 | fn bytes(self: Box, doc_id: usize, bytes: Bytes, callback: SharedCallback); 21 | } 22 | 23 | /// A type which accepts the parsed result of a network request and sends it back to the Document 24 | /// (or does arbitrary things with it) 25 | pub trait NetCallback: Send + Sync + 'static { 26 | fn call(&self, doc_id: usize, result: Result>); 27 | } 28 | 29 | impl>) + Send + Sync + 'static> NetCallback for F { 30 | fn call(&self, doc_id: usize, result: Result>) { 31 | self(doc_id, result) 32 | } 33 | } 34 | 35 | #[non_exhaustive] 36 | #[derive(Debug)] 37 | /// A request type loosely representing https://fetch.spec.whatwg.org/#requests 38 | pub struct Request { 39 | pub url: Url, 40 | pub method: Method, 41 | pub headers: HeaderMap, 42 | pub body: Bytes, 43 | } 44 | impl Request { 45 | /// A get request to the specified Url and an empty body 46 | pub fn get(url: Url) -> Self { 47 | Self { 48 | url, 49 | method: Method::GET, 50 | headers: HeaderMap::new(), 51 | body: Bytes::new(), 52 | } 53 | } 54 | } 55 | 56 | /// A default noop NetProvider 57 | #[derive(Default)] 58 | pub struct DummyNetProvider; 59 | impl NetProvider for DummyNetProvider { 60 | fn fetch(&self, _doc_id: usize, _request: Request, _handler: BoxedHandler) {} 61 | } 62 | 63 | /// A default noop NetCallback 64 | #[derive(Default)] 65 | pub struct DummyNetCallback; 66 | impl NetCallback for DummyNetCallback { 67 | fn call(&self, _doc_id: usize, _result: Result>) {} 68 | } 69 | -------------------------------------------------------------------------------- /packages/blitz-traits/src/shell.rs: -------------------------------------------------------------------------------- 1 | use cursor_icon::CursorIcon; 2 | 3 | pub trait ShellProvider { 4 | fn request_redraw(&self) {} 5 | fn set_cursor(&self, icon: CursorIcon) { 6 | let _ = icon; 7 | } 8 | fn set_window_title(&self, title: String) { 9 | let _ = title; 10 | } 11 | } 12 | 13 | pub struct DummyShellProvider; 14 | impl ShellProvider for DummyShellProvider {} 15 | -------------------------------------------------------------------------------- /packages/blitz-traits/src/viewport.rs: -------------------------------------------------------------------------------- 1 | #[derive(Default, Debug, Clone, Copy)] 2 | pub enum ColorScheme { 3 | #[default] 4 | Light, 5 | Dark, 6 | } 7 | 8 | #[derive(Default, Debug, Clone)] 9 | pub struct Viewport { 10 | pub window_size: (u32, u32), 11 | 12 | hidpi_scale: f32, 13 | 14 | zoom: f32, 15 | 16 | pub font_size: f32, 17 | 18 | pub color_scheme: ColorScheme, 19 | } 20 | 21 | impl Viewport { 22 | pub fn new( 23 | physical_width: u32, 24 | physical_height: u32, 25 | scale_factor: f32, 26 | color_scheme: ColorScheme, 27 | ) -> Self { 28 | Self { 29 | window_size: (physical_width, physical_height), 30 | hidpi_scale: scale_factor, 31 | zoom: 1.0, 32 | font_size: 16.0, 33 | color_scheme, 34 | } 35 | } 36 | 37 | // Total scaling, the product of the zoom and hdpi scale 38 | pub fn scale(&self) -> f32 { 39 | self.hidpi_scale * self.zoom 40 | } 41 | // Total scaling, the product of the zoom and hdpi scale 42 | pub fn scale_f64(&self) -> f64 { 43 | self.scale() as f64 44 | } 45 | 46 | pub fn set_hidpi_scale(&mut self, scale: f32) { 47 | self.hidpi_scale = scale; 48 | } 49 | 50 | pub fn zoom(&self) -> f32 { 51 | self.zoom 52 | } 53 | 54 | pub fn set_zoom(&mut self, zoom: f32) { 55 | self.zoom = zoom; 56 | } 57 | 58 | pub fn zoom_by(&mut self, zoom: f32) { 59 | self.zoom += zoom; 60 | } 61 | 62 | pub fn zoom_mut(&mut self) -> &mut f32 { 63 | &mut self.zoom 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/blitz/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "blitz" 3 | description = "High-level APIs for rendering HTML with Blitz" 4 | version = "0.1.0-alpha.2" 5 | homepage = "https://github.com/dioxuslabs/blitz" 6 | repository = "https://github.com/dioxuslabs/blitz" 7 | documentation = "https://docs.rs/blitz" 8 | license.workspace = true 9 | edition = "2024" 10 | 11 | [features] 12 | default = ["net", "accessibility", "menu", "tracing"] 13 | net = ["dep:tokio", "dep:url", "dep:blitz-net"] 14 | accessibility = ["blitz-shell/accessibility"] 15 | menu = ["blitz-shell/menu"] 16 | tracing = ["blitz-shell/tracing"] 17 | 18 | [dependencies] 19 | # Blitz dependencies 20 | anyrender_vello = { version = "0.1", path = "../anyrender_vello" } 21 | blitz-html = { version = "0.1.0-alpha.2", path = "../blitz-html" } 22 | blitz-shell = { version = "0.1.0-alpha.2", path = "../blitz-shell" } 23 | blitz-net = { version = "0.1.0-alpha.2", path = "../blitz-net", optional = true } 24 | blitz-traits = { version = "0.1.0-alpha.2", path = "../blitz-traits" } 25 | 26 | # IO & Networking 27 | url = { workspace = true, features = ["serde"], optional = true } 28 | tokio = { workspace = true, features = ["rt-multi-thread"], optional = true } 29 | 30 | [package.metadata.docs.rs] 31 | all-features = true 32 | rustdoc-args = ["--cfg", "docsrs"] 33 | -------------------------------------------------------------------------------- /packages/blitz/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(docsrs, feature(doc_cfg))] 2 | 3 | //! A native renderer for Dioxus. 4 | //! 5 | //! ## Feature flags 6 | //! - `default`: Enables the features listed below. 7 | //! - `accessibility`: Enables [`accesskit`] accessibility support. 8 | //! - `hot-reload`: Enables hot-reloading of Dioxus RSX. 9 | //! - `menu`: Enables the [`muda`] menubar. 10 | //! - `tracing`: Enables tracing support. 11 | 12 | use std::sync::Arc; 13 | 14 | use anyrender_vello::VelloWindowRenderer; 15 | use blitz_html::HtmlDocument; 16 | use blitz_shell::{ 17 | BlitzApplication, BlitzShellEvent, BlitzShellNetCallback, Config, WindowConfig, 18 | create_default_event_loop, 19 | }; 20 | use blitz_traits::navigation::DummyNavigationProvider; 21 | 22 | #[cfg(feature = "net")] 23 | pub fn launch_url(url: &str) { 24 | // Assert that url is valid 25 | println!("{url}"); 26 | let url = url.to_owned(); 27 | url::Url::parse(&url).expect("Invalid url"); 28 | 29 | // Turn on the runtime and enter it 30 | let rt = tokio::runtime::Builder::new_multi_thread() 31 | .enable_all() 32 | .build() 33 | .unwrap(); 34 | let _guard = rt.enter(); 35 | 36 | let html = rt.block_on(blitz_net::get_text(&url)); 37 | 38 | launch_internal( 39 | &html, 40 | Config { 41 | stylesheets: Vec::new(), 42 | base_url: Some(url), 43 | }, 44 | ) 45 | } 46 | 47 | pub fn launch_static_html(html: &str) { 48 | launch_static_html_cfg(html, Config::default()) 49 | } 50 | 51 | pub fn launch_static_html_cfg(html: &str, cfg: Config) { 52 | // Turn on the runtime and enter it 53 | #[cfg(feature = "net")] 54 | let rt = tokio::runtime::Builder::new_multi_thread() 55 | .enable_all() 56 | .build() 57 | .unwrap(); 58 | #[cfg(feature = "net")] 59 | let _guard = rt.enter(); 60 | 61 | launch_internal(html, cfg) 62 | } 63 | 64 | fn launch_internal(html: &str, cfg: Config) { 65 | let event_loop = create_default_event_loop::(); 66 | 67 | #[cfg(feature = "net")] 68 | let net_provider = { 69 | let proxy = event_loop.create_proxy(); 70 | let callback = BlitzShellNetCallback::shared(proxy); 71 | blitz_net::Provider::shared(callback) 72 | }; 73 | #[cfg(not(feature = "net"))] 74 | let net_provider = { 75 | use blitz_traits::net::DummyNetProvider; 76 | Arc::new(DummyNetProvider::default()) 77 | }; 78 | 79 | let navigation_provider = Arc::new(DummyNavigationProvider); 80 | 81 | let doc = HtmlDocument::from_html( 82 | html, 83 | cfg.base_url, 84 | cfg.stylesheets, 85 | net_provider, 86 | None, 87 | navigation_provider, 88 | ); 89 | let window: WindowConfig = WindowConfig::new(Box::new(doc) as _); 90 | 91 | // Create application 92 | let mut application = BlitzApplication::new(event_loop.create_proxy()); 93 | application.add_window(window); 94 | 95 | // Run event loop 96 | event_loop.run_app(&mut application).unwrap() 97 | } 98 | -------------------------------------------------------------------------------- /packages/mini-dxn/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mini-dxn" 3 | description = "Mini Dioxus Native for testing Blitz" 4 | version = "0.0.0" 5 | homepage = "https://github.com/dioxuslabs/blitz" 6 | repository = "https://github.com/dioxuslabs/blitz" 7 | documentation = "https://docs.rs/mini-dxn" 8 | license.workspace = true 9 | edition = "2024" 10 | # This crate is only for internal use when developing Blitz. 11 | # The main dioxus-native crate lives in the Dioxus repo. 12 | publish = false 13 | 14 | [features] 15 | default = ["accessibility", "hot-reload", "menu", "tracing", "net", "svg", "gpu_backend"] 16 | svg = ["blitz-dom/svg", "blitz-paint/svg"] 17 | net = ["dep:tokio", "dep:blitz-net"] 18 | accessibility = ["blitz-shell/accessibility", "blitz-dom/accessibility"] 19 | autofocus = ["blitz-dom/autofocus"] 20 | menu = ["blitz-shell/menu"] 21 | tracing = ["dep:tracing", "blitz-shell/tracing", "blitz-dom/tracing"] 22 | hot-reload = ["dep:dioxus-cli-config", "dep:dioxus-devtools"] 23 | gpu_backend = ["dep:anyrender_vello"] 24 | cpu_backend = ["dep:anyrender_vello_cpu"] 25 | 26 | [dependencies] 27 | # Blitz dependencies 28 | anyrender_vello = { version = "0.1", path = "../anyrender_vello", default-features = false, optional = true } 29 | anyrender_vello_cpu = { version = "0.1", path = "../anyrender_vello_cpu", default-features = false, optional = true } 30 | blitz-paint = { version = "0.1.0-alpha.2", path = "../blitz-paint", default-features = false } 31 | blitz-dom = { version = "0.1.0-alpha.2", path = "../blitz-dom", default-features = false } 32 | blitz-net = { version = "0.1.0-alpha.2", path = "../blitz-net", optional = true } 33 | blitz-traits = { version = "0.1.0-alpha.2", path = "../blitz-traits" } 34 | blitz-shell = { version = "0.1.0-alpha.2", path = "../blitz-shell", default-features = false } 35 | 36 | # DioxusLabs dependencies 37 | dioxus-core = { workspace = true } 38 | dioxus-html = { workspace = true } 39 | dioxus-cli-config = { workspace = true, optional = true } 40 | dioxus-devtools = { workspace = true, optional = true } 41 | 42 | # Windowing & Input 43 | winit = { workspace = true } 44 | keyboard-types = { workspace = true } 45 | 46 | # IO & Networking 47 | tokio = { workspace = true, features = ["rt-multi-thread"], optional = true } 48 | 49 | # Other dependencies 50 | tracing = { workspace = true, optional = true } 51 | rustc-hash = { workspace = true } 52 | futures-util = { workspace = true } 53 | 54 | 55 | 56 | [package.metadata.docs.rs] 57 | all-features = true 58 | rustdoc-args = ["--cfg", "docsrs"] 59 | -------------------------------------------------------------------------------- /packages/mini-dxn/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(docsrs, feature(doc_cfg))] 2 | 3 | //! A native renderer for Dioxus. 4 | //! 5 | //! ## Feature flags 6 | //! - `default`: Enables the features listed below. 7 | //! - `accessibility`: Enables [`accesskit`] accessibility support. 8 | //! - `hot-reload`: Enables hot-reloading of Dioxus RSX. 9 | //! - `menu`: Enables the [`muda`] menubar. 10 | //! - `tracing`: Enables tracing support. 11 | 12 | mod dioxus_document; 13 | mod events; 14 | mod mutation_writer; 15 | 16 | #[cfg(feature = "gpu_backend")] 17 | use anyrender_vello::VelloWindowRenderer; 18 | #[cfg(feature = "cpu_backend")] 19 | use anyrender_vello_cpu::VelloCpuWindowRenderer as VelloWindowRenderer; 20 | 21 | pub use dioxus_document::DioxusDocument; 22 | pub use mutation_writer::MutationWriter; 23 | 24 | use blitz_dom::{Atom, QualName, ns}; 25 | use blitz_shell::{ 26 | BlitzApplication, BlitzShellEvent, Config, WindowConfig, create_default_event_loop, 27 | }; 28 | use dioxus_core::{ComponentFunction, Element, VirtualDom}; 29 | 30 | type NodeId = usize; 31 | 32 | /// Launch an interactive HTML/CSS renderer driven by the Dioxus virtualdom 33 | pub fn launch(root: fn() -> Element) { 34 | launch_cfg(root, Config::default()) 35 | } 36 | 37 | pub fn launch_cfg(root: fn() -> Element, cfg: Config) { 38 | launch_cfg_with_props(root, (), cfg) 39 | } 40 | 41 | // todo: props shouldn't have the clone bound - should try and match dioxus-desktop behavior 42 | pub fn launch_cfg_with_props( 43 | root: impl ComponentFunction, 44 | props: P, 45 | _cfg: Config, 46 | ) { 47 | let event_loop = create_default_event_loop::(); 48 | 49 | #[cfg(feature = "net")] 50 | // Turn on the runtime and enter it 51 | let rt = tokio::runtime::Builder::new_multi_thread() 52 | .enable_all() 53 | .build() 54 | .unwrap(); 55 | #[cfg(feature = "net")] 56 | let _guard = rt.enter(); 57 | 58 | #[cfg(feature = "net")] 59 | let net_provider = { 60 | use blitz_net::Provider; 61 | use blitz_shell::BlitzShellNetCallback; 62 | 63 | let proxy = event_loop.create_proxy(); 64 | let net_callback = BlitzShellNetCallback::shared(proxy); 65 | let net_provider = Provider::shared(net_callback); 66 | 67 | Some(net_provider) 68 | }; 69 | 70 | #[cfg(not(feature = "net"))] 71 | let net_provider = None; 72 | 73 | // Spin up the virtualdom 74 | // We're going to need to hit it with a special waker 75 | let vdom = VirtualDom::new_with_props(root, props); 76 | let doc = DioxusDocument::new(vdom, net_provider); 77 | let window = WindowConfig::new(Box::new(doc) as _); 78 | 79 | // Create application 80 | let mut application = BlitzApplication::::new(event_loop.create_proxy()); 81 | application.add_window(window); 82 | 83 | // Run event loop 84 | event_loop.run_app(&mut application).unwrap(); 85 | } 86 | 87 | pub(crate) fn qual_name(local_name: &str, namespace: Option<&str>) -> QualName { 88 | QualName { 89 | prefix: None, 90 | ns: namespace.map(Atom::from).unwrap_or(ns!(html)), 91 | local: Atom::from(local_name), 92 | } 93 | } 94 | 95 | // Syntax sugar to make tracing calls less noisy in function below 96 | macro_rules! trace { 97 | ($pattern:literal) => {{ 98 | #[cfg(feature = "tracing")] 99 | tracing::info!($pattern); 100 | }}; 101 | ($pattern:literal, $item1:expr) => {{ 102 | #[cfg(feature = "tracing")] 103 | tracing::info!($pattern, $item1); 104 | }}; 105 | ($pattern:literal, $item1:expr, $item2:expr) => {{ 106 | #[cfg(feature = "tracing")] 107 | tracing::info!($pattern, $item1, $item2); 108 | }}; 109 | ($pattern:literal, $item1:expr, $item2:expr, $item3:expr) => {{ 110 | #[cfg(feature = "tracing")] 111 | tracing::info!($pattern, $item1, $item2); 112 | }}; 113 | ($pattern:literal, $item1:expr, $item2:expr, $item3:expr, $item4:expr) => {{ 114 | #[cfg(feature = "tracing")] 115 | tracing::info!($pattern, $item1, $item2, $item3, $item4); 116 | }}; 117 | } 118 | pub(crate) use trace; 119 | -------------------------------------------------------------------------------- /packages/stylo_taffy/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stylo_taffy" 3 | version = "0.1.0-alpha.2" 4 | license = "MIT OR Apache-2.0 OR MPL-2.0" 5 | description = "Interop crate for the stylo and taffy crates" 6 | keywords = ["css", "layout"] 7 | categories = ["gui"] 8 | edition = "2024" 9 | repository = "https://github.com/dioxuslabs/blitz" 10 | 11 | [dependencies] 12 | taffy = { workspace = true } 13 | style = { workspace = true } 14 | 15 | [features] 16 | default = ["std", "block", "flexbox", "grid"] 17 | std = ["taffy/std"] 18 | block = ["taffy/block_layout"] 19 | flexbox = ["taffy/flexbox"] 20 | grid = ["taffy/grid"] 21 | -------------------------------------------------------------------------------- /packages/stylo_taffy/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Conversion functions from Stylo types to Taffy types 2 | 3 | mod wrapper; 4 | pub use wrapper::TaffyStyloStyle; 5 | 6 | pub mod convert; 7 | pub use convert::to_taffy_style; 8 | -------------------------------------------------------------------------------- /tests/renders_boxes.rs: -------------------------------------------------------------------------------- 1 | // https://www.w3schools.com/css/tryit.asp?filename=trycss_outline-style 2 | -------------------------------------------------------------------------------- /wpt/runner/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wpt" 3 | version = "0.1.0" 4 | edition = "2024" 5 | license.workspace = true 6 | 7 | [features] 8 | default = ["gpu"] 9 | gpu = ["dep:anyrender_vello"] 10 | cpu = ["dep:anyrender_vello_cpu"] 11 | 12 | [dependencies] 13 | blitz-dom = { path = "../../packages/blitz-dom", default-features = false, features = ["svg", "woff-c"] } 14 | blitz-html = { path = "../../packages/blitz-html" } 15 | blitz-traits = { path = "../../packages/blitz-traits" } 16 | blitz-paint = { path = "../../packages/blitz-paint" } 17 | anyrender_vello = { path = "../../packages/anyrender_vello", optional = true } 18 | anyrender_vello_cpu = { path = "../../packages/anyrender_vello_cpu", optional = true } 19 | anyrender = { path = "../../packages/anyrender" } 20 | taffy = { workspace = true } 21 | parley = { workspace = true } 22 | image = { workspace = true } 23 | url = { workspace = true } 24 | data-url = { workspace = true } 25 | png = { version = "0.17" } 26 | glob = { version = "0.3.1" } 27 | dify = { version = "0.7.4", default-features = false } 28 | env_logger = { version = "0.11.5" } 29 | owo-colors = "4.1.0" 30 | log = "0.4.22" 31 | regex = "1.11.1" 32 | rayon = "1.10.0" 33 | thread_local = "1.1.8" 34 | bitflags = "2.6.0" 35 | pollster = "0.4.0" 36 | atomic_float = "1" 37 | supports-hyperlinks = "3.1.0" 38 | terminal-link = "0.1.0" 39 | wptreport = { version = "0.0.5", default-features = false } 40 | os_info = "3.10.0" 41 | serde_json = "1.0.140" 42 | -------------------------------------------------------------------------------- /wpt/runner/src/panic_backtrace.rs: -------------------------------------------------------------------------------- 1 | use std::backtrace::{Backtrace, BacktraceStatus}; 2 | use std::{cell::Cell, panic::PanicHookInfo}; 3 | 4 | thread_local! { 5 | static STASHED_PANIC_INFO: Cell> = const { Cell::new(None) }; 6 | } 7 | 8 | pub fn take_stashed_panic_info() -> Option { 9 | STASHED_PANIC_INFO.take() 10 | } 11 | 12 | pub struct StashedPanicInfo { 13 | pub message: Option, 14 | pub file: String, 15 | pub line: u32, 16 | pub column: u32, 17 | pub backtrace: Backtrace, 18 | } 19 | 20 | pub fn stash_panic_handler(info: &PanicHookInfo) { 21 | let backtrace = Backtrace::force_capture(); 22 | let payload = info.payload(); 23 | let location = info.location().unwrap(); 24 | 25 | let str_msg = payload.downcast_ref::<&str>().map(|s| s.to_string()); 26 | let string_msg = payload.downcast_ref::().map(|s| s.to_owned()); 27 | let message = str_msg.or(string_msg); 28 | 29 | let info = StashedPanicInfo { 30 | message, 31 | backtrace, 32 | file: location.file().to_owned(), 33 | line: location.line(), 34 | column: location.column(), 35 | }; 36 | 37 | STASHED_PANIC_INFO.with(move |b| b.set(Some(info))); 38 | } 39 | 40 | #[inline(never)] 41 | pub fn backtrace_cutoff R>(cb: T) -> R { 42 | cb() 43 | } 44 | 45 | pub fn trim_backtrace(backtrace: &Backtrace) -> Option { 46 | if backtrace.status() != BacktraceStatus::Captured { 47 | return None; 48 | } 49 | 50 | let string_backtrace = backtrace.to_string(); 51 | let mut filtered = String::with_capacity(string_backtrace.len()); 52 | let mut started = false; 53 | 54 | for line in string_backtrace.lines() { 55 | if line.contains("wpt::panic_backtrace::backtrace_cutoff") { 56 | break; 57 | } 58 | 59 | if started { 60 | filtered.push_str(line); 61 | filtered.push('\n'); 62 | } 63 | 64 | if line.contains("core::panicking::panic") { 65 | started = true; 66 | } 67 | } 68 | 69 | Some(filtered) 70 | } 71 | -------------------------------------------------------------------------------- /wpt/runner/src/report.rs: -------------------------------------------------------------------------------- 1 | //! Code related to writing a report in "WPT Report" format 2 | 3 | use std::{path::Path, process::Command}; 4 | use wptreport::{ 5 | reports::wpt_report::{self, WptRunInfo}, 6 | wpt_report::WptReport, 7 | }; 8 | 9 | use crate::{TestResult, TestStatus}; 10 | 11 | fn get_git_hash(path: &Path) -> String { 12 | let output = Command::new("git") 13 | .current_dir(path) 14 | .args(["rev-parse", "HEAD"]) 15 | .output() 16 | .expect("Failed to run git rev-parse HEAD"); 17 | if !output.status.success() { 18 | panic!("Failed to run git rev-parse HEAD (command failed)") 19 | } 20 | let hash = String::from_utf8(output.stdout) 21 | .expect("Failed to run git rev-parse HEAD (non-utf8 output)"); 22 | // Remove trailing newline 23 | hash.trim().to_string() 24 | } 25 | 26 | pub fn generate_run_info(wpt_dir: &Path) -> WptRunInfo { 27 | let os_info = os_info::get(); 28 | 29 | WptRunInfo { 30 | product: String::from("blitz"), 31 | revision: get_git_hash(wpt_dir), 32 | browser_version: Some(get_git_hash(&std::env::current_dir().unwrap())), 33 | automation: true, 34 | debug: cfg!(debug_assertions), 35 | display: None, 36 | has_sandbox: false, 37 | headless: true, 38 | verify: false, 39 | wasm: false, 40 | os: String::new(), 41 | os_version: String::new(), 42 | version: String::new(), 43 | processor: String::new(), 44 | bits: match os_info.bitness() { 45 | os_info::Bitness::X32 => 32, 46 | os_info::Bitness::X64 => 64, 47 | os_info::Bitness::Unknown | _ => 0, 48 | }, 49 | python_version: 0, 50 | apple_catalina: false, 51 | apple_silicon: false, 52 | win10_2004: false, 53 | win10_2009: false, 54 | win11_2009: false, 55 | } 56 | } 57 | 58 | fn convert_status(status: TestStatus) -> wpt_report::TestStatus { 59 | match status { 60 | TestStatus::Pass => wpt_report::TestStatus::Pass, 61 | TestStatus::Fail => wpt_report::TestStatus::Fail, 62 | TestStatus::Skip => wpt_report::TestStatus::Skip, 63 | TestStatus::Crash => wpt_report::TestStatus::Crash, 64 | } 65 | } 66 | 67 | fn convert_subtest_status(status: TestStatus) -> wpt_report::SubtestStatus { 68 | match status { 69 | TestStatus::Pass => wpt_report::SubtestStatus::Pass, 70 | TestStatus::Fail => wpt_report::SubtestStatus::Fail, 71 | TestStatus::Skip => wpt_report::SubtestStatus::Skip, 72 | TestStatus::Crash => unreachable!(), 73 | } 74 | } 75 | 76 | pub fn generate_report( 77 | wpt_dir: &Path, 78 | results: Vec, 79 | time_start: u64, 80 | time_end: u64, 81 | ) -> WptReport { 82 | let results: Vec<_> = results 83 | .into_iter() 84 | .map(|test| wpt_report::TestResult { 85 | test: test.name, 86 | status: convert_status(test.status), 87 | duration: test.duration.as_millis() as i64, 88 | message: test.panic_info.and_then(|info| info.message), 89 | known_intermittent: Vec::new(), 90 | subsuite: String::new(), 91 | subtests: test 92 | .subtest_results 93 | .into_iter() 94 | .map(|subtest| wpt_report::SubtestResult { 95 | name: subtest.name, 96 | status: convert_subtest_status(subtest.status), 97 | message: if subtest.errors.is_empty() { 98 | None 99 | } else { 100 | Some(subtest.errors.join("\n")) 101 | }, 102 | known_intermittent: Vec::new(), 103 | }) 104 | .collect(), 105 | }) 106 | .collect(); 107 | 108 | WptReport { 109 | time_start, 110 | time_end, 111 | run_info: generate_run_info(wpt_dir), 112 | results, 113 | } 114 | } 115 | 116 | pub fn generate_expectations(results: &[TestResult]) -> String { 117 | let mut out = String::with_capacity(10 * 1024 * 1024); // 10MB 118 | 119 | for test in results { 120 | out.push_str(&test.name); 121 | out.push(' '); 122 | out.push_str(test.status.as_str()); 123 | out.push(' '); 124 | 125 | for subtest in &test.subtest_results { 126 | let c = match subtest.status { 127 | TestStatus::Pass => 'Y', 128 | TestStatus::Fail => 'N', 129 | TestStatus::Skip => '.', 130 | TestStatus::Crash => unreachable!(), 131 | }; 132 | out.push(c); 133 | } 134 | 135 | out.push('\n'); 136 | } 137 | 138 | out 139 | } 140 | -------------------------------------------------------------------------------- /wpt/runner/src/test_runners/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, sync::Arc, time::Instant}; 2 | 3 | use blitz_dom::{BaseDocument, net::Resource}; 4 | use blitz_html::HtmlDocument; 5 | use blitz_traits::net::SharedProvider; 6 | use log::info; 7 | 8 | use crate::{SubtestCounts, TestFlags, TestKind, TestStatus, ThreadCtx}; 9 | 10 | mod attr_test; 11 | mod ref_test; 12 | 13 | pub use attr_test::process_attr_test; 14 | pub use ref_test::process_ref_test; 15 | 16 | pub struct SubtestResult { 17 | pub name: String, 18 | pub status: TestStatus, 19 | pub errors: Vec, 20 | } 21 | 22 | #[allow(clippy::too_many_arguments)] 23 | pub fn process_test_file( 24 | ctx: &mut ThreadCtx, 25 | relative_path: &str, 26 | ) -> (TestKind, TestFlags, SubtestCounts, Vec) { 27 | info!("Processing test file: {relative_path}"); 28 | 29 | let file_contents = fs::read_to_string(ctx.wpt_dir.join(relative_path)).unwrap(); 30 | 31 | // Compute flags 32 | let mut flags = TestFlags::empty(); 33 | if ctx.float_re.is_match(&file_contents) { 34 | flags |= TestFlags::USES_FLOAT; 35 | } 36 | if ctx.intrinsic_re.is_match(&file_contents) { 37 | flags |= TestFlags::USES_INTRINSIC_SIZE; 38 | } 39 | if ctx.calc_re.is_match(&file_contents) { 40 | flags |= TestFlags::USES_CALC; 41 | } 42 | if ctx.direction_re.is_match(&file_contents) { 43 | flags |= TestFlags::USES_DIRECTION; 44 | } 45 | if ctx.writing_mode_re.is_match(&file_contents) { 46 | flags |= TestFlags::USES_WRITING_MODE; 47 | } 48 | if ctx.subgrid_re.is_match(&file_contents) { 49 | flags |= TestFlags::USES_SUBGRID; 50 | } 51 | if ctx.masonry_re.is_match(&file_contents) { 52 | flags |= TestFlags::USES_MASONRY; 53 | } 54 | if ctx.script_re.is_match(&file_contents) { 55 | flags |= TestFlags::USES_SCRIPT; 56 | } 57 | 58 | // Ref Test 59 | let reference = ctx 60 | .reftest_re 61 | .captures(&file_contents) 62 | .and_then(|captures| captures.get(1).map(|href| href.as_str().to_string())); 63 | if let Some(reference) = reference { 64 | let counts = process_ref_test( 65 | ctx, 66 | relative_path, 67 | file_contents.as_str(), 68 | reference.as_str(), 69 | &mut flags, 70 | ); 71 | 72 | return (TestKind::Ref, flags, counts, Vec::new()); 73 | } 74 | 75 | // Attr Test 76 | let mut matches = ctx.attrtest_re.captures_iter(&file_contents); 77 | let first = matches.next(); 78 | let second = matches.next(); 79 | if first.is_some() && second.is_none() { 80 | // TODO: handle tests with multiple calls to checkLayout. 81 | let captures = first.unwrap(); 82 | let selector = captures.get(1).unwrap().as_str().to_string(); 83 | drop(matches); 84 | 85 | println!("{selector}"); 86 | 87 | let (counts, results) = process_attr_test(ctx, &selector, &file_contents, relative_path); 88 | 89 | return (TestKind::Attr, flags, counts, results); 90 | } 91 | 92 | // TODO: Handle other test formats. 93 | ( 94 | TestKind::Unknown, 95 | flags, 96 | SubtestCounts::ZERO_OF_ZERO, 97 | Vec::new(), 98 | ) 99 | } 100 | 101 | fn parse_and_resolve_document( 102 | ctx: &mut ThreadCtx, 103 | html: &str, 104 | relative_path: &str, 105 | ) -> BaseDocument { 106 | ctx.net_provider.reset(); 107 | let mut document = HtmlDocument::from_html( 108 | html, 109 | Some(ctx.dummy_base_url.join(relative_path).unwrap().to_string()), 110 | Vec::new(), 111 | Arc::clone(&ctx.net_provider) as SharedProvider, 112 | Some(ctx.font_ctx.clone()), 113 | ctx.navigation_provider.clone(), 114 | ); 115 | 116 | document.as_mut().set_viewport(ctx.viewport.clone()); 117 | document.as_mut().resolve(); 118 | 119 | // Load resources. 120 | // Loop because loading a resource may result in further resources being requested 121 | let start = Instant::now(); 122 | while ctx.net_provider.pending_item_count() > 0 { 123 | ctx.net_provider 124 | .for_each(|res| document.as_mut().load_resource(res)); 125 | document.as_mut().resolve(); 126 | if Instant::now().duration_since(start).as_millis() > 500 { 127 | ctx.net_provider.log_pending_items(); 128 | panic!( 129 | "Timeout. {} pending items.", 130 | ctx.net_provider.pending_item_count() 131 | ); 132 | } 133 | } 134 | 135 | ctx.net_provider 136 | .for_each(|res| document.as_mut().load_resource(res)); 137 | document.as_mut().resolve(); 138 | 139 | document.into() 140 | } 141 | -------------------------------------------------------------------------------- /wpt/runner/src/test_runners/ref_test.rs: -------------------------------------------------------------------------------- 1 | use anyrender::ImageRenderer as _; 2 | use blitz_paint::paint_scene; 3 | use image::{ImageBuffer, ImageFormat}; 4 | use std::fs; 5 | use std::fs::File; 6 | use std::io::Write; 7 | use std::path::Path; 8 | use url::Url; 9 | 10 | use super::parse_and_resolve_document; 11 | use crate::{BufferKind, HEIGHT, SCALE, SubtestCounts, TestFlags, ThreadCtx, WIDTH}; 12 | 13 | #[allow(clippy::too_many_arguments)] 14 | pub fn process_ref_test( 15 | ctx: &mut ThreadCtx, 16 | test_relative_path: &str, 17 | test_html: &str, 18 | ref_file: &str, 19 | flags: &mut TestFlags, 20 | ) -> SubtestCounts { 21 | let ref_url: Url = ctx 22 | .dummy_base_url 23 | .join(test_relative_path) 24 | .unwrap() 25 | .join(ref_file) 26 | .unwrap(); 27 | let ref_relative_path = ref_url.path().strip_prefix('/').unwrap().to_string(); 28 | let ref_path = ctx.wpt_dir.join(&ref_relative_path); 29 | let ref_html = fs::read_to_string(ref_path).expect("Ref file not found."); 30 | 31 | if ctx.float_re.is_match(&ref_html) { 32 | *flags |= TestFlags::USES_FLOAT; 33 | } 34 | if ctx.intrinsic_re.is_match(&ref_html) { 35 | *flags |= TestFlags::USES_INTRINSIC_SIZE; 36 | } 37 | if ctx.calc_re.is_match(&ref_html) { 38 | *flags |= TestFlags::USES_CALC; 39 | } 40 | if ctx.direction_re.is_match(&ref_html) { 41 | *flags |= TestFlags::USES_DIRECTION; 42 | } 43 | if ctx.writing_mode_re.is_match(&ref_html) { 44 | *flags |= TestFlags::USES_WRITING_MODE; 45 | } 46 | if ctx.subgrid_re.is_match(&ref_html) { 47 | *flags |= TestFlags::USES_SUBGRID; 48 | } 49 | if ctx.masonry_re.is_match(&ref_html) { 50 | *flags |= TestFlags::USES_MASONRY; 51 | } 52 | 53 | let test_out_path = ctx 54 | .out_dir 55 | .join(format!("{}{}", test_relative_path, "-test.png")); 56 | render_html_to_buffer( 57 | ctx, 58 | BufferKind::Test, 59 | test_relative_path, 60 | &test_out_path, 61 | test_html, 62 | ); 63 | 64 | let ref_out_path = ctx 65 | .out_dir 66 | .join(format!("{}{}", test_relative_path, "-ref.png")); 67 | render_html_to_buffer( 68 | ctx, 69 | BufferKind::Ref, 70 | &ref_relative_path, 71 | &ref_out_path, 72 | &ref_html, 73 | ); 74 | 75 | if ctx.buffers.test_buffer == ctx.buffers.ref_buffer { 76 | return SubtestCounts::ONE_OF_ONE; 77 | } 78 | 79 | let test_image = ImageBuffer::from_raw(WIDTH, HEIGHT, ctx.buffers.test_buffer.clone()).unwrap(); 80 | let ref_image = ImageBuffer::from_raw(WIDTH, HEIGHT, ctx.buffers.ref_buffer.clone()).unwrap(); 81 | 82 | let diff = dify::diff::get_results(test_image, ref_image, 0.1f32, true, None, &None, &None); 83 | 84 | if let Some(diff) = diff { 85 | let path = ctx 86 | .out_dir 87 | .join(format!("{}{}", test_relative_path, "-diff.png")); 88 | let parent = path.parent().unwrap(); 89 | fs::create_dir_all(parent).unwrap(); 90 | diff.1.save_with_format(path, ImageFormat::Png).unwrap(); 91 | SubtestCounts::ZERO_OF_ONE 92 | } else { 93 | SubtestCounts::ONE_OF_ONE 94 | } 95 | } 96 | 97 | fn render_html_to_buffer( 98 | ctx: &mut ThreadCtx, 99 | buffer_kind: BufferKind, 100 | relative_path: &str, 101 | out_path: &Path, 102 | html: &str, 103 | ) { 104 | let document = parse_and_resolve_document(ctx, html, relative_path); 105 | 106 | // Determine height to render 107 | // let computed_height = document.as_ref().root_element().final_layout.size.height; 108 | // let render_height = (computed_height as u32).clamp(HEIGHT, 4000); 109 | let render_height = HEIGHT; 110 | 111 | // Render document to RGBA buffer 112 | let buf = ctx.buffers.get_mut(buffer_kind); 113 | ctx.renderer.render( 114 | |scene| paint_scene(scene, document.as_ref(), SCALE, WIDTH, HEIGHT), 115 | buf, 116 | ); 117 | 118 | fs::create_dir_all(out_path.parent().unwrap()).unwrap(); 119 | let mut file = File::create(out_path).unwrap(); 120 | write_png(&mut file, buf, WIDTH, render_height); 121 | } 122 | 123 | // Copied from screenshot.rs 124 | fn write_png(writer: W, buffer: &[u8], width: u32, height: u32) { 125 | // Set pixels-per-meter. TODO: make configurable. 126 | const PPM: u32 = (144.0 * 39.3701) as u32; 127 | 128 | // Create PNG encoder 129 | let mut encoder = png::Encoder::new(writer, width, height); 130 | encoder.set_color(png::ColorType::Rgba); 131 | encoder.set_depth(png::BitDepth::Eight); 132 | encoder.set_pixel_dims(Some(png::PixelDimensions { 133 | xppu: PPM, 134 | yppu: PPM, 135 | unit: png::Unit::Meter, 136 | })); 137 | 138 | // Write PNG data to writer 139 | let mut writer = encoder.write_header().unwrap(); 140 | writer.write_image_data(buffer).unwrap(); 141 | writer.finish().unwrap(); 142 | } 143 | --------------------------------------------------------------------------------