mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-11 07:04:13 +00:00
Merge remote-tracking branch 'upstream/master' into optimize-templates-v3
This commit is contained in:
commit
b79ad4f50e
68 changed files with 1030 additions and 1281 deletions
7
.github/dependabot.yml
vendored
Normal file
7
.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
version: 2
|
||||
updates:
|
||||
# Maintain dependencies for GitHub Actions
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
|
@ -406,6 +406,8 @@ fn app(cx: Scope) -> Element {
|
|||
wrap_through: "a",
|
||||
writing_mode: "a",
|
||||
z_index: "a",
|
||||
|
||||
"This example isn't quite useful yet"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -29,9 +29,7 @@ fn app(cx: Scope) -> Element {
|
|||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
Child1 {
|
||||
text: first
|
||||
}
|
||||
Child1 { text: first }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -59,9 +57,7 @@ struct C2Props<'a> {
|
|||
|
||||
fn Child2<'a>(cx: Scope<'a, C2Props<'a>>) -> Element {
|
||||
cx.render(rsx! {
|
||||
Child3 {
|
||||
text: cx.props.text
|
||||
}
|
||||
Child3 { text: cx.props.text }
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ fn app(cx: Scope) -> Element {
|
|||
if val.get() == "0" {
|
||||
val.set(String::new());
|
||||
}
|
||||
|
||||
val.make_mut().push_str(num.to_string().as_str());
|
||||
};
|
||||
|
||||
|
@ -99,12 +100,8 @@ fn app(cx: Scope) -> Element {
|
|||
}
|
||||
}
|
||||
div { class: "digit-keys",
|
||||
button { class: "calculator-key key-0", onclick: move |_| input_digit(0),
|
||||
"0"
|
||||
}
|
||||
button { class: "calculator-key key-dot", onclick: move |_| val.make_mut().push('.'),
|
||||
"●"
|
||||
}
|
||||
button { class: "calculator-key key-0", onclick: move |_| input_digit(0), "0" }
|
||||
button { class: "calculator-key key-dot", onclick: move |_| val.make_mut().push('.'), "●" }
|
||||
(1..10).map(|k| rsx!{
|
||||
button {
|
||||
class: "calculator-key {k}",
|
||||
|
@ -116,22 +113,13 @@ fn app(cx: Scope) -> Element {
|
|||
}
|
||||
}
|
||||
div { class: "operator-keys",
|
||||
button { class: "calculator-key key-divide", onclick: move |_| input_operator("/"),
|
||||
"÷"
|
||||
}
|
||||
button { class: "calculator-key key-multiply", onclick: move |_| input_operator("*"),
|
||||
"×"
|
||||
}
|
||||
button { class: "calculator-key key-subtract", onclick: move |_| input_operator("-"),
|
||||
"−"
|
||||
}
|
||||
button { class: "calculator-key key-add", onclick: move |_| input_operator("+"),
|
||||
"+"
|
||||
}
|
||||
button { class: "calculator-key key-equals",
|
||||
onclick: move |_| {
|
||||
val.set(format!("{}", calc_val(val.as_str())));
|
||||
},
|
||||
button { class: "calculator-key key-divide", onclick: move |_| input_operator("/"), "÷" }
|
||||
button { class: "calculator-key key-multiply", onclick: move |_| input_operator("*"), "×" }
|
||||
button { class: "calculator-key key-subtract", onclick: move |_| input_operator("-"), "−" }
|
||||
button { class: "calculator-key key-add", onclick: move |_| input_operator("+"), "+" }
|
||||
button {
|
||||
class: "calculator-key key-equals",
|
||||
onclick: move |_| val.set(format!("{}", calc_val(val.as_str()))),
|
||||
"="
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ fn main() {
|
|||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let login = use_callback!(cx, || move |evt: MouseEvent| async move {
|
||||
let login = use_callback!(cx, move |_| async move {
|
||||
let res = reqwest::get("https://dog.ceo/api/breeds/list/all")
|
||||
.await
|
||||
.unwrap()
|
||||
|
@ -13,7 +13,7 @@ fn app(cx: Scope) -> Element {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("{}, ", res);
|
||||
println!("{:#?}, ", res);
|
||||
});
|
||||
|
||||
cx.render(rsx! {
|
||||
|
|
|
@ -10,7 +10,7 @@ fn main() {
|
|||
let mut dom = VirtualDom::new(app);
|
||||
let _ = dom.rebuild();
|
||||
|
||||
let output = dioxus_ssr::render_vdom(&dom);
|
||||
let output = dioxus_ssr::render(&dom);
|
||||
|
||||
println!("{}", output);
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ fn app(cx: Scope) -> Element {
|
|||
button {
|
||||
onclick: move |evt| {
|
||||
println!("clicked! bottom no bubbling");
|
||||
evt.cancel_bubble();
|
||||
evt.stop_propogation();
|
||||
},
|
||||
"Dont propogate"
|
||||
}
|
||||
|
|
|
@ -2,14 +2,14 @@
|
|||
//! It also proves that lifetimes work properly, especially when used with use_ref
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_ssr::config::SsrConfig;
|
||||
|
||||
fn main() {
|
||||
let mut vdom = VirtualDom::new(example);
|
||||
vdom.rebuild();
|
||||
_ = vdom.rebuild();
|
||||
|
||||
let out = dioxus_ssr::render_vdom_cfg(&vdom, SsrConfig::default().newline(true).indent(true));
|
||||
println!("{}", out);
|
||||
let mut renderer = dioxus_ssr::Renderer::new();
|
||||
renderer.pretty = true;
|
||||
renderer.render(&vdom);
|
||||
}
|
||||
|
||||
fn example(cx: Scope) -> Element {
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus_desktop::launch(app);
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let mut idx = use_state(cx, || 0);
|
||||
let onhover = |h| println!("go!");
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
button { onclick: move |_| idx += 1, "+" }
|
||||
button { onclick: move |_| idx -= 1, "-" }
|
||||
ul {
|
||||
(0..**idx).map(|i| rsx! {
|
||||
Child { i: i, onhover: onhover }
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[inline_props]
|
||||
fn Child<'a>(cx: Scope<'a>, i: i32, onhover: EventHandler<'a, MouseEvent>) -> Element {
|
||||
cx.render(rsx! {
|
||||
li {
|
||||
onmouseover: move |e| onhover.call(e),
|
||||
"{i}"
|
||||
}
|
||||
})
|
||||
}
|
|
@ -5,7 +5,7 @@
|
|||
use std::fmt::Write;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_ssr::config::SsrConfig;
|
||||
use dioxus_ssr::config::Config;
|
||||
|
||||
fn main() {
|
||||
// We can render VirtualDoms
|
||||
|
@ -26,7 +26,7 @@ fn main() {
|
|||
// We can configure the SSR rendering to add ids for rehydration
|
||||
println!(
|
||||
"{}",
|
||||
dioxus_ssr::render_vdom_cfg(&vdom, SsrConfig::default().pre_render(true))
|
||||
dioxus_ssr::render_vdom_cfg(&vdom, Config::default().pre_render(true))
|
||||
);
|
||||
|
||||
// We can even render as a writer
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
use dioxus::prelude::*;
|
||||
use dioxus_desktop::{Config, LogicalSize, WindowBuilder};
|
||||
|
||||
fn main() {
|
||||
dioxus_desktop::launch(|cx| cx.render(rsx! { async_app {} }));
|
||||
}
|
||||
|
||||
async fn async_app(cx: Scope<'_>) -> Element {
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
"hi!"
|
||||
}
|
||||
})
|
||||
}
|
|
@ -22,10 +22,10 @@ pub struct TodoItem {
|
|||
}
|
||||
|
||||
pub fn app(cx: Scope<()>) -> Element {
|
||||
let todos = use_state(&cx, im_rc::HashMap::<u32, TodoItem>::default);
|
||||
let filter = use_state(&cx, || FilterState::All);
|
||||
let draft = use_state(&cx, || "".to_string());
|
||||
let todo_id = use_state(&cx, || 0);
|
||||
let todos = use_state(cx, im_rc::HashMap::<u32, TodoItem>::default);
|
||||
let filter = use_state(cx, || FilterState::All);
|
||||
let draft = use_state(cx, || "".to_string());
|
||||
let todo_id = use_state(cx, || 0);
|
||||
|
||||
// Filter the todos based on the filter state
|
||||
let mut filtered_todos = todos
|
||||
|
@ -58,7 +58,6 @@ pub fn app(cx: Scope<()>) -> Element {
|
|||
value: "{draft}",
|
||||
autofocus: "true",
|
||||
oninput: move |evt| {
|
||||
println!("calling oninput");
|
||||
draft.set(evt.value.clone());
|
||||
},
|
||||
onkeydown: move |evt| {
|
||||
|
@ -117,15 +116,13 @@ pub struct TodoEntryProps<'a> {
|
|||
}
|
||||
|
||||
pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element {
|
||||
let is_editing = use_state(&cx, || false);
|
||||
let is_editing = use_state(cx, || false);
|
||||
|
||||
let todos = cx.props.todos.get();
|
||||
let todo = &todos[&cx.props.id];
|
||||
let completed = if todo.checked { "completed" } else { "" };
|
||||
let editing = if **is_editing { "editing" } else { "" };
|
||||
|
||||
println!("rendering todo entry");
|
||||
|
||||
cx.render(rsx!{
|
||||
li {
|
||||
class: "{completed} {editing}",
|
||||
|
|
|
@ -35,13 +35,13 @@ fn app(cx: Scope) -> Element {
|
|||
nav { class: "md:ml-auto flex flex-wrap items-center text-base justify-center" }
|
||||
button {
|
||||
class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0",
|
||||
onmousedown: |evt| evt.cancel_bubble(),
|
||||
onmousedown: |evt| evt.stop_propogation(),
|
||||
onclick: move |_| window.set_minimized(true),
|
||||
"Minimize"
|
||||
}
|
||||
button {
|
||||
class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0",
|
||||
onmousedown: |evt| evt.cancel_bubble(),
|
||||
onmousedown: |evt| evt.stop_propogation(),
|
||||
onclick: move |_| {
|
||||
|
||||
window.set_fullscreen(!**fullscreen);
|
||||
|
@ -52,7 +52,7 @@ fn app(cx: Scope) -> Element {
|
|||
}
|
||||
button {
|
||||
class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0",
|
||||
onmousedown: |evt| evt.cancel_bubble(),
|
||||
onmousedown: |evt| evt.stop_propogation(),
|
||||
onclick: move |_| window.close(),
|
||||
"Close"
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ fn app(cx: Scope) -> Element {
|
|||
div {
|
||||
button {
|
||||
class: "inline-flex items-center text-white bg-green-500 border-0 py-1 px-3 hover:bg-green-700 rounded",
|
||||
onmousedown: |evt| evt.cancel_bubble(),
|
||||
onmousedown: |evt| evt.stop_propogation(),
|
||||
onclick: move |_| {
|
||||
window.set_always_on_top(!always_on_top);
|
||||
always_on_top.set(!always_on_top);
|
||||
|
@ -77,7 +77,7 @@ fn app(cx: Scope) -> Element {
|
|||
div {
|
||||
button {
|
||||
class: "inline-flex items-center text-white bg-blue-500 border-0 py-1 px-3 hover:bg-green-700 rounded",
|
||||
onmousedown: |evt| evt.cancel_bubble(),
|
||||
onmousedown: |evt| evt.stop_propogation(),
|
||||
onclick: move |_| {
|
||||
window.set_decorations(!decorations);
|
||||
decorations.set(!decorations);
|
||||
|
@ -88,7 +88,7 @@ fn app(cx: Scope) -> Element {
|
|||
div {
|
||||
button {
|
||||
class: "inline-flex items-center text-white bg-blue-500 border-0 py-1 px-3 hover:bg-green-700 rounded",
|
||||
onmousedown: |evt| evt.cancel_bubble(),
|
||||
onmousedown: |evt| evt.stop_propogation(),
|
||||
onclick: move |_| window.set_title("Dioxus Application"),
|
||||
"Change Title"
|
||||
}
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
<div align="center">
|
||||
<h1>🌗🚀 Dioxus</h1>
|
||||
<p>
|
||||
<strong>Frontend that scales.</strong>
|
||||
</p>
|
||||
</div>
|
||||
<p align="center">
|
||||
<img src="../header.svg">
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
<!-- Crates version -->
|
||||
|
@ -26,9 +23,7 @@
|
|||
<img src="https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg"
|
||||
alt="CI status" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<!--Awesome -->
|
||||
<a href="https://github.com/dioxuslabs/awesome-dioxus">
|
||||
<img src="https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg" alt="Awesome Page" />
|
||||
|
@ -39,29 +34,23 @@
|
|||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
<div align="center">
|
||||
<h3>
|
||||
<a href="https://dioxuslabs.com"> 官网 </a>
|
||||
<a href="https://dioxuslabs.com"> 官方网站 </a>
|
||||
<span> | </span>
|
||||
<a href="https://dioxus.mrxzx.info/"> 手册 </a>
|
||||
<a href="https://github.com/DioxusLabs/example-projects"> 代码示例 </a>
|
||||
<span> | </span>
|
||||
<a href="https://dioxuslabs.com/guide"> 开发指南 </a>
|
||||
<span> | </span>
|
||||
<a href="https://github.com/DioxusLabs/example-projects"> 示例 </a>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<h4>
|
||||
<a href="https://github.com/DioxusLabs/dioxus/blob/master/README.md"> English </a>
|
||||
<span> | </span>
|
||||
<a href="https://github.com/DioxusLabs/dioxus/blob/master/README.md"> 中文 </a>
|
||||
<a href="https://github.com/DioxusLabs/dioxus/blob/master/translations/pt-br/README.md"> PT-BR </a>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
|
||||
<br/>
|
||||
|
||||
Dioxus 是一个可移植、高性能的框架,用于在 Rust 中构建跨平台的用户界面。
|
||||
Dioxus 是一个可移植的、高性能的、符合人体工程学的框架,使用 Rust 语言构建跨平台的用户界面。
|
||||
|
||||
```rust
|
||||
fn app(cx: Scope) -> Element {
|
||||
|
@ -75,103 +64,111 @@ fn app(cx: Scope) -> Element {
|
|||
}
|
||||
```
|
||||
|
||||
Dioxus 可用于制作 网页程序、桌面应用、静态站点、移动端应用。
|
||||
Dioxus 可用于生成 网页前端、桌面应用、静态网站、移动端应用、TUI程序、等多类平台应用。
|
||||
|
||||
Dioxus 为不同的平台都提供了很好的开发文档。
|
||||
如果你能够熟悉使用 React 框架,那 Dioxus 对你来说将非常简单。
|
||||
|
||||
如果你会使用 React ,那 Dioxus 对你来说会很简单。
|
||||
## 独特的特性:
|
||||
- 桌面程序完全基于本地环境运行(并非 Electron 的封装)
|
||||
- 符合人体工程学的设计以及拥有强大的状态管理
|
||||
- 全面的内联文档 - 包含所有 HTML 元素、监听器 和 事件 指南。
|
||||
- 极快的运行效率和极高的内存效率
|
||||
- 智能项目热更新和高效的项目迭代
|
||||
- 一流的异步支持🔥
|
||||
- 更多内容请查看 [版本发布信息](https://dioxuslabs.com/blog/introducing-dioxus/).
|
||||
|
||||
### 项目特点:
|
||||
- 对桌面应用的原生支持。
|
||||
- 强大的状态管理工具。
|
||||
- 支持所有 HTML 标签,监听器和事件。
|
||||
- 超高的内存使用率,稳定的组件分配器。
|
||||
- 多通道异步调度器,超强的异步支持。
|
||||
- 更多信息请查阅: [版本发布文档](https://dioxuslabs.com/blog/introducing-dioxus/).
|
||||
|
||||
### 示例
|
||||
|
||||
本项目中的所有例子都是 `桌面应用` 程序,请使用 `cargo run --example XYZ` 运行这些例子。
|
||||
|
||||
```
|
||||
cargo run --example EXAMPLE
|
||||
```
|
||||
|
||||
## 进入学习
|
||||
|
||||
<table style="width:100%" align="center">
|
||||
<tr >
|
||||
<th><a href="https://dioxuslabs.com/guide/">教程</a></th>
|
||||
<th><a href="https://dioxuslabs.com/reference/web">网页端</a></th>
|
||||
<th><a href="https://dioxuslabs.com/reference/desktop/">桌面端</a></th>
|
||||
<th><a href="https://dioxuslabs.com/reference/ssr/">SSR</a></th>
|
||||
<th><a href="https://dioxuslabs.com/reference/mobile/">移动端</a></th>
|
||||
<th><a href="https://dioxuslabs.com/guide/concepts/managing_state.html">状态管理</a></th>
|
||||
## 已支持的平台
|
||||
<div align="center">
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
</table>
|
||||
<td><em>网站项目</em></td>
|
||||
<td>
|
||||
<ul>
|
||||
<li>使用 WebAssembly 直接对 DOM 进行渲染</li>
|
||||
<li>为 SSR 提供预渲染或作为客户端使用</li>
|
||||
<li>简单的 "Hello World" 仅仅 65kb, 媲美 React 框架</li>
|
||||
<li>CLI 提供热更新支持,方便项目快速迭代</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><em>桌面应用</em></td>
|
||||
<td>
|
||||
<ul>
|
||||
<li>使用 Webview 进行渲染 或 使用 WGPU 和 Skia(试验性的)</li>
|
||||
<li>无多余配置,使用 `cargo build` 即可快速构建</li>
|
||||
<li>对原生系统的全面支持</li>
|
||||
<li>支持 Macos、Linux、Windows 等系统,极小的二进制文件</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><em>移动端应用</em></td>
|
||||
<td>
|
||||
<ul>
|
||||
<li>使用 Webview 进行渲染 或 使用 WGPU 和 Skia(试验性的)</li>
|
||||
<li>支持 IOS 和 安卓系统</li>
|
||||
<li><em>显著的</em> 性能强于 React Native 框架 </li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><em>Liveview</em></td>
|
||||
<td>
|
||||
<ul>
|
||||
<li>使用服务器渲染组件与应用程序</li>
|
||||
<li>与受欢迎的后端框架进行融合(Axum、Wrap)</li>
|
||||
<li>及低的延迟</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><em>终端程序</em></td>
|
||||
<td>
|
||||
<ul>
|
||||
<li>在终端程序中渲染,类似于: <a href="https://github.com/vadimdemedes/ink"> ink.js</a></li>
|
||||
<li>支持 CSS 相关模型(类似于浏览器内的)</li>
|
||||
<li>Built-in widgets like text input, buttons, and focus system</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
## Why Dioxus?
|
||||
|
||||
目前有非常多的应用开发选择,为什么偏偏要选择 Dioxus 呢?
|
||||
|
||||
首先,Dioxus将开发者的经验放在首位。这反映在 Dioxus 特有的各种功能上。
|
||||
|
||||
- 自动格式化 RSX 格式代码,并拥有 VSCode 插件作为支持。
|
||||
- 热加载基于 RSX 代码解析器,同时支持桌面程序和网页程序。
|
||||
- 强调文档的重要性,我们对所有 HTML 元素都提供文档支持。
|
||||
|
||||
Dioxus 也是一个可扩展化的平台。
|
||||
|
||||
- 通过实现一个非常简单的优化堆栈机,轻松构建新的渲染器。
|
||||
- 构建并分享开发者自定义的组件代码。
|
||||
|
||||
Dioxus 那么优秀,但什么时候它不适合我呢?
|
||||
- 它还没有完全成熟。api仍在变化,可能会出现故障(尽管我们试图避免)
|
||||
- 您需要运行在 no-std 的环境之中。
|
||||
- 你不喜欢使用 React-like 的方式构建 UI 项目。
|
||||
|
||||
|
||||
## Dioxus 项目
|
||||
|
||||
| 文件浏览器 (桌面应用) | WiFi 扫描器 (桌面应用) | Todo管理 (所有平台) | 商城系统 (SSR/liveview) |
|
||||
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [![File Explorer](https://github.com/DioxusLabs/example-projects/raw/master/file-explorer/image.png)](https://github.com/DioxusLabs/example-projects/blob/master/file-explorer) | [![Wifi Scanner Demo](https://github.com/DioxusLabs/example-projects/raw/master/wifi-scanner/demo_small.png)](https://github.com/DioxusLabs/example-projects/blob/master/wifi-scanner) | [![TodoMVC example](https://github.com/DioxusLabs/example-projects/raw/master/todomvc/example.png)](https://github.com/DioxusLabs/example-projects/blob/master/todomvc) | [![E-commerce Example](https://github.com/DioxusLabs/example-projects/raw/master/ecommerce-site/demo.png)](https://github.com/DioxusLabs/example-projects/blob/master/ecommerce-site) |
|
||||
## 贡献代码
|
||||
- 在我们的 [问题追踪](https://github.com/dioxuslabs/dioxus/issues) 中汇报你遇到的问题。
|
||||
- 加入我们的 Discord 与我们交流。
|
||||
|
||||
|
||||
查看 [awesome-dioxus](https://github.com/DioxusLabs/awesome-dioxus) 查看更多有趣(~~NiuBi~~)的项目!
|
||||
<a href="https://github.com/dioxuslabs/dioxus/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=dioxuslabs/dioxus&max=30&columns=10" />
|
||||
</a>
|
||||
|
||||
## 为什么使用 Dioxus 和 Rust ?
|
||||
## 开源协议
|
||||
|
||||
TypeScript 是一个不错的 JavaScript 拓展集,但它仍然算是 JavaScript。
|
||||
本项目使用 [MIT license].
|
||||
|
||||
TS 代码运行效率不高,而且有大量的配置项。
|
||||
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
|
||||
|
||||
相比之下,Dioxus 使用 Rust 编写将大大的提高效能。
|
||||
|
||||
使用 Rust 开发,我们能获得:
|
||||
|
||||
- 静态类型支持。
|
||||
- 变量默认不变性。
|
||||
- 简单直观的模块系统。
|
||||
- 内部集成的文档系统。
|
||||
- 先进的模式匹配系统。
|
||||
- 简洁、高效、强大的迭代器。
|
||||
- 内置的 单元测试 / 集成测试。
|
||||
- 优秀的异常处理系统。
|
||||
- 强大且健全的标准库。
|
||||
- 灵活的 `宏` 系统。
|
||||
- 使用 `crates.io` 管理包。
|
||||
|
||||
Dioxus 能为开发者提供的:
|
||||
|
||||
- 安全使用数据结构。
|
||||
- 安全的错误处理结果。
|
||||
- 拥有原生移动端的性能。
|
||||
- 直接访问系统的IO层。
|
||||
|
||||
Dioxus 使 Rust 应用程序的编写速度和 React 应用程序一样快,但提供了更多的健壮性,让团队能在更短的时间内做出强大功能。
|
||||
|
||||
### 不建议使用 Dioxus 的情况?
|
||||
|
||||
您不该在这些情况下使用 Dioxus :
|
||||
|
||||
- 您不喜欢类似 React 的开发风格。
|
||||
- 您需要一个 `no-std` 的渲染器。
|
||||
- 您希望应用运行在 `不支持 Wasm 或 asm.js` 的浏览器。
|
||||
- 您需要一个 `Send + Sync` UI 解决方案(目前不支持)。
|
||||
|
||||
### 项目生态
|
||||
|
||||
想要加入我们一起为 Dioxus 生态努力吗?有很多项目都能在您的帮助下获得改变:
|
||||
|
||||
- [TUI 渲染器](https://github.com/dioxusLabs/rink)
|
||||
- [CLI 开发工具](https://github.com/dioxusLabs/cli)
|
||||
- [官网及文档](https://github.com/dioxusLabs/docsite)
|
||||
- 动态网站 及 Web 服务器
|
||||
- 资源系统
|
||||
|
||||
## 协议
|
||||
|
||||
这个项目使用 [MIT 协议].
|
||||
|
||||
[MIT 协议]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
|
||||
除非您另有明确声明,否则有意提交的任何贡献将被授权为 MIT 协议,没有任何附加条款或条件。
|
||||
|
|
|
@ -41,7 +41,7 @@ fn app(cx: Scope) -> Element {
|
|||
|
||||
Then, we'll want to create a new VirtualDom using this app as the root component.
|
||||
|
||||
```rust, ingore
|
||||
```rust, ignore
|
||||
let mut dom = VirtualDom::new(app);
|
||||
```
|
||||
|
||||
|
@ -65,9 +65,9 @@ dom.render_with_deadline(tokio::time::sleep(Duration::from_millis(16)));
|
|||
|
||||
If an event occurs from outside the virtualdom while waiting for work, then we can cancel the wait using a `select!` block and inject the event.
|
||||
|
||||
```rust
|
||||
```rust, ignore
|
||||
loop {
|
||||
tokio::select! {
|
||||
select! {
|
||||
evt = real_dom.event() => dom.handle_event("click", evt.data, evt.element, evt.bubbles),
|
||||
_ = dom.wait_for_work() => {}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
use crate::{
|
||||
nodes::RenderReturn, nodes::VNode, virtual_dom::VirtualDom, AttributeValue, DynamicNode,
|
||||
ScopeId,
|
||||
};
|
||||
use crate::{nodes::RenderReturn, nodes::VNode, virtual_dom::VirtualDom, DynamicNode, ScopeId};
|
||||
use bumpalo::boxed::Box as BumpBox;
|
||||
|
||||
/// An Element's unique identifier.
|
||||
|
@ -37,21 +34,20 @@ impl ElementRef {
|
|||
|
||||
impl VirtualDom {
|
||||
pub(crate) fn next_element(&mut self, template: &VNode, path: &'static [u8]) -> ElementId {
|
||||
let entry = self.elements.vacant_entry();
|
||||
let id = entry.key();
|
||||
entry.insert(ElementRef {
|
||||
template: template as *const _ as *mut _,
|
||||
path: ElementPath::Deep(path),
|
||||
});
|
||||
ElementId(id)
|
||||
self.next(template, ElementPath::Deep(path))
|
||||
}
|
||||
|
||||
pub(crate) fn next_root(&mut self, template: &VNode, path: usize) -> ElementId {
|
||||
self.next(template, ElementPath::Root(path))
|
||||
}
|
||||
|
||||
fn next(&mut self, template: &VNode, path: ElementPath) -> ElementId {
|
||||
let entry = self.elements.vacant_entry();
|
||||
let id = entry.key();
|
||||
|
||||
entry.insert(ElementRef {
|
||||
template: template as *const _ as *mut _,
|
||||
path: ElementPath::Root(path),
|
||||
path,
|
||||
});
|
||||
ElementId(id)
|
||||
}
|
||||
|
@ -64,9 +60,9 @@ impl VirtualDom {
|
|||
pub(crate) fn try_reclaim(&mut self, el: ElementId) -> Option<ElementRef> {
|
||||
if el.0 == 0 {
|
||||
panic!(
|
||||
"Invalid element set to 0 - {:#?}",
|
||||
"Cannot reclaim the root element - {:#?}",
|
||||
std::backtrace::Backtrace::force_capture()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
self.elements.try_remove(el.0)
|
||||
|
@ -79,17 +75,15 @@ impl VirtualDom {
|
|||
|
||||
// Drop a scope and all its children
|
||||
pub(crate) fn drop_scope(&mut self, id: ScopeId) {
|
||||
let scope = self.scopes.get(id.0).unwrap();
|
||||
|
||||
if let Some(root) = scope.as_ref().try_root_node() {
|
||||
let root = unsafe { root.extend_lifetime_ref() };
|
||||
if let RenderReturn::Sync(Ok(node)) = root {
|
||||
if let Some(root) = self.scopes[id.0].as_ref().try_root_node() {
|
||||
if let RenderReturn::Sync(Ok(node)) = unsafe { root.extend_lifetime_ref() } {
|
||||
self.drop_scope_inner(node)
|
||||
}
|
||||
}
|
||||
|
||||
let scope = self.scopes.get_mut(id.0).unwrap();
|
||||
scope.props.take();
|
||||
self.scopes[id.0].props.take();
|
||||
|
||||
let scope = &mut self.scopes[id.0];
|
||||
|
||||
// Drop all the hooks once the children are dropped
|
||||
// this means we'll drop hooks bottom-up
|
||||
|
@ -99,24 +93,55 @@ impl VirtualDom {
|
|||
}
|
||||
|
||||
fn drop_scope_inner(&mut self, node: &VNode) {
|
||||
for attr in node.dynamic_attrs {
|
||||
if let AttributeValue::Listener(l) = &attr.value {
|
||||
l.borrow_mut().take();
|
||||
node.clear_listeners();
|
||||
node.dynamic_nodes.iter().for_each(|node| match node {
|
||||
DynamicNode::Component(c) => self.drop_scope(c.scope.get().unwrap()),
|
||||
DynamicNode::Fragment(nodes) => {
|
||||
nodes.iter().for_each(|node| self.drop_scope_inner(node))
|
||||
}
|
||||
}
|
||||
DynamicNode::Placeholder(t) => {
|
||||
self.try_reclaim(t.get());
|
||||
}
|
||||
DynamicNode::Text(t) => {
|
||||
self.try_reclaim(t.id.get());
|
||||
}
|
||||
});
|
||||
|
||||
for (idx, _) in node.template.roots.iter().enumerate() {
|
||||
match node.dynamic_root(idx) {
|
||||
Some(DynamicNode::Component(c)) => self.drop_scope(c.scope.get().unwrap()),
|
||||
Some(DynamicNode::Fragment(nodes)) => {
|
||||
for node in *nodes {
|
||||
self.drop_scope_inner(node);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
for root in node.root_ids {
|
||||
let id = root.get();
|
||||
if id.0 != 0 {
|
||||
self.try_reclaim(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Descend through the tree, removing any borrowed props and listeners
|
||||
pub(crate) fn ensure_drop_safety(&self, scope: ScopeId) {
|
||||
let node = unsafe { self.scopes[scope.0].previous_frame().try_load_node() };
|
||||
|
||||
// And now we want to make sure the previous frame has dropped anything that borrows self
|
||||
if let Some(RenderReturn::Sync(Ok(node))) = node {
|
||||
self.ensure_drop_safety_inner(node);
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_drop_safety_inner(&self, node: &VNode) {
|
||||
node.clear_listeners();
|
||||
|
||||
node.dynamic_nodes.iter().for_each(|child| match child {
|
||||
// Only descend if the props are borrowed
|
||||
DynamicNode::Component(c) if !c.static_props => {
|
||||
self.ensure_drop_safety(c.scope.get().unwrap());
|
||||
c.props.set(None);
|
||||
}
|
||||
|
||||
DynamicNode::Fragment(f) => f
|
||||
.iter()
|
||||
.for_each(|node| self.ensure_drop_safety_inner(node)),
|
||||
|
||||
_ => {}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ElementPath {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use std::cell::Cell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::innerlude::{VComponent, VText};
|
||||
use crate::mutations::Mutation;
|
||||
|
@ -42,7 +43,7 @@ impl<'b> VirtualDom {
|
|||
for (root_idx, root) in template.template.roots.iter().enumerate() {
|
||||
// We might need to generate an ID for the root node
|
||||
on_stack += match root {
|
||||
TemplateNode::DynamicText(id) | TemplateNode::Dynamic(id) => {
|
||||
TemplateNode::DynamicText { id } | TemplateNode::Dynamic { id } => {
|
||||
match &template.dynamic_nodes[*id] {
|
||||
// a dynamic text node doesn't replace a template node, instead we create it on the fly
|
||||
DynamicNode::Text(VText { id: slot, value }) => {
|
||||
|
@ -72,7 +73,7 @@ impl<'b> VirtualDom {
|
|||
}
|
||||
}
|
||||
|
||||
TemplateNode::Element { .. } | TemplateNode::Text(_) => {
|
||||
TemplateNode::Element { .. } | TemplateNode::Text { .. } => {
|
||||
let this_id = self.next_root(template, root_idx);
|
||||
|
||||
template.root_ids[root_idx].set(this_id);
|
||||
|
@ -213,7 +214,7 @@ impl<'b> VirtualDom {
|
|||
if template.template.roots.iter().all(|root| {
|
||||
matches!(
|
||||
root,
|
||||
TemplateNode::Dynamic(_) | TemplateNode::DynamicText(_)
|
||||
TemplateNode::Dynamic { .. } | TemplateNode::DynamicText { .. }
|
||||
)
|
||||
}) {
|
||||
return;
|
||||
|
@ -302,7 +303,8 @@ impl<'b> VirtualDom {
|
|||
|
||||
let unbounded_props = unsafe { std::mem::transmute(props) };
|
||||
|
||||
let scope = self.new_scope(unbounded_props).id;
|
||||
let scope = self.new_scope(unbounded_props, component.name);
|
||||
let scope = scope.id;
|
||||
component.scope.set(Some(scope));
|
||||
|
||||
let return_nodes = unsafe { self.run_scope(scope).extend_lifetime_ref() };
|
||||
|
@ -336,8 +338,8 @@ impl<'b> VirtualDom {
|
|||
}
|
||||
|
||||
// If running the scope has collected some leaves and *this* component is a boundary, then handle the suspense
|
||||
let boundary = match self.scopes[scope.0].has_context::<SuspenseContext>() {
|
||||
Some(boundary) => unsafe { &*(boundary as *const SuspenseContext) },
|
||||
let boundary = match self.scopes[scope.0].has_context::<Rc<SuspenseContext>>() {
|
||||
Some(boundary) => boundary,
|
||||
_ => return created,
|
||||
};
|
||||
|
||||
|
|
|
@ -25,40 +25,35 @@ impl<'b> VirtualDom {
|
|||
.previous_frame()
|
||||
.try_load_node()
|
||||
.expect("Call rebuild before diffing");
|
||||
|
||||
let new = scope_state
|
||||
.current_frame()
|
||||
.try_load_node()
|
||||
.expect("Call rebuild before diffing");
|
||||
self.diff_maybe_node(old, new);
|
||||
|
||||
use RenderReturn::{Async, Sync};
|
||||
|
||||
match (old, new) {
|
||||
(Sync(Ok(l)), Sync(Ok(r))) => self.diff_node(l, r),
|
||||
|
||||
// Err cases
|
||||
(Sync(Ok(l)), Sync(Err(e))) => self.diff_ok_to_err(l, e),
|
||||
(Sync(Err(e)), Sync(Ok(r))) => self.diff_err_to_ok(e, r),
|
||||
(Sync(Err(_eo)), Sync(Err(_en))) => { /* nothing */ }
|
||||
|
||||
// Async
|
||||
(Sync(Ok(_l)), Async(_)) => todo!(),
|
||||
(Sync(Err(_e)), Async(_)) => todo!(),
|
||||
(Async(_), Sync(Ok(_r))) => todo!(),
|
||||
(Async(_), Sync(Err(_e))) => { /* nothing */ }
|
||||
(Async(_), Async(_)) => { /* nothing */ }
|
||||
};
|
||||
}
|
||||
self.scope_stack.pop();
|
||||
}
|
||||
|
||||
fn diff_maybe_node(&mut self, old: &'b RenderReturn<'b>, new: &'b RenderReturn<'b>) {
|
||||
use RenderReturn::{Async, Sync};
|
||||
match (old, new) {
|
||||
(Sync(Ok(l)), Sync(Ok(r))) => self.diff_node(l, r),
|
||||
|
||||
// Err cases
|
||||
(Sync(Ok(l)), Sync(Err(e))) => self.diff_ok_to_err(l, e),
|
||||
(Sync(Err(e)), Sync(Ok(r))) => self.diff_err_to_ok(e, r),
|
||||
(Sync(Err(_eo)), Sync(Err(_en))) => { /* nothing */ }
|
||||
|
||||
// Async
|
||||
(Sync(Ok(_l)), Async(_)) => todo!(),
|
||||
(Sync(Err(_e)), Async(_)) => todo!(),
|
||||
(Async(_), Sync(Ok(_r))) => todo!(),
|
||||
(Async(_), Sync(Err(_e))) => { /* nothing */ }
|
||||
(Async(_), Async(_)) => { /* nothing */ }
|
||||
}
|
||||
}
|
||||
|
||||
fn diff_ok_to_err(&mut self, _l: &'b VNode<'b>, _e: &anyhow::Error) {
|
||||
todo!("Not yet handling error rollover")
|
||||
}
|
||||
fn diff_err_to_ok(&mut self, _e: &anyhow::Error, _l: &'b VNode<'b>) {
|
||||
todo!("Not yet handling error rollover")
|
||||
}
|
||||
fn diff_ok_to_err(&mut self, _l: &'b VNode<'b>, _e: &anyhow::Error) {}
|
||||
fn diff_err_to_ok(&mut self, _e: &anyhow::Error, _l: &'b VNode<'b>) {}
|
||||
|
||||
fn diff_node(&mut self, left_template: &'b VNode<'b>, right_template: &'b VNode<'b>) {
|
||||
if !std::ptr::eq(left_template.template.name, right_template.template.name)
|
||||
|
@ -144,16 +139,18 @@ impl<'b> VirtualDom {
|
|||
}
|
||||
|
||||
fn replace_nodes_with_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b Cell<ElementId>) {
|
||||
// Remove the old nodes, except for one
|
||||
self.remove_nodes(&l[1..]);
|
||||
|
||||
// Now create the new one
|
||||
let first = self.replace_inner(&l[0]);
|
||||
|
||||
// Create the placeholder first, ensuring we get a dedicated ID for the placeholder
|
||||
let placeholder = self.next_element(&l[0], &[]);
|
||||
r.set(placeholder);
|
||||
self.mutations
|
||||
.push(Mutation::CreatePlaceholder { id: placeholder });
|
||||
|
||||
// Remove the old nodes, except for onea
|
||||
let first = self.replace_inner(&l[0]);
|
||||
self.remove_nodes(&l[1..]);
|
||||
|
||||
self.mutations
|
||||
.push(Mutation::ReplaceWith { id: first, m: 1 });
|
||||
|
||||
|
@ -746,7 +743,8 @@ impl<'b> VirtualDom {
|
|||
/// Remove these nodes from the dom
|
||||
/// Wont generate mutations for the inner nodes
|
||||
fn remove_nodes(&mut self, nodes: &'b [VNode<'b>]) {
|
||||
nodes.iter().for_each(|node| self.remove_node(node));
|
||||
// note that we iterate in reverse to unlink lists of nodes in their rough index order
|
||||
nodes.iter().rev().for_each(|node| self.remove_node(node));
|
||||
}
|
||||
|
||||
fn remove_node(&mut self, node: &'b VNode<'b>) {
|
||||
|
@ -911,7 +909,7 @@ fn matching_components<'a>(
|
|||
.zip(right.template.roots.iter())
|
||||
.map(|(l, r)| {
|
||||
let (l, r) = match (l, r) {
|
||||
(TemplateNode::Dynamic(l), TemplateNode::Dynamic(r)) => (l, r),
|
||||
(TemplateNode::Dynamic { id: l }, TemplateNode::Dynamic { id: r }) => (l, r),
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
|
|
|
@ -29,8 +29,8 @@ use crate::innerlude::*;
|
|||
pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element {
|
||||
let children = cx.props.0.as_ref().map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||
Ok(VNode {
|
||||
key: children.key.clone(),
|
||||
parent: children.parent.clone(),
|
||||
key: children.key,
|
||||
parent: children.parent,
|
||||
template: children.template,
|
||||
root_ids: children.root_ids,
|
||||
dynamic_nodes: children.dynamic_nodes,
|
||||
|
|
|
@ -13,6 +13,7 @@ use crate::{arena::ElementId, ScopeId, Template};
|
|||
/// Templates, however, apply to all subtrees, not just target subtree.
|
||||
///
|
||||
/// Mutations are the only link between the RealDOM and the VirtualDOM.
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
|
||||
#[derive(Debug, Default)]
|
||||
#[must_use = "not handling edits can lead to visual inconsistencies in UI"]
|
||||
pub struct Mutations<'a> {
|
||||
|
|
|
@ -78,12 +78,20 @@ impl<'a> VNode<'a> {
|
|||
/// Returns [`None`] if the root is actually a static node (Element/Text)
|
||||
pub fn dynamic_root(&self, idx: usize) -> Option<&'a DynamicNode<'a>> {
|
||||
match &self.template.roots[idx] {
|
||||
TemplateNode::Element { .. } | TemplateNode::Text(_) => None,
|
||||
TemplateNode::Dynamic(id) | TemplateNode::DynamicText(id) => {
|
||||
TemplateNode::Element { .. } | TemplateNode::Text { text: _ } => None,
|
||||
TemplateNode::Dynamic { id } | TemplateNode::DynamicText { id } => {
|
||||
Some(&self.dynamic_nodes[*id])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn clear_listeners(&self) {
|
||||
for attr in self.dynamic_attrs {
|
||||
if let AttributeValue::Listener(l) = &attr.value {
|
||||
l.borrow_mut().take();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A static layout of a UI tree that describes a set of dynamic and static nodes.
|
||||
|
@ -124,7 +132,7 @@ pub struct Template<'a> {
|
|||
///
|
||||
/// This can be created at compile time, saving the VirtualDom time when diffing the tree
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord)]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize), serde(tag = "type"))]
|
||||
pub enum TemplateNode<'a> {
|
||||
/// An statically known element in the dom.
|
||||
///
|
||||
|
@ -151,15 +159,24 @@ pub enum TemplateNode<'a> {
|
|||
},
|
||||
|
||||
/// This template node is just a piece of static text
|
||||
Text(&'a str),
|
||||
Text {
|
||||
/// The actual text
|
||||
text: &'a str,
|
||||
},
|
||||
|
||||
/// This template node is unknown, and needs to be created at runtime.
|
||||
Dynamic(usize),
|
||||
Dynamic {
|
||||
/// The index of the dynamic node in the VNode's dynamic_nodes list
|
||||
id: usize,
|
||||
},
|
||||
|
||||
/// This template node is known to be some text, but needs to be created at runtime
|
||||
///
|
||||
/// This is separate from the pure Dynamic variant for various optimizations
|
||||
DynamicText(usize),
|
||||
DynamicText {
|
||||
/// The index of the dynamic node in the VNode's dynamic_nodes list
|
||||
id: usize,
|
||||
},
|
||||
}
|
||||
|
||||
/// A node created at runtime
|
||||
|
@ -244,7 +261,11 @@ pub struct VText<'a> {
|
|||
|
||||
/// An attribute of the TemplateNode, created at compile time
|
||||
#[derive(Debug, PartialEq, Hash, Eq, PartialOrd, Ord)]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(
|
||||
feature = "serialize",
|
||||
derive(serde::Serialize, serde::Deserialize),
|
||||
serde(tag = "type")
|
||||
)]
|
||||
pub enum TemplateAttribute<'a> {
|
||||
/// This attribute is entirely known at compile time, enabling
|
||||
Static {
|
||||
|
@ -265,7 +286,10 @@ pub enum TemplateAttribute<'a> {
|
|||
/// The attribute in this position is actually determined dynamically at runtime
|
||||
///
|
||||
/// This is the index into the dynamic_attributes field on the container VNode
|
||||
Dynamic(usize),
|
||||
Dynamic {
|
||||
/// The index
|
||||
id: usize,
|
||||
},
|
||||
}
|
||||
|
||||
/// An attribute on a DOM node, such as `id="my-thing"` or `href="https://example.com"`
|
||||
|
@ -416,6 +440,8 @@ impl<'a> IntoDynNode<'a> for VNode<'a> {
|
|||
DynamicNode::Fragment(_cx.bump().alloc([self]))
|
||||
}
|
||||
}
|
||||
|
||||
// An element that's an error is currently lost into the ether
|
||||
impl<'a> IntoDynNode<'a> for Element<'a> {
|
||||
fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> {
|
||||
match self {
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
use futures_util::FutureExt;
|
||||
use std::task::{Context, Poll};
|
||||
use std::{
|
||||
rc::Rc,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
innerlude::{Mutation, Mutations, SuspenseContext},
|
||||
|
@ -31,12 +34,10 @@ impl VirtualDom {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn acquire_suspense_boundary<'a>(&self, id: ScopeId) -> &'a SuspenseContext {
|
||||
let ct = self.scopes[id.0]
|
||||
.consume_context::<SuspenseContext>()
|
||||
.unwrap();
|
||||
|
||||
unsafe { &*(ct as *const SuspenseContext) }
|
||||
pub(crate) fn acquire_suspense_boundary(&self, id: ScopeId) -> Rc<SuspenseContext> {
|
||||
self.scopes[id.0]
|
||||
.consume_context::<Rc<SuspenseContext>>()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn handle_suspense_wakeup(&mut self, id: SuspenseId) {
|
||||
|
|
|
@ -7,7 +7,6 @@ use crate::{
|
|||
scheduler::RcWake,
|
||||
scopes::{ScopeId, ScopeState},
|
||||
virtual_dom::VirtualDom,
|
||||
AttributeValue, DynamicNode, VNode,
|
||||
};
|
||||
use bumpalo::Bump;
|
||||
use futures_util::FutureExt;
|
||||
|
@ -19,7 +18,11 @@ use std::{
|
|||
};
|
||||
|
||||
impl VirtualDom {
|
||||
pub(super) fn new_scope(&mut self, props: Box<dyn AnyProps<'static>>) -> &ScopeState {
|
||||
pub(super) fn new_scope(
|
||||
&mut self,
|
||||
props: Box<dyn AnyProps<'static>>,
|
||||
name: &'static str,
|
||||
) -> &ScopeState {
|
||||
let parent = self.acquire_current_scope_raw();
|
||||
let entry = self.scopes.vacant_entry();
|
||||
let height = unsafe { parent.map(|f| (*f).height + 1).unwrap_or(0) };
|
||||
|
@ -30,6 +33,7 @@ impl VirtualDom {
|
|||
id,
|
||||
height,
|
||||
props: Some(props),
|
||||
name,
|
||||
placeholder: Default::default(),
|
||||
node_arena_1: BumpFrame::new(0),
|
||||
node_arena_2: BumpFrame::new(0),
|
||||
|
@ -50,42 +54,6 @@ impl VirtualDom {
|
|||
.and_then(|id| self.scopes.get_mut(id.0).map(|f| f.as_mut() as *mut _))
|
||||
}
|
||||
|
||||
fn ensure_drop_safety(&self, scope: ScopeId) {
|
||||
let scope = &self.scopes[scope.0];
|
||||
let node = unsafe { scope.previous_frame().try_load_node() };
|
||||
|
||||
// And now we want to make sure the previous frame has dropped anything that borrows self
|
||||
if let Some(RenderReturn::Sync(Ok(node))) = node {
|
||||
self.ensure_drop_safety_inner(node);
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_drop_safety_inner(&self, node: &VNode) {
|
||||
for attr in node.dynamic_attrs {
|
||||
if let AttributeValue::Listener(l) = &attr.value {
|
||||
l.borrow_mut().take();
|
||||
}
|
||||
}
|
||||
|
||||
for child in node.dynamic_nodes {
|
||||
match child {
|
||||
DynamicNode::Component(c) => {
|
||||
// Only descend if the props are borrowed
|
||||
if !c.static_props {
|
||||
self.ensure_drop_safety(c.scope.get().unwrap());
|
||||
c.props.set(None);
|
||||
}
|
||||
}
|
||||
DynamicNode::Fragment(f) => {
|
||||
for node in *f {
|
||||
self.ensure_drop_safety_inner(node);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> &RenderReturn {
|
||||
// Cycle to the next frame and then reset it
|
||||
// This breaks any latent references, invalidating every pointer referencing into it.
|
||||
|
|
|
@ -69,6 +69,7 @@ pub struct ScopeId(pub usize);
|
|||
/// This struct exists to provide a common interface for all scopes without relying on generics.
|
||||
pub struct ScopeState {
|
||||
pub(crate) render_cnt: Cell<usize>,
|
||||
pub(crate) name: &'static str,
|
||||
|
||||
pub(crate) node_arena_1: BumpFrame,
|
||||
pub(crate) node_arena_2: BumpFrame,
|
||||
|
@ -116,6 +117,11 @@ impl<'src> ScopeState {
|
|||
}
|
||||
}
|
||||
|
||||
/// Get the name of this component
|
||||
pub fn name(&self) -> &str {
|
||||
self.name
|
||||
}
|
||||
|
||||
/// Get the current render since the inception of this component
|
||||
///
|
||||
/// This can be used as a helpful diagnostic when debugging hooks/renders, etc
|
||||
|
@ -251,17 +257,18 @@ impl<'src> ScopeState {
|
|||
}
|
||||
|
||||
/// Return any context of type T if it exists on this scope
|
||||
pub fn has_context<T: 'static>(&self) -> Option<&T> {
|
||||
let contextex = self.shared_contexts.borrow();
|
||||
let val = contextex.get(&TypeId::of::<T>())?;
|
||||
let as_concrete = val.downcast_ref::<T>()? as *const T;
|
||||
Some(unsafe { &*as_concrete })
|
||||
pub fn has_context<T: 'static + Clone>(&self) -> Option<T> {
|
||||
self.shared_contexts
|
||||
.borrow()
|
||||
.get(&TypeId::of::<T>())?
|
||||
.downcast_ref::<T>()
|
||||
.cloned()
|
||||
}
|
||||
|
||||
/// Try to retrieve a shared state with type `T` from any parent scope.
|
||||
///
|
||||
/// To release the borrow, use `cloned` if the context is clone.
|
||||
pub fn consume_context<T: 'static>(&self) -> Option<&T> {
|
||||
/// Clones the state if it exists.
|
||||
pub fn consume_context<T: 'static + Clone>(&self) -> Option<T> {
|
||||
if let Some(this_ctx) = self.has_context() {
|
||||
return Some(this_ctx);
|
||||
}
|
||||
|
@ -271,8 +278,7 @@ impl<'src> ScopeState {
|
|||
// safety: all parent pointers are valid thanks to the bump arena
|
||||
let parent = unsafe { &*parent_ptr };
|
||||
if let Some(shared) = parent.shared_contexts.borrow().get(&TypeId::of::<T>()) {
|
||||
let as_concrete = shared.downcast_ref::<T>()? as *const T;
|
||||
return Some(unsafe { &*as_concrete });
|
||||
return shared.downcast_ref::<T>().cloned();
|
||||
}
|
||||
search_parent = parent.parent;
|
||||
}
|
||||
|
@ -284,7 +290,7 @@ impl<'src> ScopeState {
|
|||
///
|
||||
/// This is a "fundamental" operation and should only be called during initialization of a hook.
|
||||
///
|
||||
/// For a hook that provides the same functionality, use `use_provide_context` and `use_consume_context` instead.
|
||||
/// For a hook that provides the same functionality, use `use_provide_context` and `use_context` instead.
|
||||
///
|
||||
/// If a state is provided that already exists, the new value will not be inserted. Instead, this method will
|
||||
/// return the existing value. This behavior is chosen so shared values do not need to be `Clone`. This particular
|
||||
|
@ -305,20 +311,14 @@ impl<'src> ScopeState {
|
|||
/// render!(div { "hello {state.0}" })
|
||||
/// }
|
||||
/// ```
|
||||
pub fn provide_context<T: 'static>(&self, value: T) -> &T {
|
||||
let mut contexts = self.shared_contexts.borrow_mut();
|
||||
pub fn provide_context<T: 'static + Clone>(&self, value: T) -> T {
|
||||
let value2 = value.clone();
|
||||
|
||||
let any = match contexts.get(&TypeId::of::<T>()) {
|
||||
Some(item) => item.downcast_ref::<T>().unwrap() as *const T,
|
||||
None => {
|
||||
let boxed = Box::new(value);
|
||||
let boxed_ptr = boxed.as_ref() as *const T;
|
||||
contexts.insert(TypeId::of::<T>(), boxed);
|
||||
boxed_ptr
|
||||
}
|
||||
};
|
||||
self.shared_contexts
|
||||
.borrow_mut()
|
||||
.insert(TypeId::of::<T>(), Box::new(value));
|
||||
|
||||
unsafe { &*any }
|
||||
value2
|
||||
}
|
||||
|
||||
/// Pushes the future onto the poll queue to be polled after the component renders.
|
||||
|
|
|
@ -240,20 +240,19 @@ impl VirtualDom {
|
|||
mutations: Mutations::default(),
|
||||
};
|
||||
|
||||
let root = dom.new_scope(Box::new(VProps::new(
|
||||
root,
|
||||
|_, _| unreachable!(),
|
||||
root_props,
|
||||
)));
|
||||
let root = dom.new_scope(
|
||||
Box::new(VProps::new(root, |_, _| unreachable!(), root_props)),
|
||||
"app",
|
||||
);
|
||||
|
||||
// The root component is always a suspense boundary for any async children
|
||||
// This could be unexpected, so we might rethink this behavior later
|
||||
//
|
||||
// We *could* just panic if the suspense boundary is not found
|
||||
root.provide_context(SuspenseContext::new(ScopeId(0)));
|
||||
root.provide_context(Rc::new(SuspenseContext::new(ScopeId(0))));
|
||||
|
||||
// Unlike react, we provide a default error boundary that just renders the error as a string
|
||||
root.provide_context(ErrorBoundary::new(ScopeId(0)));
|
||||
root.provide_context(Rc::new(ErrorBoundary::new(ScopeId(0))));
|
||||
|
||||
// the root element is always given element ID 0 since it's the container for the entire tree
|
||||
dom.elements.insert(ElementRef::null());
|
||||
|
@ -297,7 +296,7 @@ impl VirtualDom {
|
|||
/// currently suspended.
|
||||
pub fn is_scope_suspended(&self, id: ScopeId) -> bool {
|
||||
!self.scopes[id.0]
|
||||
.consume_context::<SuspenseContext>()
|
||||
.consume_context::<Rc<SuspenseContext>>()
|
||||
.unwrap()
|
||||
.waiting_on
|
||||
.borrow()
|
||||
|
@ -528,7 +527,7 @@ impl VirtualDom {
|
|||
// first, unload any complete suspense trees
|
||||
for finished_fiber in self.finished_fibers.drain(..) {
|
||||
let scope = &mut self.scopes[finished_fiber.0];
|
||||
let context = scope.has_context::<SuspenseContext>().unwrap();
|
||||
let context = scope.has_context::<Rc<SuspenseContext>>().unwrap();
|
||||
|
||||
self.mutations
|
||||
.templates
|
||||
|
@ -566,7 +565,7 @@ impl VirtualDom {
|
|||
// No placeholder necessary since this is a diff
|
||||
if !self.collected_leaves.is_empty() {
|
||||
let mut boundary = self.scopes[dirty.id.0]
|
||||
.consume_context::<SuspenseContext>()
|
||||
.consume_context::<Rc<SuspenseContext>>()
|
||||
.unwrap();
|
||||
|
||||
let boundary_mut = boundary.borrow_mut();
|
||||
|
|
|
@ -17,12 +17,12 @@ fn app(cx: Scope) -> Element {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn it_goes() {
|
||||
fn bubbles_error() {
|
||||
let mut dom = VirtualDom::new(app);
|
||||
|
||||
let edits = dom.rebuild().santize();
|
||||
let _edits = dom.rebuild().santize();
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
|
||||
dom.render_immediate();
|
||||
_ = dom.render_immediate();
|
||||
}
|
||||
|
|
|
@ -320,9 +320,9 @@ fn remove_list() {
|
|||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
Remove { id: ElementId(3) },
|
||||
Remove { id: ElementId(5) },
|
||||
Remove { id: ElementId(4) },
|
||||
Remove { id: ElementId(5) }
|
||||
Remove { id: ElementId(3) },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
@ -345,10 +345,10 @@ fn no_common_keys() {
|
|||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
Remove { id: ElementId(2) },
|
||||
Remove { id: ElementId(3) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(3) },
|
||||
Remove { id: ElementId(2) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(2) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(3) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(4) },
|
||||
ReplaceWith { id: ElementId(1), m: 3 }
|
||||
]
|
||||
|
|
|
@ -357,11 +357,11 @@ fn remove_many() {
|
|||
assert_eq!(
|
||||
edits.edits,
|
||||
[
|
||||
Remove { id: ElementId(1,) },
|
||||
Remove { id: ElementId(5,) },
|
||||
Remove { id: ElementId(7,) },
|
||||
Remove { id: ElementId(9,) },
|
||||
CreatePlaceholder { id: ElementId(9,) },
|
||||
Remove { id: ElementId(7,) },
|
||||
Remove { id: ElementId(5,) },
|
||||
Remove { id: ElementId(1,) },
|
||||
CreatePlaceholder { id: ElementId(3,) },
|
||||
ReplaceWith { id: ElementId(2,), m: 1 },
|
||||
]
|
||||
);
|
||||
|
@ -372,8 +372,8 @@ fn remove_many() {
|
|||
edits.edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
|
||||
HydrateText { path: &[0,], value: "hello 0", id: ElementId(10,) },
|
||||
ReplaceWith { id: ElementId(9,), m: 1 },
|
||||
HydrateText { path: &[0,], value: "hello 0", id: ElementId(1,) },
|
||||
ReplaceWith { id: ElementId(3,), m: 1 },
|
||||
]
|
||||
)
|
||||
}
|
||||
|
|
|
@ -8,16 +8,16 @@ fn miri_rollover() {
|
|||
|
||||
_ = dom.rebuild();
|
||||
|
||||
for x in 0..3 {
|
||||
for _ in 0..3 {
|
||||
dom.handle_event("click", Rc::new(MouseData::default()), ElementId(2), true);
|
||||
dom.process_events();
|
||||
dom.render_immediate();
|
||||
_ = dom.render_immediate();
|
||||
}
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let mut idx = use_state(cx, || 0);
|
||||
let onhover = |h| println!("go!");
|
||||
let onhover = |_| println!("go!");
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
|
@ -31,7 +31,7 @@ fn app(cx: Scope) -> Element {
|
|||
button { onclick: move |_| idx -= 1, "-" }
|
||||
ul {
|
||||
(0..**idx).map(|i| rsx! {
|
||||
Child { i: i, onhover: onhover }
|
||||
child_example { i: i, onhover: onhover }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ fn app(cx: Scope) -> Element {
|
|||
}
|
||||
|
||||
#[inline_props]
|
||||
fn Child<'a>(cx: Scope<'a>, i: i32, onhover: EventHandler<'a, MouseEvent>) -> Element {
|
||||
fn child_example<'a>(cx: Scope<'a>, i: i32, onhover: EventHandler<'a, MouseEvent>) -> Element {
|
||||
cx.render(rsx! {
|
||||
li {
|
||||
onmouseover: move |e| onhover.call(e),
|
||||
|
|
|
@ -97,8 +97,8 @@ fn memo_works_properly() {
|
|||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
|
||||
dom.rebuild();
|
||||
todo!()
|
||||
_ = dom.rebuild();
|
||||
// todo!()
|
||||
// dom.hard_diff(ScopeId(0));
|
||||
// dom.hard_diff(ScopeId(0));
|
||||
// dom.hard_diff(ScopeId(0));
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
//! Tests related to safety of the library.
|
||||
|
||||
use std::rc::Rc;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_core::SuspenseContext;
|
||||
|
||||
|
@ -18,5 +20,5 @@ fn root_node_isnt_null() {
|
|||
|
||||
// There should be a default suspense context
|
||||
// todo: there should also be a default error boundary
|
||||
assert!(scope.has_context::<SuspenseContext>().is_some());
|
||||
assert!(scope.has_context::<Rc<SuspenseContext>>().is_some());
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
use dioxus::core::ElementId;
|
||||
use dioxus::core::{Mutation::*, SuspenseContext};
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_core::SuspenseContext;
|
||||
use std::future::IntoFuture;
|
||||
use std::{rc::Rc, time::Duration};
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::test]
|
||||
async fn it_works() {
|
||||
|
@ -51,10 +51,12 @@ fn app(cx: Scope) -> Element {
|
|||
}
|
||||
|
||||
fn suspense_boundary(cx: Scope) -> Element {
|
||||
cx.use_hook(|| cx.provide_context(Rc::new(SuspenseContext::new(cx.scope_id()))));
|
||||
cx.use_hook(|| {
|
||||
cx.provide_context(Rc::new(SuspenseContext::new(cx.scope_id())));
|
||||
});
|
||||
|
||||
// Ensure the right types are found
|
||||
cx.has_context::<SuspenseContext>().unwrap();
|
||||
cx.has_context::<Rc<SuspenseContext>>().unwrap();
|
||||
|
||||
cx.render(rsx!(async_child {}))
|
||||
}
|
||||
|
@ -72,7 +74,6 @@ async fn async_text(cx: Scope<'_>) -> Element {
|
|||
|
||||
let age = use_future!(cx, || async {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
||||
println!("long future completed");
|
||||
1234
|
||||
});
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ webbrowser = "0.8.0"
|
|||
infer = "0.11.0"
|
||||
dunce = "1.0.2"
|
||||
|
||||
interprocess = { version = "1.1.1" }
|
||||
interprocess = { version = "1.1.1", optional = true}
|
||||
futures-util = "0.3.25"
|
||||
|
||||
[target.'cfg(target_os = "ios")'.dependencies]
|
||||
|
@ -44,11 +44,12 @@ objc_id = "0.1.1"
|
|||
core-foundation = "0.9.3"
|
||||
|
||||
[features]
|
||||
default = ["tokio_runtime"]
|
||||
default = ["tokio_runtime", "hot-reload"]
|
||||
tokio_runtime = ["tokio"]
|
||||
fullscreen = ["wry/fullscreen"]
|
||||
transparent = ["wry/transparent"]
|
||||
tray = ["wry/tray"]
|
||||
hot-reload = ["interprocess"]
|
||||
|
||||
[dev-dependencies]
|
||||
dioxus-core-macro = { path = "../core-macro" }
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::desktop_context::{DesktopContext, UserWindowEvent};
|
||||
use crate::events::{decode_event, EventMessage};
|
||||
use dioxus_core::*;
|
||||
use futures_channel::mpsc::UnboundedReceiver;
|
||||
use futures_channel::mpsc::{unbounded, UnboundedSender};
|
||||
use futures_util::StreamExt;
|
||||
#[cfg(target_os = "ios")]
|
||||
use objc::runtime::Object;
|
||||
|
@ -22,6 +22,9 @@ pub(super) struct DesktopController {
|
|||
pub(super) pending_edits: Arc<Mutex<Vec<String>>>,
|
||||
pub(super) quit_app_on_close: bool,
|
||||
pub(super) is_ready: Arc<AtomicBool>,
|
||||
pub(super) proxy: EventLoopProxy<UserWindowEvent>,
|
||||
pub(super) event_tx: UnboundedSender<serde_json::Value>,
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
pub(super) views: Vec<*mut Object>,
|
||||
}
|
||||
|
@ -33,9 +36,10 @@ impl DesktopController {
|
|||
root: Component<P>,
|
||||
props: P,
|
||||
proxy: EventLoopProxy<UserWindowEvent>,
|
||||
mut event_rx: UnboundedReceiver<serde_json::Value>,
|
||||
) -> Self {
|
||||
let edit_queue = Arc::new(Mutex::new(Vec::new()));
|
||||
let (event_tx, mut event_rx) = unbounded();
|
||||
let proxy2 = proxy.clone();
|
||||
|
||||
let pending_edits = edit_queue.clone();
|
||||
let desktop_context_proxy = proxy.clone();
|
||||
|
@ -54,8 +58,7 @@ impl DesktopController {
|
|||
{
|
||||
let edits = dom.rebuild();
|
||||
let mut queue = edit_queue.lock().unwrap();
|
||||
queue.push(serde_json::to_string(&edits.templates).unwrap());
|
||||
queue.push(serde_json::to_string(&edits.edits).unwrap());
|
||||
queue.push(serde_json::to_string(&edits).unwrap());
|
||||
proxy.send_event(UserWindowEvent::EditsReady).unwrap();
|
||||
}
|
||||
|
||||
|
@ -77,12 +80,8 @@ impl DesktopController {
|
|||
.render_with_deadline(tokio::time::sleep(Duration::from_millis(16)))
|
||||
.await;
|
||||
|
||||
{
|
||||
let mut queue = edit_queue.lock().unwrap();
|
||||
queue.push(serde_json::to_string(&muts.templates).unwrap());
|
||||
queue.push(serde_json::to_string(&muts.edits).unwrap());
|
||||
let _ = proxy.send_event(UserWindowEvent::EditsReady);
|
||||
}
|
||||
edit_queue.lock().unwrap().push(serde_json::to_string(&muts).unwrap());
|
||||
let _ = proxy.send_event(UserWindowEvent::EditsReady);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
@ -92,6 +91,8 @@ impl DesktopController {
|
|||
webviews: HashMap::new(),
|
||||
is_ready: Arc::new(AtomicBool::new(false)),
|
||||
quit_app_on_close: true,
|
||||
proxy: proxy2,
|
||||
event_tx,
|
||||
#[cfg(target_os = "ios")]
|
||||
views: vec![],
|
||||
}
|
||||
|
@ -116,12 +117,14 @@ impl DesktopController {
|
|||
|
||||
let (_id, view) = self.webviews.iter_mut().next().unwrap();
|
||||
|
||||
println!("processing pending edits {:?}", new_queue.len());
|
||||
|
||||
for edit in new_queue.drain(..) {
|
||||
view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_template(&self, _serialized_template: String) {
|
||||
todo!("hot reloading currently WIP")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -168,6 +168,11 @@ pub enum UserWindowEvent {
|
|||
DragWindow,
|
||||
FocusWindow,
|
||||
|
||||
/// Set a new Dioxus template for hot-reloading
|
||||
///
|
||||
/// Is a no-op in release builds. Must fit the right format for templates
|
||||
SetTemplate(String),
|
||||
|
||||
Visible(bool),
|
||||
Minimize(bool),
|
||||
Maximize(bool),
|
||||
|
@ -195,99 +200,101 @@ pub enum UserWindowEvent {
|
|||
PopView,
|
||||
}
|
||||
|
||||
pub(super) fn handler(
|
||||
user_event: UserWindowEvent,
|
||||
desktop: &mut DesktopController,
|
||||
control_flow: &mut ControlFlow,
|
||||
) {
|
||||
// currently dioxus-desktop supports a single window only,
|
||||
// so we can grab the only webview from the map;
|
||||
// on wayland it is possible that a user event is emitted
|
||||
// before the webview is initialized. ignore the event.
|
||||
let webview = if let Some(webview) = desktop.webviews.values().next() {
|
||||
webview
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
let window = webview.window();
|
||||
impl DesktopController {
|
||||
pub(super) fn handle_event(
|
||||
&mut self,
|
||||
user_event: UserWindowEvent,
|
||||
control_flow: &mut ControlFlow,
|
||||
) {
|
||||
// currently dioxus-desktop supports a single window only,
|
||||
// so we can grab the only webview from the map;
|
||||
// on wayland it is possible that a user event is emitted
|
||||
// before the webview is initialized. ignore the event.
|
||||
let webview = if let Some(webview) = self.webviews.values().next() {
|
||||
webview
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
println!("user_event: {:?}", user_event);
|
||||
let window = webview.window();
|
||||
|
||||
match user_event {
|
||||
Initialize | EditsReady => desktop.try_load_ready_webviews(),
|
||||
CloseWindow => *control_flow = ControlFlow::Exit,
|
||||
DragWindow => {
|
||||
// if the drag_window has any errors, we don't do anything
|
||||
window.fullscreen().is_none().then(|| window.drag_window());
|
||||
}
|
||||
Visible(state) => window.set_visible(state),
|
||||
Minimize(state) => window.set_minimized(state),
|
||||
Maximize(state) => window.set_maximized(state),
|
||||
MaximizeToggle => window.set_maximized(!window.is_maximized()),
|
||||
Fullscreen(state) => {
|
||||
if let Some(handle) = window.current_monitor() {
|
||||
window.set_fullscreen(state.then_some(WryFullscreen::Borderless(Some(handle))));
|
||||
match user_event {
|
||||
Initialize | EditsReady => self.try_load_ready_webviews(),
|
||||
SetTemplate(template) => self.set_template(template),
|
||||
CloseWindow => *control_flow = ControlFlow::Exit,
|
||||
DragWindow => {
|
||||
// if the drag_window has any errors, we don't do anything
|
||||
window.fullscreen().is_none().then(|| window.drag_window());
|
||||
}
|
||||
}
|
||||
FocusWindow => window.set_focus(),
|
||||
Resizable(state) => window.set_resizable(state),
|
||||
AlwaysOnTop(state) => window.set_always_on_top(state),
|
||||
|
||||
CursorVisible(state) => window.set_cursor_visible(state),
|
||||
CursorGrab(state) => {
|
||||
let _ = window.set_cursor_grab(state);
|
||||
}
|
||||
|
||||
SetTitle(content) => window.set_title(&content),
|
||||
SetDecorations(state) => window.set_decorations(state),
|
||||
|
||||
SetZoomLevel(scale_factor) => webview.zoom(scale_factor),
|
||||
|
||||
Print => {
|
||||
if let Err(e) = webview.print() {
|
||||
// we can't panic this error.
|
||||
log::warn!("Open print modal failed: {e}");
|
||||
Visible(state) => window.set_visible(state),
|
||||
Minimize(state) => window.set_minimized(state),
|
||||
Maximize(state) => window.set_maximized(state),
|
||||
MaximizeToggle => window.set_maximized(!window.is_maximized()),
|
||||
Fullscreen(state) => {
|
||||
if let Some(handle) = window.current_monitor() {
|
||||
window.set_fullscreen(state.then_some(WryFullscreen::Borderless(Some(handle))));
|
||||
}
|
||||
}
|
||||
}
|
||||
DevTool => {
|
||||
#[cfg(debug_assertions)]
|
||||
webview.open_devtools();
|
||||
#[cfg(not(debug_assertions))]
|
||||
log::warn!("Devtools are disabled in release builds");
|
||||
}
|
||||
FocusWindow => window.set_focus(),
|
||||
Resizable(state) => window.set_resizable(state),
|
||||
AlwaysOnTop(state) => window.set_always_on_top(state),
|
||||
|
||||
Eval(code) => {
|
||||
if let Err(e) = webview.evaluate_script(code.as_str()) {
|
||||
// we can't panic this error.
|
||||
log::warn!("Eval script error: {e}");
|
||||
CursorVisible(state) => window.set_cursor_visible(state),
|
||||
CursorGrab(state) => {
|
||||
let _ = window.set_cursor_grab(state);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
PushView(view) => unsafe {
|
||||
use objc::runtime::Object;
|
||||
use objc::*;
|
||||
assert!(is_main_thread());
|
||||
let ui_view = window.ui_view() as *mut Object;
|
||||
let ui_view_frame: *mut Object = msg_send![ui_view, frame];
|
||||
let _: () = msg_send![view, setFrame: ui_view_frame];
|
||||
let _: () = msg_send![view, setAutoresizingMask: 31];
|
||||
SetTitle(content) => window.set_title(&content),
|
||||
SetDecorations(state) => window.set_decorations(state),
|
||||
|
||||
let ui_view_controller = window.ui_view_controller() as *mut Object;
|
||||
let _: () = msg_send![ui_view_controller, setView: view];
|
||||
desktop.views.push(ui_view);
|
||||
},
|
||||
SetZoomLevel(scale_factor) => webview.zoom(scale_factor),
|
||||
|
||||
Print => {
|
||||
if let Err(e) = webview.print() {
|
||||
// we can't panic this error.
|
||||
log::warn!("Open print modal failed: {e}");
|
||||
}
|
||||
}
|
||||
DevTool => {
|
||||
#[cfg(debug_assertions)]
|
||||
webview.open_devtools();
|
||||
#[cfg(not(debug_assertions))]
|
||||
log::warn!("Devtools are disabled in release builds");
|
||||
}
|
||||
|
||||
Eval(code) => {
|
||||
if let Err(e) = webview.evaluate_script(code.as_str()) {
|
||||
// we can't panic this error.
|
||||
log::warn!("Eval script error: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
PushView(view) => unsafe {
|
||||
use objc::runtime::Object;
|
||||
use objc::*;
|
||||
assert!(is_main_thread());
|
||||
let ui_view = window.ui_view() as *mut Object;
|
||||
let ui_view_frame: *mut Object = msg_send![ui_view, frame];
|
||||
let _: () = msg_send![view, setFrame: ui_view_frame];
|
||||
let _: () = msg_send![view, setAutoresizingMask: 31];
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
PopView => unsafe {
|
||||
use objc::runtime::Object;
|
||||
use objc::*;
|
||||
assert!(is_main_thread());
|
||||
if let Some(view) = desktop.views.pop() {
|
||||
let ui_view_controller = window.ui_view_controller() as *mut Object;
|
||||
let _: () = msg_send![ui_view_controller, setView: view];
|
||||
}
|
||||
},
|
||||
self.views.push(ui_view);
|
||||
},
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
PopView => unsafe {
|
||||
use objc::runtime::Object;
|
||||
use objc::*;
|
||||
assert!(is_main_thread());
|
||||
if let Some(view) = self.views.pop() {
|
||||
let ui_view_controller = window.ui_view_controller() as *mut Object;
|
||||
let _: () = msg_send![ui_view_controller, setView: view];
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
//! Convert a serialized event to an event trigger
|
||||
|
||||
use dioxus_core::ElementId;
|
||||
use dioxus_html::events::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::from_value;
|
||||
|
@ -43,7 +42,6 @@ macro_rules! match_data {
|
|||
) => {
|
||||
match $name {
|
||||
$( $($mname)|* => {
|
||||
println!("casting to type {:?}", std::any::TypeId::of::<$tip>());
|
||||
let val: $tip = from_value::<$tip>($m).ok()?;
|
||||
Rc::new(val) as Rc<dyn Any>
|
||||
})*
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
#![allow(dead_code)]
|
||||
|
||||
use dioxus_core::VirtualDom;
|
||||
|
||||
use interprocess::local_socket::{LocalSocketListener, LocalSocketStream};
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::time::Duration;
|
||||
|
@ -10,18 +13,21 @@ fn handle_error(connection: std::io::Result<LocalSocketStream>) -> Option<LocalS
|
|||
.ok()
|
||||
}
|
||||
|
||||
pub(crate) fn init(dom: &VirtualDom) {
|
||||
pub(crate) fn init(_dom: &VirtualDom) {
|
||||
let latest_in_connection: Arc<Mutex<Option<BufReader<LocalSocketStream>>>> =
|
||||
Arc::new(Mutex::new(None));
|
||||
|
||||
let latest_in_connection_handle = latest_in_connection.clone();
|
||||
|
||||
// connect to processes for incoming data
|
||||
std::thread::spawn(move || {
|
||||
// if let Ok(listener) = LocalSocketListener::bind("@dioxusin") {
|
||||
// for conn in listener.incoming().filter_map(handle_error) {
|
||||
// *latest_in_connection_handle.lock().unwrap() = Some(BufReader::new(conn));
|
||||
// }
|
||||
// }
|
||||
let temp_file = std::env::temp_dir().join("@dioxusin");
|
||||
|
||||
if let Ok(listener) = LocalSocketListener::bind(temp_file) {
|
||||
for conn in listener.incoming().filter_map(handle_error) {
|
||||
*latest_in_connection_handle.lock().unwrap() = Some(BufReader::new(conn));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
std::thread::spawn(move || {
|
||||
|
|
|
@ -8,13 +8,17 @@ mod controller;
|
|||
mod desktop_context;
|
||||
mod escape;
|
||||
mod events;
|
||||
#[cfg(any(feature = "hot-reload", debug_assertions))]
|
||||
mod hot_reload;
|
||||
mod protocol;
|
||||
|
||||
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||
mod hot_reload;
|
||||
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
|
||||
use desktop_context::UserWindowEvent;
|
||||
pub use desktop_context::{use_eval, use_window, DesktopContext};
|
||||
use futures_channel::mpsc::unbounded;
|
||||
use futures_channel::mpsc::UnboundedSender;
|
||||
pub use wry;
|
||||
pub use wry::application as tao;
|
||||
|
||||
|
@ -100,15 +104,68 @@ pub fn launch_cfg(root: Component, config_builder: Config) {
|
|||
/// ```
|
||||
pub fn launch_with_props<P: 'static + Send>(root: Component<P>, props: P, mut cfg: Config) {
|
||||
let event_loop = EventLoop::with_user_event();
|
||||
let mut desktop = DesktopController::new_on_tokio(root, props, event_loop.create_proxy());
|
||||
|
||||
let (event_tx, event_rx) = unbounded();
|
||||
let mut desktop =
|
||||
DesktopController::new_on_tokio(root, props, event_loop.create_proxy(), event_rx);
|
||||
let proxy = event_loop.create_proxy();
|
||||
event_loop.run(move |window_event, event_loop, control_flow| {
|
||||
*control_flow = ControlFlow::Wait;
|
||||
|
||||
// We assume that if the icon is None, then the user just didnt set it
|
||||
match window_event {
|
||||
Event::NewEvents(StartCause::Init) => desktop.start(&mut cfg, event_loop),
|
||||
|
||||
Event::WindowEvent {
|
||||
event, window_id, ..
|
||||
} => match event {
|
||||
WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
|
||||
WindowEvent::Destroyed { .. } => desktop.close_window(window_id, control_flow),
|
||||
_ => {}
|
||||
},
|
||||
|
||||
Event::UserEvent(user_event) => desktop.handle_event(user_event, control_flow),
|
||||
Event::MainEventsCleared => {}
|
||||
Event::Resumed => {}
|
||||
Event::Suspended => {}
|
||||
Event::LoopDestroyed => {}
|
||||
Event::RedrawRequested(_id) => {}
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
impl DesktopController {
|
||||
fn start(
|
||||
&mut self,
|
||||
cfg: &mut Config,
|
||||
event_loop: &tao::event_loop::EventLoopWindowTarget<UserWindowEvent>,
|
||||
) {
|
||||
let webview = build_webview(
|
||||
cfg,
|
||||
event_loop,
|
||||
self.is_ready.clone(),
|
||||
self.proxy.clone(),
|
||||
self.event_tx.clone(),
|
||||
);
|
||||
|
||||
self.webviews.insert(webview.window().id(), webview);
|
||||
}
|
||||
}
|
||||
|
||||
fn build_webview(
|
||||
cfg: &mut Config,
|
||||
event_loop: &tao::event_loop::EventLoopWindowTarget<UserWindowEvent>,
|
||||
is_ready: Arc<AtomicBool>,
|
||||
proxy: tao::event_loop::EventLoopProxy<UserWindowEvent>,
|
||||
event_tx: UnboundedSender<serde_json::Value>,
|
||||
) -> wry::webview::WebView {
|
||||
let builder = cfg.window.clone();
|
||||
let window = builder.build(event_loop).unwrap();
|
||||
let file_handler = cfg.file_drop_handler.take();
|
||||
let custom_head = cfg.custom_head.clone();
|
||||
let resource_dir = cfg.resource_dir.clone();
|
||||
let index_file = cfg.custom_index.clone();
|
||||
|
||||
// We assume that if the icon is None in cfg, then the user just didnt set it
|
||||
if cfg.window.window.window_icon.is_none() {
|
||||
cfg.window = cfg.window.with_window_icon(Some(
|
||||
window.set_window_icon(Some(
|
||||
tao::window::Icon::from_rgba(
|
||||
include_bytes!("./assets/default_icon.bin").to_vec(),
|
||||
460,
|
||||
|
@ -118,85 +175,62 @@ pub fn launch_with_props<P: 'static + Send>(root: Component<P>, props: P, mut cf
|
|||
));
|
||||
}
|
||||
|
||||
event_loop.run(move |window_event, event_loop, control_flow| {
|
||||
*control_flow = ControlFlow::Wait;
|
||||
|
||||
// println!("window event: {:?}", window_event);
|
||||
match window_event {
|
||||
Event::NewEvents(StartCause::Init) => {
|
||||
let builder = cfg.window.clone();
|
||||
|
||||
let window = builder.build(event_loop).unwrap();
|
||||
let window_id = window.id();
|
||||
|
||||
let (is_ready, _) = (desktop.is_ready.clone(), ());
|
||||
// let (is_ready, sender) = (desktop.is_ready.clone(), desktop.sender.clone());
|
||||
|
||||
let proxy = proxy.clone();
|
||||
|
||||
let file_handler = cfg.file_drop_handler.take();
|
||||
let custom_head = cfg.custom_head.clone();
|
||||
let resource_dir = cfg.resource_dir.clone();
|
||||
let index_file = cfg.custom_index.clone();
|
||||
let event_tx = event_tx.clone();
|
||||
|
||||
let mut webview = WebViewBuilder::new(window)
|
||||
.unwrap()
|
||||
.with_transparent(cfg.window.window.transparent)
|
||||
.with_url("dioxus://index.html/")
|
||||
.unwrap()
|
||||
.with_ipc_handler(move |_window: &Window, payload: String| {
|
||||
parse_ipc_message(&payload)
|
||||
.map(|message| match message.method() {
|
||||
"user_event" => {
|
||||
_ = event_tx.unbounded_send(message.params());
|
||||
let mut webview = WebViewBuilder::new(window)
|
||||
.unwrap()
|
||||
.with_transparent(cfg.window.window.transparent)
|
||||
.with_url("dioxus://index.html/")
|
||||
.unwrap()
|
||||
.with_ipc_handler(move |_window: &Window, payload: String| {
|
||||
parse_ipc_message(&payload)
|
||||
.map(|message| match message.method() {
|
||||
"user_event" => {
|
||||
_ = event_tx.unbounded_send(message.params());
|
||||
}
|
||||
"initialize" => {
|
||||
is_ready.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
let _ = proxy.send_event(UserWindowEvent::EditsReady);
|
||||
}
|
||||
"browser_open" => {
|
||||
let data = message.params();
|
||||
log::trace!("Open browser: {:?}", data);
|
||||
if let Some(temp) = data.as_object() {
|
||||
if temp.contains_key("href") {
|
||||
let url = temp.get("href").unwrap().as_str().unwrap();
|
||||
if let Err(e) = webbrowser::open(url) {
|
||||
log::error!("Open Browser error: {:?}", e);
|
||||
}
|
||||
"initialize" => {
|
||||
is_ready.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
println!("initializing...");
|
||||
let _ = proxy.send_event(UserWindowEvent::EditsReady);
|
||||
}
|
||||
"browser_open" => {
|
||||
let data = message.params();
|
||||
log::trace!("Open browser: {:?}", data);
|
||||
if let Some(temp) = data.as_object() {
|
||||
if temp.contains_key("href") {
|
||||
let url = temp.get("href").unwrap().as_str().unwrap();
|
||||
if let Err(e) = webbrowser::open(url) {
|
||||
log::error!("Open Browser error: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
log::warn!("invalid IPC message received");
|
||||
});
|
||||
})
|
||||
.with_custom_protocol(String::from("dioxus"), move |r| {
|
||||
protocol::desktop_handler(
|
||||
r,
|
||||
resource_dir.clone(),
|
||||
custom_head.clone(),
|
||||
index_file.clone(),
|
||||
)
|
||||
})
|
||||
.with_file_drop_handler(move |window, evet| {
|
||||
file_handler
|
||||
.as_ref()
|
||||
.map(|handler| handler(window, evet))
|
||||
.unwrap_or_default()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
log::warn!("invalid IPC message received");
|
||||
});
|
||||
})
|
||||
.with_custom_protocol(String::from("dioxus"), move |r| {
|
||||
protocol::desktop_handler(
|
||||
r,
|
||||
resource_dir.clone(),
|
||||
custom_head.clone(),
|
||||
index_file.clone(),
|
||||
)
|
||||
})
|
||||
.with_file_drop_handler(move |window, evet| {
|
||||
file_handler
|
||||
.as_ref()
|
||||
.map(|handler| handler(window, evet))
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
for (name, handler) in cfg.protocols.drain(..) {
|
||||
webview = webview.with_custom_protocol(name, handler)
|
||||
}
|
||||
for (name, handler) in cfg.protocols.drain(..) {
|
||||
webview = webview.with_custom_protocol(name, handler)
|
||||
}
|
||||
|
||||
if cfg.disable_context_menu {
|
||||
// in release mode, we don't want to show the dev tool or reload menus
|
||||
webview = webview.with_initialization_script(
|
||||
r#"
|
||||
if cfg.disable_context_menu {
|
||||
// in release mode, we don't want to show the dev tool or reload menus
|
||||
webview = webview.with_initialization_script(
|
||||
r#"
|
||||
if (document.addEventListener) {
|
||||
document.addEventListener('contextmenu', function(e) {
|
||||
e.preventDefault();
|
||||
|
@ -207,32 +241,11 @@ pub fn launch_with_props<P: 'static + Send>(root: Component<P>, props: P, mut cf
|
|||
});
|
||||
}
|
||||
"#,
|
||||
)
|
||||
} else {
|
||||
// in debug, we are okay with the reload menu showing and dev tool
|
||||
webview = webview.with_devtools(true);
|
||||
}
|
||||
)
|
||||
} else {
|
||||
// in debug, we are okay with the reload menu showing and dev tool
|
||||
webview = webview.with_devtools(true);
|
||||
}
|
||||
|
||||
desktop.webviews.insert(window_id, webview.build().unwrap());
|
||||
}
|
||||
|
||||
Event::WindowEvent {
|
||||
event, window_id, ..
|
||||
} => match event {
|
||||
WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
|
||||
WindowEvent::Destroyed { .. } => desktop.close_window(window_id, control_flow),
|
||||
_ => {}
|
||||
},
|
||||
|
||||
Event::UserEvent(user_event) => {
|
||||
desktop_context::handler(user_event, &mut desktop, control_flow)
|
||||
}
|
||||
Event::MainEventsCleared => {}
|
||||
Event::Resumed => {}
|
||||
Event::Suspended => {}
|
||||
Event::LoopDestroyed => {}
|
||||
Event::RedrawRequested(_id) => {}
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
webview.build().unwrap()
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use std::rc::Rc;
|
||||
|
||||
use crate::AtomRoot;
|
||||
use dioxus_core::ScopeState;
|
||||
use std::rc::Rc;
|
||||
|
||||
// Returns the atom root, initiaizing it at the root of the app if it does not exist.
|
||||
pub fn use_atom_root(cx: &ScopeState) -> &Rc<AtomRoot> {
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
use std::rc::Rc;
|
||||
|
||||
use dioxus_core::{prelude::EventHandler, ScopeState};
|
||||
use dioxus_core::ScopeState;
|
||||
use std::future::Future;
|
||||
|
||||
#[macro_export]
|
||||
|
@ -19,8 +17,14 @@ macro_rules! use_callback {
|
|||
move || $($rest)*
|
||||
)
|
||||
};
|
||||
($cx:ident, $($rest:tt)*) => {
|
||||
use_callback(
|
||||
$cx,
|
||||
move || $($rest)*
|
||||
)
|
||||
};
|
||||
}
|
||||
pub fn use_callback<'a, T, R, F>(cx: &'a ScopeState, make: impl FnOnce() -> R) -> impl FnMut(T) + 'a
|
||||
pub fn use_callback<T, R, F>(cx: &ScopeState, make: impl FnOnce() -> R) -> impl FnMut(T) + '_
|
||||
where
|
||||
R: FnMut(T) -> F + 'static,
|
||||
F: Future<Output = ()> + 'static,
|
||||
|
@ -30,8 +34,8 @@ where
|
|||
move |evt| cx.spawn(hook(evt))
|
||||
}
|
||||
|
||||
fn it_works(cx: &ScopeState) {
|
||||
let p = use_callback(cx, || {
|
||||
fn _it_works(cx: &ScopeState) {
|
||||
let _p = use_callback(cx, || {
|
||||
|()| async {
|
||||
//
|
||||
}
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
use dioxus_core::ScopeState;
|
||||
|
||||
/// Consume some context in the tree
|
||||
pub fn use_context<T: 'static>(cx: &ScopeState) -> Option<&T> {
|
||||
match *cx.use_hook(|| cx.consume_context::<T>().map(|t| t as *const T)) {
|
||||
Some(res) => Some(unsafe { &*res }),
|
||||
None => None,
|
||||
}
|
||||
/// Consume some context in the tree, providing a sharable handle to the value
|
||||
pub fn use_context<T: 'static + Clone>(cx: &ScopeState) -> Option<&T> {
|
||||
cx.use_hook(|| cx.consume_context::<T>()).as_ref()
|
||||
}
|
||||
|
||||
/// Provide some context via the tree and return a reference to it
|
||||
///
|
||||
/// Once the context has been provided, it is immutable. Mutations should be done via interior mutability.
|
||||
pub fn use_context_provider<T: 'static>(cx: &ScopeState, f: impl FnOnce() -> T) -> &T {
|
||||
let ptr = *cx.use_hook(|| cx.provide_context(f()) as *const T);
|
||||
unsafe { &*ptr }
|
||||
pub fn use_context_provider<T: 'static + Clone>(cx: &ScopeState, f: impl FnOnce() -> T) -> &T {
|
||||
cx.use_hook(|| {
|
||||
let val = f();
|
||||
cx.provide_context(val.clone());
|
||||
val
|
||||
})
|
||||
}
|
||||
|
|
|
@ -66,9 +66,7 @@ where
|
|||
// if there's a waker, we dont re-render the component. Instead we just progress that future
|
||||
match waker.borrow().as_ref() {
|
||||
Some(waker) => waker.wake_by_ref(),
|
||||
None => {
|
||||
// schedule_update()
|
||||
}
|
||||
None => schedule_update(),
|
||||
}
|
||||
})));
|
||||
}
|
||||
|
@ -122,7 +120,7 @@ impl<T> UseFuture<T> {
|
|||
}
|
||||
|
||||
// Manually set the value in the future slot without starting the future over
|
||||
pub fn set(&self, new_value: T) {
|
||||
pub fn set(&self, _new_value: T) {
|
||||
// self.slot.set(Some(new_value));
|
||||
// self.needs_regen.set(true);
|
||||
// (self.update)();
|
||||
|
|
|
@ -33,9 +33,9 @@ fn render_template_node(node: &TemplateNode, out: &mut String) -> std::fmt::Resu
|
|||
}
|
||||
write!(out, "</{tag}>")?;
|
||||
}
|
||||
TemplateNode::Text(t) => write!(out, "{t}")?,
|
||||
TemplateNode::Dynamic(_) => write!(out, "<pre hidden />")?,
|
||||
TemplateNode::DynamicText(t) => write!(out, "<!-- --> {t} <!-- -->")?,
|
||||
TemplateNode::Text { text: t } => write!(out, "{t}")?,
|
||||
TemplateNode::Dynamic { id: _ } => write!(out, "<pre hidden />")?,
|
||||
TemplateNode::DynamicText { id: t } => write!(out, "<!-- --> {t} <!-- -->")?,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -12,11 +12,14 @@ extern "C" {
|
|||
pub fn new(arg: Element) -> Interpreter;
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn SaveTemplate(this: &Interpreter, nodes: Vec<Node>, name: &str);
|
||||
pub fn SaveTemplate(this: &Interpreter, template: JsValue);
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn MountToRoot(this: &Interpreter);
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn AppendChildren(this: &Interpreter, m: u32);
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn AssignId(this: &Interpreter, path: &[u8], id: u32);
|
||||
|
||||
|
@ -58,8 +61,8 @@ extern "C" {
|
|||
this: &Interpreter,
|
||||
name: &str,
|
||||
id: u32,
|
||||
handler: &Function,
|
||||
bubbles: bool,
|
||||
handler: &Function,
|
||||
);
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
|
|
|
@ -76,9 +76,6 @@ export class Interpreter {
|
|||
pop() {
|
||||
return this.stack.pop();
|
||||
}
|
||||
SaveTemplate(nodes, name) {
|
||||
this.templates[name] = nodes;
|
||||
}
|
||||
MountToRoot() {
|
||||
this.AppendChildren(this.stack.length - 1);
|
||||
}
|
||||
|
@ -140,7 +137,7 @@ export class Interpreter {
|
|||
this.stack.push(el);
|
||||
this.nodes[root] = el;
|
||||
}
|
||||
NewEventListener(event_name, root, handler, bubbles) {
|
||||
NewEventListener(event_name, root, bubbles, handler) {
|
||||
const element = this.nodes[root];
|
||||
element.setAttribute("data-dioxus-id", `${root}`);
|
||||
this.listeners.create(event_name, element, handler, bubbles);
|
||||
|
@ -213,10 +210,56 @@ export class Interpreter {
|
|||
}
|
||||
}
|
||||
handleEdits(edits) {
|
||||
for (let edit of edits) {
|
||||
for (let template of edits.templates) {
|
||||
this.SaveTemplate(template);
|
||||
}
|
||||
|
||||
for (let edit of edits.edits) {
|
||||
this.handleEdit(edit);
|
||||
}
|
||||
}
|
||||
|
||||
SaveTemplate(template) {
|
||||
let roots = [];
|
||||
for (let root of template.roots) {
|
||||
roots.push(this.MakeTemplateNode(root));
|
||||
}
|
||||
this.templates[template.name] = roots;
|
||||
}
|
||||
|
||||
MakeTemplateNode(node) {
|
||||
console.log("making template node", node);
|
||||
switch (node.type) {
|
||||
case "Text":
|
||||
return document.createTextNode(node.text);
|
||||
case "Dynamic":
|
||||
let dyn = document.createElement("pre");
|
||||
dyn.hidden = true;
|
||||
return dyn;
|
||||
case "DynamicText":
|
||||
return document.createTextNode("placeholder");
|
||||
case "Element":
|
||||
let el;
|
||||
|
||||
if (node.namespace != null) {
|
||||
el = document.createElementNS(node.namespace, node.tag);
|
||||
} else {
|
||||
el = document.createElement(node.tag);
|
||||
}
|
||||
|
||||
for (let attr of node.attrs) {
|
||||
if (attr.type == "Static") {
|
||||
this.SetAttributeInner(el, attr.name, attr.value, attr.namespace);
|
||||
}
|
||||
}
|
||||
|
||||
for (let child of node.children) {
|
||||
el.appendChild(this.MakeTemplateNode(child));
|
||||
}
|
||||
|
||||
return el;
|
||||
}
|
||||
}
|
||||
AssignId(path, id) {
|
||||
this.nodes[id] = this.LoadChild(path);
|
||||
}
|
||||
|
@ -232,7 +275,16 @@ export class Interpreter {
|
|||
}
|
||||
HydrateText(path, value, id) {
|
||||
let node = this.LoadChild(path);
|
||||
node.textContent = value;
|
||||
|
||||
if (node.nodeType == Node.TEXT_NODE) {
|
||||
node.textContent = value;
|
||||
} else {
|
||||
// replace with a textnode
|
||||
let text = document.createTextNode(value);
|
||||
node.replaceWith(text);
|
||||
node = text;
|
||||
}
|
||||
|
||||
this.nodes[id] = node;
|
||||
}
|
||||
ReplacePlaceholder(path, m) {
|
||||
|
@ -296,17 +348,12 @@ export class Interpreter {
|
|||
this.RemoveAttribute(edit.id, edit.name, edit.ns);
|
||||
break;
|
||||
case "RemoveEventListener":
|
||||
this.RemoveEventListener(edit.id, edit.event_name);
|
||||
this.RemoveEventListener(edit.id, edit.name);
|
||||
break;
|
||||
case "NewEventListener":
|
||||
// console.log("creating listener! ", edit);
|
||||
|
||||
// this handler is only provided on desktop implementations since this
|
||||
// method is not used by the web implementation
|
||||
let handler = (event) => {
|
||||
|
||||
console.log("event", event);
|
||||
|
||||
let target = event.target;
|
||||
if (target != null) {
|
||||
let realId = target.getAttribute(`data-dioxus-id`);
|
||||
|
@ -387,17 +434,14 @@ export class Interpreter {
|
|||
}
|
||||
window.ipc.postMessage(
|
||||
serializeIpcMessage("user_event", {
|
||||
event: edit.event_name,
|
||||
event: edit.name,
|
||||
mounted_dom_id: parseInt(realId),
|
||||
contents: contents,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
console.log("adding event listener", edit);
|
||||
this.NewEventListener(edit.event_name, edit.id, handler, event_bubbles(edit.event_name));
|
||||
|
||||
this.NewEventListener(edit.name, edit.id, event_bubbles(edit.name), handler);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -132,7 +132,7 @@ impl<S: State> RealDom<S> {
|
|||
},
|
||||
OwnedAttributeValue::Text(value.to_string()),
|
||||
)),
|
||||
dioxus_core::TemplateAttribute::Dynamic(_) => None,
|
||||
dioxus_core::TemplateAttribute::Dynamic { .. } => None,
|
||||
})
|
||||
.collect(),
|
||||
listeners: FxHashSet::default(),
|
||||
|
@ -144,17 +144,17 @@ impl<S: State> RealDom<S> {
|
|||
}
|
||||
node_id
|
||||
}
|
||||
TemplateNode::Text(txt) => {
|
||||
TemplateNode::Text { text } => {
|
||||
let node_id = self.create_node(Node::new(NodeType::Text {
|
||||
text: txt.to_string(),
|
||||
text: text.to_string(),
|
||||
}));
|
||||
node_id
|
||||
}
|
||||
TemplateNode::Dynamic(_) => {
|
||||
TemplateNode::Dynamic { .. } => {
|
||||
let node_id = self.create_node(Node::new(NodeType::Placeholder));
|
||||
node_id
|
||||
}
|
||||
TemplateNode::DynamicText(_) => {
|
||||
TemplateNode::DynamicText { .. } => {
|
||||
let node_id = self.create_node(Node::new(NodeType::Text {
|
||||
text: String::new(),
|
||||
}));
|
||||
|
@ -220,7 +220,12 @@ impl<S: State> RealDom<S> {
|
|||
let node = self.tree.get_mut(node_id).unwrap();
|
||||
if let NodeType::Text { text } = &mut node.node_data.node_type {
|
||||
*text = value.to_string();
|
||||
} else {
|
||||
node.node_data.node_type = NodeType::Text {
|
||||
text: value.to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
mark_dirty(node_id, NodeMask::new().with_text(), &mut nodes_updated);
|
||||
}
|
||||
LoadTemplate { name, index, id } => {
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use crate::{use_route, RouterCore};
|
||||
use crate::{use_route, RouterContext};
|
||||
use dioxus::prelude::*;
|
||||
|
||||
/// Props for the [`Link`](struct.Link.html) component.
|
||||
|
@ -77,7 +75,7 @@ pub struct LinkProps<'a> {
|
|||
/// }
|
||||
/// ```
|
||||
pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
|
||||
let svc = cx.use_hook(|| cx.consume_context::<Arc<RouterCore>>());
|
||||
let svc = use_context::<RouterContext>(cx);
|
||||
|
||||
let LinkProps {
|
||||
to,
|
||||
|
@ -107,7 +105,7 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
|
|||
}
|
||||
};
|
||||
|
||||
let route = use_route(&cx);
|
||||
let route = use_route(cx);
|
||||
let url = route.url();
|
||||
let path = url.path();
|
||||
let active = path == cx.props.to;
|
||||
|
|
|
@ -32,7 +32,7 @@ pub struct RedirectProps<'a> {
|
|||
///
|
||||
/// It will replace the current route rather than pushing the current one to the stack.
|
||||
pub fn Redirect<'a>(cx: Scope<'a, RedirectProps<'a>>) -> Element {
|
||||
let router = use_router(&cx);
|
||||
let router = use_router(cx);
|
||||
|
||||
let immediate_redirect = cx.use_hook(|| {
|
||||
if let Some(from) = cx.props.from {
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
use crate::{RouteContext, RouterContext};
|
||||
use dioxus::prelude::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{RouteContext, RouterCore};
|
||||
|
||||
/// Props for the [`Route`](struct.Route.html) component.
|
||||
#[derive(Props)]
|
||||
|
@ -27,15 +25,13 @@ pub struct RouteProps<'a> {
|
|||
/// )
|
||||
/// ```
|
||||
pub fn Route<'a>(cx: Scope<'a, RouteProps<'a>>) -> Element {
|
||||
let router_root = cx
|
||||
.use_hook(|| cx.consume_context::<Arc<RouterCore>>())
|
||||
.as_ref()
|
||||
.unwrap();
|
||||
let router_root = use_context::<RouterContext>(cx).unwrap();
|
||||
let root_context = use_context::<RouteContext>(cx);
|
||||
|
||||
cx.use_hook(|| {
|
||||
// create a bigger, better, longer route if one above us exists
|
||||
let total_route = match cx.consume_context::<RouteContext>() {
|
||||
Some(ctx) => ctx.total_route,
|
||||
let total_route = match root_context {
|
||||
Some(ctx) => ctx.total_route.clone(),
|
||||
None => cx.props.to.to_string(),
|
||||
};
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
use crate::{cfg::RouterCfg, RouterCore};
|
||||
use crate::{cfg::RouterCfg, RouterContext, RouterService};
|
||||
use dioxus::prelude::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// The props for the [`Router`](fn.Router.html) component.
|
||||
#[derive(Props)]
|
||||
|
@ -21,7 +20,7 @@ pub struct RouterProps<'a> {
|
|||
///
|
||||
/// This lets you easily implement redirects
|
||||
#[props(default)]
|
||||
pub onchange: EventHandler<'a, Arc<RouterCore>>,
|
||||
pub onchange: EventHandler<'a, RouterContext>,
|
||||
|
||||
/// Set the active class of all Link components contained in this router.
|
||||
///
|
||||
|
@ -40,15 +39,15 @@ pub struct RouterProps<'a> {
|
|||
/// Will fallback to HashRouter is BrowserRouter is not available, or through configuration.
|
||||
#[allow(non_snake_case)]
|
||||
pub fn Router<'a>(cx: Scope<'a, RouterProps<'a>>) -> Element {
|
||||
let svc = cx.use_hook(|| {
|
||||
cx.provide_context(RouterCore::new(
|
||||
&cx,
|
||||
let svc = use_context_provider(cx, || {
|
||||
RouterService::new(
|
||||
cx,
|
||||
RouterCfg {
|
||||
base_url: cx.props.base_url.map(|s| s.to_string()),
|
||||
active_class: cx.props.active_class.map(|s| s.to_string()),
|
||||
initial_url: cx.props.initial_url.clone(),
|
||||
},
|
||||
))
|
||||
)
|
||||
});
|
||||
|
||||
// next time we run the rout_found will be filled
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{ParsedRoute, RouteContext, RouterCore, RouterService};
|
||||
use crate::{ParsedRoute, RouteContext, RouterContext};
|
||||
use dioxus::core::{ScopeId, ScopeState};
|
||||
use std::{borrow::Cow, str::FromStr, sync::Arc};
|
||||
use url::Url;
|
||||
|
@ -7,11 +7,11 @@ use url::Url;
|
|||
/// context of a [`Router`]. If this function is called outside of a `Router`
|
||||
/// component it will panic.
|
||||
pub fn use_route(cx: &ScopeState) -> &UseRoute {
|
||||
let handle = cx.use_hook(|| {
|
||||
let router = cx
|
||||
.consume_context::<RouterService>()
|
||||
.expect("Cannot call use_route outside the scope of a Router component");
|
||||
let router = cx
|
||||
.consume_context::<RouterContext>()
|
||||
.expect("Cannot call use_route outside the scope of a Router component");
|
||||
|
||||
let handle = cx.use_hook(|| {
|
||||
let route_context = cx.consume_context::<RouteContext>();
|
||||
|
||||
router.subscribe_onchange(cx.scope_id());
|
||||
|
@ -115,7 +115,7 @@ impl UseRoute {
|
|||
// and reveal our cached version of UseRoute to the component.
|
||||
struct UseRouteListener {
|
||||
state: UseRoute,
|
||||
router: Arc<RouterCore>,
|
||||
router: RouterContext,
|
||||
scope: ScopeId,
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
use crate::RouterService;
|
||||
use dioxus::core::ScopeState;
|
||||
use crate::RouterContext;
|
||||
use dioxus::{core::ScopeState, prelude::use_context};
|
||||
|
||||
/// This hook provides access to the `RouterService` for the app.
|
||||
pub fn use_router(cx: &ScopeState) -> &RouterService {
|
||||
cx.use_hook(|| {
|
||||
cx.consume_context::<RouterService>()
|
||||
.expect("Cannot call use_route outside the scope of a Router component")
|
||||
})
|
||||
pub fn use_router(cx: &ScopeState) -> &RouterContext {
|
||||
use_context::<RouterContext>(cx)
|
||||
.expect("Cannot call use_route outside the scope of a Router component")
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
use crate::cfg::RouterCfg;
|
||||
use dioxus::core::{ScopeId, ScopeState, VirtualDom};
|
||||
use std::any::Any;
|
||||
use std::sync::Weak;
|
||||
use std::rc::Weak;
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
collections::{HashMap, HashSet},
|
||||
|
@ -14,6 +14,9 @@ use std::{
|
|||
};
|
||||
use url::Url;
|
||||
|
||||
/// A clonable handle to the router
|
||||
pub type RouterContext = Rc<RouterService>;
|
||||
|
||||
/// An abstraction over the platform's history API.
|
||||
///
|
||||
/// The history is denoted using web-like semantics, with forward slashes delmitiing
|
||||
|
@ -41,7 +44,7 @@ use url::Url;
|
|||
/// - On the web, this is a [`BrowserHistory`](https://docs.rs/gloo/0.3.0/gloo/history/struct.BrowserHistory.html).
|
||||
/// - On desktop, mobile, and SSR, this is just a Vec of Strings. Currently on
|
||||
/// desktop, there is no way to tap into forward/back for the app unless explicitly set.
|
||||
pub struct RouterCore {
|
||||
pub struct RouterService {
|
||||
pub(crate) route_found: Cell<Option<ScopeId>>,
|
||||
|
||||
pub(crate) stack: RefCell<Vec<Arc<ParsedRoute>>>,
|
||||
|
@ -61,9 +64,6 @@ pub struct RouterCore {
|
|||
pub(crate) cfg: RouterCfg,
|
||||
}
|
||||
|
||||
/// A shared type for the RouterCore.
|
||||
pub type RouterService = Arc<RouterCore>;
|
||||
|
||||
/// A route is a combination of window title, saved state, and a URL.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ParsedRoute {
|
||||
|
@ -77,8 +77,8 @@ pub struct ParsedRoute {
|
|||
pub serialized_state: Option<String>,
|
||||
}
|
||||
|
||||
impl RouterCore {
|
||||
pub(crate) fn new(cx: &ScopeState, cfg: RouterCfg) -> Arc<Self> {
|
||||
impl RouterService {
|
||||
pub(crate) fn new(cx: &ScopeState, cfg: RouterCfg) -> RouterContext {
|
||||
#[cfg(feature = "web")]
|
||||
let history = Box::new(web::new());
|
||||
|
||||
|
@ -99,7 +99,7 @@ impl RouterCore {
|
|||
None => Arc::new(history.init_location()),
|
||||
};
|
||||
|
||||
let svc = Arc::new(Self {
|
||||
let svc = Rc::new(Self {
|
||||
cfg,
|
||||
regen_any_route: cx.schedule_update_any(),
|
||||
router_id: cx.scope_id(),
|
||||
|
@ -111,7 +111,7 @@ impl RouterCore {
|
|||
history,
|
||||
});
|
||||
|
||||
svc.history.attach_listeners(Arc::downgrade(&svc));
|
||||
svc.history.attach_listeners(Rc::downgrade(&svc));
|
||||
|
||||
svc
|
||||
}
|
||||
|
@ -247,12 +247,9 @@ impl RouterCore {
|
|||
/// that owns the router.
|
||||
///
|
||||
/// This might change in the future.
|
||||
pub fn get_router_from_vdom(
|
||||
dom: &VirtualDom,
|
||||
target_scope: Option<ScopeId>,
|
||||
) -> Option<Arc<RouterCore>> {
|
||||
dom.get_scope(target_scope.unwrap_or(ScopeId(0)))
|
||||
.and_then(|scope| scope.consume_context::<Arc<RouterCore>>())
|
||||
pub fn get_router_from_vdom(dom: &VirtualDom, target_scope: ScopeId) -> Option<RouterContext> {
|
||||
dom.get_scope(target_scope)
|
||||
.and_then(|scope| scope.consume_context::<RouterContext>())
|
||||
}
|
||||
|
||||
fn clean_route(route: String) -> String {
|
||||
|
@ -319,7 +316,7 @@ pub(crate) trait RouterProvider {
|
|||
fn replace(&self, route: &ParsedRoute);
|
||||
fn native_location(&self) -> Box<dyn Any>;
|
||||
fn init_location(&self) -> ParsedRoute;
|
||||
fn attach_listeners(&self, svc: Weak<RouterCore>);
|
||||
fn attach_listeners(&self, svc: Weak<RouterService>);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "web"))]
|
||||
|
@ -350,7 +347,7 @@ mod hash {
|
|||
|
||||
fn replace(&self, _route: &ParsedRoute) {}
|
||||
|
||||
fn attach_listeners(&self, _svc: Weak<RouterCore>) {}
|
||||
fn attach_listeners(&self, _svc: Weak<RouterService>) {}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -418,7 +415,7 @@ mod web {
|
|||
}
|
||||
}
|
||||
|
||||
fn attach_listeners(&self, svc: std::sync::Weak<crate::RouterCore>) {
|
||||
fn attach_listeners(&self, svc: std::sync::Weak<crate::RouterService>) {
|
||||
self._listener.set(Some(EventListener::new(
|
||||
&web_sys::window().unwrap(),
|
||||
"popstate",
|
||||
|
|
|
@ -60,7 +60,7 @@ impl Component {
|
|||
match self
|
||||
.fields
|
||||
.iter()
|
||||
.find(|f| f.name.to_string() == "key")
|
||||
.find(|f| f.name == "key")
|
||||
.map(|f| &f.content)
|
||||
{
|
||||
Some(ContentField::Formatted(fmt)) => Some(fmt),
|
||||
|
|
|
@ -21,10 +21,7 @@ pub struct IfmtInput {
|
|||
|
||||
impl IfmtInput {
|
||||
pub fn is_static(&self) -> bool {
|
||||
match self.segments.as_slice() {
|
||||
&[Segment::Literal(_)] => true,
|
||||
_ => false,
|
||||
}
|
||||
matches!(self.segments.as_slice(), &[Segment::Literal(_)])
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -180,10 +180,10 @@ impl<'a> DynamicContext<'a> {
|
|||
// [0, 2]
|
||||
// [0, 2, 1]
|
||||
|
||||
let static_attrs = el.attributes.iter().filter_map(|attr| match &attr.attr {
|
||||
let static_attrs = el.attributes.iter().map(|attr| match &attr.attr {
|
||||
ElementAttr::AttrText { name, value } if value.is_static() => {
|
||||
let value = value.source.as_ref().unwrap();
|
||||
Some(quote! {
|
||||
quote! {
|
||||
::dioxus::core::TemplateAttribute::Static {
|
||||
name: dioxus_elements::#el_name::#name.0,
|
||||
namespace: dioxus_elements::#el_name::#name.1,
|
||||
|
@ -192,12 +192,12 @@ impl<'a> DynamicContext<'a> {
|
|||
// todo: we don't diff these so we never apply the volatile flag
|
||||
// volatile: dioxus_elements::#el_name::#name.2,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ElementAttr::CustomAttrText { name, value } if value.is_static() => {
|
||||
let value = value.source.as_ref().unwrap();
|
||||
Some(quote! {
|
||||
quote! {
|
||||
::dioxus::core::TemplateAttribute::Static {
|
||||
name: dioxus_elements::#el_name::#name.0,
|
||||
namespace: dioxus_elements::#el_name::#name.1,
|
||||
|
@ -206,7 +206,7 @@ impl<'a> DynamicContext<'a> {
|
|||
// todo: we don't diff these so we never apply the volatile flag
|
||||
// volatile: dioxus_elements::#el_name::#name.2,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ElementAttr::AttrExpression { .. }
|
||||
|
@ -217,7 +217,7 @@ impl<'a> DynamicContext<'a> {
|
|||
let ct = self.dynamic_attributes.len();
|
||||
self.dynamic_attributes.push(attr);
|
||||
self.attr_paths.push(self.current_path.clone());
|
||||
Some(quote! { ::dioxus::core::TemplateAttribute::Dynamic(#ct) })
|
||||
quote! { ::dioxus::core::TemplateAttribute::Dynamic { id: #ct } }
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -245,7 +245,7 @@ impl<'a> DynamicContext<'a> {
|
|||
|
||||
BodyNode::Text(text) if text.is_static() => {
|
||||
let text = text.source.as_ref().unwrap();
|
||||
quote! { ::dioxus::core::TemplateNode::Text(#text) }
|
||||
quote! { ::dioxus::core::TemplateNode::Text{ text: #text } }
|
||||
}
|
||||
|
||||
BodyNode::RawExpr(_)
|
||||
|
@ -258,8 +258,10 @@ impl<'a> DynamicContext<'a> {
|
|||
self.node_paths.push(self.current_path.clone());
|
||||
|
||||
match root {
|
||||
BodyNode::Text(_) => quote! { ::dioxus::core::TemplateNode::DynamicText(#ct) },
|
||||
_ => quote! { ::dioxus::core::TemplateNode::Dynamic(#ct) },
|
||||
BodyNode::Text(_) => {
|
||||
quote! { ::dioxus::core::TemplateNode::DynamicText { id: #ct } }
|
||||
}
|
||||
_ => quote! { ::dioxus::core::TemplateNode::Dynamic { id: #ct } },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -141,7 +141,7 @@ impl ToTokens for BodyNode {
|
|||
pat, expr, body, ..
|
||||
} = exp;
|
||||
|
||||
let renderer = TemplateRenderer { roots: &body };
|
||||
let renderer = TemplateRenderer { roots: body };
|
||||
|
||||
tokens.append_all(quote! {
|
||||
__cx.make_node(
|
||||
|
|
|
@ -65,12 +65,12 @@ impl StringCache {
|
|||
TemplateAttribute::Static { name, value, .. } => {
|
||||
write!(chain, " {}=\"{}\"", name, value)?;
|
||||
}
|
||||
TemplateAttribute::Dynamic(index) => {
|
||||
TemplateAttribute::Dynamic { id: index } => {
|
||||
chain.segments.push(Segment::Attr(*index))
|
||||
}
|
||||
}
|
||||
}
|
||||
if children.len() == 0 && tag_is_self_closing(tag) {
|
||||
if children.is_empty() && tag_is_self_closing(tag) {
|
||||
write!(chain, "/>")?;
|
||||
} else {
|
||||
write!(chain, ">")?;
|
||||
|
@ -81,8 +81,8 @@ impl StringCache {
|
|||
}
|
||||
cur_path.pop();
|
||||
}
|
||||
TemplateNode::Text(text) => write!(chain, "{}", text)?,
|
||||
TemplateNode::Dynamic(idx) | TemplateNode::DynamicText(idx) => {
|
||||
TemplateNode::Text { text } => write!(chain, "{}", text)?,
|
||||
TemplateNode::Dynamic { id: idx } | TemplateNode::DynamicText { id: idx } => {
|
||||
chain.segments.push(Segment::Node(*idx))
|
||||
}
|
||||
}
|
||||
|
@ -92,9 +92,21 @@ impl StringCache {
|
|||
}
|
||||
|
||||
fn tag_is_self_closing(tag: &str) -> bool {
|
||||
match tag {
|
||||
"area" | "base" | "br" | "col" | "embed" | "hr" | "img" | "input" | "link" | "meta"
|
||||
| "param" | "source" | "track" | "wbr" => true,
|
||||
_ => false,
|
||||
}
|
||||
matches!(
|
||||
tag,
|
||||
"area"
|
||||
| "base"
|
||||
| "br"
|
||||
| "col"
|
||||
| "embed"
|
||||
| "hr"
|
||||
| "img"
|
||||
| "input"
|
||||
| "link"
|
||||
| "meta"
|
||||
| "param"
|
||||
| "source"
|
||||
| "track"
|
||||
| "wbr"
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,38 +1 @@
|
|||
#[derive(Clone, Debug, Default)]
|
||||
pub struct SsrConfig {
|
||||
/// currently not supported - control if we indent the HTML output
|
||||
indent: bool,
|
||||
|
||||
/// Control if elements are written onto a new line
|
||||
newline: bool,
|
||||
|
||||
/// Choose to write ElementIDs into elements so the page can be re-hydrated later on
|
||||
pre_render: bool,
|
||||
|
||||
// Currently not implemented
|
||||
// Don't proceed onto new components. Instead, put the name of the component.
|
||||
// TODO: components don't have names :(
|
||||
skip_components: bool,
|
||||
}
|
||||
|
||||
impl SsrConfig {
|
||||
pub fn indent(mut self, a: bool) -> Self {
|
||||
self.indent = a;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn newline(mut self, a: bool) -> Self {
|
||||
self.newline = a;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn pre_render(mut self, a: bool) -> Self {
|
||||
self.pre_render = a;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn skip_components(mut self, a: bool) -> Self {
|
||||
self.skip_components = a;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
use std::fmt::Write;
|
||||
|
||||
use dioxus_core::{LazyNodes, ScopeId, VirtualDom};
|
||||
|
||||
use crate::config::SsrConfig;
|
||||
|
||||
pub fn pre_render(dom: &VirtualDom) -> String {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn pre_render_to(dom: &VirtualDom, write: impl Write) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn render_vdom(dom: &VirtualDom) -> String {
|
||||
todo!()
|
||||
// format!("{:}", TextRenderer::from_vdom(dom, SsrConfig::default()))
|
||||
}
|
||||
|
||||
pub fn pre_render_vdom(dom: &VirtualDom) -> String {
|
||||
todo!()
|
||||
// format!(
|
||||
// "{:}",
|
||||
// TextRenderer::from_vdom(dom, SsrConfig::default().pre_render(true))
|
||||
// )
|
||||
}
|
||||
|
||||
pub fn render_vdom_cfg(dom: &VirtualDom, cfg: SsrConfig) -> String {
|
||||
todo!()
|
||||
// format!(
|
||||
// "{:}",
|
||||
// TextRenderer::from_vdom(dom, cfg(SsrConfig::default()))
|
||||
// )
|
||||
}
|
||||
|
||||
pub fn render_vdom_scope(vdom: &VirtualDom, scope: ScopeId) -> Option<String> {
|
||||
todo!()
|
||||
// Some(format!(
|
||||
// "{:}",
|
||||
// TextRenderer {
|
||||
// cfg: SsrConfig::default(),
|
||||
// root: vdom.get_scope(scope).unwrap().root_node(),
|
||||
// vdom: Some(vdom),
|
||||
// }
|
||||
// ))
|
||||
}
|
||||
|
||||
pub fn render_lazy<'a, 'b>(f: LazyNodes<'a, 'b>) -> String {
|
||||
todo!()
|
||||
}
|
|
@ -2,8 +2,54 @@
|
|||
|
||||
mod cache;
|
||||
pub mod config;
|
||||
pub mod helpers;
|
||||
pub mod renderer;
|
||||
pub mod template;
|
||||
pub use helpers::*;
|
||||
pub use template::SsrRender;
|
||||
use dioxus_core::{Element, LazyNodes, Scope, VirtualDom};
|
||||
use std::cell::Cell;
|
||||
|
||||
pub use crate::renderer::Renderer;
|
||||
|
||||
/// A convenience function to render an `rsx!` call to a string
|
||||
///
|
||||
/// For advanced rendering, create a new `SsrRender`.
|
||||
pub fn render_lazy(f: LazyNodes<'_, '_>) -> String {
|
||||
// We need to somehow get the lazy call into the virtualdom even with the lifetime
|
||||
// Since the lazy lifetime is valid for this function, we can just transmute it to static temporarily
|
||||
// This is okay since we're returning an owned value
|
||||
struct RootProps<'a, 'b> {
|
||||
caller: Cell<Option<LazyNodes<'a, 'b>>>,
|
||||
}
|
||||
|
||||
fn lazy_app<'a>(cx: Scope<'a, RootProps<'static, 'static>>) -> Element<'a> {
|
||||
let lazy = cx.props.caller.take().unwrap();
|
||||
let lazy: LazyNodes = unsafe { std::mem::transmute(lazy) };
|
||||
Ok(lazy.call(cx))
|
||||
}
|
||||
|
||||
let props: RootProps = unsafe {
|
||||
std::mem::transmute(RootProps {
|
||||
caller: Cell::new(Some(f)),
|
||||
})
|
||||
};
|
||||
|
||||
let mut dom = VirtualDom::new_with_props(lazy_app, props);
|
||||
_ = dom.rebuild();
|
||||
|
||||
Renderer::new().render(&dom)
|
||||
}
|
||||
|
||||
/// A convenience function to render an existing VirtualDom to a string
|
||||
///
|
||||
/// We generally recommend creating a new `Renderer` to take advantage of template caching.
|
||||
pub fn render(dom: &VirtualDom) -> String {
|
||||
Renderer::new().render(dom)
|
||||
}
|
||||
|
||||
/// A convenience function to pre-render an existing VirtualDom to a string
|
||||
///
|
||||
/// We generally recommend creating a new `Renderer` to take advantage of template caching.
|
||||
pub fn pre_render(dom: &VirtualDom) -> String {
|
||||
let mut renderer = Renderer::new();
|
||||
renderer.pre_render = true;
|
||||
renderer.render(dom)
|
||||
}
|
||||
|
|
|
@ -1,146 +1,181 @@
|
|||
// use dioxus_core::VirtualDom;
|
||||
use super::cache::Segment;
|
||||
use crate::cache::StringCache;
|
||||
use dioxus_core::{prelude::*, AttributeValue, DynamicNode, RenderReturn};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Write;
|
||||
use std::rc::Rc;
|
||||
|
||||
// use crate::config::SsrConfig;
|
||||
/// A virtualdom renderer that caches the templates it has seen for faster rendering
|
||||
#[derive(Default)]
|
||||
pub struct Renderer {
|
||||
/// should we do our best to prettify the output?
|
||||
pub pretty: bool,
|
||||
|
||||
// pub struct SsrRenderer {
|
||||
// vdom: VirtualDom,
|
||||
// cfg: SsrConfig,
|
||||
// }
|
||||
/// Control if elements are written onto a new line
|
||||
pub newline: bool,
|
||||
|
||||
// impl Default for SsrRenderer {
|
||||
// fn default() -> Self {
|
||||
// Self::new(SsrConfig::default())
|
||||
// }
|
||||
// }
|
||||
/// Should we sanitize text nodes? (escape HTML)
|
||||
pub sanitize: bool,
|
||||
|
||||
// impl SsrRenderer {
|
||||
// pub fn new(cfg: SsrConfig) -> Self {
|
||||
// Self {
|
||||
// vdom: VirtualDom::new(app),
|
||||
// cfg,
|
||||
// }
|
||||
// }
|
||||
/// Choose to write ElementIDs into elements so the page can be re-hydrated later on
|
||||
pub pre_render: bool,
|
||||
|
||||
// pub fn render_lazy<'a>(&'a mut self, f: LazyNodes<'a, '_>) -> String {
|
||||
// let scope = self.vdom.base_scope();
|
||||
// let root = f.call(scope);
|
||||
// format!(
|
||||
// "{:}",
|
||||
// TextRenderer {
|
||||
// cfg: self.cfg.clone(),
|
||||
// root: &root,
|
||||
// vdom: Some(&self.vdom),
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
// Currently not implemented
|
||||
// Don't proceed onto new components. Instead, put the name of the component.
|
||||
pub skip_components: bool,
|
||||
|
||||
// fn html_render(
|
||||
// &self,
|
||||
// node: &VNode,
|
||||
// f: &mut impl Write,
|
||||
// il: u16,
|
||||
// last_node_was_text: &mut bool,
|
||||
// ) -> std::fmt::Result {
|
||||
// // match &node {
|
||||
// // VNode::Text(text) => {
|
||||
// // if *last_node_was_text {
|
||||
// // write!(f, "<!--spacer-->")?;
|
||||
// // }
|
||||
/// A cache of templates that have been rendered
|
||||
template_cache: HashMap<&'static str, Rc<StringCache>>,
|
||||
}
|
||||
|
||||
// // if self.cfg.indent {
|
||||
// // for _ in 0..il {
|
||||
// // write!(f, " ")?;
|
||||
// // }
|
||||
// // }
|
||||
impl Renderer {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
// // *last_node_was_text = true;
|
||||
pub fn render(&mut self, dom: &VirtualDom) -> String {
|
||||
let mut buf = String::new();
|
||||
self.render_to(&mut buf, dom).unwrap();
|
||||
buf
|
||||
}
|
||||
|
||||
// // write!(f, "{}", text.text)?
|
||||
// // }
|
||||
// // VNode::Element(el) => {
|
||||
// // *last_node_was_text = false;
|
||||
pub fn render_to(&mut self, buf: &mut impl Write, dom: &VirtualDom) -> std::fmt::Result {
|
||||
self.render_scope(buf, dom, ScopeId(0))
|
||||
}
|
||||
|
||||
// // if self.cfg.indent {
|
||||
// // for _ in 0..il {
|
||||
// // write!(f, " ")?;
|
||||
// // }
|
||||
// // }
|
||||
pub fn render_scope(
|
||||
&mut self,
|
||||
buf: &mut impl Write,
|
||||
dom: &VirtualDom,
|
||||
scope: ScopeId,
|
||||
) -> std::fmt::Result {
|
||||
// We should never ever run into async or errored nodes in SSR
|
||||
// Error boundaries and suspense boundaries will convert these to sync
|
||||
if let RenderReturn::Sync(Ok(node)) = dom.get_scope(scope).unwrap().root_node() {
|
||||
self.render_template(buf, dom, node)?
|
||||
};
|
||||
|
||||
// // write!(f, "<{}", el.tag)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// // let inner_html = render_attributes(el.attributes.iter(), f)?;
|
||||
fn render_template(
|
||||
&mut self,
|
||||
buf: &mut impl Write,
|
||||
dom: &VirtualDom,
|
||||
template: &VNode,
|
||||
) -> std::fmt::Result {
|
||||
let entry = self
|
||||
.template_cache
|
||||
.entry(template.template.name)
|
||||
.or_insert_with(|| Rc::new(StringCache::from_template(template).unwrap()))
|
||||
.clone();
|
||||
|
||||
// // match self.cfg.newline {
|
||||
// // true => writeln!(f, ">")?,
|
||||
// // false => write!(f, ">")?,
|
||||
// // }
|
||||
for segment in entry.segments.iter() {
|
||||
match segment {
|
||||
Segment::Attr(idx) => {
|
||||
let attr = &template.dynamic_attrs[*idx];
|
||||
match attr.value {
|
||||
AttributeValue::Text(value) => write!(buf, " {}=\"{}\"", attr.name, value)?,
|
||||
AttributeValue::Bool(value) => write!(buf, " {}={}", attr.name, value)?,
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
Segment::Node(idx) => match &template.dynamic_nodes[*idx] {
|
||||
DynamicNode::Component(node) => {
|
||||
if self.skip_components {
|
||||
write!(buf, "<{}><{}/>", node.name, node.name)?;
|
||||
} else {
|
||||
let id = node.scope.get().unwrap();
|
||||
let scope = dom.get_scope(id).unwrap();
|
||||
let node = scope.root_node();
|
||||
match node {
|
||||
RenderReturn::Sync(Ok(node)) => {
|
||||
self.render_template(buf, dom, node)?
|
||||
}
|
||||
_ => todo!(
|
||||
"generally, scopes should be sync, only if being traversed"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
DynamicNode::Text(text) => {
|
||||
// in SSR, we are concerned that we can't hunt down the right text node since they might get merged
|
||||
if self.pre_render {
|
||||
write!(buf, "<!--#-->")?;
|
||||
}
|
||||
|
||||
// // if let Some(inner_html) = inner_html {
|
||||
// // write!(f, "{}", inner_html)?;
|
||||
// // } else {
|
||||
// // let mut last_node_was_text = false;
|
||||
// // for child in el.children {
|
||||
// // self.html_render(child, f, il + 1, &mut last_node_was_text)?;
|
||||
// // }
|
||||
// // }
|
||||
// todo: escape the text
|
||||
write!(buf, "{}", text.value)?;
|
||||
|
||||
// // if self.cfg.newline {
|
||||
// // writeln!(f)?;
|
||||
// // }
|
||||
// // if self.cfg.indent {
|
||||
// // for _ in 0..il {
|
||||
// // write!(f, " ")?;
|
||||
// // }
|
||||
// // }
|
||||
if self.pre_render {
|
||||
write!(buf, "<!--#-->")?;
|
||||
}
|
||||
}
|
||||
DynamicNode::Fragment(nodes) => {
|
||||
for child in *nodes {
|
||||
self.render_template(buf, dom, child)?;
|
||||
}
|
||||
}
|
||||
|
||||
// // write!(f, "</{}>", el.tag)?;
|
||||
// // if self.cfg.newline {
|
||||
// // writeln!(f)?;
|
||||
// // }
|
||||
// // }
|
||||
// // VNode::Fragment(frag) => match frag.children.len() {
|
||||
// // 0 => {
|
||||
// // *last_node_was_text = false;
|
||||
// // if self.cfg.indent {
|
||||
// // for _ in 0..il {
|
||||
// // write!(f, " ")?;
|
||||
// // }
|
||||
// // }
|
||||
// // write!(f, "<!--placeholder-->")?;
|
||||
// // }
|
||||
// // _ => {
|
||||
// // for child in frag.children {
|
||||
// // self.html_render(child, f, il + 1, last_node_was_text)?;
|
||||
// // }
|
||||
// // }
|
||||
// // },
|
||||
// // VNode::Component(vcomp) => {
|
||||
// // let idx = vcomp.scope.get().unwrap();
|
||||
DynamicNode::Placeholder(_el) => {
|
||||
if self.pre_render {
|
||||
write!(buf, "<pre><pre/>")?;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// // if let (Some(vdom), false) = (self.vdom, self.cfg.skip_components) {
|
||||
// // let new_node = vdom.get_scope(idx).unwrap().root_node();
|
||||
// // self.html_render(new_node, f, il + 1, last_node_was_text)?;
|
||||
// // } else {
|
||||
// // }
|
||||
// // }
|
||||
// // VNode::Template(t) => {
|
||||
// // if let Some(vdom) = self.vdom {
|
||||
// // todo!()
|
||||
// // } else {
|
||||
// // panic!("Cannot render template without vdom");
|
||||
// // }
|
||||
// // }
|
||||
// // VNode::Placeholder(_) => {
|
||||
// // todo!()
|
||||
// // }
|
||||
// // }
|
||||
// Ok(())
|
||||
// }
|
||||
// }
|
||||
Segment::PreRendered(contents) => write!(buf, "{}", contents)?,
|
||||
}
|
||||
}
|
||||
|
||||
// impl<'a: 'c, 'c> Display for SsrRenderer<'a, '_, 'c> {
|
||||
// fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
// let mut last_node_was_text = false;
|
||||
// self.html_render(self.root, f, 0, &mut last_node_was_text)
|
||||
// }
|
||||
// }
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_string_works() {
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let dynamic = 123;
|
||||
let dyn2 = "</diiiiiiiiv>"; // todo: escape this
|
||||
|
||||
render! {
|
||||
div { class: "asdasdasd", class: "asdasdasd", id: "id-{dynamic}",
|
||||
"Hello world 1 -->" "{dynamic}" "<-- Hello world 2"
|
||||
div { "nest 1" }
|
||||
div {}
|
||||
div { "nest 2" }
|
||||
"{dyn2}"
|
||||
(0..5).map(|i| rsx! { div { "finalize {i}" } })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
_ = dom.rebuild();
|
||||
|
||||
let mut renderer = Renderer::new();
|
||||
|
||||
let out = renderer.render(&dom);
|
||||
|
||||
use Segment::*;
|
||||
assert_eq!(
|
||||
renderer.template_cache.iter().next().unwrap().1.segments,
|
||||
vec![
|
||||
PreRendered("<div class=\"asdasdasd\" class=\"asdasdasd\"".into(),),
|
||||
Attr(0,),
|
||||
PreRendered(">Hello world 1 -->".into(),),
|
||||
Node(0,),
|
||||
PreRendered("<-- Hello world 2<div>nest 1</div><div></div><div>nest 2</div>".into(),),
|
||||
Node(1,),
|
||||
Node(2,),
|
||||
PreRendered("</div>".into(),),
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
out,
|
||||
"<div class=\"asdasdasd\" class=\"asdasdasd\" id=\"id-123\">Hello world 1 --><!--#-->123<!--/#--><-- Hello world 2<div>nest 1</div><div></div><div>nest 2</div><!--#--></diiiiiiiiv><!--/#--><div><!--#-->finalize 0<!--/#--></div><div><!--#-->finalize 1<!--/#--></div><div><!--#-->finalize 2<!--/#--></div><div><!--#-->finalize 3<!--/#--></div><div><!--#-->finalize 4<!--/#--></div></div>"
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,140 +1 @@
|
|||
use super::cache::Segment;
|
||||
use dioxus_core::{prelude::*, AttributeValue, DynamicNode, RenderReturn, VText};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Write;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::cache::StringCache;
|
||||
|
||||
/// A virtualdom renderer that caches the templates it has seen for faster rendering
|
||||
#[derive(Default)]
|
||||
pub struct SsrRender {
|
||||
template_cache: HashMap<&'static str, Rc<StringCache>>,
|
||||
}
|
||||
|
||||
impl SsrRender {
|
||||
pub fn render_vdom(&mut self, dom: &VirtualDom) -> String {
|
||||
let scope = dom.base_scope();
|
||||
let root = scope.root_node();
|
||||
|
||||
let mut out = String::new();
|
||||
|
||||
match root {
|
||||
RenderReturn::Sync(Ok(node)) => self.render_template(&mut out, dom, node).unwrap(),
|
||||
_ => {}
|
||||
};
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
fn render_template(
|
||||
&mut self,
|
||||
buf: &mut String,
|
||||
dom: &VirtualDom,
|
||||
template: &VNode,
|
||||
) -> std::fmt::Result {
|
||||
let entry = self
|
||||
.template_cache
|
||||
.entry(template.template.name)
|
||||
.or_insert_with(|| Rc::new(StringCache::from_template(template).unwrap()))
|
||||
.clone();
|
||||
|
||||
for segment in entry.segments.iter() {
|
||||
match segment {
|
||||
Segment::Attr(idx) => {
|
||||
let attr = &template.dynamic_attrs[*idx];
|
||||
match attr.value {
|
||||
AttributeValue::Text(value) => write!(buf, " {}=\"{}\"", attr.name, value)?,
|
||||
AttributeValue::Bool(value) => write!(buf, " {}={}", attr.name, value)?,
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
Segment::Node(idx) => match &template.dynamic_nodes[*idx] {
|
||||
DynamicNode::Component(node) => {
|
||||
let id = node.scope.get().unwrap();
|
||||
let scope = dom.get_scope(id).unwrap();
|
||||
let node = scope.root_node();
|
||||
match node {
|
||||
RenderReturn::Sync(Ok(node)) => self.render_template(buf, dom, node)?,
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
DynamicNode::Text(text) => {
|
||||
// in SSR, we are concerned that we can't hunt down the right text node since they might get merged
|
||||
// if !*inner {
|
||||
// write!(buf, "<!--#-->")?;
|
||||
// }
|
||||
|
||||
// todo: escape the text
|
||||
write!(buf, "{}", text.value)?;
|
||||
|
||||
// if !*inner {
|
||||
// write!(buf, "<!--/#-->")?;
|
||||
// }
|
||||
}
|
||||
DynamicNode::Fragment(nodes) => {
|
||||
for child in *nodes {
|
||||
self.render_template(buf, dom, child)?;
|
||||
}
|
||||
}
|
||||
|
||||
DynamicNode::Placeholder(_el) => {
|
||||
// todo write a placeholder if in pre-render mode
|
||||
// write!(buf, "<!--placeholder-->")?;
|
||||
}
|
||||
},
|
||||
|
||||
Segment::PreRendered(contents) => buf.push_str(contents),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_string_works() {
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let dynamic = 123;
|
||||
let dyn2 = "</diiiiiiiiv>"; // todo: escape this
|
||||
|
||||
render! {
|
||||
div { class: "asdasdasd", class: "asdasdasd", id: "id-{dynamic}",
|
||||
"Hello world 1 -->" "{dynamic}" "<-- Hello world 2"
|
||||
div { "nest 1" }
|
||||
div {}
|
||||
div { "nest 2" }
|
||||
"{dyn2}"
|
||||
(0..5).map(|i| rsx! { div { "finalize {i}" } })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
dom.rebuild();
|
||||
|
||||
use Segment::*;
|
||||
|
||||
// assert_eq!(
|
||||
// StringCache::from_template(&dom.base_scope().root_node())
|
||||
// .unwrap()
|
||||
// .segments,
|
||||
// vec![
|
||||
// PreRendered("<div class=\"asdasdasd\" class=\"asdasdasd\"".into(),),
|
||||
// Attr(0,),
|
||||
// PreRendered(">Hello world 1 -->".into(),),
|
||||
// Node(0,),
|
||||
// PreRendered("<-- Hello world 2<div>nest 1</div><div></div><div>nest 2</div>".into(),),
|
||||
// Node(1,),
|
||||
// Node(2,),
|
||||
// PreRendered("</div>".into(),),
|
||||
// ]
|
||||
// );
|
||||
|
||||
// assert_eq!(
|
||||
// SsrRender::default().render_vdom(&dom),
|
||||
// "<div class=\"asdasdasd\" class=\"asdasdasd\" id=\"id-123\">Hello world 1 --><!--#-->123<!--/#--><-- Hello world 2<div>nest 1</div><div></div><div>nest 2</div><!--#--></diiiiiiiiv><!--/#--><div><!--#-->finalize 0<!--/#--></div><div><!--#-->finalize 1<!--/#--></div><div><!--#-->finalize 2<!--/#--></div><div><!--#-->finalize 3<!--/#--></div><div><!--#-->finalize 4<!--/#--></div></div>"
|
||||
// );
|
||||
}
|
||||
|
|
|
@ -1,144 +0,0 @@
|
|||
use dioxus::prelude::*;
|
||||
use dioxus_ssr::{render_lazy, render_vdom, render_vdom_cfg, SsrConfig, SsrRenderer, TextRenderer};
|
||||
|
||||
static SIMPLE_APP: Component = |cx| {
|
||||
cx.render(rsx!(div {
|
||||
"hello world!"
|
||||
}))
|
||||
};
|
||||
|
||||
static SLIGHTLY_MORE_COMPLEX: Component = |cx| {
|
||||
cx.render(rsx! {
|
||||
div { title: "About W3Schools",
|
||||
(0..20).map(|f| rsx!{
|
||||
div {
|
||||
title: "About W3Schools",
|
||||
style: "color:blue;text-align:center",
|
||||
class: "About W3Schools",
|
||||
p {
|
||||
title: "About W3Schools",
|
||||
"Hello world!: {f}"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
static NESTED_APP: Component = |cx| {
|
||||
cx.render(rsx!(
|
||||
div {
|
||||
SIMPLE_APP {}
|
||||
}
|
||||
))
|
||||
};
|
||||
static FRAGMENT_APP: Component = |cx| {
|
||||
cx.render(rsx!(
|
||||
div { "f1" }
|
||||
div { "f2" }
|
||||
div { "f3" }
|
||||
div { "f4" }
|
||||
))
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn to_string_works() {
|
||||
let mut dom = VirtualDom::new(SIMPLE_APP);
|
||||
dom.rebuild();
|
||||
dbg!(render_vdom(&dom));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hydration() {
|
||||
let mut dom = VirtualDom::new(NESTED_APP);
|
||||
dom.rebuild();
|
||||
dbg!(render_vdom_cfg(&dom, |c| c.pre_render(true)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested() {
|
||||
let mut dom = VirtualDom::new(NESTED_APP);
|
||||
dom.rebuild();
|
||||
dbg!(render_vdom(&dom));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fragment_app() {
|
||||
let mut dom = VirtualDom::new(FRAGMENT_APP);
|
||||
dom.rebuild();
|
||||
dbg!(render_vdom(&dom));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_to_file() {
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
|
||||
let mut file = File::create("index.html").unwrap();
|
||||
|
||||
let mut dom = VirtualDom::new(SLIGHTLY_MORE_COMPLEX);
|
||||
dom.rebuild();
|
||||
|
||||
file.write_fmt(format_args!(
|
||||
"{}",
|
||||
TextRenderer::from_vdom(&dom, SsrConfig::default())
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn styles() {
|
||||
static STLYE_APP: Component = |cx| {
|
||||
cx.render(rsx! {
|
||||
div { color: "blue", font_size: "46px" }
|
||||
})
|
||||
};
|
||||
|
||||
let mut dom = VirtualDom::new(STLYE_APP);
|
||||
dom.rebuild();
|
||||
dbg!(render_vdom(&dom));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lazy() {
|
||||
let p1 = SsrRenderer::new(|c| c).render_lazy(rsx! {
|
||||
div { "ello" }
|
||||
});
|
||||
|
||||
let p2 = render_lazy(rsx! {
|
||||
div {
|
||||
"ello"
|
||||
}
|
||||
});
|
||||
assert_eq!(p1, p2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn big_lazy() {
|
||||
let s = render_lazy(rsx! {
|
||||
div {
|
||||
div {
|
||||
div {
|
||||
h1 { "ello world" }
|
||||
h1 { "ello world" }
|
||||
h1 { "ello world" }
|
||||
h1 { "ello world" }
|
||||
h1 { "ello world" }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
dbg!(s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inner_html() {
|
||||
let s = render_lazy(rsx! {
|
||||
div {
|
||||
dangerous_inner_html: "<div> ack </div>"
|
||||
}
|
||||
});
|
||||
|
||||
dbg!(s);
|
||||
}
|
|
@ -7,66 +7,43 @@ fn simple() {
|
|||
}
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
dom.rebuild();
|
||||
_ = dom.rebuild();
|
||||
|
||||
assert_eq!(dioxus_ssr::render(&dom), "<div>hello!</div>");
|
||||
|
||||
assert_eq!(
|
||||
dioxus_ssr::SsrRender::default().render_vdom(&dom),
|
||||
dioxus_ssr::render_lazy(rsx!( div {"hello!"} )),
|
||||
"<div>hello!</div>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lists() {
|
||||
fn app(cx: Scope) -> Element {
|
||||
render! {
|
||||
assert_eq!(
|
||||
dioxus_ssr::render_lazy(rsx! {
|
||||
ul {
|
||||
(0..5).map(|i| rsx! {
|
||||
li { "item {i}" }
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
dom.rebuild();
|
||||
|
||||
assert_eq!(
|
||||
dioxus_ssr::SsrRender::default().render_vdom(&dom),
|
||||
}),
|
||||
"<ul><li>item 0</li><li>item 1</li><li>item 2</li><li>item 3</li><li>item 4</li></ul>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dynamic() {
|
||||
fn app(cx: Scope) -> Element {
|
||||
let dynamic = 123;
|
||||
|
||||
render! {
|
||||
div { "Hello world 1 -->" "{dynamic}" "<-- Hello world 2" }
|
||||
}
|
||||
}
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
dom.rebuild();
|
||||
|
||||
let dynamic = 123;
|
||||
assert_eq!(
|
||||
dioxus_ssr::SsrRender::default().render_vdom(&dom),
|
||||
dioxus_ssr::render_lazy(rsx! {
|
||||
div { "Hello world 1 -->" "{dynamic}" "<-- Hello world 2" }
|
||||
}),
|
||||
"<div>Hello world 1 -->123<-- Hello world 2</div>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn components() {
|
||||
fn app(cx: Scope) -> Element {
|
||||
render! {
|
||||
div {
|
||||
(0..5).map(|name| rsx! {
|
||||
my_component { name: name }
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline_props]
|
||||
fn my_component(cx: Scope, name: i32) -> Element {
|
||||
render! {
|
||||
|
@ -74,11 +51,26 @@ fn components() {
|
|||
}
|
||||
}
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
dom.rebuild();
|
||||
|
||||
assert_eq!(
|
||||
dioxus_ssr::SsrRender::default().render_vdom(&dom),
|
||||
dioxus_ssr::render_lazy(rsx! {
|
||||
div {
|
||||
(0..5).map(|name| rsx! {
|
||||
my_component { name: name }
|
||||
})
|
||||
}
|
||||
}),
|
||||
"<div><div>component 0</div><div>component 1</div><div>component 2</div><div>component 3</div><div>component 4</div></div>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fragments() {
|
||||
assert_eq!(
|
||||
dioxus_ssr::render_lazy(rsx! {
|
||||
div {
|
||||
(0..5).map(|_| rsx! (()))
|
||||
}
|
||||
}),
|
||||
"<div></div>"
|
||||
);
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ futures-util = "0.3.19"
|
|||
smallstr = "0.2.0"
|
||||
futures-channel = "0.3.21"
|
||||
serde_json = { version = "1.0" }
|
||||
serde-wasm-bindgen = "0.4.5"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3.56"
|
||||
|
|
|
@ -11,7 +11,7 @@ use dioxus_core::{Mutation, Template, TemplateAttribute, TemplateNode};
|
|||
use dioxus_html::{event_bubbles, CompositionData, FormData};
|
||||
use dioxus_interpreter_js::{save_template, Channel};
|
||||
use futures_channel::mpsc;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::{any::Any, rc::Rc};
|
||||
use wasm_bindgen::{closure::Closure, JsCast};
|
||||
use web_sys::{Document, Element, Event, HtmlElement};
|
||||
|
@ -106,15 +106,15 @@ impl WebsysDom {
|
|||
}
|
||||
}
|
||||
for child in *children {
|
||||
el.append_child(&self.create_template_node(child));
|
||||
let _ = el.append_child(&self.create_template_node(child));
|
||||
}
|
||||
el.dyn_into().unwrap()
|
||||
}
|
||||
Text(t) => self.document.create_text_node(t).dyn_into().unwrap(),
|
||||
DynamicText(_) => self.document.create_text_node("p").dyn_into().unwrap(),
|
||||
Dynamic(_) => {
|
||||
Text { text } => self.document.create_text_node(text).dyn_into().unwrap(),
|
||||
DynamicText { .. } => self.document.create_text_node("p").dyn_into().unwrap(),
|
||||
Dynamic { .. } => {
|
||||
let el = self.document.create_element("pre").unwrap();
|
||||
el.toggle_attribute("hidden");
|
||||
let _ = el.toggle_attribute("hidden");
|
||||
el.dyn_into().unwrap()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -211,6 +211,7 @@ pub async fn run_with_props<T: 'static>(root: fn(Scope<T>) -> Element, root_prop
|
|||
let mut res = {
|
||||
let work = dom.wait_for_work().fuse();
|
||||
pin_mut!(work);
|
||||
|
||||
futures_util::select! {
|
||||
_ = work => None,
|
||||
new_template = hotreload_rx.next() => {
|
||||
|
|
Loading…
Reference in a new issue