feat: inline dioxus-native from blitz (#3675)
* Rename dioxus-blitz to dioxus-native * Add CSS import support (#139) * Remove data dependencie from NetProvider + global provider. * Feat CSS import * Move net handler code out of util * Fixing test... * Use the better suited OnceLock instead of LazyLock * Removing static and move resource callback back out of the document. Signed-off-by: koko <kokos.kekse@gmail.com> * Make things work Signed-off-by: koko <kokos.kekse@gmail.com> * Fix the servo example Signed-off-by: koko <kokos.kekse@gmail.com> * Replace servo image with a more compact version Signed-off-by: koko <kokos.kekse@gmail.com> * revert: 8467b271 Signed-off-by: koko <kokos.kekse@gmail.com> --------- Signed-off-by: koko <kokos.kekse@gmail.com> * Create readme app * WPT: reuse FontContext to avoid reloading system fonts Takes runtime for css-flexbox from 12s -> 8s * Move Devtools to traits crate * Suppress unused variable warning * Implement net for DioxusDocument Signed-off-by: Nico Burns <nico@nicoburns.com> * Eliminate BlitzWindowEvent type Signed-off-by: Nico Burns <nico@nicoburns.com> * Make net crate use document id instead of window id Signed-off-by: Nico Burns <nico@nicoburns.com> * Rename and document net traits Signed-off-by: Nico Burns <nico@nicoburns.com> * Split `dioxus-native` crate into `dioxus-native` and `blitz-shell` crates (#163) * Rename waker module to event Signed-off-by: Nico Burns <nico@nicoburns.com> * Create seperate DioxusNativeApplication Signed-off-by: Nico Burns <nico@nicoburns.com> * Rename dioxus-native crate to blitz-shell Signed-off-by: Nico Burns <nico@nicoburns.com> * Split blitz-shell out of dioxus-native crate Signed-off-by: Nico Burns <nico@nicoburns.com> * Remove need for dioxus-native to depend directly on stylo --------- Signed-off-by: Nico Burns <nico@nicoburns.com> * Unnest dioxus-native modules Signed-off-by: Nico Burns <nico@nicoburns.com> * Split MutationWriter into it's own module Signed-off-by: Nico Burns <nico@nicoburns.com> * Split HtmlDocument into it's own crate Signed-off-by: Nico Burns <nico@nicoburns.com> * Rename dom field of Window to doc Signed-off-by: Nico Burns <nico@nicoburns.com> * Support prefers-color-scheme (dark mode) * Remove use of the `todo` macro in event handling and add a comment about it being a todo (#168) Using the `todo` macro causes a panic when this event is encountered. Noop is preferable in this case as it does not crash the application. This is especially annoying on platforms where `Ime::Enabled` event is sent upon application start even when the user is using a keyboard layout that does not need an IME support. There was added a "TODO" comment that informs about this feature being missing. Most IDEs and code editors have support/plugins/extensions to work with this type of comments and this type of comment is already used widely across the codebase. As such, the information about this feature not being implemented is not lost. * Upgrade to dioxus 0.6-rc.0 Signed-off-by: Nico Burns <nico@nicoburns.com> * Fix text input in dioxus-native Signed-off-by: Nico Burns <nico@nicoburns.com> * Make dioxus-native depend dioxus-core and dioxus-html rather than dioxus * Remove "full" feature from tokio * Move Viewport to blitz_traits Signed-off-by: Nico Burns <nico@nicoburns.com> * Make tokio optional in dioxus-native and blitz-shell Signed-off-by: Nico Burns <nico@nicoburns.com> * Make SVG support optional * Abstract renderer behind a trait blitz-shell no longer depends on blitz-renderer-vello Signed-off-by: Nico Burns <nico@nicoburns.com> * Split HTML launch functions out in `blitz` crate * feat: Add mousedown and mouseup event (#172) * feat: Add mousedown and mouseup event * feat: Triggering :active pseudo-class * fix: Triggering :active pseudo-class * Rename RendererEvent to DomEvent Signed-off-by: Nico Burns <nico@nicoburns.com> * Rename Document to BaseDocument. DocumentLike to Document Signed-off-by: Nico Burns <nico@nicoburns.com> * Move event and document abstractions to blitz-traits Signed-off-by: Nico Burns <nico@nicoburns.com> * Rename BlitzEvent to BlitzShellEvent Signed-off-by: Nico Burns <nico@nicoburns.com> * Restyle when style attribute is mutated * feat: Support text box mouse selection (#182) * Rename raw_dom_data to data Signed-off-by: Nico Burns <nico@nicoburns.com> * Rename dioxus-native to native * Add to workspace * simplify launch module, integrate native * Fix it up again * Update cargo lock * FIx: propagate accessibility feature fla * implement asset resolver * add native to feature match in cli * Implement document and history for native * use no-op eval * clippy * typos * typo * Convert logs to trace * Bump blitz to alpha releases * unset image data * fix style integration with blitz * fix a few lints * wire up ids * implement a few more events * add more helpful todo note * bump ci to allow edition 2024, use newer nightly * make clippy happy * also update matrix tests to rust 2024 * Add autofocus Co-authored-by: otgerrogla 2356820+otgerrogla@users.noreply.github.com * Wait for the suspense boundary above the router to resolve before sending the first streaming chunk (#3891) * Change the suspense streaming context after suspense is resolved in the router * Wait for the suspense context before streaming the first chunk in ssr * forward more response parts * warn * extract the hydration format into a crate * Fix hydration when no data is sent * forward web and server features * Fix suspense that starts on the client * fix use_server_future on the client * Add documentation for fullstack hooks and protocol * downgrade rust edition * fix clippy * fix tests * fix clippy * Fix pure web compile * undo changes to the fullstack router example * fix formatting * hoist a bunch of deps --------- Signed-off-by: koko <kokos.kekse@gmail.com> Signed-off-by: Nico Burns <nico@nicoburns.com> Co-authored-by: Nico Burns <nico@nicoburns.com> Co-authored-by: koko <72217393+kokoISnoTarget@users.noreply.github.com> Co-authored-by: David Husička <25853767+DavidHusicka@users.noreply.github.com> Co-authored-by: luoxiaozero <48741584+luoxiaozero@users.noreply.github.com> Co-authored-by: Evan Almloff <evanalmloff@gmail.com>
This commit is contained in:
1 parent
84221f55a0
commit
12c6938346
38 files changed
+6156
-2087
No files matched your search
+13
-13
@@ -39,7 +39,7 @@ env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0 # todo(jon): cargo-cache wipes incremental artifacts, but we eventually want to cache them
|
||||
RUST_BACKTRACE: 1
|
||||
rust_nightly: nightly-2024-10-20
|
||||
rust_nightly: nightly-2025-04-07
|
||||
|
||||
jobs:
|
||||
check-msrv:
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@1.84.0
|
||||
- uses: dtolnay/rust-toolchain@1.86.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-all-crates: "true"
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
with:
|
||||
packages: libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev libxdo-dev
|
||||
version: 1.0
|
||||
- uses: dtolnay/rust-toolchain@1.84.0
|
||||
- uses: dtolnay/rust-toolchain@1.86.0
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
with:
|
||||
packages: libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev libxdo-dev
|
||||
version: 1.0
|
||||
- uses: dtolnay/rust-toolchain@1.84.0
|
||||
- uses: dtolnay/rust-toolchain@1.86.0
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@1.84.0
|
||||
- uses: dtolnay/rust-toolchain@1.86.0
|
||||
with:
|
||||
components: rustfmt
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
@@ -152,7 +152,7 @@ jobs:
|
||||
with:
|
||||
packages: libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev libxdo-dev
|
||||
version: 1.0
|
||||
- uses: dtolnay/rust-toolchain@1.84.0
|
||||
- uses: dtolnay/rust-toolchain@1.86.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-all-crates: "true"
|
||||
@@ -168,7 +168,7 @@ jobs:
|
||||
with:
|
||||
packages: libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev libxdo-dev
|
||||
version: 1.0
|
||||
- uses: dtolnay/rust-toolchain@1.84.0
|
||||
- uses: dtolnay/rust-toolchain@1.86.0
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
@@ -237,7 +237,7 @@ jobs:
|
||||
- {
|
||||
target: aarch64-apple-darwin,
|
||||
os: macos-latest,
|
||||
toolchain: "1.84.0",
|
||||
toolchain: "1.86.0",
|
||||
cross: false,
|
||||
command: "test",
|
||||
args: "--all --tests",
|
||||
@@ -245,7 +245,7 @@ jobs:
|
||||
- {
|
||||
target: x86_64-apple-darwin,
|
||||
os: macos-13,
|
||||
toolchain: "1.84.0",
|
||||
toolchain: "1.86.0",
|
||||
cross: false,
|
||||
command: "test",
|
||||
args: "--all --tests",
|
||||
@@ -253,7 +253,7 @@ jobs:
|
||||
- {
|
||||
target: aarch64-apple-ios,
|
||||
os: macos-latest,
|
||||
toolchain: "1.84.0",
|
||||
toolchain: "1.86.0",
|
||||
cross: false,
|
||||
command: "build",
|
||||
args: "--package dioxus-mobile",
|
||||
@@ -261,7 +261,7 @@ jobs:
|
||||
- {
|
||||
target: aarch64-linux-android,
|
||||
os: ubuntu-latest,
|
||||
toolchain: "1.84.0",
|
||||
toolchain: "1.86.0",
|
||||
cross: true,
|
||||
command: "build",
|
||||
args: "--package dioxus-mobile",
|
||||
@@ -302,7 +302,7 @@ jobs:
|
||||
name: "cargo test | windows"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@1.84.0
|
||||
- uses: dtolnay/rust-toolchain@1.86.0
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
@@ -335,7 +335,7 @@ jobs:
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - uses: dtolnay/rust-toolchain@1.84.0
|
||||
# - uses: dtolnay/rust-toolchain@1.86.0
|
||||
# - uses: Swatinem/rust-cache@v2
|
||||
# with:
|
||||
# cache-all-crates: "true"
|
||||
|
||||
Generated
+3181
-1729
File diff suppressed because it is too large.
Load diff
+17
@@ -68,6 +68,15 @@ members = [
|
||||
"packages/dx-wire-format",
|
||||
"packages/logger",
|
||||
"packages/config-macros",
|
||||
"packages/native",
|
||||
"packages/asset-resolver",
|
||||
|
||||
# Playwright tests
|
||||
"packages/playwright-tests/liveview",
|
||||
"packages/playwright-tests/web",
|
||||
"packages/playwright-tests/fullstack",
|
||||
"packages/playwright-tests/suspense-carousel",
|
||||
"packages/playwright-tests/nested-suspense",
|
||||
|
||||
# manganis
|
||||
"packages/manganis/manganis",
|
||||
@@ -150,6 +159,8 @@ dioxus-fullstack-protocol = { path = "packages/fullstack-protocol", version = "0
|
||||
dioxus_server_macro = { path = "packages/server-macro", version = "0.6.2", default-features = false }
|
||||
dioxus-dx-wire-format = { path = "packages/dx-wire-format", version = "0.6.2" }
|
||||
dioxus-logger = { path = "packages/logger", version = "0.6.2" }
|
||||
dioxus-native = { path = "packages/native", version = "0.6.2" }
|
||||
dioxus-asset-resolver = { path = "packages/asset-resolver", version = "0.6.2" }
|
||||
dioxus-config-macros = { path = "packages/config-macros", version = "0.6.3" }
|
||||
const-serialize = { path = "packages/const-serialize", version = "0.6.2" }
|
||||
const-serialize-macro = { path = "packages/const-serialize-macro", version = "0.6.2" }
|
||||
@@ -253,6 +264,8 @@ rayon = "1.2.0"
|
||||
wasmparser = "0.225.0"
|
||||
itertools = "0.14.0"
|
||||
macro-string = "0.1.3"
|
||||
walkdir = "2.5.0"
|
||||
url = "2.3.1"
|
||||
|
||||
# desktop
|
||||
wry = { version = "0.50.1", default-features = false }
|
||||
@@ -270,6 +283,10 @@ objc = { version = "0.2.7", features = ["exception"] }
|
||||
objc_id = "0.1.1"
|
||||
tray-icon = "0.19"
|
||||
|
||||
# native
|
||||
keyboard-types = "0.7"
|
||||
winit = { version = "0.30.2", features = ["rwh_06"] }
|
||||
|
||||
# disable debug symbols in dev builds - shouldn't matter for downstream crates but makes our binaries (examples, cli, etc) build faster
|
||||
[profile.dev]
|
||||
debug = 0
|
||||
|
||||
@@ -36,7 +36,7 @@ fn app() -> Element {
|
||||
main {
|
||||
for (dir_id, path) in files.read().path_names.iter().enumerate() {
|
||||
{
|
||||
let path_end = path.components().last().map(|p|p.as_os_str()).unwrap_or(path.as_os_str()).to_string_lossy();
|
||||
let path_end = path.components().next_back().map(|p|p.as_os_str()).unwrap_or(path.as_os_str()).to_string_lossy();
|
||||
let path = path.display();
|
||||
rsx! {
|
||||
div { class: "folder", key: "{path}",
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
html,
|
||||
body, pre {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
font-size: 100%;
|
||||
vertical-align: baseline;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
color: inherit;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
line-height: 1.4em;
|
||||
background: #f5f5f5;
|
||||
color: #4d4d4d;
|
||||
min-width: 230px;
|
||||
max-width: 550px;
|
||||
margin: 0 auto;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.todoapp {
|
||||
background: #fff;
|
||||
margin: 130px 0 40px 0;
|
||||
position: relative;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.todoapp input::-webkit-input-placeholder {
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: #e6e6e6;
|
||||
}
|
||||
|
||||
.todoapp input::-moz-placeholder {
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: #e6e6e6;
|
||||
}
|
||||
|
||||
.todoapp input::input-placeholder {
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: #e6e6e6;
|
||||
}
|
||||
|
||||
.todoapp h1 {
|
||||
position: absolute;
|
||||
top: -155px;
|
||||
width: 100%;
|
||||
font-size: 100px;
|
||||
font-weight: 100;
|
||||
text-align: center;
|
||||
color: rgba(175, 47, 47, 1.0);
|
||||
-webkit-text-rendering: optimizeLegibility;
|
||||
-moz-text-rendering: optimizeLegibility;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
.new-todo,
|
||||
.edit {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
font-size: 24px;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: 1.4em;
|
||||
border: 0;
|
||||
color: inherit;
|
||||
padding: 6px;
|
||||
border: 1px solid #999;
|
||||
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
|
||||
box-sizing: border-box;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.new-todo {
|
||||
padding: 16px 16px 16px 60px;
|
||||
border: none;
|
||||
background: rgba(0, 0, 0, 0.003);
|
||||
box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.main {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
border-top: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
.toggle-all {
|
||||
text-align: center;
|
||||
border: none;
|
||||
/* Mobile Safari */
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.toggle-all+label {
|
||||
width: 60px;
|
||||
height: 34px;
|
||||
font-size: 0;
|
||||
position: absolute;
|
||||
top: -52px;
|
||||
left: -13px;
|
||||
-webkit-transform: rotate(90deg);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.toggle-all+label:before {
|
||||
content: '❯';
|
||||
font-size: 22px;
|
||||
color: #e6e6e6;
|
||||
padding: 10px 27px 10px 27px;
|
||||
}
|
||||
|
||||
.toggle-all:checked+label:before {
|
||||
color: #737373;
|
||||
}
|
||||
|
||||
.todo-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.todo-list li {
|
||||
position: relative;
|
||||
font-size: 24px;
|
||||
border-bottom: 1px solid #ededed;
|
||||
}
|
||||
|
||||
.todo-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.todo-list li.editing {
|
||||
border-bottom: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.todo-list li.editing .edit {
|
||||
display: block;
|
||||
width: 506px;
|
||||
padding: 12px 16px;
|
||||
margin: 0 0 0 43px;
|
||||
}
|
||||
|
||||
.todo-list li.editing .view {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.todo-list li .toggle {
|
||||
text-align: center;
|
||||
width: 40px;
|
||||
/* auto, since non-WebKit browsers doesn't support input styling */
|
||||
height: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto 0;
|
||||
border: none;
|
||||
/* Mobile Safari */
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.todo-list li .toggle {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.todo-list li .toggle+label {
|
||||
/*
|
||||
Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
|
||||
IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
|
||||
*/
|
||||
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center left;
|
||||
}
|
||||
|
||||
.todo-list li .toggle:checked+label {
|
||||
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
|
||||
}
|
||||
|
||||
.todo-list li label {
|
||||
word-break: break-all;
|
||||
padding: 15px 15px 15px 60px;
|
||||
display: block;
|
||||
line-height: 1.2;
|
||||
transition: color 0.4s;
|
||||
}
|
||||
|
||||
.todo-list li.completed label {
|
||||
color: #d9d9d9;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.todo-list li .destroy {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 10px;
|
||||
bottom: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: auto 0;
|
||||
font-size: 30px;
|
||||
color: #cc9a9a;
|
||||
margin-bottom: 11px;
|
||||
transition: color 0.2s ease-out;
|
||||
}
|
||||
|
||||
.todo-list li .destroy:hover {
|
||||
color: #af5b5e;
|
||||
}
|
||||
|
||||
.todo-list li .destroy:after {
|
||||
content: '×';
|
||||
}
|
||||
|
||||
.todo-list li:hover .destroy {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.todo-list li .edit {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.todo-list li.editing:last-child {
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
color: #777;
|
||||
padding: 10px 15px;
|
||||
height: 20px;
|
||||
text-align: center;
|
||||
border-top: 1px solid #e6e6e6;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.footer:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 50px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.todo-count {
|
||||
float: left;
|
||||
text-align: left;
|
||||
white-space-collapse: preserve;
|
||||
}
|
||||
|
||||
.todo-count strong {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.filters {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.filters li {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.filters li a {
|
||||
display: inline-block;
|
||||
color: inherit;
|
||||
margin: 3px;
|
||||
padding: 3px 7px;
|
||||
text-decoration: none;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.filters li a:hover {
|
||||
border-color: rgba(175, 47, 47, 0.1);
|
||||
}
|
||||
|
||||
.filters li a.selected {
|
||||
border-color: rgba(175, 47, 47, 0.2);
|
||||
}
|
||||
|
||||
.clear-completed,
|
||||
html .clear-completed:active {
|
||||
float: right;
|
||||
position: relative;
|
||||
line-height: 20px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.clear-completed:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin: 65px auto 0;
|
||||
color: #bfbfbf;
|
||||
font-size: 10px;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.info p {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.info a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.info a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Hack to remove background from Mobile Safari.
|
||||
Can't use it globally since it destroys checkboxes in Firefox
|
||||
*/
|
||||
|
||||
@media screen and (-webkit-min-device-pixel-ratio:0) {
|
||||
.toggle-all,
|
||||
.todo-list li .toggle {
|
||||
background: none;
|
||||
}
|
||||
.todo-list li .toggle {
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 430px) {
|
||||
.footer {
|
||||
height: 50px;
|
||||
}
|
||||
.filters {
|
||||
bottom: 10px;
|
||||
}
|
||||
}
|
||||
@@ -370,4 +370,4 @@ html .clear-completed:active {
|
||||
.filters {
|
||||
bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
//! The typical TodoMVC app, implemented in Dioxus.
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
const STYLE: Asset = asset!("/examples/assets/todomvc-native.css");
|
||||
|
||||
fn main() {
|
||||
dioxus::launch(app);
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy)]
|
||||
enum FilterState {
|
||||
All,
|
||||
Active,
|
||||
Completed,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct TodoItem {
|
||||
id: u32,
|
||||
checked: bool,
|
||||
contents: String,
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
// We store the todos in a HashMap in a Signal.
|
||||
// Each key is the id of the todo, and the value is the todo itself.
|
||||
let mut todos = use_signal(HashMap::<u32, TodoItem>::new);
|
||||
|
||||
let filter = use_signal(|| FilterState::All);
|
||||
|
||||
// We use a simple memoized signal to calculate the number of active todos.
|
||||
// Whenever the todos change, the active_todo_count will be recalculated.
|
||||
let active_todo_count =
|
||||
use_memo(move || todos.read().values().filter(|item| !item.checked).count());
|
||||
|
||||
// We use a memoized signal to filter the todos based on the current filter state.
|
||||
// Whenever the todos or filter change, the filtered_todos will be recalculated.
|
||||
// Note that we're only storing the IDs of the todos, not the todos themselves.
|
||||
let filtered_todos = use_memo(move || {
|
||||
let mut filtered_todos = todos
|
||||
.read()
|
||||
.iter()
|
||||
.filter(|(_, item)| match filter() {
|
||||
FilterState::All => true,
|
||||
FilterState::Active => !item.checked,
|
||||
FilterState::Completed => item.checked,
|
||||
})
|
||||
.map(|f| *f.0)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
filtered_todos.sort_unstable();
|
||||
|
||||
filtered_todos
|
||||
});
|
||||
|
||||
// Toggle all the todos to the opposite of the current state.
|
||||
// If all todos are checked, uncheck them all. If any are unchecked, check them all.
|
||||
let toggle_all = move |_| {
|
||||
let check = active_todo_count() != 0;
|
||||
for (_, item) in todos.write().iter_mut() {
|
||||
item.checked = check;
|
||||
}
|
||||
};
|
||||
|
||||
rsx! {
|
||||
document::Link { rel: "stylesheet", href: STYLE }
|
||||
body {
|
||||
section { class: "todoapp",
|
||||
TodoHeader { todos }
|
||||
section { class: "main",
|
||||
if !todos.read().is_empty() {
|
||||
input {
|
||||
id: "toggle-all",
|
||||
class: "toggle-all",
|
||||
r#type: "checkbox",
|
||||
onchange: toggle_all,
|
||||
checked: active_todo_count() == 0
|
||||
}
|
||||
label { r#for: "toggle-all" }
|
||||
}
|
||||
|
||||
// Render the todos using the filtered_todos signal
|
||||
// We pass the ID into the TodoEntry component so it can access the todo from the todos signal.
|
||||
// Since we store the todos in a signal too, we also need to send down the todo list
|
||||
ul { class: "todo-list",
|
||||
for id in filtered_todos() {
|
||||
TodoEntry { key: "{id}", id, todos }
|
||||
}
|
||||
}
|
||||
|
||||
// We only show the footer if there are todos.
|
||||
if !todos.read().is_empty() {
|
||||
ListFooter { active_todo_count, todos, filter }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A simple info footer
|
||||
footer { class: "info",
|
||||
p { "Double-click to edit a todo" }
|
||||
p {
|
||||
"Created by "
|
||||
a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" }
|
||||
}
|
||||
p {
|
||||
"Part of "
|
||||
a { href: "http://todomvc.com", "TodoMVC" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn TodoHeader(mut todos: Signal<HashMap<u32, TodoItem>>) -> Element {
|
||||
let mut draft = use_signal(|| "".to_string());
|
||||
let mut todo_id = use_signal(|| 0);
|
||||
|
||||
let onkeydown = move |evt: KeyboardEvent| {
|
||||
if evt.key() == Key::Enter && !draft.read().is_empty() {
|
||||
let id = todo_id();
|
||||
let todo = TodoItem {
|
||||
id,
|
||||
checked: false,
|
||||
contents: draft.to_string(),
|
||||
};
|
||||
todos.write().insert(id, todo);
|
||||
todo_id += 1;
|
||||
draft.set("".to_string());
|
||||
evt.prevent_default();
|
||||
}
|
||||
};
|
||||
|
||||
rsx! {
|
||||
header { class: "header",
|
||||
h1 { "todos" }
|
||||
input {
|
||||
class: "new-todo",
|
||||
r#type: "text",
|
||||
placeholder: "What needs to be done?",
|
||||
value: "{draft}",
|
||||
autofocus: "true",
|
||||
oninput: move |evt| draft.set(evt.value()),
|
||||
onkeydown
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A single todo entry
|
||||
/// This takes the ID of the todo and the todos signal as props
|
||||
/// We can use these together to memoize the todo contents and checked state
|
||||
#[component]
|
||||
fn TodoEntry(mut todos: Signal<HashMap<u32, TodoItem>>, id: u32) -> Element {
|
||||
let mut is_editing = use_signal(|| false);
|
||||
|
||||
// To avoid re-rendering this component when the todo list changes, we isolate our reads to memos
|
||||
// This way, the component will only re-render when the contents of the todo change, or when the editing state changes.
|
||||
// This does involve taking a local clone of the todo contents, but it allows us to prevent this component from re-rendering
|
||||
let checked = use_memo(move || todos.read().get(&id).unwrap().checked);
|
||||
let contents = use_memo(move || todos.read().get(&id).unwrap().contents.clone());
|
||||
|
||||
rsx! {
|
||||
li {
|
||||
// Dioxus lets you use if statements in rsx to conditionally render attributes
|
||||
// These will get merged into a single class attribute
|
||||
class: if checked() { "completed" },
|
||||
class: if is_editing() { "editing" },
|
||||
|
||||
// Some basic controls for the todo
|
||||
div { class: "view",
|
||||
input {
|
||||
class: "toggle",
|
||||
r#type: "checkbox",
|
||||
id: "cbg-{id}",
|
||||
checked: "{checked}",
|
||||
oninput: move |evt| todos.write().get_mut(&id).unwrap().checked = evt.checked()
|
||||
}
|
||||
label {
|
||||
r#for: "cbg-{id}",
|
||||
onclick: move |evt| {
|
||||
is_editing.set(true);
|
||||
evt.prevent_default()
|
||||
},
|
||||
"{contents}"
|
||||
}
|
||||
button {
|
||||
class: "destroy",
|
||||
onclick: move |evt| {
|
||||
todos.write().remove(&id);
|
||||
evt.prevent_default();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Only render the actual input if we're editing
|
||||
if is_editing() {
|
||||
input {
|
||||
class: "edit",
|
||||
r#type: "text",
|
||||
value: "{contents}",
|
||||
oninput: move |evt| todos.write().get_mut(&id).unwrap().contents = evt.value(),
|
||||
autofocus: "true",
|
||||
onfocusout: move |_| is_editing.set(false),
|
||||
onkeydown: move |evt| {
|
||||
if matches!(evt.key(), Key::Enter | Key::Escape | Key::Tab) {
|
||||
evt.prevent_default();
|
||||
is_editing.set(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ListFooter(
|
||||
mut todos: Signal<HashMap<u32, TodoItem>>,
|
||||
active_todo_count: ReadOnlySignal<usize>,
|
||||
mut filter: Signal<FilterState>,
|
||||
) -> Element {
|
||||
// We use a memoized signal to calculate whether we should show the "Clear completed" button.
|
||||
// This will recompute whenever the todos change, and if the value is true, the button will be shown.
|
||||
let show_clear_completed = use_memo(move || todos.read().values().any(|todo| todo.checked));
|
||||
|
||||
rsx! {
|
||||
footer { class: "footer",
|
||||
span { class: "todo-count",
|
||||
strong { "{active_todo_count} " }
|
||||
span {
|
||||
match active_todo_count() {
|
||||
1 => "item",
|
||||
_ => "items",
|
||||
},
|
||||
" left"
|
||||
}
|
||||
}
|
||||
ul { class: "filters",
|
||||
for (state , state_text , url) in [
|
||||
(FilterState::All, "All", "#/"),
|
||||
(FilterState::Active, "Active", "#/active"),
|
||||
(FilterState::Completed, "Completed", "#/completed"),
|
||||
] {
|
||||
li {
|
||||
a {
|
||||
href: url,
|
||||
class: if filter() == state { "selected" },
|
||||
onclick: move |evt| {
|
||||
filter.set(state);
|
||||
evt.prevent_default();
|
||||
},
|
||||
{state_text}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if show_clear_completed() {
|
||||
button {
|
||||
class: "clear-completed",
|
||||
onclick: move |_| todos.write().retain(|_, todo| !todo.checked),
|
||||
"Clear completed"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "dioxus-asset-resolver"
|
||||
edition = "2021"
|
||||
version.workspace = true
|
||||
authors = ["Jonathan Kelley", "Dioxus Labs"]
|
||||
description = "Cross-platform asset resolver for manganis and dioxus"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||
homepage = "https://dioxuslabs.com"
|
||||
keywords = ["web", "desktop", "mobile", "gui", "wasm"]
|
||||
rust-version = "1.79.0"
|
||||
|
||||
[dependencies]
|
||||
http = { workspace = true }
|
||||
urlencoding = { workspace = true }
|
||||
infer = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
dioxus-cli-config = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
jni = "0.21.1"
|
||||
ndk = { version = "0.9.0" }
|
||||
ndk-sys = { version = "0.6.0" }
|
||||
ndk-context = { version = "0.1.1" }
|
||||
@@ -0,0 +1,180 @@
|
||||
use http::{status::StatusCode, Response};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AssetServeError {
|
||||
#[error("Failed to infer mime type for asset: {0}")]
|
||||
InferringMimeType(std::io::Error),
|
||||
|
||||
#[error("Failed to serve asset: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("Failed to construct response: {0}")]
|
||||
ResponseError(#[from] http::Error),
|
||||
}
|
||||
|
||||
pub fn serve_asset_from_raw_path(path: &str) -> Result<Response<Vec<u8>>, AssetServeError> {
|
||||
// If the user provided a custom asset handler, then call it and return the response if the request was handled.
|
||||
// The path is the first part of the URI, so we need to trim the leading slash.
|
||||
let mut uri_path = PathBuf::from(
|
||||
urlencoding::decode(path)
|
||||
.expect("expected URL to be UTF-8 encoded")
|
||||
.as_ref(),
|
||||
);
|
||||
|
||||
// Attempt to serve from the asset dir on android using its loader
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
if let Some(asset) = to_java_load_asset(path) {
|
||||
return Ok(Response::builder()
|
||||
.header("Content-Type", get_mime_by_ext(&uri_path))
|
||||
.header("Access-Control-Allow-Origin", "*")
|
||||
.body(asset)?);
|
||||
}
|
||||
}
|
||||
|
||||
// If the asset doesn't exist, or starts with `/assets/`, then we'll try to serve out of the bundle
|
||||
// This lets us handle both absolute and relative paths without being too "special"
|
||||
// It just means that our macos bundle is a little "special" because we need to place an `assets`
|
||||
// dir in the `Resources` dir.
|
||||
//
|
||||
// If there's no asset root, we use the cargo manifest dir as the root, or the current dir
|
||||
if !uri_path.exists() || uri_path.starts_with("/assets/") {
|
||||
let bundle_root = get_asset_root();
|
||||
let relative_path = uri_path.strip_prefix("/").unwrap();
|
||||
uri_path = bundle_root.join(relative_path);
|
||||
}
|
||||
|
||||
// If the asset exists, then we can serve it!
|
||||
if uri_path.exists() {
|
||||
let mime_type =
|
||||
get_mime_from_path(&uri_path).map_err(AssetServeError::InferringMimeType)?;
|
||||
let body = std::fs::read(uri_path)?;
|
||||
return Ok(Response::builder()
|
||||
.header("Content-Type", mime_type)
|
||||
.header("Access-Control-Allow-Origin", "*")
|
||||
.body(body)?);
|
||||
}
|
||||
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body(String::from("Not Found").into_bytes())?)
|
||||
}
|
||||
|
||||
/// Get the asset directory, following tauri/cargo-bundles directory discovery approach
|
||||
///
|
||||
/// Currently supports:
|
||||
/// - [x] macOS
|
||||
/// - [x] iOS
|
||||
/// - [x] Windows
|
||||
/// - [x] Linux (appimage)
|
||||
/// - [ ] Linux (rpm)
|
||||
/// - [ ] Linux (deb)
|
||||
/// - [ ] Android
|
||||
#[allow(unused)]
|
||||
fn get_asset_root() -> PathBuf {
|
||||
let cur_exe = std::env::current_exe().unwrap();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
return cur_exe
|
||||
.parent()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("Resources");
|
||||
}
|
||||
|
||||
// For all others, the structure looks like this:
|
||||
// app.(exe/appimage)
|
||||
// main.exe
|
||||
// assets/
|
||||
cur_exe.parent().unwrap().to_path_buf()
|
||||
}
|
||||
|
||||
/// Get the mime type from a path-like string
|
||||
fn get_mime_from_path(asset: &Path) -> std::io::Result<&'static str> {
|
||||
if asset.extension().is_some_and(|ext| ext == "svg") {
|
||||
return Ok("image/svg+xml");
|
||||
}
|
||||
|
||||
match infer::get_from_path(asset)?.map(|f| f.mime_type()) {
|
||||
Some(f) if f != "text/plain" => Ok(f),
|
||||
_other => Ok(get_mime_by_ext(asset)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the mime type from a URI using its extension
|
||||
fn get_mime_by_ext(trimmed: &Path) -> &'static str {
|
||||
match trimmed.extension().and_then(|e| e.to_str()) {
|
||||
// The common assets are all utf-8 encoded
|
||||
Some("js") => "text/javascript; charset=utf-8",
|
||||
Some("css") => "text/css; charset=utf-8",
|
||||
Some("json") => "application/json; charset=utf-8",
|
||||
Some("svg") => "image/svg+xml; charset=utf-8",
|
||||
Some("html") => "text/html; charset=utf-8",
|
||||
|
||||
// the rest... idk? probably not
|
||||
Some("mjs") => "text/javascript; charset=utf-8",
|
||||
Some("bin") => "application/octet-stream",
|
||||
Some("csv") => "text/csv",
|
||||
Some("ico") => "image/vnd.microsoft.icon",
|
||||
Some("jsonld") => "application/ld+json",
|
||||
Some("rtf") => "application/rtf",
|
||||
Some("mp4") => "video/mp4",
|
||||
// Assume HTML when a TLD is found for eg. `dioxus:://dioxuslabs.app` | `dioxus://hello.com`
|
||||
Some(_) => "text/html; charset=utf-8",
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
|
||||
// using octet stream according to this:
|
||||
None => "application/octet-stream",
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub(crate) fn to_java_load_asset(filepath: &str) -> Option<Vec<u8>> {
|
||||
let normalized = filepath
|
||||
.trim_start_matches("/assets/")
|
||||
.trim_start_matches('/');
|
||||
|
||||
// in debug mode, the asset might be under `/data/local/tmp/dx/` - attempt to read it from there if it exists
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let path = dioxus_cli_config::android_session_cache_dir().join(normalized);
|
||||
if path.exists() {
|
||||
return std::fs::read(path).ok();
|
||||
}
|
||||
}
|
||||
|
||||
use std::ptr::NonNull;
|
||||
|
||||
let ctx = ndk_context::android_context();
|
||||
let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) }.unwrap();
|
||||
let mut env = vm.attach_current_thread().unwrap();
|
||||
|
||||
// Query the Asset Manager
|
||||
let asset_manager_ptr = env
|
||||
.call_method(
|
||||
unsafe { jni::objects::JObject::from_raw(ctx.context().cast()) },
|
||||
"getAssets",
|
||||
"()Landroid/content/res/AssetManager;",
|
||||
&[],
|
||||
)
|
||||
.expect("Failed to get asset manager")
|
||||
.l()
|
||||
.expect("Failed to get asset manager as object");
|
||||
|
||||
unsafe {
|
||||
let asset_manager =
|
||||
ndk_sys::AAssetManager_fromJava(env.get_native_interface(), *asset_manager_ptr);
|
||||
|
||||
let asset_manager = ndk::asset::AssetManager::from_ptr(
|
||||
NonNull::new(asset_manager).expect("Invalid asset manager"),
|
||||
);
|
||||
|
||||
let cstr = std::ffi::CString::new(normalized).unwrap();
|
||||
|
||||
let mut asset = asset_manager.open(&cstr)?;
|
||||
Some(asset.buffer().unwrap().to_vec())
|
||||
}
|
||||
}
|
||||
@@ -108,7 +108,7 @@ impl Builder {
|
||||
self.expected_crates = *crate_count;
|
||||
}
|
||||
}
|
||||
BuildStage::InstallingTooling {} => {}
|
||||
BuildStage::InstallingTooling => {}
|
||||
BuildStage::Compiling {
|
||||
current,
|
||||
total,
|
||||
@@ -127,12 +127,12 @@ impl Builder {
|
||||
self.compile_start = Some(Instant::now());
|
||||
}
|
||||
}
|
||||
BuildStage::Bundling {} => {
|
||||
BuildStage::Bundling => {
|
||||
self.complete_compile();
|
||||
self.bundling_progress = 0.0;
|
||||
self.bundle_start = Some(Instant::now());
|
||||
}
|
||||
BuildStage::OptimizingWasm {} => {}
|
||||
BuildStage::OptimizingWasm => {}
|
||||
BuildStage::CopyingAssets { current, total, .. } => {
|
||||
self.bundling_progress = *current as f64 / *total as f64;
|
||||
}
|
||||
@@ -153,7 +153,7 @@ impl Builder {
|
||||
self.expected_crates = 1;
|
||||
self.bundling_progress = 0.0;
|
||||
}
|
||||
BuildStage::RunningBindgen {} => {}
|
||||
BuildStage::RunningBindgen => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,19 +21,20 @@ pub(crate) async fn pre_render_static_routes(server_exe: &Path) -> anyhow::Resul
|
||||
let address = &address;
|
||||
let port = &port;
|
||||
|
||||
tracing::info!("Running SSG at http://{address}:{port}");
|
||||
tracing::info!("Running SSG at http://{address}:{port} for {server_exe:?}");
|
||||
|
||||
// Run the server executable
|
||||
let _child = Command::new(server_exe)
|
||||
.env(dioxus_cli_config::SERVER_PORT_ENV, port)
|
||||
.env(dioxus_cli_config::SERVER_IP_ENV, address)
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.current_dir(server_exe.parent().unwrap())
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.kill_on_drop(true)
|
||||
.spawn()?;
|
||||
|
||||
let reqwest_client = reqwest::Client::new();
|
||||
// Borrow reqwest_client so we only move the reference into the futures
|
||||
let reqwest_client = reqwest::Client::new();
|
||||
let reqwest_client = &reqwest_client;
|
||||
|
||||
// Get the routes from the `/static_routes` endpoint
|
||||
@@ -42,8 +43,13 @@ pub(crate) async fn pre_render_static_routes(server_exe: &Path) -> anyhow::Resul
|
||||
// The server may take a few seconds to start up. Try fetching the route up to 5 times with a one second delay
|
||||
const RETRY_ATTEMPTS: usize = 5;
|
||||
for i in 0..=RETRY_ATTEMPTS {
|
||||
tracing::debug!(
|
||||
"Attempting to get static routes from server. Attempt {i} of {RETRY_ATTEMPTS}"
|
||||
);
|
||||
|
||||
let request = reqwest_client
|
||||
.post(format!("http://{address}:{port}/api/static_routes"))
|
||||
.body("{}".to_string())
|
||||
.send()
|
||||
.await;
|
||||
match request {
|
||||
@@ -75,12 +81,14 @@ pub(crate) async fn pre_render_static_routes(server_exe: &Path) -> anyhow::Resul
|
||||
.into_iter()
|
||||
.map(|route| async move {
|
||||
tracing::info!("Rendering {route} for SSG");
|
||||
|
||||
// For each route, ping the server to force it to cache the response for ssg
|
||||
let request = reqwest_client
|
||||
.get(format!("http://{address}:{port}{route}"))
|
||||
.header("Accept", "text/html")
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
// If it takes longer than 30 seconds to resolve the route, log a warning
|
||||
let warning_task = tokio::spawn({
|
||||
let route = route.clone();
|
||||
@@ -89,6 +97,7 @@ pub(crate) async fn pre_render_static_routes(server_exe: &Path) -> anyhow::Resul
|
||||
tracing::warn!("Route {route} has been rendering for 30 seconds");
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for the streaming response to completely finish before continuing. We don't use the html it returns directly
|
||||
// because it may contain artifacts of intermediate streaming steps while the page is loading. The SSG app should write
|
||||
// the final clean HTML to the disk automatically after the request completes.
|
||||
|
||||
@@ -686,7 +686,7 @@ impl DioxusCrate {
|
||||
.flatten()
|
||||
.map(|dir| (dir.file_name(), dir.path()))
|
||||
.sorted()
|
||||
.last()
|
||||
.next_back()
|
||||
.map(|(_, path)| path.to_path_buf())
|
||||
});
|
||||
|
||||
|
||||
@@ -160,7 +160,7 @@ impl Platform {
|
||||
pub(crate) fn autodetect_from_cargo_feature(feature: &str) -> Option<Self> {
|
||||
match feature {
|
||||
"web" => Some(Platform::Web),
|
||||
"desktop" => {
|
||||
"desktop" | "native" => {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
Some(Platform::MacOS)
|
||||
|
||||
@@ -23,11 +23,11 @@ pub(crate) use watcher::*;
|
||||
///
|
||||
/// Platform specifics:
|
||||
/// -------------------
|
||||
/// - Web: we need to attach a filesystem server to our devtools webserver to serve the project. We
|
||||
/// want to emulate GithubPages here since most folks are deploying there and expect things like
|
||||
/// basepath to match.
|
||||
/// - Desktop: We spin up the dev server but without a filesystem server.
|
||||
/// - Mobile: Basically the same as desktop.
|
||||
/// - Web: we need to attach a filesystem server to our devtools webserver to serve the project. We
|
||||
/// want to emulate GithubPages here since most folks are deploying there and expect things like
|
||||
/// basepath to match.
|
||||
/// - Desktop: We spin up the dev server but without a filesystem server.
|
||||
/// - Mobile: Basically the same as desktop.
|
||||
///
|
||||
/// When fullstack is enabled, we'll also build for the `server` target and then hotreload the server.
|
||||
/// The "server" is special here since "fullstack" is functionally just an addition to the regular client
|
||||
|
||||
@@ -521,7 +521,7 @@ impl Output {
|
||||
match &state.build_engine.stage {
|
||||
BuildStage::Initializing => lines.push("Initializing".yellow()),
|
||||
BuildStage::Starting { .. } => lines.push("Starting build".yellow()),
|
||||
BuildStage::InstallingTooling {} => lines.push("Installing tooling".yellow()),
|
||||
BuildStage::InstallingTooling => lines.push("Installing tooling".yellow()),
|
||||
BuildStage::Compiling {
|
||||
current,
|
||||
total,
|
||||
@@ -532,13 +532,13 @@ impl Output {
|
||||
lines.push(format!("{current}/{total} ").gray());
|
||||
lines.push(krate.as_str().dark_gray())
|
||||
}
|
||||
BuildStage::OptimizingWasm {} => lines.push("Optimizing wasm".yellow()),
|
||||
BuildStage::SplittingBundle {} => lines.push("Splitting bundle".yellow()),
|
||||
BuildStage::OptimizingWasm => lines.push("Optimizing wasm".yellow()),
|
||||
BuildStage::SplittingBundle => lines.push("Splitting bundle".yellow()),
|
||||
BuildStage::CompressingAssets => lines.push("Compressing assets".yellow()),
|
||||
BuildStage::PrerenderingRoutes {} => lines.push("Prerendering static routes".yellow()),
|
||||
BuildStage::RunningBindgen {} => lines.push("Running wasm-bindgen".yellow()),
|
||||
BuildStage::RunningGradle {} => lines.push("Running gradle assemble".yellow()),
|
||||
BuildStage::Bundling {} => lines.push("Bundling app".yellow()),
|
||||
BuildStage::PrerenderingRoutes => lines.push("Prerendering static routes".yellow()),
|
||||
BuildStage::RunningBindgen => lines.push("Running wasm-bindgen".yellow()),
|
||||
BuildStage::RunningGradle => lines.push("Running gradle assemble".yellow()),
|
||||
BuildStage::Bundling => lines.push("Bundling app".yellow()),
|
||||
BuildStage::CopyingAssets {
|
||||
current,
|
||||
total,
|
||||
|
||||
@@ -252,7 +252,7 @@ impl WebServer {
|
||||
BuildStage::Failed => self.send_reload_failed().await,
|
||||
BuildStage::Restarting => self.send_reload_start().await,
|
||||
BuildStage::Initializing => {}
|
||||
BuildStage::InstallingTooling {} => {}
|
||||
BuildStage::InstallingTooling => {}
|
||||
BuildStage::Compiling {
|
||||
current,
|
||||
total,
|
||||
@@ -267,7 +267,7 @@ impl WebServer {
|
||||
self.send_build_status().await;
|
||||
}
|
||||
}
|
||||
BuildStage::OptimizingWasm {} => {}
|
||||
BuildStage::OptimizingWasm => {}
|
||||
BuildStage::Aborted => {}
|
||||
BuildStage::CopyingAssets { .. } => {}
|
||||
_ => {}
|
||||
|
||||
@@ -378,6 +378,7 @@ pub struct Template {
|
||||
|
||||
// Are identical static items merged in the current build. Rust doesn't have a cfg(merge_statics) attribute
|
||||
// so we have to check this manually
|
||||
#[allow(unpredictable_function_pointer_comparisons)]
|
||||
fn static_items_merged() -> bool {
|
||||
fn a() {}
|
||||
fn b() {}
|
||||
|
||||
@@ -20,6 +20,7 @@ dioxus-document = { workspace = true }
|
||||
dioxus-signals = { workspace = true, optional = true }
|
||||
dioxus-interpreter-js = { workspace = true, features = ["binary-protocol", "serialize"] }
|
||||
dioxus-cli-config = { workspace = true }
|
||||
dioxus-asset-resolver = { workspace = true }
|
||||
generational-box = { workspace = true }
|
||||
# hotreload only works on desktop platforms.... mobile is still wip
|
||||
dioxus-devtools = { workspace = true, optional = true }
|
||||
@@ -44,13 +45,13 @@ tokio = { workspace = true, features = [
|
||||
"io-util",
|
||||
], optional = true }
|
||||
webbrowser = "0.8.0"
|
||||
infer = "0.11.0"
|
||||
infer = { workspace = true }
|
||||
dunce = { workspace = true }
|
||||
slab = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
dioxus-hooks = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
urlencoding = "2.1.2"
|
||||
urlencoding = { workspace = true }
|
||||
async-trait = "0.1.68"
|
||||
tao = { workspace = true, features = ["rwh_05"] }
|
||||
once_cell = { workspace = true }
|
||||
|
||||
@@ -2,10 +2,9 @@ use crate::document::NATIVE_EVAL_JS;
|
||||
use crate::{assets::*, webview::WebviewEdits};
|
||||
use dioxus_interpreter_js::unified_bindings::SLEDGEHAMMER_JS;
|
||||
use dioxus_interpreter_js::NATIVE_JS;
|
||||
use std::path::{Path, PathBuf};
|
||||
use wry::{
|
||||
http::{status::StatusCode, Request, Response},
|
||||
RequestAsyncResponder, Result,
|
||||
RequestAsyncResponder,
|
||||
};
|
||||
|
||||
#[cfg(any(target_os = "android", target_os = "windows"))]
|
||||
@@ -64,7 +63,7 @@ pub(super) fn desktop_handler(
|
||||
}
|
||||
}
|
||||
|
||||
match serve_asset(request) {
|
||||
match dioxus_asset_resolver::serve_asset_from_raw_path(request.uri().path()) {
|
||||
Ok(res) => responder.respond(res),
|
||||
Err(_e) => responder.respond(
|
||||
Response::builder()
|
||||
@@ -75,52 +74,6 @@ pub(super) fn desktop_handler(
|
||||
}
|
||||
}
|
||||
|
||||
fn serve_asset(request: Request<Vec<u8>>) -> Result<Response<Vec<u8>>> {
|
||||
// If the user provided a custom asset handler, then call it and return the response if the request was handled.
|
||||
// The path is the first part of the URI, so we need to trim the leading slash.
|
||||
let mut uri_path = PathBuf::from(
|
||||
urlencoding::decode(request.uri().path())
|
||||
.expect("expected URL to be UTF-8 encoded")
|
||||
.as_ref(),
|
||||
);
|
||||
|
||||
// Attempt to serve from the asset dir on android using its loader
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
if let Some(asset) = to_java_load_asset(request.uri().path()) {
|
||||
return Ok(Response::builder()
|
||||
.header("Content-Type", get_mime_by_ext(&uri_path))
|
||||
.header("Access-Control-Allow-Origin", "*")
|
||||
.body(asset)?);
|
||||
}
|
||||
}
|
||||
|
||||
// If the asset doesn't exist, or starts with `/assets/`, then we'll try to serve out of the bundle
|
||||
// This lets us handle both absolute and relative paths without being too "special"
|
||||
// It just means that our macos bundle is a little "special" because we need to place an `assets`
|
||||
// dir in the `Resources` dir.
|
||||
//
|
||||
// If there's no asset root, we use the cargo manifest dir as the root, or the current dir
|
||||
if !uri_path.exists() || uri_path.starts_with("/assets/") {
|
||||
let bundle_root = get_asset_root();
|
||||
let relative_path = uri_path.strip_prefix("/").unwrap();
|
||||
uri_path = bundle_root.join(relative_path);
|
||||
}
|
||||
|
||||
// If the asset exists, then we can serve it!
|
||||
if uri_path.exists() {
|
||||
let mime_type = get_mime_from_path(&uri_path);
|
||||
return Ok(Response::builder()
|
||||
.header("Content-Type", mime_type?)
|
||||
.header("Access-Control-Allow-Origin", "*")
|
||||
.body(std::fs::read(uri_path)?)?);
|
||||
}
|
||||
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body(String::from("Not Found").into_bytes())?)
|
||||
}
|
||||
|
||||
/// Build the index.html file we use for bootstrapping a new app
|
||||
///
|
||||
/// We use wry/webview by building a special index.html that forms a bridge between the webview and your rust code
|
||||
@@ -172,7 +125,7 @@ fn index_request(
|
||||
/// The arguments here:
|
||||
/// - root_name: the root element (by Id) that we stream edits into
|
||||
/// - headless: is this page being loaded but invisible? Important because not all windows are visible and the
|
||||
/// interpreter can't connect until the window is ready.
|
||||
/// interpreter can't connect until the window is ready.
|
||||
fn module_loader(root_id: &str, headless: bool) -> String {
|
||||
format!(
|
||||
r#"
|
||||
@@ -203,120 +156,3 @@ fn module_loader(root_id: &str, headless: bool) -> String {
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
/// Get the asset directory, following tauri/cargo-bundles directory discovery approach
|
||||
///
|
||||
/// Currently supports:
|
||||
/// - [x] macOS
|
||||
/// - [x] iOS
|
||||
/// - [x] Windows
|
||||
/// - [x] Linux (appimage)
|
||||
/// - [ ] Linux (rpm)
|
||||
/// - [ ] Linux (deb)
|
||||
/// - [ ] Android
|
||||
#[allow(unreachable_code)]
|
||||
fn get_asset_root() -> PathBuf {
|
||||
let cur_exe = std::env::current_exe().unwrap();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
return cur_exe
|
||||
.parent()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("Resources");
|
||||
}
|
||||
|
||||
// For all others, the structure looks like this:
|
||||
// app.(exe/appimage)
|
||||
// main.exe
|
||||
// assets/
|
||||
cur_exe.parent().unwrap().to_path_buf()
|
||||
}
|
||||
|
||||
/// Get the mime type from a path-like string
|
||||
fn get_mime_from_path(asset: &Path) -> Result<&'static str> {
|
||||
if asset.extension().is_some_and(|ext| ext == "svg") {
|
||||
return Ok("image/svg+xml");
|
||||
}
|
||||
|
||||
match infer::get_from_path(asset)?.map(|f| f.mime_type()) {
|
||||
Some(f) if f != "text/plain" => Ok(f),
|
||||
_other => Ok(get_mime_by_ext(asset)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the mime type from a URI using its extension
|
||||
fn get_mime_by_ext(trimmed: &Path) -> &'static str {
|
||||
match trimmed.extension().and_then(|e| e.to_str()) {
|
||||
// The common assets are all utf-8 encoded
|
||||
Some("js") => "text/javascript; charset=utf-8",
|
||||
Some("css") => "text/css; charset=utf-8",
|
||||
Some("json") => "application/json; charset=utf-8",
|
||||
Some("svg") => "image/svg+xml; charset=utf-8",
|
||||
Some("html") => "text/html; charset=utf-8",
|
||||
|
||||
// the rest... idk? probably not
|
||||
Some("mjs") => "text/javascript; charset=utf-8",
|
||||
Some("bin") => "application/octet-stream",
|
||||
Some("csv") => "text/csv",
|
||||
Some("ico") => "image/vnd.microsoft.icon",
|
||||
Some("jsonld") => "application/ld+json",
|
||||
Some("rtf") => "application/rtf",
|
||||
Some("mp4") => "video/mp4",
|
||||
// Assume HTML when a TLD is found for eg. `dioxus:://dioxuslabs.app` | `dioxus://hello.com`
|
||||
Some(_) => "text/html; charset=utf-8",
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
|
||||
// using octet stream according to this:
|
||||
None => "application/octet-stream",
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub(crate) fn to_java_load_asset(filepath: &str) -> Option<Vec<u8>> {
|
||||
let normalized = filepath
|
||||
.trim_start_matches("/assets/")
|
||||
.trim_start_matches('/');
|
||||
|
||||
// in debug mode, the asset might be under `/data/local/tmp/dx/` - attempt to read it from there if it exists
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let path = dioxus_cli_config::android_session_cache_dir().join(normalized);
|
||||
if path.exists() {
|
||||
return std::fs::read(path).ok();
|
||||
}
|
||||
}
|
||||
|
||||
use std::ptr::NonNull;
|
||||
|
||||
let ctx = ndk_context::android_context();
|
||||
let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) }.unwrap();
|
||||
let mut env = vm.attach_current_thread().unwrap();
|
||||
|
||||
// Query the Asset Manager
|
||||
let asset_manager_ptr = env
|
||||
.call_method(
|
||||
unsafe { jni::objects::JObject::from_raw(ctx.context().cast()) },
|
||||
"getAssets",
|
||||
"()Landroid/content/res/AssetManager;",
|
||||
&[],
|
||||
)
|
||||
.expect("Failed to get asset manager")
|
||||
.l()
|
||||
.expect("Failed to get asset manager as object");
|
||||
|
||||
unsafe {
|
||||
let asset_manager =
|
||||
ndk_sys::AAssetManager_fromJava(env.get_native_interface(), *asset_manager_ptr);
|
||||
|
||||
let asset_manager = ndk::asset::AssetManager::from_ptr(
|
||||
NonNull::new(asset_manager).expect("Invalid asset manager"),
|
||||
);
|
||||
|
||||
let cstr = std::ffi::CString::new(normalized).unwrap();
|
||||
|
||||
let mut asset = asset_manager.open(&cstr)?;
|
||||
Some(asset.buffer().unwrap().to_vec())
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ dioxus-desktop = { workspace = true, default-features = true, optional = true }
|
||||
dioxus-fullstack = { workspace = true, default-features = true, optional = true }
|
||||
dioxus-liveview = { workspace = true, optional = true }
|
||||
dioxus-ssr = { workspace = true, optional = true }
|
||||
dioxus-native = { workspace = true, optional = true }
|
||||
manganis = { workspace = true, features = ["dioxus"], optional = true }
|
||||
dioxus-logger = { workspace = true, optional = true }
|
||||
warnings = { workspace = true, optional = true }
|
||||
@@ -66,6 +67,7 @@ web = ["dep:dioxus-web", "dioxus-fullstack?/web", "dioxus-config-macro/web", "de
|
||||
ssr = ["dep:dioxus-ssr", "dioxus-config-macro/ssr"]
|
||||
liveview = ["dep:dioxus-liveview", "dioxus-config-macro/liveview"]
|
||||
server = ["dioxus-fullstack?/axum", "dioxus-fullstack?/server", "ssr", "dioxus-liveview?/axum"]
|
||||
native = ["dep:dioxus-native"] # todo(jon): decompose the desktop crate such that "webview" is the default and native is opt-in
|
||||
|
||||
# This feature just disables the no-renderer-enabled warning
|
||||
third-party-renderer = []
|
||||
|
||||
+165
-141
@@ -6,10 +6,58 @@ use std::any::Any;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Launch your Dioxus application with the given root component, context and config.
|
||||
/// The platform will be determined from cargo features.
|
||||
///
|
||||
/// For a builder API, see `LaunchBuilder` defined in the `dioxus` crate.
|
||||
///
|
||||
/// # Feature selection
|
||||
///
|
||||
/// - `web`: Enables the web platform.
|
||||
/// - `desktop`: Enables the desktop platform.
|
||||
/// - `mobile`: Enables the mobile (ios + android webview) platform.
|
||||
/// - `server`: Enables the server (axum + server-side-rendering) platform.
|
||||
/// - `liveview`: Enables the liveview (websocke) platform.
|
||||
/// - `native`: Enables the native (wgpu + winit renderer) platform.
|
||||
///
|
||||
/// Currently `native` is its own platform that is not compatible with desktop or mobile since it
|
||||
/// unifies both platforms into one. If "desktop" and "native" are enabled, then the native renderer
|
||||
/// will be used.
|
||||
///
|
||||
/// # Feature priority
|
||||
///
|
||||
/// If multiple renderers are enabled, the order of priority goes:
|
||||
///
|
||||
/// 1. liveview
|
||||
/// 2. server
|
||||
/// 3. native
|
||||
/// 4. desktop
|
||||
/// 5. mobile
|
||||
/// 6. web
|
||||
///
|
||||
/// However, we don't recommend enabling multiple renderers at the same time due to feature conflicts
|
||||
/// and bloating of the binary size.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// # use dioxus::prelude::*;
|
||||
///
|
||||
/// dioxus::launch(app);
|
||||
/// fn app() -> Element {
|
||||
/// rsx! {
|
||||
/// div { "Hello, world!" }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn launch(app: fn() -> Element) {
|
||||
#[allow(deprecated)]
|
||||
LaunchBuilder::new().launch(app)
|
||||
}
|
||||
|
||||
/// A builder for a fullstack app.
|
||||
#[must_use]
|
||||
pub struct LaunchBuilder {
|
||||
launch_fn: LaunchFn,
|
||||
platform: KnownPlatform,
|
||||
contexts: Vec<ContextFn>,
|
||||
configs: Vec<Box<dyn Any>>,
|
||||
}
|
||||
@@ -19,6 +67,16 @@ pub type LaunchFn = fn(fn() -> Element, Vec<ContextFn>, Vec<Box<dyn Any>>);
|
||||
/// A context function is a Send and Sync closure that returns a boxed trait object
|
||||
pub type ContextFn = Box<dyn Fn() -> Box<dyn Any> + Send + Sync + 'static>;
|
||||
|
||||
enum KnownPlatform {
|
||||
Web,
|
||||
Desktop,
|
||||
Mobile,
|
||||
Server,
|
||||
Liveview,
|
||||
Native,
|
||||
Other(LaunchFn),
|
||||
}
|
||||
|
||||
#[allow(clippy::redundant_closure)] // clippy doesnt doesn't understand our coercion to fn
|
||||
impl LaunchBuilder {
|
||||
/// Create a new builder for your application. This will create a launch configuration for the current platform based on the features enabled on the `dioxus` crate.
|
||||
@@ -38,9 +96,24 @@ impl LaunchBuilder {
|
||||
)
|
||||
)]
|
||||
pub fn new() -> LaunchBuilder {
|
||||
let platform = if cfg!(feature = "liveview") {
|
||||
KnownPlatform::Liveview
|
||||
} else if cfg!(feature = "server") {
|
||||
KnownPlatform::Server
|
||||
} else if cfg!(feature = "native") {
|
||||
KnownPlatform::Native
|
||||
} else if cfg!(feature = "desktop") {
|
||||
KnownPlatform::Desktop
|
||||
} else if cfg!(feature = "mobile") {
|
||||
KnownPlatform::Mobile
|
||||
} else if cfg!(feature = "web") {
|
||||
KnownPlatform::Web
|
||||
} else {
|
||||
panic!("No platform feature enabled. Please enable one of the following features: liveview, desktop, mobile, web, tui, fullstack to use the launch API.")
|
||||
};
|
||||
|
||||
LaunchBuilder {
|
||||
// We can't use the `current_platform::launch` function directly because it may return ! or ()
|
||||
launch_fn: |root, contexts, cfg| current_platform::launch(root, contexts, cfg),
|
||||
platform,
|
||||
contexts: Vec::new(),
|
||||
configs: Vec::new(),
|
||||
}
|
||||
@@ -51,7 +124,7 @@ impl LaunchBuilder {
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "web")))]
|
||||
pub fn web() -> LaunchBuilder {
|
||||
LaunchBuilder {
|
||||
launch_fn: web_launch,
|
||||
platform: KnownPlatform::Web,
|
||||
contexts: Vec::new(),
|
||||
configs: Vec::new(),
|
||||
}
|
||||
@@ -62,7 +135,7 @@ impl LaunchBuilder {
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "desktop")))]
|
||||
pub fn desktop() -> LaunchBuilder {
|
||||
LaunchBuilder {
|
||||
launch_fn: |root, contexts, cfg| dioxus_desktop::launch::launch(root, contexts, cfg),
|
||||
platform: KnownPlatform::Desktop,
|
||||
contexts: Vec::new(),
|
||||
configs: Vec::new(),
|
||||
}
|
||||
@@ -73,9 +146,7 @@ impl LaunchBuilder {
|
||||
#[cfg_attr(docsrs, doc(cfg(all(feature = "fullstack", feature = "server"))))]
|
||||
pub fn server() -> LaunchBuilder {
|
||||
LaunchBuilder {
|
||||
launch_fn: |root, contexts, cfg| {
|
||||
dioxus_fullstack::server::launch::launch(root, contexts, cfg)
|
||||
},
|
||||
platform: KnownPlatform::Server,
|
||||
contexts: Vec::new(),
|
||||
configs: Vec::new(),
|
||||
}
|
||||
@@ -86,7 +157,7 @@ impl LaunchBuilder {
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "mobile")))]
|
||||
pub fn mobile() -> LaunchBuilder {
|
||||
LaunchBuilder {
|
||||
launch_fn: |root, contexts, cfg| dioxus_mobile::launch_cfg(root, contexts, cfg),
|
||||
platform: KnownPlatform::Mobile,
|
||||
contexts: Vec::new(),
|
||||
configs: Vec::new(),
|
||||
}
|
||||
@@ -121,14 +192,12 @@ impl LaunchBuilder {
|
||||
/// ```
|
||||
pub fn custom(launch_fn: LaunchFn) -> LaunchBuilder {
|
||||
LaunchBuilder {
|
||||
launch_fn,
|
||||
platform: KnownPlatform::Other(launch_fn),
|
||||
contexts: vec![],
|
||||
configs: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LaunchBuilder {
|
||||
/// Inject state into the root component's context that is created on the thread that the app is launched on.
|
||||
///
|
||||
/// # Example
|
||||
@@ -186,9 +255,7 @@ impl LaunchBuilder {
|
||||
.push(Box::new(move || Box::new(state.clone())));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl LaunchBuilder {
|
||||
/// Provide a platform-specific config to the builder.
|
||||
///
|
||||
/// # Example
|
||||
@@ -211,13 +278,28 @@ impl LaunchBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
fn launch_inner(self, app: fn() -> Element) {
|
||||
/// Launch your application.
|
||||
///
|
||||
#[allow(clippy::diverging_sub_expression)]
|
||||
pub fn launch(self, app: fn() -> Element) {
|
||||
let Self {
|
||||
platform,
|
||||
contexts,
|
||||
configs,
|
||||
} = self;
|
||||
|
||||
// Make sure to turn on the logger if the user specified the logger feaature
|
||||
#[cfg(feature = "logger")]
|
||||
dioxus_logger::initialize_default();
|
||||
|
||||
#[cfg(all(feature = "fullstack", any(feature = "desktop", feature = "mobile")))]
|
||||
// Set any flags if we're running under fullstack
|
||||
#[cfg(feature = "fullstack")]
|
||||
{
|
||||
use dioxus_fullstack::prelude::server_fn::client::{get_server_url, set_server_url};
|
||||
|
||||
// Make sure to set the server_fn endpoint if the user specified the fullstack feature
|
||||
// We only set this on native targets
|
||||
#[cfg(any(feature = "desktop", feature = "mobile", feature = "native"))]
|
||||
if get_server_url().is_empty() {
|
||||
let serverurl = format!(
|
||||
"http://{}:{}",
|
||||
@@ -226,143 +308,85 @@ impl LaunchBuilder {
|
||||
std::env::var("DIOXUS_DEVSERVER_PORT").unwrap_or_else(|_| "8080".to_string())
|
||||
)
|
||||
.leak();
|
||||
|
||||
set_server_url(serverurl);
|
||||
}
|
||||
|
||||
// If there is a base path set, call server functions from that base path
|
||||
#[cfg(feature = "web")]
|
||||
if let Some(base_path) = dioxus_cli_config::base_path() {
|
||||
let base_path = base_path.trim_matches('/');
|
||||
set_server_url(format!("{}/{}", get_server_url(), base_path).leak());
|
||||
}
|
||||
}
|
||||
|
||||
let cfg = self.configs;
|
||||
(self.launch_fn)(app, self.contexts, cfg);
|
||||
}
|
||||
// If native is specified, we override the webview launcher
|
||||
#[cfg(feature = "native")]
|
||||
if matches!(platform, KnownPlatform::Native) {
|
||||
return dioxus_native::launch_cfg(app, contexts, configs);
|
||||
}
|
||||
|
||||
/// Launch your application.
|
||||
pub fn launch(self, app: fn() -> Element) {
|
||||
self.launch_inner(app);
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "mobile")]
|
||||
if matches!(platform, KnownPlatform::Mobile) {
|
||||
return dioxus_mobile::launch_bindings::launch(app, contexts, configs);
|
||||
}
|
||||
|
||||
/// Re-export the platform we expect the user wants
|
||||
///
|
||||
/// If multiple platforms are enabled, we use this priority (from highest to lowest):
|
||||
/// - `fullstack`
|
||||
/// - `desktop`
|
||||
/// - `mobile`
|
||||
/// - `web`
|
||||
/// - `liveview`
|
||||
mod current_platform {
|
||||
#[cfg(all(feature = "fullstack", feature = "server"))]
|
||||
pub use dioxus_fullstack::server::launch::*;
|
||||
#[cfg(feature = "desktop")]
|
||||
if matches!(platform, KnownPlatform::Desktop) {
|
||||
return dioxus_desktop::launch::launch(app, contexts, configs);
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
feature = "desktop",
|
||||
not(all(feature = "fullstack", feature = "server"))
|
||||
))]
|
||||
pub use dioxus_desktop::launch::*;
|
||||
#[cfg(feature = "server")]
|
||||
if matches!(platform, KnownPlatform::Server) {
|
||||
return dioxus_fullstack::server::launch::launch(app, contexts, configs);
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
feature = "mobile",
|
||||
not(feature = "desktop"),
|
||||
not(all(feature = "fullstack", feature = "server"))
|
||||
))]
|
||||
pub use dioxus_mobile::launch_bindings::*;
|
||||
#[cfg(feature = "web")]
|
||||
if matches!(platform, KnownPlatform::Web) {
|
||||
// If the server feature is enabled, launch the client with hydration enabled
|
||||
#[cfg(feature = "fullstack")]
|
||||
{
|
||||
let platform_config = configs
|
||||
.into_iter()
|
||||
.find_map(|cfg| cfg.downcast::<dioxus_web::Config>().ok())
|
||||
.unwrap_or_default()
|
||||
.hydrate(true);
|
||||
|
||||
#[cfg(all(
|
||||
feature = "web",
|
||||
not(all(feature = "fullstack", feature = "server")),
|
||||
not(all(feature = "server")),
|
||||
not(feature = "desktop"),
|
||||
not(feature = "mobile"),
|
||||
))]
|
||||
pub fn launch(
|
||||
root: fn() -> dioxus_core::Element,
|
||||
contexts: Vec<super::ContextFn>,
|
||||
platform_config: Vec<Box<dyn std::any::Any>>,
|
||||
) {
|
||||
super::web_launch(root, contexts, platform_config);
|
||||
}
|
||||
let mut vdom = dioxus_core::VirtualDom::new(app);
|
||||
for context in contexts {
|
||||
vdom.insert_any_root_context(context());
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
feature = "liveview",
|
||||
not(all(feature = "fullstack", feature = "server")),
|
||||
not(all(feature = "server")),
|
||||
not(feature = "desktop"),
|
||||
not(feature = "mobile"),
|
||||
not(feature = "web"),
|
||||
))]
|
||||
pub use dioxus_liveview::launch::*;
|
||||
#[cfg(feature = "document")]
|
||||
{
|
||||
use dioxus_fullstack::document;
|
||||
let document = std::rc::Rc::new(document::web::FullstackWebDocument)
|
||||
as std::rc::Rc<dyn crate::prelude::document::Document>;
|
||||
vdom.provide_root_context(document);
|
||||
}
|
||||
|
||||
#[cfg(not(any(
|
||||
feature = "liveview",
|
||||
all(feature = "fullstack", feature = "server"),
|
||||
all(feature = "server"),
|
||||
feature = "desktop",
|
||||
feature = "mobile",
|
||||
feature = "web",
|
||||
)))]
|
||||
pub fn launch(
|
||||
root: fn() -> dioxus_core::Element,
|
||||
contexts: Vec<super::ContextFn>,
|
||||
platform_config: Vec<Box<dyn std::any::Any>>,
|
||||
) {
|
||||
#[cfg(feature = "third-party-renderer")]
|
||||
panic!("No first party renderer feature enabled. It looks like you are trying to use a third party renderer. You will need to use the launch function from the third party renderer crate.");
|
||||
return dioxus_web::launch::launch_virtual_dom(vdom, platform_config);
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "fullstack")))]
|
||||
return dioxus_web::launch::launch(app, contexts, configs);
|
||||
}
|
||||
|
||||
#[cfg(feature = "liveview")]
|
||||
if matches!(platform, KnownPlatform::Liveview) {
|
||||
return dioxus_liveview::launch::launch(app, contexts, configs);
|
||||
}
|
||||
|
||||
// If the platform is not one of the above, then we assume it's a custom platform
|
||||
if let KnownPlatform::Other(launch_fn) = platform {
|
||||
return launch_fn(app, contexts, configs);
|
||||
}
|
||||
|
||||
// If we're here, then we have no platform feature enabled and third-party-renderer is enabled
|
||||
if cfg!(feature = "third-party-renderer") {
|
||||
panic!("No first party renderer feature enabled. It looks like you are trying to use a third party renderer. You will need to use the launch function from the third party renderer crate.");
|
||||
}
|
||||
|
||||
panic!("No platform feature enabled. Please enable one of the following features: liveview, desktop, mobile, web, tui, fullstack to use the launch API.")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn launch(app: fn() -> Element) {
|
||||
#[allow(deprecated)]
|
||||
LaunchBuilder::new().launch(app)
|
||||
}
|
||||
|
||||
#[cfg(feature = "web")]
|
||||
fn web_launch(
|
||||
root: fn() -> dioxus_core::Element,
|
||||
contexts: Vec<super::ContextFn>,
|
||||
platform_config: Vec<Box<dyn std::any::Any>>,
|
||||
) {
|
||||
// If the server feature is enabled, launch the client with hydration enabled
|
||||
#[cfg(feature = "fullstack")]
|
||||
{
|
||||
let platform_config = platform_config
|
||||
.into_iter()
|
||||
.find_map(|cfg| cfg.downcast::<dioxus_web::Config>().ok())
|
||||
.unwrap_or_default()
|
||||
.hydrate(true);
|
||||
|
||||
// If there is a base path set, call server functions from that base path
|
||||
if let Some(base_path) = dioxus_cli_config::web_base_path() {
|
||||
let base_path = base_path.trim_matches('/');
|
||||
crate::prelude::server_fn::client::set_server_url(
|
||||
format!(
|
||||
"{}/{}",
|
||||
crate::prelude::server_fn::client::get_server_url(),
|
||||
base_path
|
||||
)
|
||||
.leak(),
|
||||
);
|
||||
}
|
||||
|
||||
let factory = move || {
|
||||
let mut vdom = dioxus_core::VirtualDom::new(root);
|
||||
for context in contexts {
|
||||
vdom.insert_any_root_context(context());
|
||||
}
|
||||
|
||||
#[cfg(feature = "document")]
|
||||
{
|
||||
#[cfg(feature = "fullstack")]
|
||||
use dioxus_fullstack::document;
|
||||
let document = std::rc::Rc::new(document::web::FullstackWebDocument)
|
||||
as std::rc::Rc<dyn crate::prelude::document::Document>;
|
||||
vdom.provide_root_context(document);
|
||||
}
|
||||
vdom
|
||||
};
|
||||
|
||||
dioxus_web::launch::launch_virtual_dom(factory(), platform_config)
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "fullstack")))]
|
||||
dioxus_web::launch::launch(root, contexts, platform_config);
|
||||
}
|
||||
@@ -21,7 +21,7 @@ tracing = { workspace = true }
|
||||
slab = { workspace = true }
|
||||
futures-util = { workspace = true}
|
||||
generational-box.workspace = true
|
||||
rustversion = "1.0.17"
|
||||
rustversion = { workspace = true }
|
||||
warnings = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -28,7 +28,7 @@ tokio = { workspace = true, features = ["fs", "io-util"], optional = true }
|
||||
futures-channel = { workspace = true }
|
||||
serde_json = { version = "1", optional = true }
|
||||
tracing.workspace = true
|
||||
rustversion = "1.0.17"
|
||||
rustversion = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
lazy-js-bundle = { workspace = true }
|
||||
|
||||
@@ -16,3 +16,4 @@ thiserror = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
lru = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
walkdir = { workspace = true }
|
||||
@@ -37,7 +37,7 @@ impl IncrementalRendererConfig {
|
||||
static_dir: PathBuf::from("./static"),
|
||||
memory_cache_limit: 10000,
|
||||
invalidate_after: None,
|
||||
clear_cache: true,
|
||||
clear_cache: false,
|
||||
pre_render: false,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
map_path: None,
|
||||
|
||||
@@ -55,8 +55,26 @@ impl FileSystemCache {
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
// clear the static directory
|
||||
let _ = std::fs::remove_dir_all(&self.static_dir);
|
||||
// clear the static directory of index.html files contained within folders
|
||||
for entry in std::fs::read_dir(&self.static_dir)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.flatten()
|
||||
{
|
||||
if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) {
|
||||
for entry in walkdir::WalkDir::new(entry.path()).into_iter().flatten() {
|
||||
if entry.file_type().is_file() {
|
||||
if let Some(fnmae) = entry.file_name().to_str() {
|
||||
if fnmae.ends_with(".html") {
|
||||
if let Err(err) = std::fs::remove_file(entry.path()) {
|
||||
tracing::error!("Failed to remove file: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn invalidate(&mut self, route: &str) {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
[package]
|
||||
name = "dioxus-native"
|
||||
version = { workspace = true }
|
||||
authors = ["Jonathan Kelley", "Nico Burns"]
|
||||
edition = "2021"
|
||||
description = "Native renderer for Dioxus based on blitz"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||
homepage = "https://dioxuslabs.com/learn/0.6/getting_started"
|
||||
keywords = ["dom", "ui", "gui", "react"]
|
||||
|
||||
|
||||
[features]
|
||||
default = ["accessibility", "hot-reload", "menu", "tracing", "net", "svg"]
|
||||
svg = ["blitz-dom/svg", "blitz-renderer-vello/svg"]
|
||||
net = ["dep:tokio", "dep:blitz-net"]
|
||||
accessibility = ["blitz-shell/accessibility", "blitz-dom/accessibility"]
|
||||
menu = ["blitz-shell/menu"]
|
||||
tracing = ["dep:tracing", "blitz-shell/tracing", "blitz-dom/tracing"]
|
||||
hot-reload = ["dep:dioxus-cli-config", "dep:dioxus-devtools"]
|
||||
autofocus = []
|
||||
|
||||
[dependencies]
|
||||
# Blitz dependencies
|
||||
blitz-renderer-vello = { version = "0.1.0-alpha.1", default-features = false }
|
||||
blitz-dom = { version = "0.1.0-alpha.1", default-features = false }
|
||||
blitz-net = { version = "0.1.0-alpha.1", optional = true }
|
||||
blitz-traits = { version = "0.1.0-alpha.1" }
|
||||
blitz-shell = { version = "0.1.0-alpha.1", default-features = false }
|
||||
# blitz-renderer-vello = { git = "https://github.com/dioxuslabs/blitz", rev="c1a7ecf06d1760a268e0046dc0e43f6c796ddc3c", default-features = false }
|
||||
# blitz-dom = { git = "https://github.com/dioxuslabs/blitz", rev="c1a7ecf06d1760a268e0046dc0e43f6c796ddc3c", default-features = false }
|
||||
# blitz-net = { git = "https://github.com/dioxuslabs/blitz", rev="c1a7ecf06d1760a268e0046dc0e43f6c796ddc3c", optional = true }
|
||||
# blitz-traits = { git = "https://github.com/dioxuslabs/blitz", rev="c1a7ecf06d1760a268e0046dc0e43f6c796ddc3c" }
|
||||
# blitz-shell = { git = "https://github.com/dioxuslabs/blitz", rev="c1a7ecf06d1760a268e0046dc0e43f6c796ddc3c", default-features = false }
|
||||
|
||||
# DioxusLabs dependencies
|
||||
dioxus-core = { workspace = true }
|
||||
dioxus-html = { workspace = true }
|
||||
dioxus-asset-resolver = { workspace = true }
|
||||
dioxus-cli-config = { workspace = true, optional = true }
|
||||
dioxus-devtools = { workspace = true, optional = true }
|
||||
dioxus-history = { workspace = true }
|
||||
dioxus-document = { workspace = true }
|
||||
|
||||
# Windowing & Input
|
||||
winit = { workspace = true }
|
||||
keyboard-types = { workspace = true }
|
||||
|
||||
# IO & Networking
|
||||
tokio = { workspace = true, features = ["rt-multi-thread"], optional = true }
|
||||
|
||||
# Other dependencies
|
||||
tracing = { workspace = true, optional = true }
|
||||
rustc-hash = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
@@ -0,0 +1,80 @@
|
||||
use blitz_shell::BlitzShellNetCallback;
|
||||
use std::sync::Arc;
|
||||
|
||||
use blitz_dom::{
|
||||
net::{CssHandler, ImageHandler, Resource},
|
||||
util::ImageType,
|
||||
BaseDocument,
|
||||
};
|
||||
use blitz_shell::BlitzShellEvent;
|
||||
use blitz_traits::net::{NetCallback, NetProvider};
|
||||
use winit::event_loop::EventLoopProxy;
|
||||
|
||||
use crate::NodeId;
|
||||
|
||||
pub struct DioxusNativeNetProvider {
|
||||
callback: Arc<dyn NetCallback<Data = Resource> + 'static>,
|
||||
inner_net_provider: Arc<dyn NetProvider<Data = Resource> + 'static>,
|
||||
}
|
||||
impl DioxusNativeNetProvider {
|
||||
pub fn shared(proxy: EventLoopProxy<BlitzShellEvent>) -> Arc<dyn NetProvider<Data = Resource>> {
|
||||
Arc::new(Self::new(proxy)) as Arc<dyn NetProvider<Data = Resource>>
|
||||
}
|
||||
|
||||
pub fn new(proxy: EventLoopProxy<BlitzShellEvent>) -> Self {
|
||||
let net_callback = BlitzShellNetCallback::shared(proxy);
|
||||
let net_provider = blitz_net::Provider::shared(net_callback.clone());
|
||||
Self {
|
||||
callback: net_callback,
|
||||
inner_net_provider: net_provider,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NetProvider for DioxusNativeNetProvider {
|
||||
type Data = Resource;
|
||||
|
||||
fn fetch(
|
||||
&self,
|
||||
doc_id: usize,
|
||||
request: blitz_traits::net::Request,
|
||||
handler: blitz_traits::net::BoxedHandler<Self::Data>,
|
||||
) {
|
||||
if request.url.scheme() == "dioxus" {
|
||||
match dioxus_asset_resolver::serve_asset_from_raw_path(request.url.path()) {
|
||||
Ok(res) => {
|
||||
tracing::trace!("fetching asset from file system success {request:#?}");
|
||||
handler.bytes(doc_id, res.into_body().into(), self.callback.clone())
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::warn!("fetching asset from file system error {request:#?}");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.inner_net_provider.fetch(doc_id, request, handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fetch_linked_stylesheet(doc: &BaseDocument, node_id: NodeId, queued_url: String) {
|
||||
let url = doc.resolve_url(&queued_url);
|
||||
doc.net_provider.fetch(
|
||||
doc.id(),
|
||||
blitz_traits::net::Request::get(url.clone()),
|
||||
Box::new(CssHandler {
|
||||
node: node_id,
|
||||
source_url: url,
|
||||
guard: doc.guard.clone(),
|
||||
provider: doc.net_provider.clone(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn fetch_image(doc: &BaseDocument, node_id: usize, queued_image: String) {
|
||||
let src = doc.resolve_url(&queued_image);
|
||||
doc.net_provider.fetch(
|
||||
doc.id(),
|
||||
blitz_traits::net::Request::get(src),
|
||||
Box::new(ImageHandler::new(node_id, ImageType::Image)),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
use blitz_shell::BlitzShellEvent;
|
||||
use dioxus_document::{Document, NoOpDocument};
|
||||
use winit::{event_loop::EventLoopProxy, window::WindowId};
|
||||
|
||||
use crate::DioxusNativeEvent;
|
||||
|
||||
pub struct DioxusNativeDocument {
|
||||
pub(crate) proxy: EventLoopProxy<BlitzShellEvent>,
|
||||
pub(crate) window: WindowId,
|
||||
}
|
||||
|
||||
impl DioxusNativeDocument {
|
||||
pub(crate) fn new(proxy: EventLoopProxy<BlitzShellEvent>, window: WindowId) -> Self {
|
||||
Self { proxy, window }
|
||||
}
|
||||
}
|
||||
|
||||
impl Document for DioxusNativeDocument {
|
||||
fn eval(&self, _js: String) -> dioxus_document::Eval {
|
||||
NoOpDocument.eval(_js)
|
||||
}
|
||||
|
||||
fn create_head_element(
|
||||
&self,
|
||||
name: &str,
|
||||
attributes: &[(&str, String)],
|
||||
contents: Option<String>,
|
||||
) {
|
||||
let window = self.window;
|
||||
_ = self.proxy.send_event(BlitzShellEvent::embedder_event(
|
||||
DioxusNativeEvent::CreateHeadElement {
|
||||
name: name.to_string(),
|
||||
attributes: attributes
|
||||
.iter()
|
||||
.map(|(name, value)| (name.to_string(), value.clone()))
|
||||
.collect(),
|
||||
contents,
|
||||
window,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
fn set_title(&self, title: String) {
|
||||
self.create_head_element("title", &[], Some(title));
|
||||
}
|
||||
|
||||
fn create_meta(&self, props: dioxus_document::MetaProps) {
|
||||
let attributes = props.attributes();
|
||||
self.create_head_element("meta", &attributes, None);
|
||||
}
|
||||
|
||||
fn create_script(&self, props: dioxus_document::ScriptProps) {
|
||||
let attributes = props.attributes();
|
||||
self.create_head_element("script", &attributes, props.script_contents().ok());
|
||||
}
|
||||
|
||||
fn create_style(&self, props: dioxus_document::StyleProps) {
|
||||
let attributes = props.attributes();
|
||||
self.create_head_element("style", &attributes, props.style_contents().ok());
|
||||
}
|
||||
|
||||
fn create_link(&self, props: dioxus_document::LinkProps) {
|
||||
let attributes = props.attributes();
|
||||
self.create_head_element("link", &attributes, None);
|
||||
}
|
||||
|
||||
fn create_head_component(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
use blitz_renderer_vello::BlitzVelloRenderer;
|
||||
use blitz_shell::BlitzApplication;
|
||||
use dioxus_core::{ScopeId, VirtualDom};
|
||||
use dioxus_history::{History, MemoryHistory};
|
||||
use std::{collections::HashSet, rc::Rc};
|
||||
use winit::application::ApplicationHandler;
|
||||
use winit::event::{StartCause, WindowEvent};
|
||||
use winit::event_loop::{ActiveEventLoop, EventLoopProxy};
|
||||
use winit::window::WindowId;
|
||||
|
||||
use crate::{
|
||||
assets::DioxusNativeNetProvider, contexts::DioxusNativeDocument,
|
||||
mutation_writer::MutationWriter, BlitzShellEvent, DioxusDocument, DioxusNativeEvent,
|
||||
WindowConfig,
|
||||
};
|
||||
|
||||
pub struct DioxusNativeApplication {
|
||||
pending_vdom: Option<VirtualDom>,
|
||||
inner: BlitzApplication<DioxusDocument, BlitzVelloRenderer>,
|
||||
proxy: EventLoopProxy<BlitzShellEvent>,
|
||||
}
|
||||
|
||||
impl DioxusNativeApplication {
|
||||
pub fn new(proxy: EventLoopProxy<BlitzShellEvent>, vdom: VirtualDom) -> Self {
|
||||
Self {
|
||||
pending_vdom: Some(vdom),
|
||||
inner: BlitzApplication::new(proxy.clone()),
|
||||
proxy,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_window(&mut self, window_config: WindowConfig<DioxusDocument, BlitzVelloRenderer>) {
|
||||
self.inner.add_window(window_config);
|
||||
}
|
||||
|
||||
fn handle_blitz_shell_event(
|
||||
&mut self,
|
||||
event_loop: &ActiveEventLoop,
|
||||
event: &DioxusNativeEvent,
|
||||
) {
|
||||
match event {
|
||||
#[cfg(all(
|
||||
feature = "hot-reload",
|
||||
debug_assertions,
|
||||
not(target_os = "android"),
|
||||
not(target_os = "ios")
|
||||
))]
|
||||
DioxusNativeEvent::DevserverEvent(event) => match event {
|
||||
dioxus_devtools::DevserverMsg::HotReload(hotreload_message) => {
|
||||
for window in self.inner.windows.values_mut() {
|
||||
dioxus_devtools::apply_changes(&window.doc.vdom, hotreload_message);
|
||||
window.poll();
|
||||
}
|
||||
}
|
||||
dioxus_devtools::DevserverMsg::Shutdown => event_loop.exit(),
|
||||
dioxus_devtools::DevserverMsg::FullReloadStart => {}
|
||||
dioxus_devtools::DevserverMsg::FullReloadFailed => {}
|
||||
dioxus_devtools::DevserverMsg::FullReloadCommand => {}
|
||||
},
|
||||
|
||||
DioxusNativeEvent::CreateHeadElement {
|
||||
name,
|
||||
attributes,
|
||||
contents,
|
||||
window,
|
||||
} => {
|
||||
if let Some(window) = self.inner.windows.get_mut(window) {
|
||||
window.doc.create_head_element(name, attributes, contents);
|
||||
window.poll();
|
||||
}
|
||||
}
|
||||
|
||||
// Suppress unused variable warning
|
||||
#[cfg(not(all(
|
||||
feature = "hot-reload",
|
||||
debug_assertions,
|
||||
not(target_os = "android"),
|
||||
not(target_os = "ios")
|
||||
)))]
|
||||
_ => {
|
||||
let _ = event_loop;
|
||||
let _ = event;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ApplicationHandler<BlitzShellEvent> for DioxusNativeApplication {
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
tracing::debug!("Injecting document provider into all windows");
|
||||
let vdom = self.pending_vdom.take().unwrap();
|
||||
|
||||
#[cfg(feature = "net")]
|
||||
let net_provider = {
|
||||
let proxy = self.proxy.clone();
|
||||
let net_provider = DioxusNativeNetProvider::shared(proxy);
|
||||
Some(net_provider)
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "net"))]
|
||||
let net_provider = None;
|
||||
|
||||
// Create document + window from the baked virtualdom
|
||||
let doc = DioxusDocument::new(vdom, net_provider);
|
||||
let window = WindowConfig::new(doc);
|
||||
|
||||
// little hack since View::init is not public - fix this once alpha-2 is out
|
||||
let old_windows = self.inner.windows.keys().copied().collect::<HashSet<_>>();
|
||||
self.add_window(window);
|
||||
self.inner.resumed(event_loop);
|
||||
let new_windows = self.inner.windows.keys().cloned().collect::<HashSet<_>>();
|
||||
|
||||
// todo(jon): we should actually mess with the pending windows instead of passing along the contexts
|
||||
for window_id in new_windows.difference(&old_windows) {
|
||||
let window = self.inner.windows.get_mut(window_id).unwrap();
|
||||
window.doc.vdom.in_runtime(|| {
|
||||
let shared: Rc<dyn dioxus_document::Document> =
|
||||
Rc::new(DioxusNativeDocument::new(self.proxy.clone(), *window_id));
|
||||
ScopeId::ROOT.provide_context(shared);
|
||||
});
|
||||
|
||||
// Add history
|
||||
let history_provider: Rc<dyn History> = Rc::new(MemoryHistory::default());
|
||||
window
|
||||
.doc
|
||||
.vdom
|
||||
.in_runtime(|| ScopeId::ROOT.provide_context(history_provider));
|
||||
|
||||
// Queue rebuild
|
||||
let mut writer = MutationWriter::new(&mut window.doc.inner, &mut window.doc.vdom_state);
|
||||
window.doc.vdom.rebuild(&mut writer);
|
||||
drop(writer);
|
||||
|
||||
// And then request redraw
|
||||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
fn suspended(&mut self, event_loop: &ActiveEventLoop) {
|
||||
self.inner.suspended(event_loop);
|
||||
}
|
||||
|
||||
fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) {
|
||||
self.inner.new_events(event_loop, cause);
|
||||
}
|
||||
|
||||
fn window_event(
|
||||
&mut self,
|
||||
event_loop: &ActiveEventLoop,
|
||||
window_id: WindowId,
|
||||
event: WindowEvent,
|
||||
) {
|
||||
self.inner.window_event(event_loop, window_id, event);
|
||||
}
|
||||
|
||||
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: BlitzShellEvent) {
|
||||
match event {
|
||||
BlitzShellEvent::Embedder(event) => {
|
||||
if let Some(event) = event.downcast_ref::<DioxusNativeEvent>() {
|
||||
self.handle_blitz_shell_event(event_loop, event);
|
||||
}
|
||||
}
|
||||
event => self.inner.user_event(event_loop, event),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,564 @@
|
||||
//! Integration between Dioxus and Blitz
|
||||
|
||||
use std::{any::Any, collections::HashMap, rc::Rc, sync::Arc};
|
||||
|
||||
use blitz_dom::{
|
||||
local_name, namespace_url,
|
||||
net::Resource,
|
||||
node::{Attribute, NodeSpecificData},
|
||||
ns, Atom, BaseDocument, ElementNodeData, Node, NodeData, QualName, DEFAULT_CSS,
|
||||
};
|
||||
|
||||
use blitz_traits::{net::NetProvider, ColorScheme, Document, DomEvent, DomEventData, Viewport};
|
||||
use dioxus_core::{ElementId, Event, VirtualDom};
|
||||
use dioxus_html::{set_event_converter, FormValue, PlatformEventData};
|
||||
use futures_util::{pin_mut, FutureExt};
|
||||
|
||||
use super::event_handler::{BlitzKeyboardData, NativeClickData, NativeConverter, NativeFormData};
|
||||
use crate::mutation_writer::{DioxusState, MutationWriter};
|
||||
use crate::NodeId;
|
||||
|
||||
pub(crate) fn qual_name(local_name: &str, namespace: Option<&str>) -> QualName {
|
||||
QualName {
|
||||
prefix: None,
|
||||
ns: namespace.map(Atom::from).unwrap_or(ns!(html)),
|
||||
local: Atom::from(local_name),
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DioxusDocument {
|
||||
pub(crate) vdom: VirtualDom,
|
||||
pub(crate) vdom_state: DioxusState,
|
||||
pub(crate) inner: BaseDocument,
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) html_element_id: NodeId,
|
||||
#[allow(unused)]
|
||||
pub(crate) head_element_id: NodeId,
|
||||
#[allow(unused)]
|
||||
pub(crate) body_element_id: NodeId,
|
||||
#[allow(unused)]
|
||||
pub(crate) main_element_id: NodeId,
|
||||
}
|
||||
|
||||
// Implement DocumentLike and required traits for DioxusDocument
|
||||
|
||||
impl AsRef<BaseDocument> for DioxusDocument {
|
||||
fn as_ref(&self) -> &BaseDocument {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
impl AsMut<BaseDocument> for DioxusDocument {
|
||||
fn as_mut(&mut self) -> &mut BaseDocument {
|
||||
&mut self.inner
|
||||
}
|
||||
}
|
||||
impl From<DioxusDocument> for BaseDocument {
|
||||
fn from(doc: DioxusDocument) -> BaseDocument {
|
||||
doc.inner
|
||||
}
|
||||
}
|
||||
impl Document for DioxusDocument {
|
||||
type Doc = BaseDocument;
|
||||
|
||||
fn poll(&mut self, mut cx: std::task::Context) -> bool {
|
||||
{
|
||||
let fut = self.vdom.wait_for_work();
|
||||
pin_mut!(fut);
|
||||
|
||||
match fut.poll_unpin(&mut cx) {
|
||||
std::task::Poll::Ready(_) => {}
|
||||
std::task::Poll::Pending => return false,
|
||||
}
|
||||
}
|
||||
|
||||
let mut writer = MutationWriter::new(&mut self.inner, &mut self.vdom_state);
|
||||
self.vdom.render_immediate(&mut writer);
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn id(&self) -> usize {
|
||||
self.inner.id()
|
||||
}
|
||||
|
||||
fn handle_event(&mut self, event: &mut DomEvent) {
|
||||
let chain = self.inner.node_chain(event.target);
|
||||
|
||||
set_event_converter(Box::new(NativeConverter {}));
|
||||
|
||||
let renderer_event = event.clone();
|
||||
|
||||
let mut prevent_default = false;
|
||||
let mut stop_propagation = false;
|
||||
|
||||
match &event.data {
|
||||
DomEventData::MouseMove(data)
|
||||
| DomEventData::MouseDown(data)
|
||||
| DomEventData::MouseUp(data) => {
|
||||
let click_event_data = wrap_event_data(NativeClickData(data.clone()));
|
||||
|
||||
for node_id in chain.clone().into_iter() {
|
||||
let node = &self.inner.tree()[node_id];
|
||||
let dioxus_id = node.element_data().and_then(DioxusDocument::dioxus_id);
|
||||
|
||||
if let Some(id) = dioxus_id {
|
||||
let click_event = Event::new(click_event_data.clone(), true);
|
||||
self.vdom
|
||||
.runtime()
|
||||
.handle_event(event.name(), click_event.clone(), id);
|
||||
prevent_default |= !click_event.default_action_enabled();
|
||||
stop_propagation |= !click_event.propagates();
|
||||
}
|
||||
|
||||
if !event.bubbles || stop_propagation {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DomEventData::Click(data) => {
|
||||
let click_event_data = wrap_event_data(NativeClickData(data.clone()));
|
||||
|
||||
for node_id in chain.clone().into_iter() {
|
||||
let node = &self.inner.tree()[node_id];
|
||||
let dioxus_id = node.element_data().and_then(DioxusDocument::dioxus_id);
|
||||
let mut trigger_label = false;
|
||||
|
||||
if let Some(id) = dioxus_id {
|
||||
// Trigger click event
|
||||
let click_event = Event::new(click_event_data.clone(), true);
|
||||
self.vdom
|
||||
.runtime()
|
||||
.handle_event("click", click_event.clone(), id);
|
||||
prevent_default |= !click_event.default_action_enabled();
|
||||
stop_propagation |= !click_event.propagates();
|
||||
|
||||
if !prevent_default {
|
||||
let mut default_event =
|
||||
DomEvent::new(node_id, renderer_event.data.clone());
|
||||
self.inner.as_mut().handle_event(&mut default_event);
|
||||
prevent_default = true;
|
||||
|
||||
let element = self.inner.tree()[node_id].element_data().unwrap();
|
||||
trigger_label = element.name.local == *"label";
|
||||
|
||||
//TODO Check for other inputs which trigger input event on click here, eg radio
|
||||
let triggers_input_event = element.name.local == local_name!("input")
|
||||
&& element.attr(local_name!("type")) == Some("checkbox");
|
||||
if triggers_input_event {
|
||||
let form_data =
|
||||
wrap_event_data(self.input_event_form_data(&chain, element));
|
||||
let input_event = Event::new(form_data, true);
|
||||
self.vdom.runtime().handle_event("input", input_event, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Clicking labels triggers click, and possibly input event, of bound input
|
||||
if trigger_label {
|
||||
if let Some((dioxus_id, node_id)) = self.label_bound_input_element(node_id)
|
||||
{
|
||||
let click_event = Event::new(click_event_data.clone(), true);
|
||||
self.vdom.runtime().handle_event(
|
||||
"click",
|
||||
click_event.clone(),
|
||||
dioxus_id,
|
||||
);
|
||||
|
||||
// Handle default DOM event
|
||||
if click_event.default_action_enabled() {
|
||||
let DomEventData::Click(event) = &renderer_event.data else {
|
||||
unreachable!();
|
||||
};
|
||||
let input_click_data = self
|
||||
.inner
|
||||
.get_node(node_id)
|
||||
.unwrap()
|
||||
.synthetic_click_event(event.mods);
|
||||
let mut default_event = DomEvent::new(node_id, input_click_data);
|
||||
self.inner.as_mut().handle_event(&mut default_event);
|
||||
prevent_default = true;
|
||||
|
||||
// TODO: Generated click events should bubble separately
|
||||
// prevent_default |= !click_event.default_action_enabled();
|
||||
|
||||
//TODO Check for other inputs which trigger input event on click here, eg radio
|
||||
let element_data = self
|
||||
.inner
|
||||
.get_node(node_id)
|
||||
.unwrap()
|
||||
.element_data()
|
||||
.unwrap();
|
||||
let triggers_input_event =
|
||||
element_data.attr(local_name!("type")) == Some("checkbox");
|
||||
let form_data = wrap_event_data(
|
||||
self.input_event_form_data(&chain, element_data),
|
||||
);
|
||||
if triggers_input_event {
|
||||
let input_event = Event::new(form_data, true);
|
||||
self.vdom.runtime().handle_event(
|
||||
"input",
|
||||
input_event,
|
||||
dioxus_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !event.bubbles || stop_propagation {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DomEventData::KeyPress(kevent) => {
|
||||
let key_event_data = wrap_event_data(BlitzKeyboardData(kevent.clone()));
|
||||
|
||||
for node_id in chain.clone().into_iter() {
|
||||
let node = &self.inner.tree()[node_id];
|
||||
let dioxus_id = node.element_data().and_then(DioxusDocument::dioxus_id);
|
||||
println!("{} {:?}", node_id, dioxus_id);
|
||||
|
||||
if let Some(id) = dioxus_id {
|
||||
if kevent.state.is_pressed() {
|
||||
// Handle keydown event
|
||||
let event = Event::new(key_event_data.clone(), true);
|
||||
self.vdom
|
||||
.runtime()
|
||||
.handle_event("keydown", event.clone(), id);
|
||||
prevent_default |= !event.default_action_enabled();
|
||||
stop_propagation |= !event.propagates();
|
||||
|
||||
if !prevent_default && kevent.text.is_some() {
|
||||
// Handle keypress event
|
||||
let event = Event::new(key_event_data.clone(), true);
|
||||
self.vdom
|
||||
.runtime()
|
||||
.handle_event("keypress", event.clone(), id);
|
||||
prevent_default |= !event.default_action_enabled();
|
||||
stop_propagation |= !event.propagates();
|
||||
|
||||
if !prevent_default {
|
||||
// Handle default DOM event
|
||||
let mut default_event =
|
||||
DomEvent::new(node_id, renderer_event.data.clone());
|
||||
self.inner.as_mut().handle_event(&mut default_event);
|
||||
prevent_default = true;
|
||||
|
||||
// Handle input event
|
||||
let element =
|
||||
self.inner.tree()[node_id].element_data().unwrap();
|
||||
let triggers_input_event = &element.name.local == "input"
|
||||
&& matches!(
|
||||
element.attr(local_name!("type")),
|
||||
None | Some("text" | "password" | "email" | "search")
|
||||
);
|
||||
if triggers_input_event {
|
||||
let form_data = wrap_event_data(dbg!(
|
||||
self.input_event_form_data(&chain, element)
|
||||
));
|
||||
let input_event = Event::new(form_data, true);
|
||||
self.vdom.runtime().handle_event("input", input_event, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Handle keyup event
|
||||
let event = Event::new(key_event_data.clone(), true);
|
||||
self.vdom.runtime().handle_event("keyup", event.clone(), id);
|
||||
prevent_default |= !event.default_action_enabled();
|
||||
stop_propagation |= !event.propagates();
|
||||
}
|
||||
}
|
||||
|
||||
if !event.bubbles || stop_propagation {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DomEventData::Hover => {}
|
||||
|
||||
// TODO: Implement IME and Hover events handling
|
||||
DomEventData::Ime(_) => {}
|
||||
}
|
||||
|
||||
if !event.cancelable || !prevent_default {
|
||||
self.inner.as_mut().handle_event(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn wrap_event_data<T: Any>(value: T) -> Rc<dyn Any> {
|
||||
Rc::new(PlatformEventData::new(Box::new(value)))
|
||||
}
|
||||
|
||||
impl DioxusDocument {
|
||||
/// Generate the FormData from an input event
|
||||
/// Currently only cares about input checkboxes
|
||||
pub fn input_event_form_data(
|
||||
&self,
|
||||
parent_chain: &[usize],
|
||||
element_node_data: &ElementNodeData,
|
||||
) -> NativeFormData {
|
||||
let parent_form = parent_chain.iter().find_map(|id| {
|
||||
let node = self.inner.get_node(*id)?;
|
||||
let element_data = node.element_data()?;
|
||||
if element_data.name.local == local_name!("form") {
|
||||
Some(node)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
let values = if let Some(parent_form) = parent_form {
|
||||
let mut values = HashMap::<String, FormValue>::new();
|
||||
for form_input in self.input_descendents(parent_form).into_iter() {
|
||||
// Match html behaviour here. To be included in values:
|
||||
// - input must have a name
|
||||
// - if its an input, we only include it if checked
|
||||
// - if value is not specified, it defaults to 'on'
|
||||
if let Some(name) = form_input.attr(local_name!("name")) {
|
||||
if form_input.attr(local_name!("type")) == Some("checkbox")
|
||||
&& form_input
|
||||
.element_data()
|
||||
.and_then(|data| data.checkbox_input_checked())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let value = form_input
|
||||
.attr(local_name!("value"))
|
||||
.unwrap_or("on")
|
||||
.to_string();
|
||||
values.insert(name.to_string(), FormValue(vec![value]));
|
||||
}
|
||||
}
|
||||
}
|
||||
values
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let value = match &element_node_data.node_specific_data {
|
||||
NodeSpecificData::CheckboxInput(checked) => checked.to_string(),
|
||||
NodeSpecificData::TextInput(input_data) => input_data.editor.text().to_string(),
|
||||
_ => element_node_data
|
||||
.attr(local_name!("value"))
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
};
|
||||
|
||||
NativeFormData { value, values }
|
||||
}
|
||||
|
||||
/// Collect all the inputs which are descendents of a given node
|
||||
fn input_descendents(&self, node: &Node) -> Vec<&Node> {
|
||||
node.children
|
||||
.iter()
|
||||
.flat_map(|id| {
|
||||
let mut res = Vec::<&Node>::new();
|
||||
let Some(n) = self.inner.get_node(*id) else {
|
||||
return res;
|
||||
};
|
||||
let Some(element_data) = n.element_data() else {
|
||||
return res;
|
||||
};
|
||||
if element_data.name.local == local_name!("input") {
|
||||
res.push(n);
|
||||
}
|
||||
res.extend(self.input_descendents(n).iter());
|
||||
res
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
vdom: VirtualDom,
|
||||
net_provider: Option<Arc<dyn NetProvider<Data = Resource>>>,
|
||||
) -> Self {
|
||||
let viewport = Viewport::new(0, 0, 1.0, ColorScheme::Light);
|
||||
let mut doc = BaseDocument::new(viewport);
|
||||
|
||||
// Set net provider
|
||||
if let Some(net_provider) = net_provider {
|
||||
doc.set_net_provider(net_provider);
|
||||
}
|
||||
|
||||
// Create a virtual "html" element to act as the root element, as we won't necessarily
|
||||
// have a single root otherwise, while the rest of blitz requires that we do
|
||||
let html_element_id = doc.create_node(NodeData::Element(ElementNodeData::new(
|
||||
qual_name("html", None),
|
||||
Vec::new(),
|
||||
)));
|
||||
let root_node_id = doc.root_node().id;
|
||||
let html_element = doc.get_node_mut(html_element_id).unwrap();
|
||||
html_element.parent = Some(root_node_id);
|
||||
let root_node = doc.get_node_mut(root_node_id).unwrap();
|
||||
root_node.children.push(html_element_id);
|
||||
|
||||
// Create the body element
|
||||
let body_element_id = doc.create_node(NodeData::Element(ElementNodeData::new(
|
||||
qual_name("body", None),
|
||||
Vec::new(),
|
||||
)));
|
||||
let html_element = doc.get_node_mut(html_element_id).unwrap();
|
||||
html_element.children.push(body_element_id);
|
||||
let body_element = doc.get_node_mut(body_element_id).unwrap();
|
||||
body_element.parent = Some(html_element_id);
|
||||
|
||||
// Create another virtual element to hold the root <div id="main"></div> under the html element
|
||||
let main_element_id = doc.create_node(NodeData::Element(ElementNodeData::new(
|
||||
qual_name("div", Some("id")),
|
||||
vec![blitz_dom::node::Attribute {
|
||||
name: qual_name("id", None),
|
||||
value: "main".to_string(),
|
||||
}],
|
||||
)));
|
||||
let body_element = doc.get_node_mut(body_element_id).unwrap();
|
||||
body_element.children.push(main_element_id);
|
||||
let main_element = doc.get_node_mut(main_element_id).unwrap();
|
||||
main_element.parent = Some(body_element_id);
|
||||
|
||||
// Create the head element
|
||||
let head_element_id = doc.create_node(NodeData::Element(ElementNodeData::new(
|
||||
qual_name("head", None),
|
||||
Vec::new(),
|
||||
)));
|
||||
let head_element = doc.get_node_mut(head_element_id).unwrap();
|
||||
head_element.parent = Some(html_element_id);
|
||||
let html_element = doc.get_node_mut(html_element_id).unwrap();
|
||||
html_element.children.push(head_element_id);
|
||||
|
||||
// Include default and user-specified stylesheets
|
||||
doc.add_user_agent_stylesheet(DEFAULT_CSS);
|
||||
|
||||
let state = DioxusState::create(&mut doc, main_element_id);
|
||||
let mut doc = Self {
|
||||
vdom,
|
||||
vdom_state: state,
|
||||
inner: doc,
|
||||
html_element_id,
|
||||
head_element_id,
|
||||
body_element_id,
|
||||
main_element_id,
|
||||
};
|
||||
|
||||
doc.inner.set_base_url("dioxus://index.html");
|
||||
|
||||
// doc.initial_build();
|
||||
|
||||
// doc.inner.print_tree();
|
||||
|
||||
doc
|
||||
}
|
||||
|
||||
pub fn initial_build(&mut self) {
|
||||
// let mut writer = MutationWriter::new(&mut self.inner, &mut self.vdom_state);
|
||||
|
||||
// self.vdom.rebuild(&mut writer);
|
||||
// dbg!(self.vdom.rebuild_to_vec());
|
||||
// std::process::exit(0);
|
||||
// dbg!(writer.state);
|
||||
}
|
||||
|
||||
pub fn label_bound_input_element(&self, label_node_id: NodeId) -> Option<(ElementId, NodeId)> {
|
||||
let bound_input_elements = self.inner.label_bound_input_elements(label_node_id);
|
||||
|
||||
// Filter down bound elements to those which have dioxus id
|
||||
bound_input_elements.into_iter().find_map(|n| {
|
||||
let target_element_data = n.element_data()?;
|
||||
let node_id = n.id;
|
||||
let dioxus_id = DioxusDocument::dioxus_id(target_element_data)?;
|
||||
Some((dioxus_id, node_id))
|
||||
})
|
||||
}
|
||||
|
||||
fn dioxus_id(element_node_data: &ElementNodeData) -> Option<ElementId> {
|
||||
Some(ElementId(
|
||||
element_node_data
|
||||
.attrs
|
||||
.iter()
|
||||
.find(|attr| *attr.name.local == *"data-dioxus-id")?
|
||||
.value
|
||||
.parse::<usize>()
|
||||
.ok()?,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn create_head_element(
|
||||
&mut self,
|
||||
name: &str,
|
||||
attributes: &[(String, String)],
|
||||
contents: &Option<String>,
|
||||
) {
|
||||
let mut stylesheet = None;
|
||||
let mut title = None;
|
||||
if name == "link" {
|
||||
let is_stylesheet = attributes
|
||||
.iter()
|
||||
.any(|(name, value)| name == "rel" && value == "stylesheet");
|
||||
if is_stylesheet {
|
||||
stylesheet = attributes
|
||||
.iter()
|
||||
.find(|(name, _value)| name == "href")
|
||||
.map(|(_name, value)| value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if name == "title" {
|
||||
title = attributes
|
||||
.iter()
|
||||
.find(|(name, _value)| name == "text")
|
||||
.and_then(|(_name, _value)| contents.clone());
|
||||
}
|
||||
|
||||
let attributes = attributes
|
||||
.iter()
|
||||
.map(|(name, value)| Attribute {
|
||||
name: qual_name(name, None),
|
||||
value: value.clone(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let new_element = self
|
||||
.inner
|
||||
.create_node(NodeData::Element(ElementNodeData::new(
|
||||
qual_name(name, None),
|
||||
attributes,
|
||||
)));
|
||||
|
||||
if let Some(contents) = contents {
|
||||
let text_node = self.inner.create_text_node(contents);
|
||||
self.inner
|
||||
.get_node_mut(new_element)
|
||||
.unwrap()
|
||||
.children
|
||||
.push(text_node);
|
||||
}
|
||||
|
||||
self.inner
|
||||
.get_node_mut(self.head_element_id)
|
||||
.unwrap()
|
||||
.children
|
||||
.push(new_element);
|
||||
|
||||
if let Some(url) = stylesheet {
|
||||
crate::assets::fetch_linked_stylesheet(&self.inner, new_element, url);
|
||||
}
|
||||
|
||||
if let Some(_title) = title {
|
||||
println!("todo: set title");
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn apply_mutations(&mut self) {
|
||||
// // Apply the mutations to the actual dom
|
||||
// let mut writer = MutationWriter {
|
||||
// doc: &mut self.inner,
|
||||
// state: &mut self.vdom_state,
|
||||
// };
|
||||
// self.vdom.render_immediate(&mut writer);
|
||||
|
||||
// println!("APPLY MUTATIONS");
|
||||
// self.inner.print_tree();
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
use winit::window::WindowId;
|
||||
|
||||
/// Dioxus-native specific event type
|
||||
pub enum DioxusNativeEvent {
|
||||
/// A hotreload event, basically telling us to update our templates.
|
||||
#[cfg(all(
|
||||
feature = "hot-reload",
|
||||
debug_assertions,
|
||||
not(target_os = "android"),
|
||||
not(target_os = "ios")
|
||||
))]
|
||||
DevserverEvent(dioxus_devtools::DevserverMsg),
|
||||
|
||||
/// Create a new head element from the Link and Title elements
|
||||
///
|
||||
/// todo(jon): these should probabkly be synchronous somehow
|
||||
CreateHeadElement {
|
||||
window: WindowId,
|
||||
name: String,
|
||||
attributes: Vec<(String, String)>,
|
||||
contents: Option<String>,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
use blitz_traits::{BlitzKeyEvent, BlitzMouseButtonEvent, MouseEventButton};
|
||||
use dioxus_html::{
|
||||
geometry::{ClientPoint, ElementPoint, PagePoint, ScreenPoint},
|
||||
input_data::{MouseButton, MouseButtonSet},
|
||||
point_interaction::{
|
||||
InteractionElementOffset, InteractionLocation, ModifiersInteraction, PointerInteraction,
|
||||
},
|
||||
AnimationData, ClipboardData, CompositionData, DragData, FocusData, FormData, FormValue,
|
||||
HasFileData, HasFormData, HasMouseData, HtmlEventConverter, ImageData, KeyboardData, MediaData,
|
||||
MountedData, MouseData, PlatformEventData, PointerData, ResizeData, ScrollData, SelectionData,
|
||||
ToggleData, TouchData, TransitionData, VisibleData, WheelData,
|
||||
};
|
||||
use dioxus_html::{HasFocusData, HasKeyboardData};
|
||||
use keyboard_types::{Code, Key, Location, Modifiers};
|
||||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub struct NativeConverter {}
|
||||
|
||||
impl HtmlEventConverter for NativeConverter {
|
||||
fn convert_form_data(&self, event: &PlatformEventData) -> FormData {
|
||||
event.downcast::<NativeFormData>().unwrap().clone().into()
|
||||
}
|
||||
|
||||
fn convert_mouse_data(&self, event: &PlatformEventData) -> MouseData {
|
||||
event.downcast::<NativeClickData>().unwrap().clone().into()
|
||||
}
|
||||
|
||||
fn convert_keyboard_data(&self, event: &PlatformEventData) -> KeyboardData {
|
||||
event
|
||||
.downcast::<BlitzKeyboardData>()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.into()
|
||||
}
|
||||
|
||||
fn convert_focus_data(&self, _event: &PlatformEventData) -> FocusData {
|
||||
NativeFocusData {}.into()
|
||||
}
|
||||
|
||||
fn convert_animation_data(&self, _event: &PlatformEventData) -> AnimationData {
|
||||
todo!("todo: convert_animation_data in dioxus-native. requires support in blitz")
|
||||
}
|
||||
|
||||
fn convert_clipboard_data(&self, _event: &PlatformEventData) -> ClipboardData {
|
||||
todo!("todo: convert_clipboard_data in dioxus-native. requires support in blitz")
|
||||
}
|
||||
|
||||
fn convert_composition_data(&self, _event: &PlatformEventData) -> CompositionData {
|
||||
todo!("todo: convert_composition_data in dioxus-native. requires support in blitz")
|
||||
}
|
||||
|
||||
fn convert_drag_data(&self, _event: &PlatformEventData) -> DragData {
|
||||
todo!("todo: convert_drag_data in dioxus-native. requires support in blitz")
|
||||
}
|
||||
|
||||
fn convert_image_data(&self, _event: &PlatformEventData) -> ImageData {
|
||||
todo!("todo: convert_image_data in dioxus-native. requires support in blitz")
|
||||
}
|
||||
|
||||
fn convert_media_data(&self, _event: &PlatformEventData) -> MediaData {
|
||||
todo!("todo: convert_media_data in dioxus-native. requires support in blitz")
|
||||
}
|
||||
|
||||
fn convert_mounted_data(&self, _event: &PlatformEventData) -> MountedData {
|
||||
todo!("todo: convert_mounted_data in dioxus-native. requires support in blitz")
|
||||
}
|
||||
|
||||
fn convert_pointer_data(&self, _event: &PlatformEventData) -> PointerData {
|
||||
todo!("todo: convert_pointer_data in dioxus-native. requires support in blitz")
|
||||
}
|
||||
|
||||
fn convert_scroll_data(&self, _event: &PlatformEventData) -> ScrollData {
|
||||
todo!("todo: convert_scroll_data in dioxus-native. requires support in blitz")
|
||||
}
|
||||
|
||||
fn convert_selection_data(&self, _event: &PlatformEventData) -> SelectionData {
|
||||
todo!("todo: convert_selection_data in dioxus-native. requires support in blitz")
|
||||
}
|
||||
|
||||
fn convert_toggle_data(&self, _event: &PlatformEventData) -> ToggleData {
|
||||
todo!("todo: convert_toggle_data in dioxus-native. requires support in blitz")
|
||||
}
|
||||
|
||||
fn convert_touch_data(&self, _event: &PlatformEventData) -> TouchData {
|
||||
todo!("todo: convert_touch_data in dioxus-native. requires support in blitz")
|
||||
}
|
||||
|
||||
fn convert_transition_data(&self, _event: &PlatformEventData) -> TransitionData {
|
||||
todo!("todo: convert_transition_data in dioxus-native. requires support in blitz")
|
||||
}
|
||||
|
||||
fn convert_wheel_data(&self, _event: &PlatformEventData) -> WheelData {
|
||||
todo!("todo: convert_wheel_data in dioxus-native. requires support in blitz")
|
||||
}
|
||||
|
||||
fn convert_resize_data(&self, _event: &PlatformEventData) -> ResizeData {
|
||||
todo!("todo: convert_resize_data in dioxus-native. requires support in blitz")
|
||||
}
|
||||
|
||||
fn convert_visible_data(&self, _event: &PlatformEventData) -> VisibleData {
|
||||
todo!("todo: convert_visible_data in dioxus-native. requires support in blitz")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct NativeFormData {
|
||||
pub value: String,
|
||||
pub values: HashMap<String, FormValue>,
|
||||
}
|
||||
|
||||
impl HasFormData for NativeFormData {
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self as &dyn std::any::Any
|
||||
}
|
||||
|
||||
fn value(&self) -> String {
|
||||
self.value.clone()
|
||||
}
|
||||
|
||||
fn values(&self) -> HashMap<String, FormValue> {
|
||||
self.values.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl HasFileData for NativeFormData {}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct BlitzKeyboardData(pub(crate) BlitzKeyEvent);
|
||||
|
||||
impl ModifiersInteraction for BlitzKeyboardData {
|
||||
fn modifiers(&self) -> Modifiers {
|
||||
self.0.modifiers
|
||||
}
|
||||
}
|
||||
|
||||
impl HasKeyboardData for BlitzKeyboardData {
|
||||
fn key(&self) -> Key {
|
||||
self.0.key.clone()
|
||||
}
|
||||
|
||||
fn code(&self) -> Code {
|
||||
self.0.code
|
||||
}
|
||||
|
||||
fn location(&self) -> Location {
|
||||
self.0.location
|
||||
}
|
||||
|
||||
fn is_auto_repeating(&self) -> bool {
|
||||
self.0.is_auto_repeating
|
||||
}
|
||||
|
||||
fn is_composing(&self) -> bool {
|
||||
self.0.is_composing
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self as &dyn Any
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct NativeClickData(pub(crate) BlitzMouseButtonEvent);
|
||||
|
||||
impl InteractionLocation for NativeClickData {
|
||||
fn client_coordinates(&self) -> ClientPoint {
|
||||
ClientPoint::new(self.0.x as _, self.0.y as _)
|
||||
}
|
||||
|
||||
// these require blitz to pass them along, or a dom rect
|
||||
fn screen_coordinates(&self) -> ScreenPoint {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn page_coordinates(&self) -> PagePoint {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
impl InteractionElementOffset for NativeClickData {
|
||||
fn element_coordinates(&self) -> ElementPoint {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl ModifiersInteraction for NativeClickData {
|
||||
fn modifiers(&self) -> Modifiers {
|
||||
self.0.mods
|
||||
}
|
||||
}
|
||||
|
||||
impl PointerInteraction for NativeClickData {
|
||||
fn trigger_button(&self) -> Option<MouseButton> {
|
||||
Some(match self.0.button {
|
||||
MouseEventButton::Main => MouseButton::Primary,
|
||||
MouseEventButton::Auxiliary => MouseButton::Auxiliary,
|
||||
MouseEventButton::Secondary => MouseButton::Secondary,
|
||||
MouseEventButton::Fourth => MouseButton::Fourth,
|
||||
MouseEventButton::Fifth => MouseButton::Fifth,
|
||||
})
|
||||
}
|
||||
|
||||
fn held_buttons(&self) -> MouseButtonSet {
|
||||
dioxus_html::input_data::decode_mouse_button_set(self.0.buttons.bits() as u16)
|
||||
}
|
||||
}
|
||||
impl HasMouseData for NativeClickData {
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self as &dyn std::any::Any
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct NativeFocusData {}
|
||||
impl HasFocusData for NativeFocusData {
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self as &dyn std::any::Any
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
//! A native renderer for Dioxus.
|
||||
//!
|
||||
//! ## Feature flags
|
||||
//! - `default`: Enables the features listed below.
|
||||
//! - `accessibility`: Enables [`accesskit`] accessibility support.
|
||||
//! - `hot-reload`: Enables hot-reloading of Dioxus RSX.
|
||||
//! - `menu`: Enables the [`muda`] menubar.
|
||||
//! - `tracing`: Enables tracing support.
|
||||
|
||||
mod assets;
|
||||
mod contexts;
|
||||
mod dioxus_application;
|
||||
mod dioxus_document;
|
||||
mod event;
|
||||
mod event_handler;
|
||||
mod mutation_writer;
|
||||
|
||||
pub use dioxus_application::DioxusNativeApplication;
|
||||
pub use dioxus_document::DioxusDocument;
|
||||
pub use event::DioxusNativeEvent;
|
||||
|
||||
use blitz_shell::{create_default_event_loop, BlitzShellEvent, Config, WindowConfig};
|
||||
use dioxus_core::{ComponentFunction, Element, VirtualDom};
|
||||
use std::any::Any;
|
||||
|
||||
type NodeId = usize;
|
||||
|
||||
/// Launch an interactive HTML/CSS renderer driven by the Dioxus virtualdom
|
||||
pub fn launch(app: fn() -> Element) {
|
||||
launch_cfg(app, vec![], vec![])
|
||||
}
|
||||
|
||||
pub fn launch_cfg(
|
||||
app: fn() -> Element,
|
||||
contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
|
||||
cfg: Vec<Box<dyn Any>>,
|
||||
) {
|
||||
launch_cfg_with_props(app, (), contexts, cfg)
|
||||
}
|
||||
|
||||
// todo: props shouldn't have the clone bound - should try and match dioxus-desktop behavior
|
||||
pub fn launch_cfg_with_props<P: Clone + 'static, M: 'static>(
|
||||
app: impl ComponentFunction<P, M>,
|
||||
props: P,
|
||||
contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
|
||||
_cfg: Vec<Box<dyn Any>>,
|
||||
) {
|
||||
let _cfg = _cfg
|
||||
.into_iter()
|
||||
.find_map(|cfg| cfg.downcast::<Config>().ok())
|
||||
.unwrap_or_default();
|
||||
let event_loop = create_default_event_loop::<BlitzShellEvent>();
|
||||
|
||||
// Turn on the runtime and enter it
|
||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
let _guard = rt.enter();
|
||||
|
||||
// Setup hot-reloading if enabled.
|
||||
#[cfg(all(
|
||||
feature = "hot-reload",
|
||||
debug_assertions,
|
||||
not(target_os = "android"),
|
||||
not(target_os = "ios")
|
||||
))]
|
||||
{
|
||||
use crate::event::DioxusNativeEvent;
|
||||
if let Some(endpoint) = dioxus_cli_config::devserver_ws_endpoint() {
|
||||
let proxy = event_loop.create_proxy();
|
||||
dioxus_devtools::connect(endpoint, move |event| {
|
||||
let dxn_event = DioxusNativeEvent::DevserverEvent(event);
|
||||
let _ = proxy.send_event(BlitzShellEvent::embedder_event(dxn_event));
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Spin up the virtualdom
|
||||
// We're going to need to hit it with a special waker
|
||||
// Note that we are delaying the initialization of window-specific contexts (net provider, document, etc)
|
||||
let mut vdom = VirtualDom::new_with_props(app, props);
|
||||
|
||||
// Add contexts
|
||||
for context in contexts {
|
||||
vdom.insert_any_root_context(context());
|
||||
}
|
||||
|
||||
// Create application
|
||||
let mut application = DioxusNativeApplication::new(event_loop.create_proxy(), vdom);
|
||||
|
||||
// Run event loop
|
||||
event_loop.run_app(&mut application).unwrap();
|
||||
}
|
||||
@@ -0,0 +1,575 @@
|
||||
use crate::{dioxus_document::qual_name, NodeId};
|
||||
use blitz_dom::{
|
||||
local_name, namespace_url,
|
||||
node::{Attribute, NodeSpecificData},
|
||||
ns, BaseDocument, ElementNodeData, NodeData, QualName, RestyleHint,
|
||||
};
|
||||
use dioxus_core::{
|
||||
AttributeValue, ElementId, Template, TemplateAttribute, TemplateNode, WriteMutations,
|
||||
};
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::collections::HashSet;
|
||||
|
||||
/// The state of the Dioxus integration with the RealDom
|
||||
#[derive(Debug)]
|
||||
pub struct DioxusState {
|
||||
/// Store of templates keyed by unique name
|
||||
templates: FxHashMap<Template, Vec<NodeId>>,
|
||||
/// Stack machine state for applying dioxus mutations
|
||||
stack: Vec<NodeId>,
|
||||
/// Mapping from vdom ElementId -> rdom NodeId
|
||||
node_id_mapping: Vec<Option<NodeId>>,
|
||||
}
|
||||
|
||||
/// A writer for mutations that can be used with the RealDom.
|
||||
pub struct MutationWriter<'a> {
|
||||
/// The realdom associated with this writer
|
||||
pub doc: &'a mut BaseDocument,
|
||||
|
||||
/// The state associated with this writer
|
||||
pub state: &'a mut DioxusState,
|
||||
|
||||
pub style_nodes: HashSet<usize>,
|
||||
|
||||
/// The (latest) node which has been mounted in and had autofocus=true, if any
|
||||
#[cfg(feature = "autofocus")]
|
||||
pub node_to_autofocus: Option<usize>,
|
||||
}
|
||||
|
||||
impl<'a> MutationWriter<'a> {
|
||||
pub fn new(doc: &'a mut BaseDocument, state: &'a mut DioxusState) -> Self {
|
||||
MutationWriter {
|
||||
doc,
|
||||
state,
|
||||
style_nodes: HashSet::new(),
|
||||
|
||||
#[cfg(feature = "autofocus")]
|
||||
node_to_autofocus: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_style_node(&self, node_id: NodeId) -> bool {
|
||||
self.doc
|
||||
.get_node(node_id)
|
||||
.unwrap()
|
||||
.data
|
||||
.is_element_with_tag_name(&local_name!("style"))
|
||||
}
|
||||
|
||||
fn maybe_push_style_node(&mut self, node_id: impl Into<Option<NodeId>>) {
|
||||
if let Some(node_id) = node_id.into() {
|
||||
if self.is_style_node(node_id) {
|
||||
self.style_nodes.insert(node_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn maybe_push_parent_style_node(&mut self, node_id: NodeId) {
|
||||
let parent_id = self.doc.get_node(node_id).unwrap().parent;
|
||||
self.maybe_push_style_node(parent_id);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for MutationWriter<'_> {
|
||||
fn drop(&mut self) {
|
||||
// Add/Update inline stylesheets (<style> elements)
|
||||
for &id in &self.style_nodes {
|
||||
self.doc.upsert_stylesheet_for_node(id);
|
||||
}
|
||||
|
||||
#[cfg(feature = "autofocus")]
|
||||
if let Some(node_id) = self.node_to_autofocus {
|
||||
if self.doc.get_node(node_id).is_some() {
|
||||
self.doc.set_focus_to(node_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DioxusState {
|
||||
/// Initialize the DioxusState in the RealDom
|
||||
pub fn create(_doc: &mut BaseDocument, mount_node: NodeId) -> Self {
|
||||
Self {
|
||||
templates: FxHashMap::default(),
|
||||
stack: vec![mount_node],
|
||||
node_id_mapping: vec![Some(mount_node)],
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an ElementId to a NodeId
|
||||
pub fn element_to_node_id(&self, element_id: ElementId) -> NodeId {
|
||||
self.try_element_to_node_id(element_id).unwrap()
|
||||
}
|
||||
|
||||
/// Attempt to convert an ElementId to a NodeId. This will return None if the ElementId is not in the RealDom.
|
||||
pub fn try_element_to_node_id(&self, element_id: ElementId) -> Option<NodeId> {
|
||||
self.node_id_mapping.get(element_id.0).copied().flatten()
|
||||
}
|
||||
}
|
||||
|
||||
impl MutationWriter<'_> {
|
||||
/// Update an ElementId -> NodeId mapping
|
||||
fn set_id_mapping(&mut self, node_id: NodeId, element_id: ElementId) {
|
||||
let element_id: usize = element_id.0;
|
||||
|
||||
// Ensure node_id_mapping is large enough to contain element_id
|
||||
if self.state.node_id_mapping.len() <= element_id {
|
||||
self.state.node_id_mapping.resize(element_id + 1, None);
|
||||
}
|
||||
|
||||
// Set the new mapping
|
||||
self.state.node_id_mapping[element_id] = Some(node_id);
|
||||
}
|
||||
|
||||
/// Find a child in the document by child index path
|
||||
fn load_child(&self, path: &[u8]) -> NodeId {
|
||||
let mut current = self
|
||||
.doc
|
||||
.get_node(*self.state.stack.last().unwrap())
|
||||
.unwrap();
|
||||
for i in path {
|
||||
let new_id = current.children[*i as usize];
|
||||
current = self.doc.get_node(new_id).unwrap();
|
||||
}
|
||||
current.id
|
||||
}
|
||||
|
||||
fn create_template_node(&mut self, node: &TemplateNode) -> NodeId {
|
||||
match node {
|
||||
TemplateNode::Element {
|
||||
tag,
|
||||
namespace,
|
||||
attrs,
|
||||
children,
|
||||
} => {
|
||||
let name = qual_name(tag, *namespace);
|
||||
let attrs = attrs
|
||||
.iter()
|
||||
.filter_map(|attr| match attr {
|
||||
TemplateAttribute::Static {
|
||||
name,
|
||||
value,
|
||||
namespace,
|
||||
} => Some(Attribute {
|
||||
name: qual_name(name, *namespace),
|
||||
value: value.to_string(),
|
||||
}),
|
||||
TemplateAttribute::Dynamic { .. } => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut data = ElementNodeData::new(name, attrs);
|
||||
data.flush_style_attribute(self.doc.guard());
|
||||
|
||||
let child_ids: Vec<NodeId> = children
|
||||
.iter()
|
||||
.map(|child| self.create_template_node(child))
|
||||
.collect();
|
||||
|
||||
let id = self.doc.create_node(NodeData::Element(data));
|
||||
let node = self.doc.get_node(id).unwrap();
|
||||
|
||||
// Initialise style data
|
||||
*node.stylo_element_data.borrow_mut() = Some(Default::default());
|
||||
|
||||
if let Some(src_attr) = node.attr(local_name!("src")) {
|
||||
crate::assets::fetch_image(self.doc, id, src_attr.to_string());
|
||||
}
|
||||
|
||||
let rel_attr = node.attr(local_name!("rel"));
|
||||
let href_attr = node.attr(local_name!("href"));
|
||||
if let (Some("stylesheet"), Some(href)) = (rel_attr, href_attr) {
|
||||
crate::assets::fetch_linked_stylesheet(self.doc, id, href.to_string());
|
||||
}
|
||||
|
||||
// If the node has an "id" attribute, store it in the ID map.
|
||||
if let Some(id_attr) = node.attr(local_name!("id")) {
|
||||
self.doc.nodes_to_id.insert(id_attr.to_string(), id);
|
||||
}
|
||||
|
||||
for &child_id in &child_ids {
|
||||
self.doc.get_node_mut(child_id).unwrap().parent = Some(id);
|
||||
}
|
||||
self.doc.get_node_mut(id).unwrap().children = child_ids;
|
||||
|
||||
id
|
||||
}
|
||||
TemplateNode::Text { text } => self.doc.create_text_node(text),
|
||||
TemplateNode::Dynamic { .. } => self.doc.create_node(NodeData::Comment),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WriteMutations for MutationWriter<'_> {
|
||||
fn append_children(&mut self, id: ElementId, m: usize) {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::info!("append_children id:{} m:{}", id.0, m);
|
||||
|
||||
let children = self.state.stack.split_off(self.state.stack.len() - m);
|
||||
let parent = self.state.element_to_node_id(id);
|
||||
for child in children {
|
||||
self.doc.get_node_mut(parent).unwrap().children.push(child);
|
||||
self.doc.get_node_mut(child).unwrap().parent = Some(parent);
|
||||
}
|
||||
|
||||
self.maybe_push_style_node(parent);
|
||||
}
|
||||
|
||||
fn assign_node_id(&mut self, path: &'static [u8], id: ElementId) {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::info!("assign_node_id path:{:?} id:{}", path, id.0);
|
||||
|
||||
// If there is an existing node already mapped to that ID and
|
||||
// it has no parent, then drop it
|
||||
if let Some(node_id) = self.state.try_element_to_node_id(id) {
|
||||
if let Some(node) = self.doc.get_node(node_id) {
|
||||
if node.parent.is_none() {
|
||||
self.doc.remove_and_drop_node(node_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let node_id = self.load_child(path);
|
||||
self.set_id_mapping(node_id, id);
|
||||
}
|
||||
|
||||
fn create_placeholder(&mut self, id: ElementId) {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::info!("create_placeholder id:{}", id.0);
|
||||
|
||||
let node_id = self.doc.create_node(NodeData::Comment);
|
||||
self.set_id_mapping(node_id, id);
|
||||
self.state.stack.push(node_id);
|
||||
}
|
||||
|
||||
fn create_text_node(&mut self, value: &str, id: ElementId) {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::info!("create_text_node id:{} text:{}", id.0, value);
|
||||
|
||||
let node_id = self.doc.create_text_node(value);
|
||||
self.set_id_mapping(node_id, id);
|
||||
self.state.stack.push(node_id);
|
||||
}
|
||||
|
||||
#[allow(clippy::map_entry)]
|
||||
fn load_template(&mut self, template: Template, index: usize, id: ElementId) {
|
||||
if !self.state.templates.contains_key(&template) {
|
||||
let template_root_ids: Vec<NodeId> = template
|
||||
.roots
|
||||
.iter()
|
||||
.map(|root| self.create_template_node(root))
|
||||
.collect();
|
||||
|
||||
self.state.templates.insert(template, template_root_ids);
|
||||
}
|
||||
|
||||
let template_entry = &self.state.templates[&template];
|
||||
|
||||
let template_node_id = template_entry[index];
|
||||
let clone_id = self.doc.deep_clone_node(template_node_id);
|
||||
|
||||
#[cfg(feature = "autofocus")]
|
||||
autofocus_node_recursive(self.doc, &mut self.node_to_autofocus, clone_id);
|
||||
|
||||
self.set_id_mapping(clone_id, id);
|
||||
self.state.stack.push(clone_id);
|
||||
}
|
||||
|
||||
fn replace_node_with(&mut self, id: ElementId, m: usize) {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::info!("replace_node_with id:{} m:{}", id.0, m);
|
||||
|
||||
let new_nodes = self.state.stack.split_off(self.state.stack.len() - m);
|
||||
let anchor_node_id = self.state.element_to_node_id(id);
|
||||
self.maybe_push_parent_style_node(anchor_node_id);
|
||||
self.doc.insert_before(anchor_node_id, &new_nodes);
|
||||
self.doc.remove_node(anchor_node_id);
|
||||
}
|
||||
|
||||
fn replace_placeholder_with_nodes(&mut self, path: &'static [u8], m: usize) {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::info!("replace_placeholder_with_nodes path:{:?} m:{}", path, m);
|
||||
|
||||
let new_nodes = self.state.stack.split_off(self.state.stack.len() - m);
|
||||
let anchor_node_id = self.load_child(path);
|
||||
self.maybe_push_parent_style_node(anchor_node_id);
|
||||
self.doc.insert_before(anchor_node_id, &new_nodes);
|
||||
self.doc.remove_node(anchor_node_id);
|
||||
}
|
||||
|
||||
fn insert_nodes_after(&mut self, id: ElementId, m: usize) {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::info!("insert_nodes_after id:{} m:{}", id.0, m);
|
||||
|
||||
let new_nodes = self.state.stack.split_off(self.state.stack.len() - m);
|
||||
let anchor_node_id = self.state.element_to_node_id(id);
|
||||
let next_sibling_id = self
|
||||
.doc
|
||||
.get_node(anchor_node_id)
|
||||
.unwrap()
|
||||
.forward(1)
|
||||
.map(|node| node.id);
|
||||
|
||||
match next_sibling_id {
|
||||
Some(anchor_node_id) => {
|
||||
self.doc.insert_before(anchor_node_id, &new_nodes);
|
||||
}
|
||||
None => self.doc.append(anchor_node_id, &new_nodes),
|
||||
}
|
||||
|
||||
self.maybe_push_parent_style_node(anchor_node_id);
|
||||
}
|
||||
|
||||
fn insert_nodes_before(&mut self, id: ElementId, m: usize) {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::info!("insert_nodes_before id:{} m:{}", id.0, m);
|
||||
|
||||
let new_nodes = self.state.stack.split_off(self.state.stack.len() - m);
|
||||
let anchor_node_id = self.state.element_to_node_id(id);
|
||||
self.doc.insert_before(anchor_node_id, &new_nodes);
|
||||
|
||||
self.maybe_push_parent_style_node(anchor_node_id);
|
||||
}
|
||||
|
||||
fn set_attribute(
|
||||
&mut self,
|
||||
name: &'static str,
|
||||
ns: Option<&'static str>,
|
||||
value: &AttributeValue,
|
||||
id: ElementId,
|
||||
) {
|
||||
let node_id = self.state.element_to_node_id(id);
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::info!(
|
||||
"set_attribute node_id:{} ns: {:?} name:{}, value:{:?}",
|
||||
node_id,
|
||||
ns,
|
||||
name,
|
||||
value
|
||||
);
|
||||
|
||||
self.doc.snapshot_node(node_id);
|
||||
|
||||
let mut queued_image = None;
|
||||
let mut queued_stylesheet = None;
|
||||
|
||||
{
|
||||
let node = &mut self.doc.nodes[node_id];
|
||||
|
||||
let stylo_element_data = &mut *node.stylo_element_data.borrow_mut();
|
||||
if let Some(data) = stylo_element_data {
|
||||
data.hint |= RestyleHint::restyle_subtree();
|
||||
}
|
||||
|
||||
if let NodeData::Element(ref mut element) = node.data {
|
||||
if element.name.local == local_name!("input") && name == "checked" {
|
||||
set_input_checked_state(element, value);
|
||||
}
|
||||
// FIXME: support other non-text attributes
|
||||
else if let AttributeValue::Text(val) = value {
|
||||
if name == "value" {
|
||||
// Update text input value
|
||||
if let Some(input_data) = element.text_input_data_mut() {
|
||||
input_data.set_text(
|
||||
&mut self.doc.font_ctx,
|
||||
&mut self.doc.layout_ctx,
|
||||
val,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME check namespace
|
||||
let existing_attr = element
|
||||
.attrs
|
||||
.iter_mut()
|
||||
.find(|attr| attr.name.local == *name);
|
||||
|
||||
if let Some(existing_attr) = existing_attr {
|
||||
existing_attr.value.clear();
|
||||
existing_attr.value.push_str(val);
|
||||
} else {
|
||||
// we have overloaded the style namespace to accumulate style attributes without a `style` block
|
||||
if ns == Some("style") {
|
||||
// todo: need to accumulate style attributes into a single style
|
||||
//
|
||||
// element.
|
||||
} else {
|
||||
element.attrs.push(Attribute {
|
||||
name: qual_name(name, ns),
|
||||
value: val.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if name == "style" {
|
||||
element.flush_style_attribute(&self.doc.guard);
|
||||
}
|
||||
|
||||
if name == "src" && !val.is_empty() {
|
||||
element.node_specific_data = NodeSpecificData::None;
|
||||
queued_image = Some(val.to_string());
|
||||
}
|
||||
|
||||
if element.name.local == local_name!("link")
|
||||
&& name == "href"
|
||||
&& !val.is_empty()
|
||||
{
|
||||
queued_stylesheet = Some(val.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if let AttributeValue::None = value {
|
||||
// Update text input value
|
||||
if name == "value" {
|
||||
if let Some(input_data) = element.text_input_data_mut() {
|
||||
input_data.set_text(
|
||||
&mut self.doc.font_ctx,
|
||||
&mut self.doc.layout_ctx,
|
||||
"",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: check namespace
|
||||
element.attrs.retain(|attr| attr.name.local != *name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(queued_image) = queued_image {
|
||||
crate::assets::fetch_image(self.doc, node_id, queued_image);
|
||||
}
|
||||
|
||||
if let Some(queued_stylesheet) = queued_stylesheet {
|
||||
crate::assets::fetch_linked_stylesheet(self.doc, node_id, queued_stylesheet);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_node_text(&mut self, value: &str, id: ElementId) {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::info!("set_node_text id:{} value:{}", id.0, value);
|
||||
|
||||
let node_id = self.state.element_to_node_id(id);
|
||||
let node = self.doc.get_node_mut(node_id).unwrap();
|
||||
|
||||
let text = match node.data {
|
||||
NodeData::Text(ref mut text) => text,
|
||||
// todo: otherwise this is basically element.textContent which is a bit different - need to parse as html
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let changed = text.content != value;
|
||||
if changed {
|
||||
text.content.clear();
|
||||
text.content.push_str(value);
|
||||
let parent = node.parent;
|
||||
self.maybe_push_style_node(parent);
|
||||
}
|
||||
}
|
||||
|
||||
fn create_event_listener(&mut self, _name: &'static str, _id: ElementId) {
|
||||
// we're going to actually set the listener here as a placeholder - in JS this would also be a placeholder
|
||||
// we might actually just want to attach the attribute to the root element (delegation)
|
||||
self.set_attribute(
|
||||
_name,
|
||||
None,
|
||||
&AttributeValue::Text("<rust func>".into()),
|
||||
_id,
|
||||
);
|
||||
|
||||
// also set the data-dioxus-id attribute so we can find the element later
|
||||
self.set_attribute(
|
||||
"data-dioxus-id",
|
||||
None,
|
||||
&AttributeValue::Text(_id.0.to_string()),
|
||||
_id,
|
||||
);
|
||||
|
||||
// let node_id = self.state.element_to_node_id(id);
|
||||
// let mut node = self.rdom.get_mut(node_id).unwrap();
|
||||
// node.add_event_listener(name);
|
||||
}
|
||||
|
||||
fn remove_event_listener(&mut self, _name: &'static str, _id: ElementId) {
|
||||
// let node_id = self.state.element_to_node_id(id);
|
||||
// let mut node = self.rdom.get_mut(node_id).unwrap();
|
||||
// node.remove_event_listener(name);
|
||||
}
|
||||
|
||||
fn remove_node(&mut self, id: ElementId) {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::info!("remove_node id:{}", id.0);
|
||||
|
||||
let node_id = self.state.element_to_node_id(id);
|
||||
self.doc.remove_node(node_id);
|
||||
}
|
||||
|
||||
fn push_root(&mut self, id: ElementId) {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::info!("push_root id:{}", id.0,);
|
||||
|
||||
let node_id = self.state.element_to_node_id(id);
|
||||
self.state.stack.push(node_id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Set 'checked' state on an input based on given attributevalue
|
||||
fn set_input_checked_state(element: &mut ElementNodeData, value: &AttributeValue) {
|
||||
let checked: bool;
|
||||
match value {
|
||||
AttributeValue::Bool(checked_bool) => {
|
||||
checked = *checked_bool;
|
||||
}
|
||||
AttributeValue::Text(val) => {
|
||||
if let Ok(checked_bool) = val.parse() {
|
||||
checked = checked_bool;
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
}
|
||||
_ => {
|
||||
return;
|
||||
}
|
||||
};
|
||||
match element.node_specific_data {
|
||||
NodeSpecificData::CheckboxInput(ref mut checked_mut) => *checked_mut = checked,
|
||||
// If we have just constructed the element, set the node attribute,
|
||||
// and NodeSpecificData will be created from that later
|
||||
// this simulates the checked attribute being set in html,
|
||||
// and the element's checked property being set from that
|
||||
NodeSpecificData::None => element.attrs.push(Attribute {
|
||||
name: QualName {
|
||||
prefix: None,
|
||||
ns: ns!(html),
|
||||
local: local_name!("checked"),
|
||||
},
|
||||
value: checked.to_string(),
|
||||
}),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "autofocus")]
|
||||
fn autofocus_node_recursive(
|
||||
doc: &BaseDocument,
|
||||
node_to_autofocus: &mut Option<usize>,
|
||||
node_id: usize,
|
||||
) {
|
||||
if let Some(node) = doc.get_node(node_id) {
|
||||
if node.is_focussable() {
|
||||
if let NodeData::Element(ref element) = node.data {
|
||||
if let Some(value) = element.attr(local_name!("autofocus")) {
|
||||
if value == "true" {
|
||||
*node_to_autofocus = Some(node_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for child_node_id in &node.children {
|
||||
autofocus_node_recursive(doc, node_to_autofocus, *child_node_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ use dioxus::prelude::*;
|
||||
use nested_suspense::app;
|
||||
|
||||
fn main() {
|
||||
dioxus::logger::init(dioxus::logger::tracing::Level::TRACE).expect("logger failed to init");
|
||||
|
||||
dioxus::LaunchBuilder::new()
|
||||
.with_cfg(server_only! {
|
||||
ServeConfig::builder()
|
||||
@@ -15,6 +17,7 @@ fn main() {
|
||||
.unwrap()
|
||||
.join("public")
|
||||
)
|
||||
.clear_cache(false)
|
||||
)
|
||||
.enable_out_of_order_streaming()
|
||||
})
|
||||
|
||||
@@ -15,10 +15,10 @@ dioxus-history = { workspace = true }
|
||||
dioxus-router-macro = { workspace = true }
|
||||
dioxus-fullstack-hooks = { workspace = true, optional = true }
|
||||
tracing = { workspace = true }
|
||||
urlencoding = "2.1.3"
|
||||
url = "2.3.1"
|
||||
urlencoding = { workspace = true }
|
||||
url = { workspace = true }
|
||||
dioxus-cli-config = { workspace = true }
|
||||
rustversion = "1.0.17"
|
||||
rustversion = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
@@ -37,7 +37,7 @@ use syn::{
|
||||
///
|
||||
/// You can any combination of the following named arguments:
|
||||
/// - `name`: sets the identifier for the server function’s type, which is a struct created
|
||||
/// to hold the arguments (defaults to the function identifier in PascalCase)
|
||||
/// to hold the arguments (defaults to the function identifier in PascalCase)
|
||||
/// - `prefix`: a prefix at which the server function handler will be mounted (defaults to `/api`)
|
||||
/// - `endpoint`: specifies the exact path at which the server function handler will be mounted,
|
||||
/// relative to the prefix (defaults to the function name followed by unique hash)
|
||||
|
||||
Reference in new issue
Block a user