Merge remote-tracking branch 'upstream/master' into optimize-templates-v3

This commit is contained in:
Evan Almloff 2022-12-07 17:20:31 -06:00
commit b79ad4f50e
68 changed files with 1030 additions and 1281 deletions

7
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,7 @@
version: 2
updates:
# Maintain dependencies for GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

View file

@ -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"
}
})
}

View file

@ -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 }
})
}

View file

@ -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()))),
"="
}
}

View file

@ -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! {

View file

@ -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);
}

View file

@ -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"
}

View file

@ -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 {

View file

@ -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}"
}
})
}

View file

@ -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

View file

@ -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!"
}
})
}

View file

@ -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}",

View file

@ -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"
}

View file

@ -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 协议,没有任何附加条款或条件。

View file

@ -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() => {}
}

View file

@ -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 {

View file

@ -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,
};

View file

@ -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,
};

View file

@ -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,

View file

@ -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> {

View file

@ -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 {

View file

@ -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) {

View file

@ -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.

View file

@ -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.

View file

@ -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();

View file

@ -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();
}

View file

@ -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 }
]

View file

@ -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 },
]
)
}

View file

@ -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),

View file

@ -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));

View file

@ -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());
}

View file

@ -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
});

View file

@ -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" }

View file

@ -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")
}
}

View file

@ -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];
}
},
}
}
}

View file

@ -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>
})*

View file

@ -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 || {

View file

@ -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()
}

View file

@ -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> {

View file

@ -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 {
//
}

View file

@ -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
})
}

View file

@ -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)();

View file

@ -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(())
}

View file

@ -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)]

View file

@ -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;
}
}

View file

@ -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 } => {

View file

@ -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;

View file

@ -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 {

View file

@ -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(),
};

View file

@ -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

View file

@ -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,
}

View file

@ -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")
}

View file

@ -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",

View file

@ -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),

View file

@ -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(_)])
}
}

View file

@ -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 } },
}
}
}

View file

@ -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(

View file

@ -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"
)
}

View file

@ -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
}
}

View file

@ -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!()
}

View file

@ -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)
}

View file

@ -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>"
);
}

View file

@ -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>"
// );
}

View file

@ -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);
}

View file

@ -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>"
);
}

View file

@ -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"

View file

@ -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()
}
}

View file

@ -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() => {