merge upstream changes
This commit is contained in:
343 files changed
+20245
-14076
No files matched your search
@@ -1,18 +1,23 @@
|
||||
[package]
|
||||
name = "dioxus-autofmt"
|
||||
version = "0.0.0"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
|
||||
authors = ["Jonathan Kelley"]
|
||||
description = "Autofomatter for Dioxus RSX"
|
||||
license = "MIT/Apache-2.0"
|
||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||
homepage = "https://dioxuslabs.com"
|
||||
documentation = "https://dioxuslabs.com"
|
||||
keywords = ["dom", "ui", "gui", "react"]
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus-rsx = { path = "../rsx" }
|
||||
dioxus-rsx = { path = "../rsx", version = "^0.0.3" }
|
||||
proc-macro2 = { version = "1.0.6", features = ["span-locations"] }
|
||||
quote = "1.0"
|
||||
syn = { version = "1.0.11", features = ["full", "extra-traits"] }
|
||||
triple_accel = "0.4.0"
|
||||
serde = { version = "1.0.136", features = ["derive"] }
|
||||
prettyplease = { git = "https://github.com/DioxusLabs/prettyplease-macro-fmt.git", features = [
|
||||
prettyplease = { package = "prettier-please", version = "0.1.16", features = [
|
||||
"verbatim",
|
||||
] }
|
||||
|
||||
|
||||
+35
-74
@@ -1,88 +1,49 @@
|
||||
# This crate autofmts blocks of rsx!
|
||||
|
||||
This crate formats rsx! by parsing call bodies and pretty-printing them back out.
|
||||
# dioxus-autofmt
|
||||
|
||||
|
||||
[![Crates.io][crates-badge]][crates-url]
|
||||
[![MIT licensed][mit-badge]][mit-url]
|
||||
[![Build Status][actions-badge]][actions-url]
|
||||
[![Discord chat][discord-badge]][discord-url]
|
||||
|
||||
# Todo:
|
||||
Sorted roughly in order of what's possible
|
||||
[crates-badge]: https://img.shields.io/crates/v/dioxus-autofmt.svg
|
||||
[crates-url]: https://crates.io/crates/dioxus-autofmt
|
||||
|
||||
- [x] Oneline rsx! calls - blocker because this wrecks formatting
|
||||
- [ ] Nested RSX calls (important) - unnecessary but desirable
|
||||
- [ ] RSX edits overstepping each other
|
||||
- [ ] Collapse components and elements under syntax -
|
||||
- [ ] Don't eat comments in exprs
|
||||
- [ ] Format regular exprs
|
||||
- [ ] Fix prettyplease around chaining
|
||||
- [ ] Don't eat comments in prettyplease
|
||||
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
|
||||
|
||||
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
|
||||
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
|
||||
|
||||
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
|
||||
[discord-url]: https://discord.gg/XgGxMSkvUM
|
||||
|
||||
[Website](https://dioxuslabs.com) |
|
||||
[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) |
|
||||
[API Docs](https://docs.rs/dioxus-autofmt/latest/dioxus_autofmt) |
|
||||
[Chat](https://discord.gg/XgGxMSkvUM)
|
||||
|
||||
|
||||
# Technique
|
||||
## Overview
|
||||
|
||||
`dioxus-autofmt` provides a pretty printer for the `rsx` syntax tree.
|
||||
|
||||
|
||||
div {
|
||||
div {}
|
||||
div {}
|
||||
}
|
||||
This is done manually with a via set of formatting rules. The output is not guaranteed to be stable between minor versions of the crate as we might tweak the output.
|
||||
|
||||
`dioxus-autofmt` provides an API to perform precision edits as well as just spit out a block of formatted RSX from any RSX syntax tree. This is used by the `rsx-rosetta` crate which can accept various input languages and output valid RSX.
|
||||
|
||||
|
||||
div
|
||||
## Contributing
|
||||
|
||||
possible line break
|
||||
div
|
||||
div
|
||||
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
|
||||
- Join the discord and ask questions!
|
||||
|
||||
## License
|
||||
This project is licensed under the [MIT license].
|
||||
|
||||
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
|
||||
|
||||
string of possible items within a nesting
|
||||
div {
|
||||
attr_pair
|
||||
expr
|
||||
text
|
||||
comment
|
||||
}
|
||||
a nesting is either a component or an element
|
||||
|
||||
idea:
|
||||
collect all items into a queue
|
||||
q
|
||||
```rust
|
||||
section {
|
||||
div {
|
||||
h1 { p { "asdasd" } }
|
||||
h1 { p { "asdasd" } }
|
||||
}
|
||||
}
|
||||
|
||||
section {}
|
||||
```
|
||||
|
||||
|
||||
// space
|
||||
// space
|
||||
// space
|
||||
|
||||
|
||||
3 - section
|
||||
3 - section div
|
||||
3 - section div h1
|
||||
3 - section div h1 p
|
||||
3 - section div h1 p text
|
||||
3 - section
|
||||
3 - section div
|
||||
3 - section div h1
|
||||
3 - section div h1 p
|
||||
3 - section div h1 p text
|
||||
|
||||
block
|
||||
|
||||
- when we hit the end of a trail, we can make a decision what needs to be hard breaked
|
||||
- most nestings cannot be merged into a single one, so at some point we need to write the line break
|
||||
- this is the scan section. we scan forward until it's obvious where to place a hard break
|
||||
- when a line is finished, we can print it out by unloading our queued items
|
||||
- never double nested
|
||||
|
||||
|
||||
Terms
|
||||
- break is a whitespace than can flex, dependent on the situation
|
||||
- ‹›
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in Dioxus by you shall be licensed as MIT without any additional
|
||||
terms or conditions.
|
||||
+14
-175
@@ -1,33 +1,18 @@
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
fmt::{Result, Write},
|
||||
};
|
||||
//! The output buffer that supports some helpful methods
|
||||
//! These are separate from the input so we can lend references between the two
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
|
||||
use dioxus_rsx::{BodyNode, ElementAttr, ElementAttrNamed, IfmtInput};
|
||||
use proc_macro2::{LineColumn, Span};
|
||||
use syn::{spanned::Spanned, Expr};
|
||||
use std::fmt::{Result, Write};
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
use dioxus_rsx::IfmtInput;
|
||||
|
||||
/// The output buffer that tracks indent and string
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Buffer {
|
||||
pub src: Vec<String>,
|
||||
pub cached_formats: HashMap<Location, String>,
|
||||
pub buf: String,
|
||||
pub indent: usize,
|
||||
pub comments: VecDeque<usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Hash, PartialEq, Eq, Debug)]
|
||||
pub struct Location {
|
||||
pub line: usize,
|
||||
pub col: usize,
|
||||
}
|
||||
impl Location {
|
||||
pub fn new(start: LineColumn) -> Self {
|
||||
Self {
|
||||
line: start.line,
|
||||
col: start.column,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
@@ -62,160 +47,14 @@ impl Buffer {
|
||||
writeln!(self.buf)
|
||||
}
|
||||
|
||||
// Expects to be written directly into place
|
||||
pub fn write_ident(&mut self, node: &BodyNode) -> Result {
|
||||
match node {
|
||||
BodyNode::Element(el) => self.write_element(el),
|
||||
BodyNode::Component(component) => self.write_component(component),
|
||||
BodyNode::Text(text) => self.write_text(text),
|
||||
BodyNode::RawExpr(exp) => self.write_raw_expr(exp),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_text(&mut self, text: &IfmtInput) -> Result {
|
||||
write!(self.buf, "\"{}\"", text.source.as_ref().unwrap().value())
|
||||
}
|
||||
|
||||
pub fn consume(self) -> Option<String> {
|
||||
Some(self.buf)
|
||||
}
|
||||
|
||||
pub fn write_comments(&mut self, child: Span) -> Result {
|
||||
// collect all comments upwards
|
||||
let start = child.start();
|
||||
let line_start = start.line - 1;
|
||||
|
||||
for (id, line) in self.src[..line_start].iter().enumerate().rev() {
|
||||
if line.trim().starts_with("//") || line.is_empty() {
|
||||
if id != 0 {
|
||||
self.comments.push_front(id);
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let mut last_was_empty = false;
|
||||
while let Some(comment_line) = self.comments.pop_front() {
|
||||
let line = &self.src[comment_line];
|
||||
if line.is_empty() {
|
||||
if !last_was_empty {
|
||||
self.new_line()?;
|
||||
}
|
||||
last_was_empty = true;
|
||||
} else {
|
||||
last_was_empty = false;
|
||||
self.tabbed_line()?;
|
||||
write!(self.buf, "{}", self.src[comment_line].trim())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Push out the indent level and write each component, line by line
|
||||
pub fn write_body_indented(&mut self, children: &[BodyNode]) -> Result {
|
||||
self.indent += 1;
|
||||
|
||||
self.write_body_no_indent(children)?;
|
||||
|
||||
self.indent -= 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write_body_no_indent(&mut self, children: &[BodyNode]) -> Result {
|
||||
let last_child = children.len();
|
||||
|
||||
for (idx, child) in children.iter().enumerate() {
|
||||
match child {
|
||||
// check if the expr is a short
|
||||
BodyNode::RawExpr { .. } => {
|
||||
self.tabbed_line()?;
|
||||
self.write_ident(child)?;
|
||||
if idx != last_child - 1 {
|
||||
write!(self.buf, ",")?;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if self.current_span_is_primary(child.span()) {
|
||||
self.write_comments(child.span())?;
|
||||
}
|
||||
self.tabbed_line()?;
|
||||
self.write_ident(child)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn is_short_attrs(&mut self, attributes: &[ElementAttrNamed]) -> usize {
|
||||
let mut total = 0;
|
||||
|
||||
for attr in attributes {
|
||||
if self.current_span_is_primary(attr.attr.flart()) {
|
||||
'line: for line in self.src[..attr.attr.flart().start().line - 1].iter().rev() {
|
||||
match (line.trim().starts_with("//"), line.is_empty()) {
|
||||
(true, _) => return 100000,
|
||||
(_, true) => continue 'line,
|
||||
_ => break 'line,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
total += match &attr.attr {
|
||||
ElementAttr::AttrText { value, name } => {
|
||||
value.source.as_ref().unwrap().value().len() + name.span().line_length() + 3
|
||||
}
|
||||
ElementAttr::AttrExpression { name, value } => {
|
||||
value.span().line_length() + name.span().line_length() + 3
|
||||
}
|
||||
ElementAttr::CustomAttrText { value, name } => {
|
||||
value.source.as_ref().unwrap().value().len() + name.value().len() + 3
|
||||
}
|
||||
ElementAttr::CustomAttrExpression { name, value } => {
|
||||
name.value().len() + value.span().line_length() + 3
|
||||
}
|
||||
ElementAttr::EventTokens { tokens, name } => {
|
||||
let location = Location::new(tokens.span().start());
|
||||
|
||||
let len = if let std::collections::hash_map::Entry::Vacant(e) =
|
||||
self.cached_formats.entry(location)
|
||||
{
|
||||
let formatted = prettyplease::unparse_expr(tokens);
|
||||
let len = if formatted.contains('\n') {
|
||||
10000
|
||||
} else {
|
||||
formatted.len()
|
||||
};
|
||||
e.insert(formatted);
|
||||
len
|
||||
} else {
|
||||
self.cached_formats[&location].len()
|
||||
};
|
||||
|
||||
len + name.span().line_length() + 3
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
total
|
||||
}
|
||||
|
||||
pub fn retrieve_formatted_expr(&mut self, expr: &Expr) -> &str {
|
||||
self.cached_formats
|
||||
.entry(Location::new(expr.span().start()))
|
||||
.or_insert_with(|| prettyplease::unparse_expr(expr))
|
||||
.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
trait SpanLength {
|
||||
fn line_length(&self) -> usize;
|
||||
}
|
||||
impl SpanLength for Span {
|
||||
fn line_length(&self) -> usize {
|
||||
self.end().line - self.start().line
|
||||
impl std::fmt::Write for Buffer {
|
||||
fn write_str(&mut self, s: &str) -> std::fmt::Result {
|
||||
self.buf.push_str(s);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
//! Collect macros from a file
|
||||
//!
|
||||
//! Returns all macros that match a pattern. You can use this information to autoformat them later
|
||||
|
||||
use proc_macro2::LineColumn;
|
||||
use syn::{Block, Expr, File, Item, Macro, Stmt};
|
||||
|
||||
type CollectedMacro<'a> = &'a Macro;
|
||||
|
||||
pub fn collect_from_file<'a>(file: &'a File, macros: &mut Vec<CollectedMacro<'a>>) {
|
||||
for item in file.items.iter() {
|
||||
collect_from_item(item, macros);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn collect_from_item<'a>(item: &'a Item, macros: &mut Vec<CollectedMacro<'a>>) {
|
||||
match item {
|
||||
Item::Fn(f) => collect_from_block(&f.block, macros),
|
||||
|
||||
// Ignore macros if they're not rsx or render
|
||||
Item::Macro(macro_) => {
|
||||
if macro_.mac.path.segments[0].ident == "rsx"
|
||||
|| macro_.mac.path.segments[0].ident == "render"
|
||||
{
|
||||
macros.push(¯o_.mac);
|
||||
}
|
||||
}
|
||||
|
||||
// Currently disabled since we're not focused on autoformatting these
|
||||
Item::Impl(_imp) => {}
|
||||
Item::Trait(_) => {}
|
||||
|
||||
// Global-ish things
|
||||
Item::Static(f) => collect_from_expr(&f.expr, macros),
|
||||
Item::Const(f) => collect_from_expr(&f.expr, macros),
|
||||
Item::Mod(s) => {
|
||||
if let Some((_, block)) = &s.content {
|
||||
for item in block {
|
||||
collect_from_item(item, macros);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// None of these we can really do anything with at the item level
|
||||
Item::Macro2(_)
|
||||
| Item::Enum(_)
|
||||
| Item::ExternCrate(_)
|
||||
| Item::ForeignMod(_)
|
||||
| Item::TraitAlias(_)
|
||||
| Item::Type(_)
|
||||
| Item::Struct(_)
|
||||
| Item::Union(_)
|
||||
| Item::Use(_)
|
||||
| Item::Verbatim(_) => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn collect_from_block<'a>(block: &'a Block, macros: &mut Vec<CollectedMacro<'a>>) {
|
||||
for stmt in &block.stmts {
|
||||
match stmt {
|
||||
Stmt::Item(item) => collect_from_item(item, macros),
|
||||
Stmt::Local(local) => {
|
||||
if let Some((_eq, init)) = &local.init {
|
||||
collect_from_expr(init, macros);
|
||||
}
|
||||
}
|
||||
Stmt::Expr(exp) | Stmt::Semi(exp, _) => collect_from_expr(exp, macros),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn collect_from_expr<'a>(expr: &'a Expr, macros: &mut Vec<CollectedMacro<'a>>) {
|
||||
// collect an expr from the exprs, descending into blocks
|
||||
match expr {
|
||||
Expr::Macro(macro_) => {
|
||||
if macro_.mac.path.segments[0].ident == "rsx"
|
||||
|| macro_.mac.path.segments[0].ident == "render"
|
||||
{
|
||||
macros.push(¯o_.mac);
|
||||
}
|
||||
}
|
||||
|
||||
Expr::MethodCall(e) => {
|
||||
collect_from_expr(&e.receiver, macros);
|
||||
for expr in e.args.iter() {
|
||||
collect_from_expr(expr, macros);
|
||||
}
|
||||
}
|
||||
Expr::Assign(exp) => {
|
||||
collect_from_expr(&exp.left, macros);
|
||||
collect_from_expr(&exp.right, macros);
|
||||
}
|
||||
|
||||
Expr::Async(b) => collect_from_block(&b.block, macros),
|
||||
Expr::Block(b) => collect_from_block(&b.block, macros),
|
||||
Expr::Closure(c) => collect_from_expr(&c.body, macros),
|
||||
Expr::Let(l) => collect_from_expr(&l.expr, macros),
|
||||
Expr::Unsafe(u) => collect_from_block(&u.block, macros),
|
||||
Expr::Loop(l) => collect_from_block(&l.body, macros),
|
||||
|
||||
Expr::Call(c) => {
|
||||
collect_from_expr(&c.func, macros);
|
||||
for expr in c.args.iter() {
|
||||
collect_from_expr(expr, macros);
|
||||
}
|
||||
}
|
||||
|
||||
Expr::ForLoop(b) => {
|
||||
collect_from_expr(&b.expr, macros);
|
||||
collect_from_block(&b.body, macros);
|
||||
}
|
||||
Expr::If(f) => {
|
||||
collect_from_expr(&f.cond, macros);
|
||||
collect_from_block(&f.then_branch, macros);
|
||||
if let Some((_, else_branch)) = &f.else_branch {
|
||||
collect_from_expr(else_branch, macros);
|
||||
}
|
||||
}
|
||||
Expr::Yield(y) => {
|
||||
if let Some(expr) = &y.expr {
|
||||
collect_from_expr(expr, macros);
|
||||
}
|
||||
}
|
||||
|
||||
Expr::Return(r) => {
|
||||
if let Some(expr) = &r.expr {
|
||||
collect_from_expr(expr, macros);
|
||||
}
|
||||
}
|
||||
|
||||
Expr::Match(l) => {
|
||||
collect_from_expr(&l.expr, macros);
|
||||
for arm in l.arms.iter() {
|
||||
if let Some((_, expr)) = &arm.guard {
|
||||
collect_from_expr(expr, macros);
|
||||
}
|
||||
|
||||
collect_from_expr(&arm.body, macros);
|
||||
}
|
||||
}
|
||||
|
||||
Expr::While(w) => {
|
||||
collect_from_expr(&w.cond, macros);
|
||||
collect_from_block(&w.body, macros);
|
||||
}
|
||||
|
||||
// don't both formatting these for now
|
||||
Expr::Array(_)
|
||||
| Expr::AssignOp(_)
|
||||
| Expr::Await(_)
|
||||
| Expr::Binary(_)
|
||||
| Expr::Box(_)
|
||||
| Expr::Break(_)
|
||||
| Expr::Cast(_)
|
||||
| Expr::Continue(_)
|
||||
| Expr::Field(_)
|
||||
| Expr::Group(_)
|
||||
| Expr::Index(_)
|
||||
| Expr::Lit(_)
|
||||
| Expr::Paren(_)
|
||||
| Expr::Path(_)
|
||||
| Expr::Range(_)
|
||||
| Expr::Reference(_)
|
||||
| Expr::Repeat(_)
|
||||
| Expr::Struct(_)
|
||||
| Expr::Try(_)
|
||||
| Expr::TryBlock(_)
|
||||
| Expr::Tuple(_)
|
||||
| Expr::Type(_)
|
||||
| Expr::Unary(_)
|
||||
| Expr::Verbatim(_) => {}
|
||||
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn byte_offset(input: &str, location: LineColumn) -> usize {
|
||||
let mut offset = 0;
|
||||
for _ in 1..location.line {
|
||||
offset += input[offset..].find('\n').unwrap() + 1;
|
||||
}
|
||||
offset
|
||||
+ input[offset..]
|
||||
.chars()
|
||||
.take(location.column)
|
||||
.map(char::len_utf8)
|
||||
.sum::<usize>()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_file_and_collects_rsx_macros() {
|
||||
let contents = include_str!("../tests/samples/long.rsx");
|
||||
let parsed = syn::parse_file(contents).unwrap();
|
||||
let mut macros = vec![];
|
||||
collect_from_file(&parsed, &mut macros);
|
||||
assert_eq!(macros.len(), 3);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{buffer::Location, Buffer};
|
||||
use crate::{writer::Location, Writer};
|
||||
use dioxus_rsx::*;
|
||||
use quote::ToTokens;
|
||||
use std::fmt::{Result, Write};
|
||||
@@ -19,7 +19,7 @@ enum ShortOptimization {
|
||||
NoOpt,
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
impl Writer<'_> {
|
||||
pub fn write_component(
|
||||
&mut self,
|
||||
Component {
|
||||
@@ -28,6 +28,7 @@ impl Buffer {
|
||||
children,
|
||||
manual_props,
|
||||
prop_gen_args,
|
||||
..
|
||||
}: &Component,
|
||||
) -> Result {
|
||||
self.write_component_name(name, prop_gen_args)?;
|
||||
@@ -82,46 +83,46 @@ impl Buffer {
|
||||
match opt_level {
|
||||
ShortOptimization::Empty => {}
|
||||
ShortOptimization::Oneliner => {
|
||||
write!(self.buf, " ")?;
|
||||
write!(self.out, " ")?;
|
||||
|
||||
self.write_component_fields(fields, manual_props, true)?;
|
||||
|
||||
if !children.is_empty() && !fields.is_empty() {
|
||||
write!(self.buf, ", ")?;
|
||||
write!(self.out, ", ")?;
|
||||
}
|
||||
|
||||
for child in children {
|
||||
self.write_ident(child)?;
|
||||
}
|
||||
|
||||
write!(self.buf, " ")?;
|
||||
write!(self.out, " ")?;
|
||||
}
|
||||
|
||||
ShortOptimization::PropsOnTop => {
|
||||
write!(self.buf, " ")?;
|
||||
write!(self.out, " ")?;
|
||||
self.write_component_fields(fields, manual_props, true)?;
|
||||
|
||||
if !children.is_empty() && !fields.is_empty() {
|
||||
write!(self.buf, ",")?;
|
||||
write!(self.out, ",")?;
|
||||
}
|
||||
|
||||
self.write_body_indented(children)?;
|
||||
self.tabbed_line()?;
|
||||
self.out.tabbed_line()?;
|
||||
}
|
||||
|
||||
ShortOptimization::NoOpt => {
|
||||
self.write_component_fields(fields, manual_props, false)?;
|
||||
|
||||
if !children.is_empty() && !fields.is_empty() {
|
||||
write!(self.buf, ",")?;
|
||||
write!(self.out, ",")?;
|
||||
}
|
||||
|
||||
self.write_body_indented(children)?;
|
||||
self.tabbed_line()?;
|
||||
self.out.tabbed_line()?;
|
||||
}
|
||||
}
|
||||
|
||||
write!(self.buf, "}}")?;
|
||||
write!(self.out, "}}")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -133,16 +134,16 @@ impl Buffer {
|
||||
let mut name = name.to_token_stream().to_string();
|
||||
name.retain(|c| !c.is_whitespace());
|
||||
|
||||
write!(self.buf, "{name}")?;
|
||||
write!(self.out, "{name}")?;
|
||||
|
||||
if let Some(generics) = generics {
|
||||
let mut written = generics.to_token_stream().to_string();
|
||||
written.retain(|c| !c.is_whitespace());
|
||||
|
||||
write!(self.buf, "{}", written)?;
|
||||
write!(self.out, "{written}")?;
|
||||
}
|
||||
|
||||
write!(self.buf, " {{")?;
|
||||
write!(self.out, " {{")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -157,18 +158,18 @@ impl Buffer {
|
||||
|
||||
while let Some(field) = field_iter.next() {
|
||||
if !sameline {
|
||||
self.indented_tabbed_line()?;
|
||||
self.out.indented_tabbed_line()?;
|
||||
}
|
||||
|
||||
let name = &field.name;
|
||||
match &field.content {
|
||||
ContentField::ManExpr(exp) => {
|
||||
let out = prettyplease::unparse_expr(exp);
|
||||
write!(self.buf, "{}: {}", name, out)?;
|
||||
write!(self.out, "{name}: {out}")?;
|
||||
}
|
||||
ContentField::Formatted(s) => {
|
||||
write!(
|
||||
self.buf,
|
||||
self.out,
|
||||
"{}: \"{}\"",
|
||||
name,
|
||||
s.source.as_ref().unwrap().value()
|
||||
@@ -178,27 +179,27 @@ impl Buffer {
|
||||
let out = prettyplease::unparse_expr(exp);
|
||||
let mut lines = out.split('\n').peekable();
|
||||
let first = lines.next().unwrap();
|
||||
write!(self.buf, "{}: {}", name, first)?;
|
||||
write!(self.out, "{name}: {first}")?;
|
||||
for line in lines {
|
||||
self.new_line()?;
|
||||
self.indented_tab()?;
|
||||
write!(self.buf, "{}", line)?;
|
||||
self.out.new_line()?;
|
||||
self.out.indented_tab()?;
|
||||
write!(self.out, "{line}")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if field_iter.peek().is_some() || manual_props.is_some() {
|
||||
write!(self.buf, ",")?;
|
||||
write!(self.out, ",")?;
|
||||
|
||||
if sameline {
|
||||
write!(self.buf, " ")?;
|
||||
write!(self.out, " ")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(exp) = manual_props {
|
||||
if !sameline {
|
||||
self.indented_tabbed_line()?;
|
||||
self.out.indented_tabbed_line()?;
|
||||
}
|
||||
self.write_manual_props(exp)?;
|
||||
}
|
||||
@@ -258,10 +259,10 @@ impl Buffer {
|
||||
|
||||
let first_line = lines.next().unwrap();
|
||||
|
||||
write!(self.buf, "..{first_line}")?;
|
||||
write!(self.out, "..{first_line}")?;
|
||||
for line in lines {
|
||||
self.indented_tabbed_line()?;
|
||||
write!(self.buf, "{line}")?;
|
||||
self.out.indented_tabbed_line()?;
|
||||
write!(self.out, "{line}")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
+115
-56
@@ -1,35 +1,56 @@
|
||||
use crate::Buffer;
|
||||
use crate::Writer;
|
||||
use dioxus_rsx::*;
|
||||
use proc_macro2::Span;
|
||||
use std::{fmt::Result, fmt::Write};
|
||||
use syn::{spanned::Spanned, Expr};
|
||||
use std::{
|
||||
fmt::Result,
|
||||
fmt::{self, Write},
|
||||
};
|
||||
use syn::{spanned::Spanned, token::Brace, Expr};
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ShortOptimization {
|
||||
// Special because we want to print the closing bracket immediately
|
||||
/// Special because we want to print the closing bracket immediately
|
||||
///
|
||||
/// IE
|
||||
/// `div {}` instead of `div { }`
|
||||
Empty,
|
||||
|
||||
// Special optimization to put everything on the same line
|
||||
/// Special optimization to put everything on the same line and add some buffer spaces
|
||||
///
|
||||
/// IE
|
||||
///
|
||||
/// `div { "asdasd" }` instead of a multiline variant
|
||||
Oneliner,
|
||||
|
||||
// Optimization where children flow but props remain fixed on top
|
||||
/// Optimization where children flow but props remain fixed on top
|
||||
PropsOnTop,
|
||||
|
||||
// The noisiest optimization where everything flows
|
||||
/// The noisiest optimization where everything flows
|
||||
NoOpt,
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
pub fn write_element(
|
||||
&mut self,
|
||||
Element {
|
||||
/*
|
||||
// whitespace
|
||||
div {
|
||||
// some whitespace
|
||||
class: "asdasd"
|
||||
|
||||
// whjiot
|
||||
asdasd // whitespace
|
||||
}
|
||||
*/
|
||||
|
||||
impl Writer<'_> {
|
||||
pub fn write_element(&mut self, el: &Element) -> Result {
|
||||
let Element {
|
||||
name,
|
||||
key,
|
||||
attributes,
|
||||
children,
|
||||
_is_static,
|
||||
}: &Element,
|
||||
) -> Result {
|
||||
brace,
|
||||
} = el;
|
||||
|
||||
/*
|
||||
1. Write the tag
|
||||
2. Write the key
|
||||
@@ -37,7 +58,7 @@ impl Buffer {
|
||||
4. Write the children
|
||||
*/
|
||||
|
||||
write!(self.buf, "{name} {{")?;
|
||||
write!(self.out, "{name} {{")?;
|
||||
|
||||
// decide if we have any special optimizations
|
||||
// Default with none, opt the cases in one-by-one
|
||||
@@ -45,8 +66,9 @@ impl Buffer {
|
||||
|
||||
// check if we have a lot of attributes
|
||||
let attr_len = self.is_short_attrs(attributes);
|
||||
let is_short_attr_list = attr_len < 80;
|
||||
let is_small_children = self.is_short_children(children).is_some();
|
||||
let is_short_attr_list = (attr_len + self.out.indent * 4) < 80;
|
||||
let children_len = self.is_short_children(children);
|
||||
let is_small_children = children_len.is_some();
|
||||
|
||||
// if we have few attributes and a lot of children, place the attrs on top
|
||||
if is_short_attr_list && !is_small_children {
|
||||
@@ -64,12 +86,19 @@ impl Buffer {
|
||||
|
||||
// if we have few children and few attributes, make it a one-liner
|
||||
if is_short_attr_list && is_small_children {
|
||||
opt_level = ShortOptimization::Oneliner;
|
||||
if children_len.unwrap() + attr_len + self.out.indent * 4 < 100 {
|
||||
opt_level = ShortOptimization::Oneliner;
|
||||
} else {
|
||||
opt_level = ShortOptimization::PropsOnTop;
|
||||
}
|
||||
}
|
||||
|
||||
// If there's nothing at all, empty optimization
|
||||
if attributes.is_empty() && children.is_empty() && key.is_none() {
|
||||
opt_level = ShortOptimization::Empty;
|
||||
|
||||
// Write comments if they exist
|
||||
self.write_todo_body(brace)?;
|
||||
}
|
||||
|
||||
// multiline handlers bump everything down
|
||||
@@ -80,55 +109,56 @@ impl Buffer {
|
||||
match opt_level {
|
||||
ShortOptimization::Empty => {}
|
||||
ShortOptimization::Oneliner => {
|
||||
write!(self.buf, " ")?;
|
||||
write!(self.out, " ")?;
|
||||
|
||||
self.write_attributes(attributes, key, true)?;
|
||||
|
||||
if !children.is_empty() && (!attributes.is_empty() || key.is_some()) {
|
||||
write!(self.buf, ", ")?;
|
||||
write!(self.out, ", ")?;
|
||||
}
|
||||
|
||||
for (id, child) in children.iter().enumerate() {
|
||||
self.write_ident(child)?;
|
||||
if id != children.len() - 1 && children.len() > 1 {
|
||||
write!(self.buf, ", ")?;
|
||||
write!(self.out, ", ")?;
|
||||
}
|
||||
}
|
||||
|
||||
write!(self.buf, " ")?;
|
||||
write!(self.out, " ")?;
|
||||
}
|
||||
|
||||
ShortOptimization::PropsOnTop => {
|
||||
if !attributes.is_empty() || key.is_some() {
|
||||
write!(self.buf, " ")?;
|
||||
write!(self.out, " ")?;
|
||||
}
|
||||
self.write_attributes(attributes, key, true)?;
|
||||
|
||||
if !children.is_empty() && (!attributes.is_empty() || key.is_some()) {
|
||||
write!(self.buf, ",")?;
|
||||
write!(self.out, ",")?;
|
||||
}
|
||||
|
||||
if !children.is_empty() {
|
||||
self.write_body_indented(children)?;
|
||||
}
|
||||
self.tabbed_line()?;
|
||||
self.out.tabbed_line()?;
|
||||
}
|
||||
|
||||
ShortOptimization::NoOpt => {
|
||||
self.write_attributes(attributes, key, false)?;
|
||||
|
||||
if !children.is_empty() && (!attributes.is_empty() || key.is_some()) {
|
||||
write!(self.buf, ",")?;
|
||||
write!(self.out, ",")?;
|
||||
}
|
||||
|
||||
if !children.is_empty() {
|
||||
self.write_body_indented(children)?;
|
||||
}
|
||||
self.tabbed_line()?;
|
||||
|
||||
self.out.tabbed_line()?;
|
||||
}
|
||||
}
|
||||
|
||||
write!(self.buf, "}}")?;
|
||||
write!(self.out, "}}")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -143,39 +173,39 @@ impl Buffer {
|
||||
|
||||
if let Some(key) = key {
|
||||
if !sameline {
|
||||
self.indented_tabbed_line()?;
|
||||
self.out.indented_tabbed_line()?;
|
||||
}
|
||||
write!(
|
||||
self.buf,
|
||||
self.out,
|
||||
"key: \"{}\"",
|
||||
key.source.as_ref().unwrap().value()
|
||||
)?;
|
||||
if !attributes.is_empty() {
|
||||
write!(self.buf, ",")?;
|
||||
write!(self.out, ",")?;
|
||||
if sameline {
|
||||
write!(self.buf, " ")?;
|
||||
write!(self.out, " ")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(attr) = attr_iter.next() {
|
||||
self.indent += 1;
|
||||
self.out.indent += 1;
|
||||
if !sameline {
|
||||
self.write_comments(attr.attr.flart())?;
|
||||
self.write_comments(attr.attr.start())?;
|
||||
}
|
||||
self.indent -= 1;
|
||||
self.out.indent -= 1;
|
||||
|
||||
if !sameline {
|
||||
self.indented_tabbed_line()?;
|
||||
self.out.indented_tabbed_line()?;
|
||||
}
|
||||
|
||||
self.write_attribute(attr)?;
|
||||
|
||||
if attr_iter.peek().is_some() {
|
||||
write!(self.buf, ",")?;
|
||||
write!(self.out, ",")?;
|
||||
|
||||
if sameline {
|
||||
write!(self.buf, " ")?;
|
||||
write!(self.out, " ")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,19 +217,19 @@ impl Buffer {
|
||||
match &attr.attr {
|
||||
ElementAttr::AttrText { name, value } => {
|
||||
write!(
|
||||
self.buf,
|
||||
self.out,
|
||||
"{name}: \"{value}\"",
|
||||
value = value.source.as_ref().unwrap().value()
|
||||
)?;
|
||||
}
|
||||
ElementAttr::AttrExpression { name, value } => {
|
||||
let out = prettyplease::unparse_expr(value);
|
||||
write!(self.buf, "{}: {}", name, out)?;
|
||||
write!(self.out, "{name}: {out}")?;
|
||||
}
|
||||
|
||||
ElementAttr::CustomAttrText { name, value } => {
|
||||
write!(
|
||||
self.buf,
|
||||
self.out,
|
||||
"\"{name}\": \"{value}\"",
|
||||
name = name.value(),
|
||||
value = value.source.as_ref().unwrap().value()
|
||||
@@ -208,7 +238,7 @@ impl Buffer {
|
||||
|
||||
ElementAttr::CustomAttrExpression { name, value } => {
|
||||
let out = prettyplease::unparse_expr(value);
|
||||
write!(self.buf, "\"{}\": {}", name.value(), out)?;
|
||||
write!(self.out, "\"{}\": {}", name.value(), out)?;
|
||||
}
|
||||
|
||||
ElementAttr::EventTokens { name, tokens } => {
|
||||
@@ -220,17 +250,17 @@ impl Buffer {
|
||||
// a one-liner for whatever reason
|
||||
// Does not need a new line
|
||||
if lines.peek().is_none() {
|
||||
write!(self.buf, "{}: {}", name, first)?;
|
||||
write!(self.out, "{name}: {first}")?;
|
||||
} else {
|
||||
writeln!(self.buf, "{}: {}", name, first)?;
|
||||
writeln!(self.out, "{name}: {first}")?;
|
||||
|
||||
while let Some(line) = lines.next() {
|
||||
self.indented_tab()?;
|
||||
write!(self.buf, "{}", line)?;
|
||||
self.out.indented_tab()?;
|
||||
write!(self.out, "{line}")?;
|
||||
if lines.peek().is_none() {
|
||||
write!(self.buf, "")?;
|
||||
write!(self.out, "")?;
|
||||
} else {
|
||||
writeln!(self.buf)?;
|
||||
writeln!(self.out)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -246,7 +276,7 @@ impl Buffer {
|
||||
let start = location.start();
|
||||
let line_start = start.line - 1;
|
||||
|
||||
let this_line = self.src[line_start].as_str();
|
||||
let this_line = self.src[line_start];
|
||||
|
||||
let beginning = if this_line.len() > start.column {
|
||||
this_line[..start.column].trim()
|
||||
@@ -254,8 +284,6 @@ impl Buffer {
|
||||
""
|
||||
};
|
||||
|
||||
// dbg!(beginning);
|
||||
|
||||
beginning.is_empty()
|
||||
}
|
||||
|
||||
@@ -268,6 +296,10 @@ impl Buffer {
|
||||
if children.is_empty() {
|
||||
// todo: allow elements with comments but no children
|
||||
// like div { /* comment */ }
|
||||
// or
|
||||
// div {
|
||||
// // some helpful
|
||||
// }
|
||||
return Some(0);
|
||||
}
|
||||
|
||||
@@ -296,10 +328,8 @@ impl Buffer {
|
||||
None
|
||||
}
|
||||
}
|
||||
[BodyNode::RawExpr(ref expr)] => {
|
||||
// TODO: let rawexprs to be inlined
|
||||
get_expr_length(expr)
|
||||
}
|
||||
// TODO: let rawexprs to be inlined
|
||||
[BodyNode::RawExpr(ref expr)] => get_expr_length(expr),
|
||||
[BodyNode::Element(ref el)] => {
|
||||
let attr_len = self.is_short_attrs(&el.attributes);
|
||||
|
||||
@@ -333,8 +363,8 @@ impl Buffer {
|
||||
Some(len) => total_count += len,
|
||||
None => return None,
|
||||
},
|
||||
BodyNode::ForLoop(_) => todo!(),
|
||||
BodyNode::IfChain(_) => todo!(),
|
||||
BodyNode::ForLoop(_forloop) => return None,
|
||||
BodyNode::IfChain(_chain) => return None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,6 +372,35 @@ impl Buffer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// empty everything except for some comments
|
||||
fn write_todo_body(&mut self, brace: &Brace) -> fmt::Result {
|
||||
let span = brace.span.span();
|
||||
let start = span.start();
|
||||
let end = span.end();
|
||||
|
||||
if start.line == end.line {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
writeln!(self.out)?;
|
||||
|
||||
for idx in start.line..end.line {
|
||||
let line = &self.src[idx];
|
||||
if line.trim().starts_with("//") {
|
||||
for _ in 0..self.out.indent + 1 {
|
||||
write!(self.out, " ")?
|
||||
}
|
||||
writeln!(self.out, "{}", line.trim()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
for _ in 0..self.out.indent {
|
||||
write!(self.out, " ")?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn get_expr_length(expr: &Expr) -> Option<usize> {
|
||||
|
||||
@@ -1,48 +1,26 @@
|
||||
//! pretty printer for rsx!
|
||||
use std::fmt::{Result, Write};
|
||||
|
||||
use crate::Buffer;
|
||||
use proc_macro2::Span;
|
||||
|
||||
impl Buffer {
|
||||
pub fn write_raw_expr(&mut self, exp: &syn::Expr) -> Result {
|
||||
use crate::{collect_macros::byte_offset, Writer};
|
||||
|
||||
impl Writer<'_> {
|
||||
pub fn write_raw_expr(&mut self, placement: Span) -> Result {
|
||||
/*
|
||||
We want to normalize the expr to the appropriate indent level.
|
||||
*/
|
||||
|
||||
// in a perfect world, just fire up the rust pretty printer
|
||||
// pretty_print_rust_code_as_if_it_were_rustfmt()
|
||||
|
||||
use syn::spanned::Spanned;
|
||||
let placement = exp.span();
|
||||
let start = placement.start();
|
||||
let end = placement.end();
|
||||
// let num_spaces_desired = (self.indent * 4) as isize;
|
||||
|
||||
// print comments
|
||||
// let mut queued_comments = vec![];
|
||||
// let mut offset = 2;
|
||||
// loop {
|
||||
// let line = &self.src[start.line - offset];
|
||||
// if line.trim_start().starts_with("//") {
|
||||
// queued_comments.push(line);
|
||||
// } else {
|
||||
// break;
|
||||
// }
|
||||
|
||||
// offset += 1;
|
||||
// }
|
||||
// let had_comments = !queued_comments.is_empty();
|
||||
// for comment in queued_comments.into_iter().rev() {
|
||||
// writeln!(self.buf, "{}", comment)?;
|
||||
// }
|
||||
|
||||
// if the expr is on one line, just write it directly
|
||||
if start.line == end.line {
|
||||
write!(
|
||||
self.buf,
|
||||
"{}",
|
||||
&self.src[start.line - 1][start.column - 1..end.column].trim()
|
||||
)?;
|
||||
// split counting utf8 chars
|
||||
let start = byte_offset(self.raw_src, start);
|
||||
let end = byte_offset(self.raw_src, end);
|
||||
let row = self.raw_src[start..end].trim();
|
||||
write!(self.out, "{row}")?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -50,7 +28,7 @@ impl Buffer {
|
||||
// This involves unshifting the first line if it's aligned
|
||||
let first_line = &self.src[start.line - 1];
|
||||
write!(
|
||||
self.buf,
|
||||
self.out,
|
||||
"{}",
|
||||
&first_line[start.column - 1..first_line.len()].trim()
|
||||
)?;
|
||||
@@ -66,7 +44,7 @@ impl Buffer {
|
||||
};
|
||||
|
||||
for (id, line) in self.src[start.line..end.line].iter().enumerate() {
|
||||
writeln!(self.buf)?;
|
||||
writeln!(self.out)?;
|
||||
// trim the leading whitespace
|
||||
let line = match id {
|
||||
x if x == (end.line - start.line) - 1 => &line[..end.column],
|
||||
@@ -75,52 +53,17 @@ impl Buffer {
|
||||
|
||||
if offset < 0 {
|
||||
for _ in 0..-offset {
|
||||
write!(self.buf, " ")?;
|
||||
write!(self.out, " ")?;
|
||||
}
|
||||
|
||||
write!(self.buf, "{}", line)?;
|
||||
write!(self.out, "{line}")?;
|
||||
} else {
|
||||
let offset = offset as usize;
|
||||
let right = &line[offset..];
|
||||
write!(self.buf, "{}", right)?;
|
||||
write!(self.out, "{right}")?;
|
||||
}
|
||||
}
|
||||
|
||||
// let first = &self.src[start.line - 1];
|
||||
// let num_spaces_real = first.chars().take_while(|c| c.is_whitespace()).count() as isize;
|
||||
// let offset = num_spaces_real - num_spaces_desired;
|
||||
|
||||
// for (row, line) in self.src[start.line - 1..end.line].iter().enumerate() {
|
||||
// let line = match row {
|
||||
// 0 => &line[start.column - 1..],
|
||||
// a if a == (end.line - start.line) => &line[..end.column - 1],
|
||||
// _ => line,
|
||||
// };
|
||||
|
||||
// writeln!(self.buf)?;
|
||||
// // trim the leading whitespace
|
||||
// if offset < 0 {
|
||||
// for _ in 0..-offset {
|
||||
// write!(self.buf, " ")?;
|
||||
// }
|
||||
|
||||
// write!(self.buf, "{}", line)?;
|
||||
// } else {
|
||||
// let offset = offset as usize;
|
||||
// let right = &line[offset..];
|
||||
// write!(self.buf, "{}", right)?;
|
||||
// }
|
||||
// }
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// :(
|
||||
// fn pretty_print_rust_code_as_if_it_were_rustfmt(code: &str) -> String {
|
||||
// let formatted = prettyplease::unparse_expr(exp);
|
||||
// for line in formatted.lines() {
|
||||
// write!(self.buf, "{}", line)?;
|
||||
// self.new_line()?;
|
||||
// }
|
||||
// }
|
||||
+107
-51
@@ -1,11 +1,15 @@
|
||||
use crate::buffer::*;
|
||||
use crate::util::*;
|
||||
use crate::writer::*;
|
||||
use collect_macros::byte_offset;
|
||||
use dioxus_rsx::{BodyNode, CallBody};
|
||||
use proc_macro2::LineColumn;
|
||||
use syn::{ExprMacro, MacroDelimiter};
|
||||
|
||||
mod buffer;
|
||||
mod collect_macros;
|
||||
mod component;
|
||||
mod element;
|
||||
mod expr;
|
||||
mod util;
|
||||
mod writer;
|
||||
|
||||
/// A modification to the original file to be applied by an IDE
|
||||
///
|
||||
@@ -31,87 +35,141 @@ pub struct FormattedBlock {
|
||||
/// Format a file into a list of `FormattedBlock`s to be applied by an IDE for autoformatting.
|
||||
///
|
||||
/// This function expects a complete file, not just a block of code. To format individual rsx! blocks, use fmt_block instead.
|
||||
///
|
||||
/// The point here is to provide precise modifications of a source file so an accompanying IDE tool can map these changes
|
||||
/// back to the file precisely.
|
||||
///
|
||||
/// Nested blocks of RSX will be handled automatically
|
||||
pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
|
||||
let mut formatted_blocks = Vec::new();
|
||||
let mut last_bracket_end = 0;
|
||||
|
||||
use triple_accel::{levenshtein_search, Match};
|
||||
let parsed = syn::parse_file(contents).unwrap();
|
||||
|
||||
for Match { end, start, k } in levenshtein_search(b"rsx! {", contents.as_bytes()) {
|
||||
if k > 1 {
|
||||
let mut macros = vec![];
|
||||
collect_macros::collect_from_file(&parsed, &mut macros);
|
||||
|
||||
// No macros, no work to do
|
||||
if macros.is_empty() {
|
||||
return formatted_blocks;
|
||||
}
|
||||
|
||||
let mut writer = Writer::new(contents);
|
||||
|
||||
// Dont parse nested macros
|
||||
let mut end_span = LineColumn { column: 0, line: 0 };
|
||||
for item in macros {
|
||||
let macro_path = &item.path.segments[0].ident;
|
||||
|
||||
// this macro is inside the last macro we parsed, skip it
|
||||
if macro_path.span().start() < end_span {
|
||||
continue;
|
||||
}
|
||||
|
||||
// ensure the marker is not nested
|
||||
if start < last_bracket_end {
|
||||
continue;
|
||||
// item.parse_body::<CallBody>();
|
||||
let body = item.parse_body::<CallBody>().unwrap();
|
||||
|
||||
let rsx_start = macro_path.span().start();
|
||||
|
||||
writer.out.indent = &writer.src[rsx_start.line - 1]
|
||||
.chars()
|
||||
.take_while(|c| *c == ' ')
|
||||
.count()
|
||||
/ 4;
|
||||
|
||||
write_body(&mut writer, &body);
|
||||
|
||||
// writing idents leaves the final line ended at the end of the last ident
|
||||
if writer.out.buf.contains('\n') {
|
||||
writer.out.new_line().unwrap();
|
||||
writer.out.tab().unwrap();
|
||||
}
|
||||
|
||||
let mut indent_level = {
|
||||
// walk backwards from start until we find a new line
|
||||
let mut lines = contents[..start].lines().rev();
|
||||
match lines.next() {
|
||||
Some(line) => {
|
||||
if line.starts_with("//") || line.starts_with("///") {
|
||||
continue;
|
||||
}
|
||||
|
||||
line.chars().take_while(|c| *c == ' ').count() / 4
|
||||
}
|
||||
None => 0,
|
||||
}
|
||||
let span = match item.delimiter {
|
||||
MacroDelimiter::Paren(b) => b.span,
|
||||
MacroDelimiter::Brace(b) => b.span,
|
||||
MacroDelimiter::Bracket(b) => b.span,
|
||||
};
|
||||
|
||||
let remaining = &contents[end - 1..];
|
||||
let bracket_end = find_bracket_end(remaining).unwrap();
|
||||
let sub_string = &contents[end..bracket_end + end - 1];
|
||||
last_bracket_end = bracket_end + end - 1;
|
||||
let mut formatted = String::new();
|
||||
|
||||
let mut new = fmt_block(sub_string, indent_level).unwrap();
|
||||
std::mem::swap(&mut formatted, &mut writer.out.buf);
|
||||
|
||||
if new.len() <= 80 && !new.contains('\n') {
|
||||
new = format!(" {new} ");
|
||||
let start = byte_offset(contents, span.start()) + 1;
|
||||
let end = byte_offset(contents, span.end()) - 1;
|
||||
|
||||
// if the new string is not multiline, don't try to adjust the marker ending
|
||||
// We want to trim off any indentation that there might be
|
||||
indent_level = 0;
|
||||
// Rustfmt will remove the space between the macro and the opening paren if the macro is a single expression
|
||||
let body_is_solo_expr = body.roots.len() == 1
|
||||
&& matches!(body.roots[0], BodyNode::RawExpr(_) | BodyNode::Text(_));
|
||||
|
||||
if formatted.len() <= 80 && !formatted.contains('\n') && !body_is_solo_expr {
|
||||
formatted = format!(" {formatted} ");
|
||||
}
|
||||
|
||||
let end_marker = end + bracket_end - indent_level * 4 - 1;
|
||||
end_span = span.end();
|
||||
|
||||
if new == contents[end..end_marker] {
|
||||
if contents[start..end] == formatted {
|
||||
continue;
|
||||
}
|
||||
|
||||
formatted_blocks.push(FormattedBlock {
|
||||
formatted: new,
|
||||
start: end,
|
||||
end: end_marker,
|
||||
formatted,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
}
|
||||
|
||||
formatted_blocks
|
||||
}
|
||||
|
||||
pub fn fmt_block(block: &str, indent_level: usize) -> Option<String> {
|
||||
let mut buf = Buffer {
|
||||
src: block.lines().map(|f| f.to_string()).collect(),
|
||||
indent: indent_level,
|
||||
..Buffer::default()
|
||||
};
|
||||
pub fn write_block_out(body: CallBody) -> Option<String> {
|
||||
let mut buf = Writer::new("");
|
||||
|
||||
let body = syn::parse_str::<dioxus_rsx::CallBody>(block).unwrap();
|
||||
write_body(&mut buf, &body);
|
||||
|
||||
buf.consume()
|
||||
}
|
||||
|
||||
fn write_body(buf: &mut Writer, body: &CallBody) {
|
||||
use std::fmt::Write;
|
||||
|
||||
// Oneliner optimization
|
||||
if buf.is_short_children(&body.roots).is_some() {
|
||||
buf.write_ident(&body.roots[0]).unwrap();
|
||||
// write all the indents with spaces and commas between
|
||||
for idx in 0..body.roots.len() - 1 {
|
||||
let ident = &body.roots[idx];
|
||||
buf.write_ident(ident).unwrap();
|
||||
write!(&mut buf.out.buf, ", ").unwrap();
|
||||
}
|
||||
|
||||
// write the last ident without a comma
|
||||
let ident = &body.roots[body.roots.len() - 1];
|
||||
buf.write_ident(ident).unwrap();
|
||||
} else {
|
||||
buf.write_body_indented(&body.roots).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fmt_block_from_expr(raw: &str, expr: ExprMacro) -> Option<String> {
|
||||
let body = syn::parse2::<CallBody>(expr.mac.tokens).unwrap();
|
||||
|
||||
let mut buf = Writer::new(raw);
|
||||
|
||||
write_body(&mut buf, &body);
|
||||
|
||||
buf.consume()
|
||||
}
|
||||
|
||||
pub fn fmt_block(block: &str, indent_level: usize) -> Option<String> {
|
||||
let body = syn::parse_str::<dioxus_rsx::CallBody>(block).unwrap();
|
||||
|
||||
let mut buf = Writer::new(block);
|
||||
|
||||
buf.out.indent = indent_level;
|
||||
|
||||
write_body(&mut buf, &body);
|
||||
|
||||
// writing idents leaves the final line ended at the end of the last ident
|
||||
if buf.buf.contains('\n') {
|
||||
buf.new_line().unwrap();
|
||||
if buf.out.buf.contains('\n') {
|
||||
buf.out.new_line().unwrap();
|
||||
}
|
||||
|
||||
buf.consume()
|
||||
@@ -124,8 +182,6 @@ pub fn apply_format(input: &str, block: FormattedBlock) -> String {
|
||||
let (left, _) = input.split_at(start);
|
||||
let (_, right) = input.split_at(end);
|
||||
|
||||
// dbg!(&block.formatted);
|
||||
|
||||
format!("{}{}{}", left, block.formatted, right)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
pub fn find_bracket_end(contents: &str) -> Option<usize> {
|
||||
let mut depth = 0;
|
||||
|
||||
for (i, c) in contents.chars().enumerate() {
|
||||
if c == '{' {
|
||||
depth += 1;
|
||||
} else if c == '}' {
|
||||
depth -= 1;
|
||||
|
||||
if depth == 0 {
|
||||
return Some(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
use dioxus_rsx::{BodyNode, ElementAttr, ElementAttrNamed, ForLoop};
|
||||
use proc_macro2::{LineColumn, Span};
|
||||
use quote::ToTokens;
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
fmt::{Result, Write},
|
||||
};
|
||||
use syn::{spanned::Spanned, Expr, ExprIf};
|
||||
|
||||
use crate::buffer::Buffer;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Writer<'a> {
|
||||
pub raw_src: &'a str,
|
||||
pub src: Vec<&'a str>,
|
||||
pub cached_formats: HashMap<Location, String>,
|
||||
pub comments: VecDeque<usize>,
|
||||
pub out: Buffer,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Hash, PartialEq, Eq, Debug)]
|
||||
pub struct Location {
|
||||
pub line: usize,
|
||||
pub col: usize,
|
||||
}
|
||||
impl Location {
|
||||
pub fn new(start: LineColumn) -> Self {
|
||||
Self {
|
||||
line: start.line,
|
||||
col: start.column,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Writer<'a> {
|
||||
pub fn new(raw_src: &'a str) -> Self {
|
||||
let src = raw_src.lines().collect();
|
||||
Self {
|
||||
raw_src,
|
||||
src,
|
||||
cached_formats: HashMap::new(),
|
||||
comments: VecDeque::new(),
|
||||
out: Buffer::default(),
|
||||
}
|
||||
}
|
||||
|
||||
// Expects to be written directly into place
|
||||
pub fn write_ident(&mut self, node: &BodyNode) -> Result {
|
||||
match node {
|
||||
BodyNode::Element(el) => self.write_element(el),
|
||||
BodyNode::Component(component) => self.write_component(component),
|
||||
BodyNode::Text(text) => self.out.write_text(text),
|
||||
BodyNode::RawExpr(exp) => self.write_raw_expr(exp.span()),
|
||||
BodyNode::ForLoop(forloop) => self.write_for_loop(forloop),
|
||||
BodyNode::IfChain(ifchain) => self.write_if_chain(ifchain),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn consume(self) -> Option<String> {
|
||||
Some(self.out.buf)
|
||||
}
|
||||
|
||||
pub fn write_comments(&mut self, child: Span) -> Result {
|
||||
// collect all comments upwards
|
||||
let start = child.start();
|
||||
let line_start = start.line - 1;
|
||||
|
||||
for (id, line) in self.src[..line_start].iter().enumerate().rev() {
|
||||
if line.trim().starts_with("//") || line.is_empty() {
|
||||
if id != 0 {
|
||||
self.comments.push_front(id);
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let mut last_was_empty = false;
|
||||
while let Some(comment_line) = self.comments.pop_front() {
|
||||
let line = &self.src[comment_line];
|
||||
if line.is_empty() {
|
||||
if !last_was_empty {
|
||||
self.out.new_line()?;
|
||||
}
|
||||
last_was_empty = true;
|
||||
} else {
|
||||
last_was_empty = false;
|
||||
self.out.tabbed_line()?;
|
||||
write!(self.out, "{}", self.src[comment_line].trim())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Push out the indent level and write each component, line by line
|
||||
pub fn write_body_indented(&mut self, children: &[BodyNode]) -> Result {
|
||||
self.out.indent += 1;
|
||||
|
||||
self.write_body_no_indent(children)?;
|
||||
|
||||
self.out.indent -= 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write_body_no_indent(&mut self, children: &[BodyNode]) -> Result {
|
||||
let last_child = children.len();
|
||||
let iter = children.iter().peekable().enumerate();
|
||||
|
||||
for (idx, child) in iter {
|
||||
if self.current_span_is_primary(child.span()) {
|
||||
self.write_comments(child.span())?;
|
||||
}
|
||||
|
||||
match child {
|
||||
// check if the expr is a short
|
||||
BodyNode::RawExpr { .. } => {
|
||||
self.out.tabbed_line()?;
|
||||
self.write_ident(child)?;
|
||||
if idx != last_child - 1 {
|
||||
write!(self.out, ",")?;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
self.out.tabbed_line()?;
|
||||
self.write_ident(child)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn is_short_attrs(&mut self, attributes: &[ElementAttrNamed]) -> usize {
|
||||
let mut total = 0;
|
||||
|
||||
for attr in attributes {
|
||||
if self.current_span_is_primary(attr.attr.start()) {
|
||||
'line: for line in self.src[..attr.attr.start().start().line - 1].iter().rev() {
|
||||
match (line.trim().starts_with("//"), line.is_empty()) {
|
||||
(true, _) => return 100000,
|
||||
(_, true) => continue 'line,
|
||||
_ => break 'line,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
total += match &attr.attr {
|
||||
ElementAttr::AttrText { value, name } => {
|
||||
value.source.as_ref().unwrap().value().len() + name.span().line_length() + 6
|
||||
}
|
||||
ElementAttr::AttrExpression { name, value } => {
|
||||
value.span().line_length() + name.span().line_length() + 6
|
||||
}
|
||||
ElementAttr::CustomAttrText { value, name } => {
|
||||
value.source.as_ref().unwrap().value().len() + name.value().len() + 6
|
||||
}
|
||||
ElementAttr::CustomAttrExpression { name, value } => {
|
||||
name.value().len() + value.span().line_length() + 6
|
||||
}
|
||||
ElementAttr::EventTokens { tokens, name } => {
|
||||
let location = Location::new(tokens.span().start());
|
||||
|
||||
let len = if let std::collections::hash_map::Entry::Vacant(e) =
|
||||
self.cached_formats.entry(location)
|
||||
{
|
||||
let formatted = prettyplease::unparse_expr(tokens);
|
||||
let len = if formatted.contains('\n') {
|
||||
10000
|
||||
} else {
|
||||
formatted.len()
|
||||
};
|
||||
e.insert(formatted);
|
||||
len
|
||||
} else {
|
||||
self.cached_formats[&location].len()
|
||||
};
|
||||
|
||||
len + name.span().line_length() + 6
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
total
|
||||
}
|
||||
|
||||
pub fn retrieve_formatted_expr(&mut self, expr: &Expr) -> &str {
|
||||
self.cached_formats
|
||||
.entry(Location::new(expr.span().start()))
|
||||
.or_insert_with(|| prettyplease::unparse_expr(expr))
|
||||
.as_str()
|
||||
}
|
||||
|
||||
fn write_for_loop(&mut self, forloop: &ForLoop) -> std::fmt::Result {
|
||||
write!(
|
||||
self.out,
|
||||
"for {} in {} {{",
|
||||
forloop.pat.clone().into_token_stream(),
|
||||
prettyplease::unparse_expr(&forloop.expr)
|
||||
)?;
|
||||
|
||||
if forloop.body.is_empty() {
|
||||
write!(self.out, "}}")?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.write_body_indented(&forloop.body)?;
|
||||
|
||||
self.out.tabbed_line()?;
|
||||
write!(self.out, "}}")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_if_chain(&mut self, ifchain: &ExprIf) -> std::fmt::Result {
|
||||
self.write_raw_expr(ifchain.span())
|
||||
}
|
||||
}
|
||||
|
||||
trait SpanLength {
|
||||
fn line_length(&self) -> usize;
|
||||
}
|
||||
impl SpanLength for Span {
|
||||
fn line_length(&self) -> usize {
|
||||
self.end().line - self.start().line
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,44 @@
|
||||
macro_rules! twoway {
|
||||
($val:literal => $name:ident) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let src = include_str!(concat!("./samples/", $val, ".rsx"));
|
||||
let formatted = dioxus_autofmt::fmt_file(src);
|
||||
let out = dioxus_autofmt::apply_formats(src, formatted);
|
||||
pretty_assertions::assert_eq!(&src, &out);
|
||||
}
|
||||
(
|
||||
$(
|
||||
|
||||
// doc attrs
|
||||
$( #[doc = $doc:expr] )*
|
||||
$name:ident
|
||||
),*
|
||||
) => {
|
||||
$(
|
||||
$( #[doc = $doc] )*
|
||||
#[test]
|
||||
fn $name() {
|
||||
let src = include_str!(concat!("./samples/", stringify!($name), ".rsx"));
|
||||
let formatted = dioxus_autofmt::fmt_file(src);
|
||||
let out = dioxus_autofmt::apply_formats(src, formatted);
|
||||
pretty_assertions::assert_eq!(&src, &out);
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
twoway! ("simple" => simple);
|
||||
|
||||
twoway! ("comments" => comments);
|
||||
|
||||
twoway! ("attributes" => attributes);
|
||||
|
||||
twoway! ("manual_props" => manual_props);
|
||||
|
||||
twoway! ("complex" => complex);
|
||||
|
||||
twoway! ("tiny" => tiny);
|
||||
|
||||
twoway! ("tinynoopt" => tinynoopt);
|
||||
|
||||
twoway! ("long" => long);
|
||||
|
||||
twoway! ("key" => key);
|
||||
|
||||
// Disabled because we can't handle comments on exprs yet
|
||||
twoway! ("multirsx" => multirsx);
|
||||
twoway![
|
||||
simple,
|
||||
comments,
|
||||
attributes,
|
||||
manual_props,
|
||||
complex,
|
||||
tiny,
|
||||
tinynoopt,
|
||||
long,
|
||||
key,
|
||||
multirsx,
|
||||
commentshard,
|
||||
emoji,
|
||||
messy_indent,
|
||||
long_exprs,
|
||||
ifchain_forloop,
|
||||
t2,
|
||||
reallylong,
|
||||
immediate_expr,
|
||||
collapse_expr,
|
||||
trailing_expr
|
||||
];
|
||||
@@ -45,6 +45,15 @@ rsx! {
|
||||
a: "123"
|
||||
}
|
||||
|
||||
// Short attributes
|
||||
div { a: "123", a: "123", a: "123", a: "123", a: "123", a: "123", a: "123", a: "123", a: "123" }
|
||||
div {
|
||||
a: "123",
|
||||
a: "123",
|
||||
a: "123",
|
||||
a: "123",
|
||||
a: "123",
|
||||
a: "123",
|
||||
a: "123",
|
||||
a: "123",
|
||||
a: "123"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
fn itworks() {
|
||||
rsx!( "{name}", "{name}", "{name}" )
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
rsx! {
|
||||
// Comments
|
||||
div {
|
||||
// Comments
|
||||
class: "asdasd",
|
||||
|
||||
// Comments
|
||||
"hello world"
|
||||
|
||||
// Comments
|
||||
expr1,
|
||||
|
||||
// Comments
|
||||
expr2,
|
||||
|
||||
// Comments
|
||||
// Comments
|
||||
// Comments
|
||||
// Comments
|
||||
// Comments
|
||||
expr3,
|
||||
|
||||
div {
|
||||
// todo some work in here
|
||||
}
|
||||
|
||||
div {
|
||||
// todo some work in here
|
||||
// todo some work in here
|
||||
//
|
||||
// todo some work in here
|
||||
}
|
||||
|
||||
div {
|
||||
// todo some work in here
|
||||
class: "hello world",
|
||||
|
||||
// todo some work in here
|
||||
class: "hello world"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
rsx! {
|
||||
div { class: "asdasdasd asdasdasd asdasdasd asdasdasd asdasdasd asdasdasd asdasdasd asdasdasd asdasdasd",
|
||||
section { "🦀🦀🦀🦀🦀🦀🦀🦀🦀🦀" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
rsx! {
|
||||
// Does this work?
|
||||
for i in b {
|
||||
// Hey it works?
|
||||
div {}
|
||||
}
|
||||
|
||||
// Some ifchain
|
||||
if a > 10 {
|
||||
//
|
||||
rsx! { div {} }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
fn it_works() {
|
||||
cx.render(rsx!(()))
|
||||
}
|
||||
|
||||
@@ -33,6 +33,9 @@ pub fn Explainer<'a>(
|
||||
}
|
||||
|
||||
cx.render(rsx! {
|
||||
div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", left, right }
|
||||
div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light",
|
||||
left,
|
||||
right
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
rsx! {
|
||||
div {
|
||||
div {
|
||||
div {
|
||||
div {
|
||||
section { class: "body-font overflow-hidden dark:bg-ideblack",
|
||||
div { class: "container px-6 mx-auto",
|
||||
div { class: "-my-8 divide-y-2 divide-gray-100",
|
||||
POSTS.iter().enumerate().map(|(id, post)| rsx! { BlogPostItem { post: post, id: id } })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
fn SaveClipboard(cx: Scope) -> Element {
|
||||
rsx! {
|
||||
div { class: "relative w-1/2 {align} max-w-md leading-8",
|
||||
h2 { class: "mb-6 text-3xl leading-tight md:text-4xl md:leading-tight lg:text-3xl lg:leading-tight font-heading font-mono font-bold",
|
||||
"{title}"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
cx.render(rsx! {
|
||||
div { "hello world", "hello world", "hello world" }
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
pub static Icon3: Component<()> = |cx| {
|
||||
cx.render(rsx! {
|
||||
svg {
|
||||
class: "w-6 h-6",
|
||||
stroke_linecap: "round",
|
||||
fill: "none",
|
||||
stroke_linejoin: "round",
|
||||
stroke_width: "2",
|
||||
stroke: "currentColor",
|
||||
view_box: "0 0 24 24",
|
||||
path { d: "M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2" }
|
||||
circle { cx: "12", cy: "7", r: "4" }
|
||||
}
|
||||
})
|
||||
};
|
||||
@@ -37,7 +37,11 @@ rsx! {
|
||||
|
||||
// One level compression
|
||||
div {
|
||||
a { class: "py-2 px-3 bg-indigo-500 hover:bg-indigo-600 rounded text-xs text-white", href: "#", "Send invitation" }
|
||||
a {
|
||||
class: "py-2 px-3 bg-indigo-500 hover:bg-indigo-600 rounded text-xs text-white",
|
||||
href: "#",
|
||||
"Send invitation"
|
||||
}
|
||||
}
|
||||
|
||||
// Components
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
rsx! {
|
||||
div {}
|
||||
div {
|
||||
// div {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,10 @@
|
||||
rsx! { div {} }
|
||||
fn ItWorks() {
|
||||
rsx! {
|
||||
div {
|
||||
div {
|
||||
div {}
|
||||
div {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fn it_works() {
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
span { "Description: ", package.description.as_deref().unwrap_or("❌❌❌❌ missing") }
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,3 +1,8 @@
|
||||
cx.render(rsx! {
|
||||
div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", left, right }
|
||||
})
|
||||
fn ItWroks() {
|
||||
cx.render(rsx! {
|
||||
div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light",
|
||||
left,
|
||||
right
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
cx.render(rsx! {
|
||||
div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", left, right }
|
||||
})
|
||||
fn ItWroks() {
|
||||
cx.render(rsx! {
|
||||
div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", left, right }
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "dioxus-core-macro"
|
||||
version = "0.2.1"
|
||||
version = "0.3.0"
|
||||
authors = ["Jonathan Kelley"]
|
||||
edition = "2021"
|
||||
description = "Core macro for Dioxus Virtual DOM"
|
||||
@@ -8,7 +8,7 @@ license = "MIT/Apache-2.0"
|
||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||
homepage = "https://dioxuslabs.com"
|
||||
documentation = "https://dioxuslabs.com"
|
||||
keywords = ["dom", "ui", "gui", "react", "wasm"]
|
||||
keywords = ["dom", "ui", "gui", "react"]
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
@@ -18,7 +18,7 @@ proc-macro = true
|
||||
proc-macro2 = { version = "1.0" }
|
||||
quote = "1.0"
|
||||
syn = { version = "1.0", features = ["full", "extra-traits"] }
|
||||
dioxus-rsx = { path = "../rsx", version = "0.0.1" }
|
||||
dioxus-rsx = { path = "../rsx", version = "^0.0.3" }
|
||||
|
||||
# testing
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -1,7 +1,48 @@
|
||||
# core-macro
|
||||
# dioxus-core-macro
|
||||
|
||||
This crate implements these macros:
|
||||
- `format_args_f`: for f-string interpolation inside of text blocks
|
||||
- `Props`: derive macro for typed-builder with props configurations
|
||||
-
|
||||
-
|
||||
|
||||
[![Crates.io][crates-badge]][crates-url]
|
||||
[![MIT licensed][mit-badge]][mit-url]
|
||||
[![Build Status][actions-badge]][actions-url]
|
||||
[![Discord chat][discord-badge]][discord-url]
|
||||
|
||||
[crates-badge]: https://img.shields.io/crates/v/dioxus-core-macro.svg
|
||||
[crates-url]: https://crates.io/crates/dioxus-core-macro
|
||||
|
||||
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
|
||||
|
||||
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
|
||||
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
|
||||
|
||||
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
|
||||
[discord-url]: https://discord.gg/XgGxMSkvUM
|
||||
|
||||
[Website](https://dioxuslabs.com) |
|
||||
[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) |
|
||||
[API Docs](https://docs.rs/dioxus-core-macro/latest/dioxus_core_macro) |
|
||||
[Chat](https://discord.gg/XgGxMSkvUM)
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
`dioxus-core-macro` provides a handful of helpful macros used by the `dioxus` crate. These include:
|
||||
|
||||
- The `rsx!` macro that underpins templates and node creation
|
||||
- The `inline_props` macro transforms function arguments into an auto-derived struct
|
||||
- The `format_args_f` macro which allows f-string formatting with support for expressions
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
|
||||
- Join the discord and ask questions!
|
||||
|
||||
## License
|
||||
This project is licensed under the [MIT license].
|
||||
|
||||
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in Dioxus by you shall be licensed as MIT without any additional
|
||||
terms or conditions.
|
||||
@@ -95,7 +95,7 @@ impl ToTokens for InlinePropsBody {
|
||||
quote! { #vis #f }
|
||||
});
|
||||
|
||||
let struct_name = Ident::new(&format!("{}Props", ident), Span::call_site());
|
||||
let struct_name = Ident::new(&format!("{ident}Props"), Span::call_site());
|
||||
|
||||
let field_names = inputs.iter().filter_map(|f| match f {
|
||||
FnArg::Receiver(_) => todo!(),
|
||||
@@ -145,6 +145,25 @@ impl ToTokens for InlinePropsBody {
|
||||
(quote! { #lifetime, }, fn_generics, quote! { #generics })
|
||||
};
|
||||
|
||||
let generics_no_bounds = {
|
||||
let mut generics = generics.clone();
|
||||
generics.params = generics
|
||||
.params
|
||||
.iter()
|
||||
.map(|it| match it {
|
||||
GenericParam::Type(tp) => {
|
||||
let mut tp = tp.clone();
|
||||
tp.bounds.clear();
|
||||
|
||||
GenericParam::Type(tp)
|
||||
}
|
||||
_ => it.clone(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
generics
|
||||
};
|
||||
|
||||
out_tokens.append_all(quote! {
|
||||
#modifiers
|
||||
#[allow(non_camel_case_types)]
|
||||
@@ -155,10 +174,10 @@ impl ToTokens for InlinePropsBody {
|
||||
}
|
||||
|
||||
#(#attrs)*
|
||||
#maybe_async #vis fn #ident #fn_generics (#cx_token: Scope<#scope_lifetime #struct_name #generics>) #output
|
||||
#maybe_async #vis fn #ident #fn_generics (#cx_token: Scope<#scope_lifetime #struct_name #generics_no_bounds>) #output
|
||||
#where_clause
|
||||
{
|
||||
let #struct_name { #(#field_names),* } = &cx.props;
|
||||
let #struct_name { #(#field_names),* } = &#cx_token.props;
|
||||
#block
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use proc_macro::TokenStream;
|
||||
use quote::ToTokens;
|
||||
use rsx::RenderCallBody;
|
||||
use syn::parse_macro_input;
|
||||
|
||||
mod inlineprops;
|
||||
@@ -41,10 +42,7 @@ pub fn rsx(s: TokenStream) -> TokenStream {
|
||||
pub fn render(s: TokenStream) -> TokenStream {
|
||||
match syn::parse::<rsx::CallBody>(s) {
|
||||
Err(err) => err.to_compile_error().into(),
|
||||
Ok(mut body) => {
|
||||
body.inline_cx = true;
|
||||
body.into_token_stream().into()
|
||||
}
|
||||
Ok(body) => RenderCallBody(body).into_token_stream().into(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -323,7 +323,7 @@ mod field_info {
|
||||
let tokenized_code = TokenStream::from_str(&code.value())?;
|
||||
self.default = Some(
|
||||
syn::parse(tokenized_code.into())
|
||||
.map_err(|e| Error::new_spanned(code, format!("{}", e)))?,
|
||||
.map_err(|e| Error::new_spanned(code, format!("{e}")))?,
|
||||
);
|
||||
} else {
|
||||
return Err(Error::new_spanned(assign.right, "Expected string"));
|
||||
@@ -332,7 +332,7 @@ mod field_info {
|
||||
}
|
||||
_ => Err(Error::new_spanned(
|
||||
&assign,
|
||||
format!("Unknown parameter {:?}", name),
|
||||
format!("Unknown parameter {name:?}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -503,11 +503,11 @@ mod struct_info {
|
||||
builder_attr,
|
||||
builder_name: syn::Ident::new(&builder_name, proc_macro2::Span::call_site()),
|
||||
conversion_helper_trait_name: syn::Ident::new(
|
||||
&format!("{}_Optional", builder_name),
|
||||
&format!("{builder_name}_Optional"),
|
||||
proc_macro2::Span::call_site(),
|
||||
),
|
||||
core: syn::Ident::new(
|
||||
&format!("{}_core", builder_name),
|
||||
&format!("{builder_name}_core"),
|
||||
proc_macro2::Span::call_site(),
|
||||
),
|
||||
})
|
||||
@@ -594,7 +594,6 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||
None => {
|
||||
let doc = format!(
|
||||
"Builder for [`{name}`] instances.\n\nSee [`{name}::builder()`] for more info.",
|
||||
name = name
|
||||
);
|
||||
quote!(#[doc = #doc])
|
||||
}
|
||||
@@ -709,9 +708,9 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||
});
|
||||
let reconstructing = self.included_fields().map(|f| f.name);
|
||||
|
||||
let &FieldInfo {
|
||||
name: ref field_name,
|
||||
ty: ref field_type,
|
||||
let FieldInfo {
|
||||
name: field_name,
|
||||
ty: field_type,
|
||||
..
|
||||
} = field;
|
||||
let mut ty_generics: Vec<syn::GenericArgument> = self
|
||||
@@ -810,7 +809,7 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||
),
|
||||
proc_macro2::Span::call_site(),
|
||||
);
|
||||
let repeated_fields_error_message = format!("Repeated field {}", field_name);
|
||||
let repeated_fields_error_message = format!("Repeated field {field_name}");
|
||||
|
||||
Ok(quote! {
|
||||
#[allow(dead_code, non_camel_case_types, missing_docs)]
|
||||
@@ -929,7 +928,7 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||
),
|
||||
proc_macro2::Span::call_site(),
|
||||
);
|
||||
let early_build_error_message = format!("Missing required field {}", field_name);
|
||||
let early_build_error_message = format!("Missing required field {field_name}");
|
||||
|
||||
Ok(quote! {
|
||||
#[doc(hidden)]
|
||||
@@ -1037,7 +1036,7 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||
// I’d prefer “a” or “an” to “its”, but determining which is grammatically
|
||||
// correct is roughly impossible.
|
||||
let doc =
|
||||
format!("Finalise the builder and create its [`{}`] instance", name);
|
||||
format!("Finalise the builder and create its [`{name}`] instance");
|
||||
quote!(#[doc = #doc])
|
||||
}
|
||||
}
|
||||
@@ -1132,7 +1131,7 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||
}
|
||||
_ => Err(Error::new_spanned(
|
||||
&assign,
|
||||
format!("Unknown parameter {:?}", name),
|
||||
format!("Unknown parameter {name:?}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -1146,7 +1145,7 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||
}
|
||||
_ => Err(Error::new_spanned(
|
||||
&path,
|
||||
format!("Unknown parameter {:?}", name),
|
||||
format!("Unknown parameter {name:?}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -1161,7 +1160,7 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||
let call_func = quote!(#call_func);
|
||||
Error::new_spanned(
|
||||
&call.func,
|
||||
format!("Illegal builder setting group {}", call_func),
|
||||
format!("Illegal builder setting group {call_func}"),
|
||||
)
|
||||
})?;
|
||||
match subsetting_name.as_str() {
|
||||
@@ -1173,7 +1172,7 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||
}
|
||||
_ => Err(Error::new_spanned(
|
||||
&call.func,
|
||||
format!("Illegal builder setting group name {}", subsetting_name),
|
||||
format!("Illegal builder setting group name {subsetting_name}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@ fn formatting_compiles() {
|
||||
// escape sequences work
|
||||
assert_eq!(
|
||||
format_args_f!("{x:?} {{}}}}").to_string(),
|
||||
format!("{:?} {{}}}}", x)
|
||||
format!("{x:?} {{}}}}")
|
||||
);
|
||||
assert_eq!(
|
||||
format_args_f!("{{{{}} {x:?}").to_string(),
|
||||
format!("{{{{}} {:?}", x)
|
||||
format!("{{{{}} {x:?}")
|
||||
);
|
||||
|
||||
// paths in formating works
|
||||
@@ -27,6 +27,6 @@ fn formatting_compiles() {
|
||||
// allows duplicate format args
|
||||
assert_eq!(
|
||||
format_args_f!("{x:?} {x:?}").to_string(),
|
||||
format!("{:?} {:?}", x, x)
|
||||
format!("{x:?} {x:?}")
|
||||
);
|
||||
}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"rust-analyzer.checkOnSave.allTargets": false
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "dioxus-core"
|
||||
version = "0.2.1"
|
||||
version = "0.3.2"
|
||||
authors = ["Jonathan Kelley"]
|
||||
edition = "2018"
|
||||
description = "Core functionality for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences"
|
||||
@@ -8,7 +8,7 @@ license = "MIT/Apache-2.0"
|
||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||
homepage = "https://dioxuslabs.com"
|
||||
documentation = "https://dioxuslabs.com"
|
||||
keywords = ["dom", "ui", "gui", "react", "wasm"]
|
||||
keywords = ["dom", "ui", "gui", "react"]
|
||||
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
@@ -23,7 +23,9 @@ rustc-hash = "1.1.0"
|
||||
# Used in diffing
|
||||
longest-increasing-subsequence = "0.1.0"
|
||||
|
||||
futures-util = { version = "0.3", default-features = false }
|
||||
futures-util = { version = "0.3", default-features = false, features = [
|
||||
"alloc",
|
||||
] }
|
||||
|
||||
slab = "0.4"
|
||||
|
||||
@@ -37,10 +39,13 @@ log = "0.4.17"
|
||||
# Serialize the Edits for use in Webview/Liveview instances
|
||||
serde = { version = "1", features = ["derive"], optional = true }
|
||||
|
||||
bumpslab = { version = "0.2.0" }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "*", features = ["full"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
dioxus = { path = "../dioxus" }
|
||||
pretty_assertions = "1.3.0"
|
||||
rand = "0.8.5"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
+37
-26
@@ -13,7 +13,7 @@ dioxus-core is a fast and featureful VirtualDom implementation written in and fo
|
||||
- Error boundaries through the `anyhow` crate
|
||||
- Customizable memoization
|
||||
|
||||
If just starting out, check out the Guides first.
|
||||
If you are just starting, check out the Guides first.
|
||||
|
||||
# General Theory
|
||||
|
||||
@@ -21,7 +21,7 @@ The dioxus-core `VirtualDom` object is built around the concept of a `Template`.
|
||||
|
||||
Each component in the VirtualDom works as a dedicated render loop where re-renders are triggered by events external to the VirtualDom, or from the components themselves.
|
||||
|
||||
When each component re-renders, it must return an `Element`. In Dioxus, the `Element` type is an alias for `Result<VNode>`. Between two renders, Dioxus compares the inner `VNode` object, and calculates the differences of the dynamic portions of each internal `Template`. If any attributes or elements are different between the old layout and new layout, Dioxus will write modifications to the `Mutations` object.
|
||||
When each component re-renders, it must return an `Element`. In Dioxus, the `Element` type is an alias for `Result<VNode>`. Between two renders, Dioxus compares the inner `VNode` object and calculates the differences between the dynamic portions of each internal `Template`. If any attributes or elements are different between the old layout and the new layout, Dioxus will write modifications to the `Mutations` object.
|
||||
|
||||
Dioxus expects the target renderer to save its nodes in a list. Each element is given a numerical ID which can be used to directly index into that list for O(1) lookups.
|
||||
|
||||
@@ -33,37 +33,48 @@ The `dioxus` crate exports the `rsx` macro which transforms a helpful, simpler s
|
||||
|
||||
First, start with your app:
|
||||
|
||||
```rust, ignore
|
||||
```rust
|
||||
# use dioxus::core::Mutations;
|
||||
use dioxus::prelude::*;
|
||||
|
||||
// First, declare a root component
|
||||
fn app(cx: Scope) -> Element {
|
||||
cx.render(rsx!( div { "hello world" } ))
|
||||
cx.render(rsx!{
|
||||
div { "hello world" }
|
||||
})
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Next, create a new VirtualDom using this app as the root component.
|
||||
let mut dom = VirtualDom::new(app);
|
||||
|
||||
// The initial render of the dom will generate a stream of edits for the real dom to apply
|
||||
let mutations = dom.rebuild();
|
||||
|
||||
// Somehow, you can apply these edits to the real dom
|
||||
apply_edits_to_real_dom(mutations);
|
||||
}
|
||||
|
||||
# fn apply_edits_to_real_dom(mutations: Mutations) {}
|
||||
```
|
||||
|
||||
Then, we'll want to create a new VirtualDom using this app as the root component.
|
||||
|
||||
```rust, ignore
|
||||
let mut dom = VirtualDom::new(app);
|
||||
```
|
||||
|
||||
To build the app into a stream of mutations, we'll use [`VirtualDom::rebuild`]:
|
||||
|
||||
```rust, ignore
|
||||
let mutations = dom.rebuild();
|
||||
|
||||
apply_edits_to_real_dom(mutations);
|
||||
```
|
||||
|
||||
We can then wait for any asynchronous components or pending futures using the `wait_for_work()` method. If we have a deadline, then we can use render_with_deadline instead:
|
||||
```rust
|
||||
# #![allow(unused)]
|
||||
# use dioxus::prelude::*;
|
||||
|
||||
```rust, ignore
|
||||
# use std::time::Duration;
|
||||
# async fn wait(mut dom: VirtualDom) {
|
||||
// Wait for the dom to be marked dirty internally
|
||||
dom.wait_for_work().await;
|
||||
|
||||
// Or wait for a deadline and then collect edits
|
||||
dom.render_with_deadline(tokio::time::sleep(Duration::from_millis(16)));
|
||||
let mutations = 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.
|
||||
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, ignore
|
||||
loop {
|
||||
@@ -88,18 +99,18 @@ Dioxus-core builds off the many frameworks that came before it. Notably, Dioxus
|
||||
- React: hooks, concurrency, suspense
|
||||
- Dodrio: bump allocation, double buffering, and some diffing architecture
|
||||
|
||||
Dioxus-core leverages some really cool techniques and hits a very high level of parity with mature frameworks. However, Dioxus also brings some new unique features:
|
||||
Dioxus-core hits a very high level of parity with mature frameworks. However, Dioxus also brings some new unique features:
|
||||
|
||||
- managed lifetimes for borrowed data
|
||||
- placeholder approach for suspended vnodes
|
||||
- fiber/interruptible diffing algorithm
|
||||
- custom memory allocator for vnodes and all text content
|
||||
- custom memory allocator for VNodes and all text content
|
||||
- support for fragments w/ lazy normalization
|
||||
- slab allocator for scopes
|
||||
- mirrored-slab approach for remote vdoms
|
||||
- mirrored-slab approach for remote VirtualDoms
|
||||
- dedicated subtrees for rendering into separate contexts from the same app
|
||||
|
||||
There's certainly more to the story, but these optimizations make Dioxus memory use and allocation count extremely minimal. For an average application, it is possible that zero allocations will need to be performed once the app has been loaded. Only when new components are added to the dom will allocations occur. For a given component, the space of old VNodes is dynamically recycled as new nodes are added. Additionally, Dioxus tracks the average memory footprint of previous components to estimate how much memory allocate for future components.
|
||||
There's certainly more to the story, but these optimizations make Dioxus memory use and allocation count extremely minimal. For an average application, no allocations may be needed once the app has been loaded. Only when new components are added to the dom will allocations occur. For a given component, the space of old VNodes is dynamically recycled as new nodes are added. Additionally, Dioxus tracks the average memory footprint of previous components to estimate how much memory allocate for future components.
|
||||
|
||||
All in all, Dioxus treats memory as a valuable resource. Combined with the memory-efficient footprint of Wasm compilation, Dioxus apps can scale to thousands of components and still stay snappy.
|
||||
|
||||
@@ -112,5 +123,5 @@ The final implementation of Dioxus must:
|
||||
- Be concurrent. Components should be able to pause rendering to let the screen paint the next frame.
|
||||
- Be disconnected from a specific renderer (no WebSys dependency in the core crate).
|
||||
- Support server-side-rendering (SSR). VNodes should render to a string that can be served via a web server.
|
||||
- Be "live". Components should be able to be both server rendered and client rendered without needing frontend APIs.
|
||||
- Be modular. Components and hooks should be work anywhere without worrying about target platform.
|
||||
- Be "live". Components should be able to be both server-rendered and client rendered without needing frontend APIs.
|
||||
- Be modular. Components and hooks should work anywhere without worrying about the target platform.
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::marker::PhantomData;
|
||||
use std::{marker::PhantomData, panic::AssertUnwindSafe};
|
||||
|
||||
use crate::{
|
||||
innerlude::Scoped,
|
||||
@@ -62,12 +62,19 @@ where
|
||||
}
|
||||
|
||||
fn render(&'a self, cx: &'a ScopeState) -> RenderReturn<'a> {
|
||||
let scope: &mut Scoped<P> = cx.bump().alloc(Scoped {
|
||||
props: &self.props,
|
||||
scope: cx,
|
||||
});
|
||||
let res = std::panic::catch_unwind(AssertUnwindSafe(move || {
|
||||
// Call the render function directly
|
||||
let scope: &mut Scoped<P> = cx.bump().alloc(Scoped {
|
||||
props: &self.props,
|
||||
scope: cx,
|
||||
});
|
||||
|
||||
// Call the render function directly
|
||||
(self.render_fn)(scope).into_return(cx)
|
||||
(self.render_fn)(scope).into_return(cx)
|
||||
}));
|
||||
|
||||
match res {
|
||||
Ok(e) => e,
|
||||
Err(_) => RenderReturn::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
+54
-45
@@ -1,6 +1,8 @@
|
||||
use std::ptr::NonNull;
|
||||
|
||||
use crate::{
|
||||
nodes::RenderReturn, nodes::VNode, virtual_dom::VirtualDom, AttributeValue, DynamicNode,
|
||||
ScopeId,
|
||||
innerlude::DirtyScope, nodes::RenderReturn, nodes::VNode, virtual_dom::VirtualDom,
|
||||
AttributeValue, DynamicNode, ScopeId,
|
||||
};
|
||||
use bumpalo::boxed::Box as BumpBox;
|
||||
|
||||
@@ -17,7 +19,7 @@ pub(crate) struct ElementRef {
|
||||
pub path: ElementPath,
|
||||
|
||||
// The actual template
|
||||
pub template: *const VNode<'static>,
|
||||
pub template: Option<NonNull<VNode<'static>>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -27,9 +29,9 @@ pub enum ElementPath {
|
||||
}
|
||||
|
||||
impl ElementRef {
|
||||
pub(crate) fn null() -> Self {
|
||||
pub(crate) fn none() -> Self {
|
||||
Self {
|
||||
template: std::ptr::null_mut(),
|
||||
template: None,
|
||||
path: ElementPath::Root(0),
|
||||
}
|
||||
}
|
||||
@@ -44,12 +46,21 @@ impl VirtualDom {
|
||||
self.next_reference(template, ElementPath::Root(path))
|
||||
}
|
||||
|
||||
pub(crate) fn next_null(&mut self) -> ElementId {
|
||||
let entry = self.elements.vacant_entry();
|
||||
let id = entry.key();
|
||||
|
||||
entry.insert(ElementRef::none());
|
||||
ElementId(id)
|
||||
}
|
||||
|
||||
fn next_reference(&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 _,
|
||||
// We know this is non-null because it comes from a reference
|
||||
template: Some(unsafe { NonNull::new_unchecked(template as *const _ as *mut _) }),
|
||||
path,
|
||||
});
|
||||
ElementId(id)
|
||||
@@ -77,67 +88,59 @@ impl VirtualDom {
|
||||
}
|
||||
|
||||
// Drop a scope and all its children
|
||||
pub(crate) fn drop_scope(&mut self, id: ScopeId) {
|
||||
//
|
||||
// Note: This will not remove any ids from the arena
|
||||
pub(crate) fn drop_scope(&mut self, id: ScopeId, recursive: bool) {
|
||||
self.dirty_scopes.remove(&DirtyScope {
|
||||
height: self.scopes[id].height,
|
||||
id,
|
||||
});
|
||||
|
||||
self.ensure_drop_safety(id);
|
||||
|
||||
if let Some(root) = self.scopes[id.0].as_ref().try_root_node() {
|
||||
if let RenderReturn::Sync(Some(node)) = unsafe { root.extend_lifetime_ref() } {
|
||||
self.drop_scope_inner(node)
|
||||
}
|
||||
}
|
||||
if let Some(root) = unsafe { self.scopes[id.0].as_ref().previous_frame().try_load_node() } {
|
||||
if let RenderReturn::Sync(Some(node)) = unsafe { root.extend_lifetime_ref() } {
|
||||
self.drop_scope_inner(node)
|
||||
if recursive {
|
||||
if let Some(root) = self.scopes[id].try_root_node() {
|
||||
if let RenderReturn::Ready(node) = unsafe { root.extend_lifetime_ref() } {
|
||||
self.drop_scope_inner(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.scopes[id.0].props.take();
|
||||
|
||||
let scope = &mut self.scopes[id.0];
|
||||
let scope = &mut self.scopes[id];
|
||||
|
||||
// Drop all the hooks once the children are dropped
|
||||
// this means we'll drop hooks bottom-up
|
||||
for hook in scope.hook_list.get_mut().drain(..) {
|
||||
drop(unsafe { BumpBox::from_raw(hook) });
|
||||
}
|
||||
|
||||
// Drop all the futures once the hooks are dropped
|
||||
for task_id in scope.spawned_tasks.borrow_mut().drain() {
|
||||
scope.tasks.remove(task_id);
|
||||
}
|
||||
|
||||
self.scopes.remove(id);
|
||||
}
|
||||
|
||||
fn drop_scope_inner(&mut self, node: &VNode) {
|
||||
node.clear_listeners();
|
||||
node.dynamic_nodes.iter().for_each(|node| match node {
|
||||
DynamicNode::Component(c) => {
|
||||
if let Some(f) = c.scope.get() {
|
||||
self.drop_scope(f);
|
||||
self.drop_scope(f, true);
|
||||
}
|
||||
c.props.take();
|
||||
}
|
||||
DynamicNode::Fragment(nodes) => {
|
||||
nodes.iter().for_each(|node| self.drop_scope_inner(node))
|
||||
}
|
||||
DynamicNode::Placeholder(t) => {
|
||||
if let Some(id) = t.id.get() {
|
||||
self.try_reclaim(id);
|
||||
}
|
||||
}
|
||||
DynamicNode::Text(t) => {
|
||||
if let Some(id) = t.id.get() {
|
||||
self.try_reclaim(id);
|
||||
}
|
||||
}
|
||||
DynamicNode::Placeholder(_) => {}
|
||||
DynamicNode::Text(_) => {}
|
||||
});
|
||||
|
||||
for root in node.root_ids {
|
||||
if let Some(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_id: ScopeId) {
|
||||
let scope = &self.scopes[scope_id.0];
|
||||
let scope = &self.scopes[scope_id];
|
||||
|
||||
// make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we
|
||||
// run the hooks (which hold an &mut Reference)
|
||||
@@ -155,21 +158,27 @@ impl VirtualDom {
|
||||
});
|
||||
|
||||
// Now that all the references are gone, we can safely drop our own references in our listeners.
|
||||
let mut listeners = scope.listeners.borrow_mut();
|
||||
let mut listeners = scope.attributes_to_drop.borrow_mut();
|
||||
listeners.drain(..).for_each(|listener| {
|
||||
let listener = unsafe { &*listener };
|
||||
if let AttributeValue::Listener(l) = &listener.value {
|
||||
_ = l.take();
|
||||
match &listener.value {
|
||||
AttributeValue::Listener(l) => {
|
||||
_ = l.take();
|
||||
}
|
||||
AttributeValue::Any(a) => {
|
||||
_ = a.take();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ElementPath {
|
||||
pub(crate) fn is_ascendant(&self, big: &&[u8]) -> bool {
|
||||
pub(crate) fn is_decendant(&self, small: &&[u8]) -> bool {
|
||||
match *self {
|
||||
ElementPath::Deep(small) => small.len() <= big.len() && small == &big[..small.len()],
|
||||
ElementPath::Root(r) => big.len() == 1 && big[0] == r as u8,
|
||||
ElementPath::Deep(big) => small.len() <= big.len() && *small == &big[..small.len()],
|
||||
ElementPath::Root(r) => small.len() == 1 && small[0] == r as u8,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::nodes::RenderReturn;
|
||||
use bumpalo::Bump;
|
||||
use std::cell::Cell;
|
||||
use std::cell::{Cell, UnsafeCell};
|
||||
|
||||
pub(crate) struct BumpFrame {
|
||||
pub bump: Bump,
|
||||
pub bump: UnsafeCell<Bump>,
|
||||
pub node: Cell<*const RenderReturn<'static>>,
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ impl BumpFrame {
|
||||
pub(crate) fn new(capacity: usize) -> Self {
|
||||
let bump = Bump::with_capacity(capacity);
|
||||
Self {
|
||||
bump,
|
||||
bump: UnsafeCell::new(bump),
|
||||
node: Cell::new(std::ptr::null()),
|
||||
}
|
||||
}
|
||||
@@ -26,4 +26,13 @@ impl BumpFrame {
|
||||
|
||||
unsafe { std::mem::transmute(&*node) }
|
||||
}
|
||||
|
||||
pub(crate) fn bump(&self) -> &Bump {
|
||||
unsafe { &*self.bump.get() }
|
||||
}
|
||||
|
||||
#[allow(clippy::mut_from_ref)]
|
||||
pub(crate) unsafe fn bump_mut(&self) -> &mut Bump {
|
||||
unsafe { &mut *self.bump.get() }
|
||||
}
|
||||
}
|
||||
+272
-98
@@ -1,17 +1,67 @@
|
||||
use crate::any_props::AnyProps;
|
||||
use crate::innerlude::{VComponent, VPlaceholder, VText};
|
||||
use crate::innerlude::{BorrowedAttributeValue, VComponent, VPlaceholder, VText};
|
||||
use crate::mutations::Mutation;
|
||||
use crate::mutations::Mutation::*;
|
||||
use crate::nodes::VNode;
|
||||
use crate::nodes::{DynamicNode, TemplateNode};
|
||||
use crate::virtual_dom::VirtualDom;
|
||||
use crate::{AttributeValue, ElementId, RenderReturn, ScopeId, SuspenseContext};
|
||||
use crate::{AttributeValue, ElementId, RenderReturn, ScopeId, SuspenseContext, Template};
|
||||
use std::cell::Cell;
|
||||
use std::iter::{Enumerate, Peekable};
|
||||
use std::iter::Peekable;
|
||||
use std::rc::Rc;
|
||||
use std::slice;
|
||||
use TemplateNode::*;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
fn sort_bfs(paths: &[&'static [u8]]) -> Vec<(usize, &'static [u8])> {
|
||||
let mut with_indecies = paths.iter().copied().enumerate().collect::<Vec<_>>();
|
||||
with_indecies.sort_unstable_by(|(_, a), (_, b)| {
|
||||
let mut a = a.iter();
|
||||
let mut b = b.iter();
|
||||
loop {
|
||||
match (a.next(), b.next()) {
|
||||
(Some(a), Some(b)) => {
|
||||
if a != b {
|
||||
return a.cmp(b);
|
||||
}
|
||||
}
|
||||
// The shorter path goes first
|
||||
(None, Some(_)) => return std::cmp::Ordering::Less,
|
||||
(Some(_), None) => return std::cmp::Ordering::Greater,
|
||||
(None, None) => return std::cmp::Ordering::Equal,
|
||||
}
|
||||
}
|
||||
});
|
||||
with_indecies
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(debug_assertions)]
|
||||
fn sorting() {
|
||||
let r: [(usize, &[u8]); 5] = [
|
||||
(0, &[0, 1]),
|
||||
(1, &[0, 2]),
|
||||
(2, &[1, 0]),
|
||||
(3, &[1, 0, 1]),
|
||||
(4, &[1, 2]),
|
||||
];
|
||||
assert_eq!(
|
||||
sort_bfs(&[&[0, 1,], &[0, 2,], &[1, 0,], &[1, 0, 1,], &[1, 2,],]),
|
||||
r
|
||||
);
|
||||
let r: [(usize, &[u8]); 6] = [
|
||||
(0, &[0]),
|
||||
(1, &[0, 1]),
|
||||
(2, &[0, 1, 2]),
|
||||
(3, &[1]),
|
||||
(4, &[1, 2]),
|
||||
(5, &[2]),
|
||||
];
|
||||
assert_eq!(
|
||||
sort_bfs(&[&[0], &[0, 1], &[0, 1, 2], &[1], &[1, 2], &[2],]),
|
||||
r
|
||||
);
|
||||
}
|
||||
|
||||
impl<'b> VirtualDom {
|
||||
/// Create a new template [`VNode`] and write it to the [`Mutations`] buffer.
|
||||
///
|
||||
@@ -25,21 +75,77 @@ impl<'b> VirtualDom {
|
||||
|
||||
/// Create this template and write its mutations
|
||||
pub(crate) fn create(&mut self, node: &'b VNode<'b>) -> usize {
|
||||
// The best renderers will have templates prehydrated and registered
|
||||
// Just in case, let's create the template using instructions anyways
|
||||
if !self.templates.contains_key(&node.template.name) {
|
||||
self.register_template(node);
|
||||
// check for a overriden template
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let (path, byte_index) = node.template.get().name.rsplit_once(':').unwrap();
|
||||
if let Some(template) = self
|
||||
.templates
|
||||
.get(path)
|
||||
.and_then(|map| map.get(&byte_index.parse().unwrap()))
|
||||
{
|
||||
node.template.set(*template);
|
||||
}
|
||||
}
|
||||
|
||||
// Intialize the root nodes slice
|
||||
node.root_ids
|
||||
.intialize(vec![ElementId(0); node.template.get().roots.len()].into_boxed_slice());
|
||||
|
||||
// The best renderers will have templates prehydrated and registered
|
||||
// Just in case, let's create the template using instructions anyways
|
||||
self.register_template(node.template.get());
|
||||
|
||||
// we know that this will generate at least one mutation per node
|
||||
self.mutations.edits.reserve(node.template.roots.len());
|
||||
self.mutations
|
||||
.edits
|
||||
.reserve(node.template.get().roots.len());
|
||||
|
||||
// Walk the roots, creating nodes and assigning IDs
|
||||
// nodes in an iterator of ((dynamic_node_index, sorted_index), path)
|
||||
// todo: adjust dynamic nodes to be in the order of roots and then leaves (ie BFS)
|
||||
let mut attrs = node.template.attr_paths.iter().enumerate().peekable();
|
||||
let mut nodes = node.template.node_paths.iter().enumerate().peekable();
|
||||
#[cfg(not(debug_assertions))]
|
||||
let (mut attrs, mut nodes) = (
|
||||
node.template
|
||||
.get()
|
||||
.attr_paths
|
||||
.iter()
|
||||
.copied()
|
||||
.enumerate()
|
||||
.peekable(),
|
||||
node.template
|
||||
.get()
|
||||
.node_paths
|
||||
.iter()
|
||||
.copied()
|
||||
.enumerate()
|
||||
.map(|(i, path)| ((i, i), path))
|
||||
.peekable(),
|
||||
);
|
||||
// If this is a debug build, we need to check that the paths are in the correct order because hot reloading can cause scrambled states
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
let (attrs_sorted, nodes_sorted) = {
|
||||
(
|
||||
sort_bfs(node.template.get().attr_paths),
|
||||
sort_bfs(node.template.get().node_paths),
|
||||
)
|
||||
};
|
||||
#[cfg(debug_assertions)]
|
||||
let (mut attrs, mut nodes) = {
|
||||
(
|
||||
attrs_sorted.into_iter().peekable(),
|
||||
nodes_sorted
|
||||
.iter()
|
||||
.copied()
|
||||
.enumerate()
|
||||
.map(|(i, (id, path))| ((id, i), path))
|
||||
.peekable(),
|
||||
)
|
||||
};
|
||||
|
||||
node.template
|
||||
.get()
|
||||
.roots
|
||||
.iter()
|
||||
.enumerate()
|
||||
@@ -48,7 +154,14 @@ impl<'b> VirtualDom {
|
||||
nodes.next().unwrap();
|
||||
self.write_dynamic_root(node, *id)
|
||||
}
|
||||
Element { .. } => self.write_element_root(node, idx, &mut attrs, &mut nodes),
|
||||
Element { .. } => {
|
||||
#[cfg(not(debug_assertions))]
|
||||
let id = self.write_element_root(node, idx, &mut attrs, &mut nodes, &[]);
|
||||
#[cfg(debug_assertions)]
|
||||
let id =
|
||||
self.write_element_root(node, idx, &mut attrs, &mut nodes, &nodes_sorted);
|
||||
id
|
||||
}
|
||||
Text { .. } => self.write_static_text_root(node, idx),
|
||||
})
|
||||
.sum()
|
||||
@@ -65,8 +178,9 @@ impl<'b> VirtualDom {
|
||||
fn write_dynamic_root(&mut self, template: &'b VNode<'b>, idx: usize) -> usize {
|
||||
use DynamicNode::*;
|
||||
match &template.dynamic_nodes[idx] {
|
||||
node @ Fragment(_) => self.create_dynamic_node(template, node, idx),
|
||||
node @ Component { .. } => self.create_dynamic_node(template, node, idx),
|
||||
node @ Component { .. } | node @ Fragment(_) => {
|
||||
self.create_dynamic_node(template, node, idx)
|
||||
}
|
||||
Placeholder(VPlaceholder { id }) => {
|
||||
let id = self.set_slot(template, id, idx);
|
||||
self.mutations.push(CreatePlaceholder { id });
|
||||
@@ -98,17 +212,18 @@ impl<'b> VirtualDom {
|
||||
&mut self,
|
||||
template: &'b VNode<'b>,
|
||||
root_idx: usize,
|
||||
dynamic_attrs: &mut Peekable<Enumerate<slice::Iter<&'static [u8]>>>,
|
||||
dynamic_nodes: &mut Peekable<Enumerate<slice::Iter<&'static [u8]>>>,
|
||||
dynamic_attrs: &mut Peekable<impl Iterator<Item = (usize, &'static [u8])>>,
|
||||
dynamic_nodes_iter: &mut Peekable<impl Iterator<Item = ((usize, usize), &'static [u8])>>,
|
||||
dynamic_nodes: &[(usize, &'static [u8])],
|
||||
) -> usize {
|
||||
// Load the template root and get the ID for the node on the stack
|
||||
let root_on_stack = self.load_template_root(template, root_idx);
|
||||
|
||||
// Write all the attributes below this root
|
||||
self.write_attrs_on_root(dynamic_attrs, root_idx, root_on_stack, template);
|
||||
self.write_attrs_on_root(dynamic_attrs, root_idx as u8, root_on_stack, template);
|
||||
|
||||
// Load in all of the placeholder or dynamic content under this root too
|
||||
self.load_placeholders(dynamic_nodes, root_idx, template);
|
||||
self.load_placeholders(dynamic_nodes_iter, dynamic_nodes, root_idx as u8, template);
|
||||
|
||||
1
|
||||
}
|
||||
@@ -126,22 +241,32 @@ impl<'b> VirtualDom {
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[allow(unused)]
|
||||
fn load_placeholders(
|
||||
&mut self,
|
||||
dynamic_nodes: &mut Peekable<Enumerate<slice::Iter<&'static [u8]>>>,
|
||||
root_idx: usize,
|
||||
dynamic_nodes_iter: &mut Peekable<impl Iterator<Item = ((usize, usize), &'static [u8])>>,
|
||||
dynamic_nodes: &[(usize, &'static [u8])],
|
||||
root_idx: u8,
|
||||
template: &'b VNode<'b>,
|
||||
) {
|
||||
let (start, end) = match collect_dyn_node_range(dynamic_nodes, root_idx) {
|
||||
let (start, end) = match collect_dyn_node_range(dynamic_nodes_iter, root_idx) {
|
||||
Some((a, b)) => (a, b),
|
||||
None => return,
|
||||
};
|
||||
|
||||
for idx in (start..=end).rev() {
|
||||
// If hot reloading is enabled, we need to map the sorted index to the original index of the dynamic node. If it is disabled, we can just use the sorted index
|
||||
#[cfg(not(debug_assertions))]
|
||||
let reversed_iter = (start..=end).rev();
|
||||
#[cfg(debug_assertions)]
|
||||
let reversed_iter = (start..=end)
|
||||
.rev()
|
||||
.map(|sorted_index| dynamic_nodes[sorted_index].0);
|
||||
|
||||
for idx in reversed_iter {
|
||||
let m = self.create_dynamic_node(template, &template.dynamic_nodes[idx], idx);
|
||||
if m > 0 {
|
||||
// The path is one shorter because the top node is the root
|
||||
let path = &template.template.node_paths[idx][1..];
|
||||
let path = &template.template.get().node_paths[idx][1..];
|
||||
self.mutations.push(ReplacePlaceholder { m, path });
|
||||
}
|
||||
}
|
||||
@@ -149,12 +274,14 @@ impl<'b> VirtualDom {
|
||||
|
||||
fn write_attrs_on_root(
|
||||
&mut self,
|
||||
attrs: &mut Peekable<Enumerate<slice::Iter<&'static [u8]>>>,
|
||||
root_idx: usize,
|
||||
attrs: &mut Peekable<impl Iterator<Item = (usize, &'static [u8])>>,
|
||||
root_idx: u8,
|
||||
root: ElementId,
|
||||
node: &VNode,
|
||||
) {
|
||||
while let Some((mut attr_id, path)) = attrs.next_if(|(_, p)| p[0] == root_idx as u8) {
|
||||
while let Some((mut attr_id, path)) =
|
||||
attrs.next_if(|(_, p)| p.first().copied() == Some(root_idx))
|
||||
{
|
||||
let id = self.assign_static_node_as_dynamic(path, root, node, attr_id);
|
||||
|
||||
loop {
|
||||
@@ -169,7 +296,7 @@ impl<'b> VirtualDom {
|
||||
}
|
||||
}
|
||||
|
||||
fn write_attribute(&mut self, attribute: &crate::Attribute, id: ElementId) {
|
||||
fn write_attribute(&mut self, attribute: &'b crate::Attribute<'b>, id: ElementId) {
|
||||
// Make sure we set the attribute's associated id
|
||||
attribute.mounted_element.set(id);
|
||||
|
||||
@@ -177,9 +304,17 @@ impl<'b> VirtualDom {
|
||||
let unbounded_name: &str = unsafe { std::mem::transmute(attribute.name) };
|
||||
|
||||
match &attribute.value {
|
||||
AttributeValue::Text(value) => {
|
||||
AttributeValue::Listener(_) => {
|
||||
self.mutations.push(NewEventListener {
|
||||
// all listeners start with "on"
|
||||
name: &unbounded_name[2..],
|
||||
id,
|
||||
})
|
||||
}
|
||||
_ => {
|
||||
// Safety: we promise not to re-alias this text later on after committing it to the mutation
|
||||
let unbounded_value: &str = unsafe { std::mem::transmute(*value) };
|
||||
let value: BorrowedAttributeValue<'b> = (&attribute.value).into();
|
||||
let unbounded_value = unsafe { std::mem::transmute(value) };
|
||||
|
||||
self.mutations.push(SetAttribute {
|
||||
name: unbounded_name,
|
||||
@@ -188,32 +323,16 @@ impl<'b> VirtualDom {
|
||||
id,
|
||||
})
|
||||
}
|
||||
AttributeValue::Bool(value) => self.mutations.push(SetBoolAttribute {
|
||||
name: unbounded_name,
|
||||
value: *value,
|
||||
id,
|
||||
}),
|
||||
AttributeValue::Listener(_) => {
|
||||
self.mutations.push(NewEventListener {
|
||||
// all listeners start with "on"
|
||||
name: &unbounded_name[2..],
|
||||
id,
|
||||
})
|
||||
}
|
||||
AttributeValue::Float(_) => todo!(),
|
||||
AttributeValue::Int(_) => todo!(),
|
||||
AttributeValue::Any(_) => todo!(),
|
||||
AttributeValue::None => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn load_template_root(&mut self, template: &VNode, root_idx: usize) -> ElementId {
|
||||
// Get an ID for this root since it's a real root
|
||||
let this_id = self.next_root(template, root_idx);
|
||||
template.root_ids[root_idx].set(Some(this_id));
|
||||
template.root_ids.set(root_idx, this_id);
|
||||
|
||||
self.mutations.push(LoadTemplate {
|
||||
name: template.template.name,
|
||||
name: template.template.get().name,
|
||||
index: root_idx,
|
||||
id: this_id,
|
||||
});
|
||||
@@ -241,7 +360,7 @@ impl<'b> VirtualDom {
|
||||
|
||||
// if attribute is on a root node, then we've already created the element
|
||||
// Else, it's deep in the template and we should create a new id for it
|
||||
let id = self.next_element(template, template.template.attr_paths[attr_id]);
|
||||
let id = self.next_element(template, template.template.get().attr_paths[attr_id]);
|
||||
|
||||
self.mutations.push(Mutation::AssignId {
|
||||
path: &path[1..],
|
||||
@@ -252,14 +371,67 @@ impl<'b> VirtualDom {
|
||||
}
|
||||
|
||||
/// Insert a new template into the VirtualDom's template registry
|
||||
fn register_template(&mut self, template: &'b VNode<'b>) {
|
||||
pub(crate) fn register_template_first_byte_index(&mut self, mut template: Template<'static>) {
|
||||
// First, make sure we mark the template as seen, regardless if we process it
|
||||
self.templates
|
||||
.insert(template.template.name, template.template);
|
||||
let (path, _) = template.name.rsplit_once(':').unwrap();
|
||||
if let Some((_, old_template)) = self
|
||||
.templates
|
||||
.entry(path)
|
||||
.or_default()
|
||||
.iter_mut()
|
||||
.min_by_key(|(byte_index, _)| **byte_index)
|
||||
{
|
||||
// the byte index of the hot reloaded template could be different
|
||||
template.name = old_template.name;
|
||||
*old_template = template;
|
||||
} else {
|
||||
// This is a template without any current instances
|
||||
self.templates
|
||||
.entry(path)
|
||||
.or_default()
|
||||
.insert(usize::MAX, template);
|
||||
}
|
||||
|
||||
// If it's all dynamic nodes, then we don't need to register it
|
||||
if !template.template.is_completely_dynamic() {
|
||||
self.mutations.templates.push(template.template);
|
||||
if !template.is_completely_dynamic() {
|
||||
self.mutations.templates.push(template);
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert a new template into the VirtualDom's template registry
|
||||
// used in conditional compilation
|
||||
#[allow(unused_mut)]
|
||||
pub(crate) fn register_template(&mut self, mut template: Template<'static>) {
|
||||
let (path, byte_index) = template.name.rsplit_once(':').unwrap();
|
||||
let byte_index = byte_index.parse::<usize>().unwrap();
|
||||
// First, check if we've already seen this template
|
||||
if self
|
||||
.templates
|
||||
.get(&path)
|
||||
.filter(|set| set.contains_key(&byte_index))
|
||||
.is_none()
|
||||
{
|
||||
// if hot reloading is enabled, then we need to check for a template that has overriten this one
|
||||
#[cfg(debug_assertions)]
|
||||
if let Some(mut new_template) = self
|
||||
.templates
|
||||
.get_mut(path)
|
||||
.and_then(|map| map.remove(&usize::MAX))
|
||||
{
|
||||
// the byte index of the hot reloaded template could be different
|
||||
new_template.name = template.name;
|
||||
template = new_template;
|
||||
}
|
||||
|
||||
self.templates
|
||||
.entry(path)
|
||||
.or_default()
|
||||
.insert(byte_index, template);
|
||||
|
||||
// If it's all dynamic nodes, then we don't need to register it
|
||||
if !template.is_completely_dynamic() {
|
||||
self.mutations.templates.push(template);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,9 +444,9 @@ impl<'b> VirtualDom {
|
||||
use DynamicNode::*;
|
||||
match node {
|
||||
Text(text) => self.create_dynamic_text(template, text, idx),
|
||||
Fragment(frag) => self.create_fragment(frag),
|
||||
Placeholder(frag) => self.create_placeholder(frag, template, idx),
|
||||
Placeholder(place) => self.create_placeholder(place, template, idx),
|
||||
Component(component) => self.create_component_node(template, component, idx),
|
||||
Fragment(frag) => frag.iter().map(|child| self.create(child)).sum(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,7 +457,7 @@ impl<'b> VirtualDom {
|
||||
idx: usize,
|
||||
) -> usize {
|
||||
// Allocate a dynamic element reference for this text node
|
||||
let new_id = self.next_element(template, template.template.node_paths[idx]);
|
||||
let new_id = self.next_element(template, template.template.get().node_paths[idx]);
|
||||
|
||||
// Make sure the text node is assigned to the correct element
|
||||
text.id.set(Some(new_id));
|
||||
@@ -296,7 +468,7 @@ impl<'b> VirtualDom {
|
||||
// Add the mutation to the list
|
||||
self.mutations.push(HydrateText {
|
||||
id: new_id,
|
||||
path: &template.template.node_paths[idx][1..],
|
||||
path: &template.template.get().node_paths[idx][1..],
|
||||
value,
|
||||
});
|
||||
|
||||
@@ -311,14 +483,14 @@ impl<'b> VirtualDom {
|
||||
idx: usize,
|
||||
) -> usize {
|
||||
// Allocate a dynamic element reference for this text node
|
||||
let id = self.next_element(template, template.template.node_paths[idx]);
|
||||
let id = self.next_element(template, template.template.get().node_paths[idx]);
|
||||
|
||||
// Make sure the text node is assigned to the correct element
|
||||
placeholder.id.set(Some(id));
|
||||
|
||||
// Assign the ID to the existing node in the template
|
||||
self.mutations.push(AssignId {
|
||||
path: &template.template.node_paths[idx][1..],
|
||||
path: &template.template.get().node_paths[idx][1..],
|
||||
id,
|
||||
});
|
||||
|
||||
@@ -326,40 +498,38 @@ impl<'b> VirtualDom {
|
||||
0
|
||||
}
|
||||
|
||||
pub(crate) fn create_fragment(&mut self, nodes: &'b [VNode<'b>]) -> usize {
|
||||
nodes.iter().map(|child| self.create(child)).sum()
|
||||
}
|
||||
|
||||
pub(super) fn create_component_node(
|
||||
&mut self,
|
||||
template: &'b VNode<'b>,
|
||||
component: &'b VComponent<'b>,
|
||||
idx: usize,
|
||||
) -> usize {
|
||||
let scope = match component.props.take() {
|
||||
Some(props) => {
|
||||
let unbounded_props: Box<dyn AnyProps> = unsafe { std::mem::transmute(props) };
|
||||
let scope = self.new_scope(unbounded_props, component.name);
|
||||
scope.id
|
||||
}
|
||||
use RenderReturn::*;
|
||||
|
||||
// Component is coming back, it probably still exists, right?
|
||||
None => component.scope.get().unwrap(),
|
||||
};
|
||||
// Load up a ScopeId for this vcomponent
|
||||
let scope = self.load_scope_from_vcomponent(component);
|
||||
|
||||
component.scope.set(Some(scope));
|
||||
|
||||
let return_nodes = unsafe { self.run_scope(scope).extend_lifetime_ref() };
|
||||
|
||||
use RenderReturn::*;
|
||||
|
||||
match return_nodes {
|
||||
Sync(Some(t)) => self.mount_component(scope, template, t, idx),
|
||||
Sync(None) => todo!("Propogate error upwards"),
|
||||
Async(_) => self.mount_component_placeholder(template, idx, scope),
|
||||
match unsafe { self.run_scope(scope).extend_lifetime_ref() } {
|
||||
Ready(t) => self.mount_component(scope, template, t, idx),
|
||||
Aborted(t) => self.mount_aborted(template, t),
|
||||
Pending(_) => self.mount_async(template, idx, scope),
|
||||
}
|
||||
}
|
||||
|
||||
/// Load a scope from a vcomponent. If the props don't exist, that means the component is currently "live"
|
||||
fn load_scope_from_vcomponent(&mut self, component: &VComponent) -> ScopeId {
|
||||
component
|
||||
.props
|
||||
.take()
|
||||
.map(|props| {
|
||||
let unbounded_props: Box<dyn AnyProps> = unsafe { std::mem::transmute(props) };
|
||||
self.new_scope(unbounded_props, component.name).id
|
||||
})
|
||||
.unwrap_or_else(|| component.scope.get().unwrap())
|
||||
}
|
||||
|
||||
fn mount_component(
|
||||
&mut self,
|
||||
scope: ScopeId,
|
||||
@@ -380,16 +550,16 @@ 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::<Rc<SuspenseContext>>() {
|
||||
let boundary = match self.scopes[scope].has_context::<Rc<SuspenseContext>>() {
|
||||
Some(boundary) => boundary,
|
||||
_ => return created,
|
||||
};
|
||||
|
||||
// Since this is a boundary, use its placeholder within the template as the placeholder for the suspense tree
|
||||
let new_id = self.next_element(new, parent.template.node_paths[idx]);
|
||||
let new_id = self.next_element(new, parent.template.get().node_paths[idx]);
|
||||
|
||||
// Now connect everything to the boundary
|
||||
self.scopes[scope.0].placeholder.set(Some(new_id));
|
||||
self.scopes[scope].placeholder.set(Some(new_id));
|
||||
|
||||
// This involves breaking off the mutations to this point, and then creating a new placeholder for the boundary
|
||||
// Note that we break off dynamic mutations only - since static mutations aren't rendered immediately
|
||||
@@ -408,30 +578,32 @@ impl<'b> VirtualDom {
|
||||
// Now assign the placeholder in the DOM
|
||||
self.mutations.push(AssignId {
|
||||
id: new_id,
|
||||
path: &parent.template.node_paths[idx][1..],
|
||||
path: &parent.template.get().node_paths[idx][1..],
|
||||
});
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
fn mount_aborted(&mut self, parent: &'b VNode<'b>, placeholder: &VPlaceholder) -> usize {
|
||||
let id = self.next_element(parent, &[]);
|
||||
self.mutations.push(Mutation::CreatePlaceholder { id });
|
||||
placeholder.id.set(Some(id));
|
||||
1
|
||||
}
|
||||
|
||||
/// Take the rendered nodes from a component and handle them if they were async
|
||||
///
|
||||
/// IE simply assign an ID to the placeholder
|
||||
fn mount_component_placeholder(
|
||||
&mut self,
|
||||
template: &VNode,
|
||||
idx: usize,
|
||||
scope: ScopeId,
|
||||
) -> usize {
|
||||
let new_id = self.next_element(template, template.template.node_paths[idx]);
|
||||
fn mount_async(&mut self, template: &VNode, idx: usize, scope: ScopeId) -> usize {
|
||||
let new_id = self.next_element(template, template.template.get().node_paths[idx]);
|
||||
|
||||
// Set the placeholder of the scope
|
||||
self.scopes[scope.0].placeholder.set(Some(new_id));
|
||||
self.scopes[scope].placeholder.set(Some(new_id));
|
||||
|
||||
// Since the placeholder is already in the DOM, we don't create any new nodes
|
||||
self.mutations.push(AssignId {
|
||||
id: new_id,
|
||||
path: &template.template.node_paths[idx][1..],
|
||||
path: &template.template.get().node_paths[idx][1..],
|
||||
});
|
||||
|
||||
0
|
||||
@@ -443,24 +615,26 @@ impl<'b> VirtualDom {
|
||||
slot: &'b Cell<Option<ElementId>>,
|
||||
id: usize,
|
||||
) -> ElementId {
|
||||
let id = self.next_element(template, template.template.node_paths[id]);
|
||||
let id = self.next_element(template, template.template.get().node_paths[id]);
|
||||
slot.set(Some(id));
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_dyn_node_range(
|
||||
dynamic_nodes: &mut Peekable<Enumerate<slice::Iter<&[u8]>>>,
|
||||
root_idx: usize,
|
||||
dynamic_nodes: &mut Peekable<impl Iterator<Item = ((usize, usize), &'static [u8])>>,
|
||||
root_idx: u8,
|
||||
) -> Option<(usize, usize)> {
|
||||
let start = match dynamic_nodes.peek() {
|
||||
Some((idx, p)) if p[0] == root_idx as u8 => *idx,
|
||||
Some(((_, idx), [first, ..])) if *first == root_idx => *idx,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let mut end = start;
|
||||
|
||||
while let Some((idx, p)) = dynamic_nodes.next_if(|(_, p)| p[0] == root_idx as u8) {
|
||||
while let Some(((_, idx), p)) =
|
||||
dynamic_nodes.next_if(|(_, p)| matches!(p, [idx, ..] if *idx == root_idx))
|
||||
{
|
||||
if p.len() == 1 {
|
||||
continue;
|
||||
}
|
||||
|
||||
+177
-100
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
any_props::AnyProps,
|
||||
arena::ElementId,
|
||||
innerlude::{DirtyScope, VComponent, VPlaceholder, VText},
|
||||
innerlude::{BorrowedAttributeValue, DirtyScope, VComponent, VPlaceholder, VText},
|
||||
mutations::Mutation,
|
||||
nodes::RenderReturn,
|
||||
nodes::{DynamicNode, VNode},
|
||||
@@ -15,7 +15,7 @@ use DynamicNode::*;
|
||||
|
||||
impl<'b> VirtualDom {
|
||||
pub(super) fn diff_scope(&mut self, scope: ScopeId) {
|
||||
let scope_state = &mut self.scopes[scope.0];
|
||||
let scope_state = &mut self.scopes[scope];
|
||||
|
||||
self.scope_stack.push(scope);
|
||||
unsafe {
|
||||
@@ -30,31 +30,79 @@ impl<'b> VirtualDom {
|
||||
.try_load_node()
|
||||
.expect("Call rebuild before diffing");
|
||||
|
||||
use RenderReturn::{Async, Sync};
|
||||
use RenderReturn::{Aborted, Pending, Ready};
|
||||
|
||||
match (old, new) {
|
||||
(Sync(Some(l)), Sync(Some(r))) => self.diff_node(l, r),
|
||||
// Normal pathway
|
||||
(Ready(l), Ready(r)) => self.diff_node(l, r),
|
||||
|
||||
// Err cases
|
||||
(Sync(Some(l)), Sync(None)) => self.diff_ok_to_err(l),
|
||||
(Sync(None), Sync(Some(r))) => self.diff_err_to_ok(r),
|
||||
(Sync(None), Sync(None)) => { /* nothing */ }
|
||||
// Unwind the mutations if need be
|
||||
(Ready(l), Aborted(p)) => self.diff_ok_to_err(l, p),
|
||||
|
||||
// Async
|
||||
(Sync(Some(_l)), Async(_)) => todo!(),
|
||||
(Sync(None), Async(_)) => todo!(),
|
||||
(Async(_), Sync(Some(_r))) => todo!(),
|
||||
(Async(_), Sync(None)) => { /* nothing */ }
|
||||
(Async(_), Async(_)) => { /* nothing */ }
|
||||
// Just move over the placeholder
|
||||
(Aborted(l), Aborted(r)) => r.id.set(l.id.get()),
|
||||
|
||||
// Becomes async, do nothing while we wait
|
||||
(Ready(_nodes), Pending(_fut)) => self.diff_ok_to_async(_nodes, scope),
|
||||
|
||||
// Placeholder becomes something
|
||||
// We should also clear the error now
|
||||
(Aborted(l), Ready(r)) => self.replace_placeholder(l, [r]),
|
||||
|
||||
(Aborted(_), Pending(_)) => todo!("async should not resolve here"),
|
||||
(Pending(_), Ready(_)) => todo!("async should not resolve here"),
|
||||
(Pending(_), Aborted(_)) => todo!("async should not resolve here"),
|
||||
(Pending(_), Pending(_)) => {
|
||||
// All suspense should resolve before we diff it again
|
||||
panic!("Should not roll from suspense to suspense.");
|
||||
}
|
||||
};
|
||||
}
|
||||
self.scope_stack.pop();
|
||||
}
|
||||
|
||||
fn diff_ok_to_err(&mut self, _l: &'b VNode<'b>) {}
|
||||
fn diff_err_to_ok(&mut self, _l: &'b VNode<'b>) {}
|
||||
fn diff_ok_to_async(&mut self, _new: &'b VNode<'b>, _scope: ScopeId) {
|
||||
//
|
||||
}
|
||||
|
||||
fn diff_ok_to_err(&mut self, l: &'b VNode<'b>, p: &'b VPlaceholder) {
|
||||
let id = self.next_null();
|
||||
p.id.set(Some(id));
|
||||
self.mutations.push(Mutation::CreatePlaceholder { id });
|
||||
|
||||
let pre_edits = self.mutations.edits.len();
|
||||
|
||||
self.remove_node(l, true);
|
||||
|
||||
// We should always have a remove mutation
|
||||
// Eventually we don't want to generate placeholders, so this might not be true. But it's true today
|
||||
assert!(self.mutations.edits.len() > pre_edits);
|
||||
|
||||
// We want to optimize the replace case to use one less mutation if possible
|
||||
// Since mutations are done in reverse, the last node removed will be the first in the stack
|
||||
// Instead of *just* removing it, we can use the replace mutation
|
||||
match self.mutations.edits.pop().unwrap() {
|
||||
Mutation::Remove { id } => self.mutations.push(Mutation::ReplaceWith { id, m: 1 }),
|
||||
_ => panic!("Expected remove mutation from remove_node"),
|
||||
};
|
||||
}
|
||||
|
||||
fn diff_node(&mut self, left_template: &'b VNode<'b>, right_template: &'b VNode<'b>) {
|
||||
// If hot reloading is enabled, we need to make sure we're using the latest template
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let (path, byte_index) = right_template.template.get().name.rsplit_once(':').unwrap();
|
||||
if let Some(map) = self.templates.get(path) {
|
||||
let byte_index = byte_index.parse::<usize>().unwrap();
|
||||
if let Some(&template) = map.get(&byte_index) {
|
||||
right_template.template.set(template);
|
||||
if template != left_template.template.get() {
|
||||
return self.replace(left_template, [right_template]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the templates are the same, we don't need to do anything, nor do we want to
|
||||
if templates_are_the_same(left_template, right_template) {
|
||||
return;
|
||||
@@ -99,11 +147,14 @@ impl<'b> VirtualDom {
|
||||
});
|
||||
|
||||
// Make sure the roots get transferred over while we're here
|
||||
left_template
|
||||
.root_ids
|
||||
.iter()
|
||||
.zip(right_template.root_ids.iter())
|
||||
.for_each(|(left, right)| right.set(left.get()));
|
||||
right_template.root_ids.transfer(&left_template.root_ids);
|
||||
|
||||
// Update the node refs
|
||||
for i in 0..right_template.root_ids.len() {
|
||||
if let Some(root_id) = right_template.root_ids.get(i) {
|
||||
self.update_template(root_id, right_template);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn diff_dynamic_node(
|
||||
@@ -118,28 +169,22 @@ impl<'b> VirtualDom {
|
||||
(Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right),
|
||||
(Placeholder(left), Placeholder(right)) => right.id.set(left.id.get()),
|
||||
(Component(left), Component(right)) => self.diff_vcomponent(left, right, node, idx),
|
||||
(Placeholder(left), Fragment(right)) => self.replace_placeholder(left, right),
|
||||
(Placeholder(left), Fragment(right)) => self.replace_placeholder(left, *right),
|
||||
(Fragment(left), Placeholder(right)) => self.node_to_placeholder(left, right),
|
||||
_ => todo!("This is an usual custom case for dynamic nodes. We don't know how to handle it yet."),
|
||||
};
|
||||
}
|
||||
|
||||
fn update_attribute(&mut self, right_attr: &Attribute, left_attr: &Attribute) {
|
||||
// todo: add more types of attribute values
|
||||
match right_attr.value {
|
||||
AttributeValue::Text(text) => {
|
||||
let name = unsafe { std::mem::transmute(left_attr.name) };
|
||||
let value = unsafe { std::mem::transmute(text) };
|
||||
self.mutations.push(Mutation::SetAttribute {
|
||||
id: left_attr.mounted_element.get(),
|
||||
ns: right_attr.namespace,
|
||||
name,
|
||||
value,
|
||||
});
|
||||
}
|
||||
// todo: more types of attribute values
|
||||
_ => todo!("other attribute types"),
|
||||
}
|
||||
fn update_attribute(&mut self, right_attr: &'b Attribute<'b>, left_attr: &'b Attribute) {
|
||||
let name = unsafe { std::mem::transmute(left_attr.name) };
|
||||
let value: BorrowedAttributeValue<'b> = (&right_attr.value).into();
|
||||
let value = unsafe { std::mem::transmute(value) };
|
||||
self.mutations.push(Mutation::SetAttribute {
|
||||
id: left_attr.mounted_element.get(),
|
||||
ns: right_attr.namespace,
|
||||
name,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
fn diff_vcomponent(
|
||||
@@ -164,7 +209,7 @@ impl<'b> VirtualDom {
|
||||
right.scope.set(Some(scope_id));
|
||||
|
||||
// copy out the box for both
|
||||
let old = self.scopes[scope_id.0].props.as_ref();
|
||||
let old = self.scopes[scope_id].props.as_ref();
|
||||
let new: Box<dyn AnyProps> = right.props.take().unwrap();
|
||||
let new: Box<dyn AnyProps> = unsafe { std::mem::transmute(new) };
|
||||
|
||||
@@ -176,14 +221,14 @@ impl<'b> VirtualDom {
|
||||
}
|
||||
|
||||
// First, move over the props from the old to the new, dropping old props in the process
|
||||
self.scopes[scope_id.0].props = Some(new);
|
||||
self.scopes[scope_id].props = Some(new);
|
||||
|
||||
// Now run the component and diff it
|
||||
self.run_scope(scope_id);
|
||||
self.diff_scope(scope_id);
|
||||
|
||||
self.dirty_scopes.remove(&DirtyScope {
|
||||
height: self.scopes[scope_id.0].height,
|
||||
height: self.scopes[scope_id].height,
|
||||
id: scope_id,
|
||||
});
|
||||
}
|
||||
@@ -197,8 +242,12 @@ impl<'b> VirtualDom {
|
||||
) {
|
||||
let m = self.create_component_node(right_template, right, idx);
|
||||
|
||||
let pre_edits = self.mutations.edits.len();
|
||||
|
||||
self.remove_component_node(left, true);
|
||||
|
||||
assert!(self.mutations.edits.len() > pre_edits);
|
||||
|
||||
// We want to optimize the replace case to use one less mutation if possible
|
||||
// Since mutations are done in reverse, the last node removed will be the first in the stack
|
||||
// Instead of *just* removing it, we can use the replace mutation
|
||||
@@ -586,10 +635,12 @@ impl<'b> VirtualDom {
|
||||
}
|
||||
|
||||
let id = self.find_last_element(&new[last]);
|
||||
self.mutations.push(Mutation::InsertAfter {
|
||||
id,
|
||||
m: nodes_created,
|
||||
});
|
||||
if nodes_created > 0 {
|
||||
self.mutations.push(Mutation::InsertAfter {
|
||||
id,
|
||||
m: nodes_created,
|
||||
})
|
||||
}
|
||||
nodes_created = 0;
|
||||
}
|
||||
|
||||
@@ -610,10 +661,12 @@ impl<'b> VirtualDom {
|
||||
}
|
||||
|
||||
let id = self.find_first_element(&new[last]);
|
||||
self.mutations.push(Mutation::InsertBefore {
|
||||
id,
|
||||
m: nodes_created,
|
||||
});
|
||||
if nodes_created > 0 {
|
||||
self.mutations.push(Mutation::InsertBefore {
|
||||
id,
|
||||
m: nodes_created,
|
||||
});
|
||||
}
|
||||
|
||||
nodes_created = 0;
|
||||
}
|
||||
@@ -634,16 +687,19 @@ impl<'b> VirtualDom {
|
||||
}
|
||||
|
||||
let id = self.find_first_element(&new[first_lis]);
|
||||
self.mutations.push(Mutation::InsertBefore {
|
||||
id,
|
||||
m: nodes_created,
|
||||
});
|
||||
if nodes_created > 0 {
|
||||
self.mutations.push(Mutation::InsertBefore {
|
||||
id,
|
||||
m: nodes_created,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Push all the real nodes on the stack
|
||||
fn push_all_real_nodes(&mut self, node: &'b VNode<'b>) -> usize {
|
||||
node.template
|
||||
.get()
|
||||
.roots
|
||||
.iter()
|
||||
.enumerate()
|
||||
@@ -652,7 +708,7 @@ impl<'b> VirtualDom {
|
||||
Some(node) => node,
|
||||
None => {
|
||||
self.mutations.push(Mutation::PushRoot {
|
||||
id: node.root_ids[idx].get().unwrap(),
|
||||
id: node.root_ids.get(idx).unwrap(),
|
||||
});
|
||||
return 1;
|
||||
}
|
||||
@@ -674,18 +730,19 @@ impl<'b> VirtualDom {
|
||||
Fragment(nodes) => nodes
|
||||
.iter()
|
||||
.map(|node| self.push_all_real_nodes(node))
|
||||
.count(),
|
||||
.sum(),
|
||||
|
||||
Component(comp) => {
|
||||
let scope = comp.scope.get().unwrap();
|
||||
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
|
||||
RenderReturn::Sync(Some(node)) => self.push_all_real_nodes(node),
|
||||
match unsafe { self.scopes[scope].root_node().extend_lifetime_ref() } {
|
||||
RenderReturn::Ready(node) => self.push_all_real_nodes(node),
|
||||
RenderReturn::Aborted(_node) => todo!(),
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.count()
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn create_children(&mut self, nodes: impl IntoIterator<Item = &'b VNode<'b>>) -> usize {
|
||||
@@ -707,7 +764,11 @@ impl<'b> VirtualDom {
|
||||
}
|
||||
|
||||
/// Simply replace a placeholder with a list of nodes
|
||||
fn replace_placeholder(&mut self, l: &'b VPlaceholder, r: &'b [VNode<'b>]) {
|
||||
fn replace_placeholder(
|
||||
&mut self,
|
||||
l: &'b VPlaceholder,
|
||||
r: impl IntoIterator<Item = &'b VNode<'b>>,
|
||||
) {
|
||||
let m = self.create_children(r);
|
||||
let id = l.id.get().unwrap();
|
||||
self.mutations.push(Mutation::ReplaceWith { id, m });
|
||||
@@ -779,11 +840,11 @@ impl<'b> VirtualDom {
|
||||
}
|
||||
|
||||
fn reclaim_roots(&mut self, node: &VNode, gen_muts: bool) {
|
||||
for (idx, _) in node.template.roots.iter().enumerate() {
|
||||
for (idx, _) in node.template.get().roots.iter().enumerate() {
|
||||
if let Some(dy) = node.dynamic_root(idx) {
|
||||
self.remove_dynamic_node(dy, gen_muts);
|
||||
} else {
|
||||
let id = node.root_ids[idx].get().unwrap();
|
||||
let id = node.root_ids.get(idx).unwrap();
|
||||
if gen_muts {
|
||||
self.mutations.push(Mutation::Remove { id });
|
||||
}
|
||||
@@ -796,8 +857,19 @@ impl<'b> VirtualDom {
|
||||
let mut id = None;
|
||||
for (idx, attr) in node.dynamic_attrs.iter().enumerate() {
|
||||
// We'll clean up the root nodes either way, so don't worry
|
||||
if node.template.attr_paths[idx].len() == 1 {
|
||||
continue;
|
||||
let path_len = node
|
||||
.template
|
||||
.get()
|
||||
.attr_paths
|
||||
.get(idx)
|
||||
.map(|path| path.len());
|
||||
// if the path is 1 the attribute is in the root, so we don't need to clean it up
|
||||
// if the path is 0, the attribute is a not attached at all, so we don't need to clean it up
|
||||
|
||||
if let Some(len) = path_len {
|
||||
if (..=1).contains(&len) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let next_id = attr.mounted_element.get();
|
||||
@@ -814,12 +886,16 @@ impl<'b> VirtualDom {
|
||||
|
||||
fn remove_nested_dyn_nodes(&mut self, node: &VNode) {
|
||||
for (idx, dyn_node) in node.dynamic_nodes.iter().enumerate() {
|
||||
// Roots are cleaned up automatically above
|
||||
if node.template.node_paths[idx].len() == 1 {
|
||||
continue;
|
||||
let path_len = node
|
||||
.template
|
||||
.get()
|
||||
.node_paths
|
||||
.get(idx)
|
||||
.map(|path| path.len());
|
||||
// Roots are cleaned up automatically above and nodes with a empty path are placeholders
|
||||
if let Some(2..) = path_len {
|
||||
self.remove_dynamic_node(dyn_node, false)
|
||||
}
|
||||
|
||||
self.remove_dynamic_node(dyn_node, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -853,40 +929,37 @@ impl<'b> VirtualDom {
|
||||
}
|
||||
|
||||
fn remove_component_node(&mut self, comp: &VComponent, gen_muts: bool) {
|
||||
let scope = comp.scope.take().unwrap();
|
||||
// Remove the component reference from the vcomponent so they're not tied together
|
||||
let scope = comp
|
||||
.scope
|
||||
.take()
|
||||
.expect("VComponents to always have a scope");
|
||||
|
||||
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
|
||||
RenderReturn::Sync(Some(t)) => {
|
||||
println!("Removing component node sync {:?}", gen_muts);
|
||||
self.remove_node(t, gen_muts)
|
||||
}
|
||||
_ => todo!("cannot handle nonstandard nodes"),
|
||||
// Remove the component from the dom
|
||||
match unsafe { self.scopes[scope].root_node().extend_lifetime_ref() } {
|
||||
RenderReturn::Ready(t) => self.remove_node(t, gen_muts),
|
||||
RenderReturn::Aborted(placeholder) => self.remove_placeholder(placeholder, gen_muts),
|
||||
_ => todo!(),
|
||||
};
|
||||
|
||||
let props = self.scopes[scope.0].props.take();
|
||||
|
||||
self.dirty_scopes.remove(&DirtyScope {
|
||||
height: self.scopes[scope.0].height,
|
||||
id: scope,
|
||||
});
|
||||
|
||||
// Restore the props back to the vcomponent in case it gets rendered again
|
||||
let props = self.scopes[scope].props.take();
|
||||
*comp.props.borrow_mut() = unsafe { std::mem::transmute(props) };
|
||||
|
||||
// make sure to wipe any of its props and listeners
|
||||
self.ensure_drop_safety(scope);
|
||||
self.scopes.remove(scope.0);
|
||||
// Now drop all the resouces
|
||||
self.drop_scope(scope, false);
|
||||
}
|
||||
|
||||
fn find_first_element(&self, node: &'b VNode<'b>) -> ElementId {
|
||||
match node.dynamic_root(0) {
|
||||
None => node.root_ids[0].get().unwrap(),
|
||||
None => node.root_ids.get(0).unwrap(),
|
||||
Some(Text(t)) => t.id.get().unwrap(),
|
||||
Some(Fragment(t)) => self.find_first_element(&t[0]),
|
||||
Some(Placeholder(t)) => t.id.get().unwrap(),
|
||||
Some(Component(comp)) => {
|
||||
let scope = comp.scope.get().unwrap();
|
||||
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
|
||||
RenderReturn::Sync(Some(t)) => self.find_first_element(t),
|
||||
match unsafe { self.scopes[scope].root_node().extend_lifetime_ref() } {
|
||||
RenderReturn::Ready(t) => self.find_first_element(t),
|
||||
_ => todo!("cannot handle nonstandard nodes"),
|
||||
}
|
||||
}
|
||||
@@ -894,15 +967,15 @@ impl<'b> VirtualDom {
|
||||
}
|
||||
|
||||
fn find_last_element(&self, node: &'b VNode<'b>) -> ElementId {
|
||||
match node.dynamic_root(node.template.roots.len() - 1) {
|
||||
None => node.root_ids.last().unwrap().get().unwrap(),
|
||||
match node.dynamic_root(node.template.get().roots.len() - 1) {
|
||||
None => node.root_ids.last().unwrap(),
|
||||
Some(Text(t)) => t.id.get().unwrap(),
|
||||
Some(Fragment(t)) => self.find_last_element(t.last().unwrap()),
|
||||
Some(Placeholder(t)) => t.id.get().unwrap(),
|
||||
Some(Component(comp)) => {
|
||||
let scope = comp.scope.get().unwrap();
|
||||
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
|
||||
RenderReturn::Sync(Some(t)) => self.find_last_element(t),
|
||||
match unsafe { self.scopes[scope].root_node().extend_lifetime_ref() } {
|
||||
RenderReturn::Ready(t) => self.find_last_element(t),
|
||||
_ => todo!("cannot handle nonstandard nodes"),
|
||||
}
|
||||
}
|
||||
@@ -917,27 +990,30 @@ impl<'b> VirtualDom {
|
||||
/// We use the pointer of the dynamic_node list in this case
|
||||
fn templates_are_the_same<'b>(left_template: &'b VNode<'b>, right_template: &'b VNode<'b>) -> bool {
|
||||
std::ptr::eq(left_template, right_template)
|
||||
|| std::ptr::eq(left_template.dynamic_nodes, right_template.dynamic_nodes)
|
||||
}
|
||||
|
||||
fn templates_are_different(left_template: &VNode, right_template: &VNode) -> bool {
|
||||
!std::ptr::eq(left_template.template.name, right_template.template.name)
|
||||
&& left_template.template.name != right_template.template.name
|
||||
let left_template_name = left_template.template.get().name;
|
||||
let right_template_name = right_template.template.get().name;
|
||||
// we want to re-create the node if the template name is different by pointer even if the value is the same so that we can detect when hot reloading changes the template
|
||||
!std::ptr::eq(left_template_name, right_template_name)
|
||||
}
|
||||
|
||||
fn matching_components<'a>(
|
||||
left: &'a VNode<'a>,
|
||||
right: &'a VNode<'a>,
|
||||
) -> Option<Vec<(&'a VComponent<'a>, &'a VComponent<'a>)>> {
|
||||
if left.template.roots.len() != right.template.roots.len() {
|
||||
let left_template = left.template.get();
|
||||
let right_template = right.template.get();
|
||||
if left_template.roots.len() != right_template.roots.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// run through the components, ensuring they're the same
|
||||
left.template
|
||||
left_template
|
||||
.roots
|
||||
.iter()
|
||||
.zip(right.template.roots.iter())
|
||||
.zip(right_template.roots.iter())
|
||||
.map(|(l, r)| {
|
||||
let (l, r) = match (l, r) {
|
||||
(TemplateNode::Dynamic { id: l }, TemplateNode::Dynamic { id: r }) => (l, r),
|
||||
@@ -962,11 +1038,12 @@ fn matching_components<'a>(
|
||||
/// - for appending children we can use AppendChildren
|
||||
#[allow(dead_code)]
|
||||
fn is_dyn_node_only_child(node: &VNode, idx: usize) -> bool {
|
||||
let path = node.template.node_paths[idx];
|
||||
let template = node.template.get();
|
||||
let path = template.node_paths[idx];
|
||||
|
||||
// use a loop to index every static node's children until the path has run out
|
||||
// only break if the last path index is a dynamic node
|
||||
let mut static_node = &node.template.roots[path[0] as usize];
|
||||
let mut static_node = &template.roots[path[0] as usize];
|
||||
|
||||
for i in 1..path.len() - 1 {
|
||||
match static_node {
|
||||
|
||||
@@ -1,19 +1,203 @@
|
||||
use std::cell::RefCell;
|
||||
|
||||
use crate::ScopeId;
|
||||
use crate::{ScopeId, ScopeState};
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
cell::RefCell,
|
||||
fmt::Debug,
|
||||
};
|
||||
|
||||
/// A boundary that will capture any errors from child components
|
||||
#[allow(dead_code)]
|
||||
pub struct ErrorBoundary {
|
||||
error: RefCell<Option<ScopeId>>,
|
||||
id: ScopeId,
|
||||
error: RefCell<Option<CapturedError>>,
|
||||
_id: ScopeId,
|
||||
}
|
||||
|
||||
/// An instance of an error captured by a descendant component.
|
||||
pub struct CapturedError {
|
||||
/// The error captured by the error boundary
|
||||
pub error: Box<dyn Debug + 'static>,
|
||||
|
||||
/// The scope that threw the error
|
||||
pub scope: ScopeId,
|
||||
}
|
||||
|
||||
impl CapturedError {
|
||||
/// Downcast the error type into a concrete error type
|
||||
pub fn downcast<T: 'static>(&self) -> Option<&T> {
|
||||
if TypeId::of::<T>() == self.error.type_id() {
|
||||
let raw = self.error.as_ref() as *const _ as *const T;
|
||||
Some(unsafe { &*raw })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrorBoundary {
|
||||
pub fn new(id: ScopeId) -> Self {
|
||||
Self {
|
||||
error: RefCell::new(None),
|
||||
id,
|
||||
_id: id,
|
||||
}
|
||||
}
|
||||
|
||||
/// Push an error into this Error Boundary
|
||||
pub fn insert_error(&self, scope: ScopeId, error: Box<dyn Debug + 'static>) {
|
||||
self.error.replace(Some(CapturedError { error, scope }));
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait to allow results to be thrown upwards to the nearest Error Boundary
|
||||
///
|
||||
/// The canonical way of using this trait is to throw results from hooks, aborting rendering
|
||||
/// through question mark synax. The throw method returns an option that evalutes to None
|
||||
/// if there is an error, injecting the error to the nearest error boundary.
|
||||
///
|
||||
/// If the value is `Ok`, then throw returns the value, not aborting the rendering preocess.
|
||||
///
|
||||
/// The call stack is saved for this component and provided to the error boundary
|
||||
///
|
||||
/// ```rust, ignore
|
||||
///
|
||||
/// #[inline_props]
|
||||
/// fn app(cx: Scope, count: String) -> Element {
|
||||
/// let id: i32 = count.parse().throw(cx)?;
|
||||
///
|
||||
/// cx.render(rsx! {
|
||||
/// div { "Count {}" }
|
||||
/// })
|
||||
/// }
|
||||
/// ```
|
||||
pub trait Throw<S = ()>: Sized {
|
||||
/// The value that will be returned in if the given value is `Ok`.
|
||||
type Out;
|
||||
|
||||
/// Returns an option that evalutes to None if there is an error, injecting the error to the nearest error boundary.
|
||||
///
|
||||
/// If the value is `Ok`, then throw returns the value, not aborting the rendering preocess.
|
||||
///
|
||||
/// The call stack is saved for this component and provided to the error boundary
|
||||
///
|
||||
///
|
||||
/// Note that you can also manually throw errors using the throw method on `ScopeState` directly,
|
||||
/// which is what this trait shells out to.
|
||||
///
|
||||
///
|
||||
/// ```rust, ignore
|
||||
///
|
||||
/// #[inline_props]
|
||||
/// fn app(cx: Scope, count: String) -> Element {
|
||||
/// let id: i32 = count.parse().throw(cx)?;
|
||||
///
|
||||
/// cx.render(rsx! {
|
||||
/// div { "Count {}" }
|
||||
/// })
|
||||
/// }
|
||||
/// ```
|
||||
fn throw(self, cx: &ScopeState) -> Option<Self::Out>;
|
||||
|
||||
/// Returns an option that evalutes to None if there is an error, injecting the error to the nearest error boundary.
|
||||
///
|
||||
/// If the value is `Ok`, then throw returns the value, not aborting the rendering preocess.
|
||||
///
|
||||
/// The call stack is saved for this component and provided to the error boundary
|
||||
///
|
||||
///
|
||||
/// Note that you can also manually throw errors using the throw method on `ScopeState` directly,
|
||||
/// which is what this trait shells out to.
|
||||
///
|
||||
///
|
||||
/// ```rust, ignore
|
||||
///
|
||||
/// #[inline_props]
|
||||
/// fn app(cx: Scope, count: String) -> Element {
|
||||
/// let id: i32 = count.parse().throw(cx)?;
|
||||
///
|
||||
/// cx.render(rsx! {
|
||||
/// div { "Count {}" }
|
||||
/// })
|
||||
/// }
|
||||
/// ```
|
||||
fn throw_with<D: Debug + 'static>(
|
||||
self,
|
||||
cx: &ScopeState,
|
||||
e: impl FnOnce() -> D,
|
||||
) -> Option<Self::Out>;
|
||||
}
|
||||
|
||||
/// We call clone on any errors that can be owned out of a reference
|
||||
impl<'a, T, O: Debug + 'static, E: ToOwned<Owned = O>> Throw for &'a Result<T, E> {
|
||||
type Out = &'a T;
|
||||
|
||||
fn throw(self, cx: &ScopeState) -> Option<Self::Out> {
|
||||
match self {
|
||||
Ok(t) => Some(t),
|
||||
Err(e) => {
|
||||
cx.throw(e.to_owned());
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn throw_with<D: Debug + 'static>(
|
||||
self,
|
||||
cx: &ScopeState,
|
||||
err: impl FnOnce() -> D,
|
||||
) -> Option<Self::Out> {
|
||||
match self {
|
||||
Ok(t) => Some(t),
|
||||
Err(_e) => {
|
||||
cx.throw(err());
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Or just throw errors we know about
|
||||
impl<T, E: Debug + 'static> Throw for Result<T, E> {
|
||||
type Out = T;
|
||||
|
||||
fn throw(self, cx: &ScopeState) -> Option<T> {
|
||||
match self {
|
||||
Ok(t) => Some(t),
|
||||
Err(e) => {
|
||||
cx.throw(e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn throw_with<D: Debug + 'static>(
|
||||
self,
|
||||
cx: &ScopeState,
|
||||
error: impl FnOnce() -> D,
|
||||
) -> Option<Self::Out> {
|
||||
self.ok().or_else(|| {
|
||||
cx.throw(error());
|
||||
None
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Or just throw errors we know about
|
||||
impl<T> Throw for Option<T> {
|
||||
type Out = T;
|
||||
|
||||
fn throw(self, cx: &ScopeState) -> Option<T> {
|
||||
self.or_else(|| {
|
||||
cx.throw("None error.");
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
fn throw_with<D: Debug + 'static>(
|
||||
self,
|
||||
cx: &ScopeState,
|
||||
error: impl FnOnce() -> D,
|
||||
) -> Option<Self::Out> {
|
||||
self.or_else(|| {
|
||||
cx.throw(error());
|
||||
None
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ use std::{
|
||||
pub struct Event<T: 'static + ?Sized> {
|
||||
/// The data associated with this event
|
||||
pub data: Rc<T>,
|
||||
pub(crate) propogates: Rc<Cell<bool>>,
|
||||
pub(crate) propagates: Rc<Cell<bool>>,
|
||||
}
|
||||
|
||||
impl<T> Event<T> {
|
||||
@@ -40,9 +40,9 @@ impl<T> Event<T> {
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[deprecated = "use stop_propogation instead"]
|
||||
#[deprecated = "use stop_propagation instead"]
|
||||
pub fn cancel_bubble(&self) {
|
||||
self.propogates.set(false);
|
||||
self.propagates.set(false);
|
||||
}
|
||||
|
||||
/// Prevent this event from continuing to bubble up the tree to parent elements.
|
||||
@@ -58,8 +58,8 @@ impl<T> Event<T> {
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn stop_propogation(&self) {
|
||||
self.propogates.set(false);
|
||||
pub fn stop_propagation(&self) {
|
||||
self.propagates.set(false);
|
||||
}
|
||||
|
||||
/// Get a reference to the inner data from this event
|
||||
@@ -84,7 +84,7 @@ impl<T> Event<T> {
|
||||
impl<T: ?Sized> Clone for Event<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
propogates: self.propogates.clone(),
|
||||
propagates: self.propagates.clone(),
|
||||
data: self.data.clone(),
|
||||
}
|
||||
}
|
||||
@@ -100,12 +100,14 @@ impl<T> std::ops::Deref for Event<T> {
|
||||
impl<T: std::fmt::Debug> std::fmt::Debug for Event<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("UiEvent")
|
||||
.field("bubble_state", &self.propogates)
|
||||
.field("bubble_state", &self.propagates)
|
||||
.field("data", &self.data)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
|
||||
/// The callback type generated by the `rsx!` macro when an `on` field is specified for components.
|
||||
///
|
||||
/// This makes it possible to pass `move |evt| {}` style closures into components as property fields.
|
||||
|
||||
@@ -31,8 +31,8 @@ pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element {
|
||||
Some(VNode {
|
||||
key: children.key,
|
||||
parent: children.parent,
|
||||
template: children.template,
|
||||
root_ids: children.root_ids,
|
||||
template: children.template.clone(),
|
||||
root_ids: children.root_ids.clone(),
|
||||
dynamic_nodes: children.dynamic_nodes,
|
||||
dynamic_attrs: children.dynamic_attrs,
|
||||
})
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
//! The logic for this was borrowed from <https://docs.rs/stack_dst/0.6.1/stack_dst/>. Unfortunately, this crate does not
|
||||
//! support non-static closures, so we've implemented the core logic of `ValueA` in this module.
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use smallbox::{smallbox, space::S16, SmallBox};
|
||||
|
||||
use crate::{innerlude::VNode, ScopeState};
|
||||
|
||||
@@ -70,10 +70,11 @@ pub(crate) mod innerlude {
|
||||
}
|
||||
|
||||
pub use crate::innerlude::{
|
||||
fc_to_builder, Attribute, AttributeValue, Component, DynamicNode, Element, ElementId, Event,
|
||||
Fragment, IntoDynNode, LazyNodes, Mutation, Mutations, Properties, RenderReturn, Scope,
|
||||
ScopeId, ScopeState, Scoped, SuspenseContext, TaskId, Template, TemplateAttribute,
|
||||
TemplateNode, VComponent, VNode, VText, VirtualDom,
|
||||
fc_to_builder, AnyValue, Attribute, AttributeValue, BorrowedAttributeValue, CapturedError,
|
||||
Component, DynamicNode, Element, ElementId, Event, Fragment, IntoDynNode, LazyNodes, Mutation,
|
||||
Mutations, Properties, RenderReturn, Scope, ScopeId, ScopeState, Scoped, SuspenseContext,
|
||||
TaskId, Template, TemplateAttribute, TemplateNode, VComponent, VNode, VPlaceholder, VText,
|
||||
VirtualDom,
|
||||
};
|
||||
|
||||
/// The purpose of this module is to alleviate imports of many common types
|
||||
@@ -81,9 +82,9 @@ pub use crate::innerlude::{
|
||||
/// This includes types like [`Scope`], [`Element`], and [`Component`].
|
||||
pub mod prelude {
|
||||
pub use crate::innerlude::{
|
||||
fc_to_builder, Component, Element, Event, EventHandler, Fragment, LazyNodes, Properties,
|
||||
Scope, ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode,
|
||||
VNode, VirtualDom,
|
||||
fc_to_builder, AnyValue, Component, Element, Event, EventHandler, Fragment,
|
||||
IntoAttributeValue, LazyNodes, Properties, Scope, ScopeId, ScopeState, Scoped, TaskId,
|
||||
Template, TemplateAttribute, TemplateNode, Throw, VNode, VirtualDom,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
use crate::{arena::ElementId, ScopeId, Template};
|
||||
use crate::{arena::ElementId, innerlude::BorrowedAttributeValue, ScopeId, Template};
|
||||
|
||||
/// A container for all the relevant steps to modify the Real DOM
|
||||
///
|
||||
@@ -61,14 +61,14 @@ impl<'a> Mutations<'a> {
|
||||
derive(serde::Serialize, serde::Deserialize),
|
||||
serde(tag = "type")
|
||||
)]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Mutation<'a> {
|
||||
/// Add these m children to the target element
|
||||
AppendChildren {
|
||||
/// The ID of the element being mounted to
|
||||
id: ElementId,
|
||||
|
||||
/// The number of nodes on the stack
|
||||
/// The number of nodes on the stack to append to the target element
|
||||
m: usize,
|
||||
},
|
||||
|
||||
@@ -155,7 +155,7 @@ pub enum Mutation<'a> {
|
||||
/// The ID of the node we're going to replace with
|
||||
id: ElementId,
|
||||
|
||||
/// The number of nodes on the stack to use to replace
|
||||
/// The number of nodes on the stack to replace the target element with
|
||||
m: usize,
|
||||
},
|
||||
|
||||
@@ -167,7 +167,7 @@ pub enum Mutation<'a> {
|
||||
/// `[0,1,2]` represents 1st child's 2nd child's 3rd child.
|
||||
path: &'static [u8],
|
||||
|
||||
/// The number of nodes on the stack to use to replace
|
||||
/// The number of nodes on the stack to replace the target element with
|
||||
m: usize,
|
||||
},
|
||||
|
||||
@@ -176,7 +176,7 @@ pub enum Mutation<'a> {
|
||||
/// The ID of the node to insert after.
|
||||
id: ElementId,
|
||||
|
||||
/// The ids of the nodes to insert after the target node.
|
||||
/// The number of nodes on the stack to insert after the target node.
|
||||
m: usize,
|
||||
},
|
||||
|
||||
@@ -185,7 +185,7 @@ pub enum Mutation<'a> {
|
||||
/// The ID of the node to insert before.
|
||||
id: ElementId,
|
||||
|
||||
/// The ids of the nodes to insert before the target node.
|
||||
/// The number of nodes on the stack to insert before the target node.
|
||||
m: usize,
|
||||
},
|
||||
|
||||
@@ -193,8 +193,9 @@ pub enum Mutation<'a> {
|
||||
SetAttribute {
|
||||
/// The name of the attribute to set.
|
||||
name: &'a str,
|
||||
|
||||
/// The value of the attribute.
|
||||
value: &'a str,
|
||||
value: BorrowedAttributeValue<'a>,
|
||||
|
||||
/// The ID of the node to set the attribute of.
|
||||
id: ElementId,
|
||||
@@ -204,18 +205,6 @@ pub enum Mutation<'a> {
|
||||
ns: Option<&'a str>,
|
||||
},
|
||||
|
||||
/// Set the value of a node's attribute.
|
||||
SetBoolAttribute {
|
||||
/// The name of the attribute to set.
|
||||
name: &'a str,
|
||||
|
||||
/// The value of the attribute.
|
||||
value: bool,
|
||||
|
||||
/// The ID of the node to set the attribute of.
|
||||
id: ElementId,
|
||||
},
|
||||
|
||||
/// Set the textcontent of a node.
|
||||
SetText {
|
||||
/// The textcontent of the node
|
||||
|
||||
+339
-42
@@ -5,8 +5,8 @@ use bumpalo::boxed::Box as BumpBox;
|
||||
use bumpalo::Bump;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
cell::{Cell, RefCell},
|
||||
fmt::Arguments,
|
||||
cell::{Cell, RefCell, UnsafeCell},
|
||||
fmt::{Arguments, Debug},
|
||||
future::Future,
|
||||
};
|
||||
|
||||
@@ -21,10 +21,22 @@ pub type TemplateId = &'static str;
|
||||
/// you might need to handle the case where there's no node immediately ready.
|
||||
pub enum RenderReturn<'a> {
|
||||
/// A currently-available element
|
||||
Sync(Element<'a>),
|
||||
Ready(VNode<'a>),
|
||||
|
||||
/// The component aborted rendering early. It might've thrown an error.
|
||||
///
|
||||
/// In its place we've produced a placeholder to locate its spot in the dom when
|
||||
/// it recovers.
|
||||
Aborted(VPlaceholder),
|
||||
|
||||
/// An ongoing future that will resolve to a [`Element`]
|
||||
Async(BumpBox<'a, dyn Future<Output = Element<'a>> + 'a>),
|
||||
Pending(BumpBox<'a, dyn Future<Output = Element<'a>> + 'a>),
|
||||
}
|
||||
|
||||
impl<'a> Default for RenderReturn<'a> {
|
||||
fn default() -> Self {
|
||||
RenderReturn::Aborted(VPlaceholder::default())
|
||||
}
|
||||
}
|
||||
|
||||
/// A reference to a template along with any context needed to hydrate it
|
||||
@@ -42,11 +54,11 @@ pub struct VNode<'a> {
|
||||
pub parent: Option<ElementId>,
|
||||
|
||||
/// The static nodes and static descriptor of the template
|
||||
pub template: Template<'static>,
|
||||
pub template: Cell<Template<'static>>,
|
||||
|
||||
/// The IDs for the roots of this template - to be used when moving the template around and removing it from
|
||||
/// the actual Dom
|
||||
pub root_ids: &'a [Cell<Option<ElementId>>],
|
||||
pub root_ids: BoxedCellSlice,
|
||||
|
||||
/// The dynamic parts of the template
|
||||
pub dynamic_nodes: &'a [DynamicNode<'a>],
|
||||
@@ -55,21 +67,120 @@ pub struct VNode<'a> {
|
||||
pub dynamic_attrs: &'a [Attribute<'a>],
|
||||
}
|
||||
|
||||
// Saftey: There is no way to get references to the internal data of this struct so no refrences will be invalidated by mutating the data with a immutable reference (The same principle behind Cell)
|
||||
#[derive(Debug, Default)]
|
||||
pub struct BoxedCellSlice(UnsafeCell<Option<Box<[ElementId]>>>);
|
||||
|
||||
impl Clone for BoxedCellSlice {
|
||||
fn clone(&self) -> Self {
|
||||
Self(UnsafeCell::new(unsafe { (*self.0.get()).clone() }))
|
||||
}
|
||||
}
|
||||
|
||||
impl BoxedCellSlice {
|
||||
pub fn last(&self) -> Option<ElementId> {
|
||||
unsafe {
|
||||
(*self.0.get())
|
||||
.as_ref()
|
||||
.and_then(|inner| inner.as_ref().last().copied())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, idx: usize) -> Option<ElementId> {
|
||||
unsafe {
|
||||
(*self.0.get())
|
||||
.as_ref()
|
||||
.and_then(|inner| inner.as_ref().get(idx).copied())
|
||||
}
|
||||
}
|
||||
|
||||
pub unsafe fn get_unchecked(&self, idx: usize) -> Option<ElementId> {
|
||||
(*self.0.get())
|
||||
.as_ref()
|
||||
.and_then(|inner| inner.as_ref().get(idx).copied())
|
||||
}
|
||||
|
||||
pub fn set(&self, idx: usize, new: ElementId) {
|
||||
unsafe {
|
||||
if let Some(inner) = &mut *self.0.get() {
|
||||
inner[idx] = new;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn intialize(&self, contents: Box<[ElementId]>) {
|
||||
unsafe {
|
||||
*self.0.get() = Some(contents);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn transfer(&self, other: &Self) {
|
||||
unsafe {
|
||||
*self.0.get() = (*other.0.get()).clone();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take_from(&self, other: &Self) {
|
||||
unsafe {
|
||||
*self.0.get() = (*other.0.get()).take();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
unsafe {
|
||||
(*self.0.get())
|
||||
.as_ref()
|
||||
.map(|inner| inner.len())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a BoxedCellSlice {
|
||||
type Item = ElementId;
|
||||
|
||||
type IntoIter = BoxedCellSliceIter<'a>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
BoxedCellSliceIter {
|
||||
index: 0,
|
||||
borrow: self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BoxedCellSliceIter<'a> {
|
||||
index: usize,
|
||||
borrow: &'a BoxedCellSlice,
|
||||
}
|
||||
|
||||
impl Iterator for BoxedCellSliceIter<'_> {
|
||||
type Item = ElementId;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let result = self.borrow.get(self.index);
|
||||
if result.is_some() {
|
||||
self.index += 1;
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> VNode<'a> {
|
||||
/// Create a template with no nodes that will be skipped over during diffing
|
||||
pub fn empty() -> Element<'a> {
|
||||
Some(VNode {
|
||||
key: None,
|
||||
parent: None,
|
||||
root_ids: &[],
|
||||
root_ids: BoxedCellSlice::default(),
|
||||
dynamic_nodes: &[],
|
||||
dynamic_attrs: &[],
|
||||
template: Template {
|
||||
template: Cell::new(Template {
|
||||
name: "dioxus-empty",
|
||||
roots: &[],
|
||||
node_paths: &[],
|
||||
attr_paths: &[],
|
||||
},
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -77,21 +188,13 @@ 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] {
|
||||
match &self.template.get().roots[idx] {
|
||||
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.
|
||||
@@ -102,32 +205,85 @@ impl<'a> VNode<'a> {
|
||||
///
|
||||
/// For this to work properly, the [`Template::name`] *must* be unique across your entire project. This can be done via variety of
|
||||
/// ways, with the suggested approach being the unique code location (file, line, col, etc).
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord)]
|
||||
pub struct Template<'a> {
|
||||
/// The name of the template. This must be unique across your entire program for template diffing to work properly
|
||||
///
|
||||
/// If two templates have the same name, it's likely that Dioxus will panic when diffing.
|
||||
#[cfg_attr(
|
||||
feature = "serialize",
|
||||
serde(deserialize_with = "deserialize_string_leaky")
|
||||
)]
|
||||
pub name: &'a str,
|
||||
|
||||
/// The list of template nodes that make up the template
|
||||
///
|
||||
/// Unlike react, calls to `rsx!` can have multiple roots. This list supports that paradigm.
|
||||
#[cfg_attr(feature = "serialize", serde(deserialize_with = "deserialize_leaky"))]
|
||||
pub roots: &'a [TemplateNode<'a>],
|
||||
|
||||
/// The paths of each node relative to the root of the template.
|
||||
///
|
||||
/// These will be one segment shorter than the path sent to the renderer since those paths are relative to the
|
||||
/// topmost element, not the `roots` field.
|
||||
#[cfg_attr(
|
||||
feature = "serialize",
|
||||
serde(deserialize_with = "deserialize_bytes_leaky")
|
||||
)]
|
||||
pub node_paths: &'a [&'a [u8]],
|
||||
|
||||
/// The paths of each dynamic attribute relative to the root of the template
|
||||
///
|
||||
/// These will be one segment shorter than the path sent to the renderer since those paths are relative to the
|
||||
/// topmost element, not the `roots` field.
|
||||
#[cfg_attr(
|
||||
feature = "serialize",
|
||||
serde(deserialize_with = "deserialize_bytes_leaky")
|
||||
)]
|
||||
pub attr_paths: &'a [&'a [u8]],
|
||||
}
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
fn deserialize_string_leaky<'a, 'de, D>(deserializer: D) -> Result<&'a str, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
use serde::Deserialize;
|
||||
|
||||
let deserialized = String::deserialize(deserializer)?;
|
||||
Ok(&*Box::leak(deserialized.into_boxed_str()))
|
||||
}
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
fn deserialize_bytes_leaky<'a, 'de, D>(deserializer: D) -> Result<&'a [&'a [u8]], D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
use serde::Deserialize;
|
||||
|
||||
let deserialized = Vec::<Vec<u8>>::deserialize(deserializer)?;
|
||||
let deserialized = deserialized
|
||||
.into_iter()
|
||||
.map(|v| &*Box::leak(v.into_boxed_slice()))
|
||||
.collect::<Vec<_>>();
|
||||
Ok(&*Box::leak(deserialized.into_boxed_slice()))
|
||||
}
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
fn deserialize_leaky<'a, 'de, T: serde::Deserialize<'de>, D>(
|
||||
deserializer: D,
|
||||
) -> Result<&'a [T], D::Error>
|
||||
where
|
||||
T: serde::Deserialize<'de>,
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
use serde::Deserialize;
|
||||
|
||||
let deserialized = Box::<[T]>::deserialize(deserializer)?;
|
||||
Ok(&*Box::leak(deserialized))
|
||||
}
|
||||
|
||||
impl<'a> Template<'a> {
|
||||
/// Is this template worth caching at all, since it's completely runtime?
|
||||
///
|
||||
@@ -144,7 +300,11 @@ impl<'a> 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), serde(tag = "type"))]
|
||||
#[cfg_attr(
|
||||
feature = "serialize",
|
||||
derive(serde::Serialize, serde::Deserialize),
|
||||
serde(tag = "type")
|
||||
)]
|
||||
pub enum TemplateNode<'a> {
|
||||
/// An statically known element in the dom.
|
||||
///
|
||||
@@ -164,9 +324,11 @@ pub enum TemplateNode<'a> {
|
||||
/// A list of possibly dynamic attribues for this element
|
||||
///
|
||||
/// An attribute on a DOM node, such as `id="my-thing"` or `href="https://example.com"`.
|
||||
#[cfg_attr(feature = "serialize", serde(deserialize_with = "deserialize_leaky"))]
|
||||
attrs: &'a [TemplateAttribute<'a>],
|
||||
|
||||
/// A list of template nodes that define another set of template nodes
|
||||
#[cfg_attr(feature = "serialize", serde(deserialize_with = "deserialize_leaky"))]
|
||||
children: &'a [TemplateNode<'a>],
|
||||
},
|
||||
|
||||
@@ -353,13 +515,108 @@ pub enum AttributeValue<'a> {
|
||||
Listener(RefCell<Option<ListenerCb<'a>>>),
|
||||
|
||||
/// An arbitrary value that implements PartialEq and is static
|
||||
Any(BumpBox<'a, dyn AnyValue>),
|
||||
Any(RefCell<Option<BumpBox<'a, dyn AnyValue>>>),
|
||||
|
||||
/// A "none" value, resulting in the removal of an attribute from the dom
|
||||
None,
|
||||
}
|
||||
|
||||
type ListenerCb<'a> = BumpBox<'a, dyn FnMut(Event<dyn Any>) + 'a>;
|
||||
pub type ListenerCb<'a> = BumpBox<'a, dyn FnMut(Event<dyn Any>) + 'a>;
|
||||
|
||||
/// Any of the built-in values that the Dioxus VirtualDom supports as dynamic attributes on elements that are borrowed
|
||||
///
|
||||
/// These varients are used to communicate what the value of an attribute is that needs to be updated
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "serialize", serde(untagged))]
|
||||
pub enum BorrowedAttributeValue<'a> {
|
||||
/// Text attribute
|
||||
Text(&'a str),
|
||||
|
||||
/// A float
|
||||
Float(f64),
|
||||
|
||||
/// Signed integer
|
||||
Int(i64),
|
||||
|
||||
/// Boolean
|
||||
Bool(bool),
|
||||
|
||||
/// An arbitrary value that implements PartialEq and is static
|
||||
#[cfg_attr(
|
||||
feature = "serialize",
|
||||
serde(
|
||||
deserialize_with = "deserialize_any_value",
|
||||
serialize_with = "serialize_any_value"
|
||||
)
|
||||
)]
|
||||
Any(std::cell::Ref<'a, dyn AnyValue>),
|
||||
|
||||
/// A "none" value, resulting in the removal of an attribute from the dom
|
||||
None,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a AttributeValue<'a>> for BorrowedAttributeValue<'a> {
|
||||
fn from(value: &'a AttributeValue<'a>) -> Self {
|
||||
match value {
|
||||
AttributeValue::Text(value) => BorrowedAttributeValue::Text(value),
|
||||
AttributeValue::Float(value) => BorrowedAttributeValue::Float(*value),
|
||||
AttributeValue::Int(value) => BorrowedAttributeValue::Int(*value),
|
||||
AttributeValue::Bool(value) => BorrowedAttributeValue::Bool(*value),
|
||||
AttributeValue::Listener(_) => {
|
||||
panic!("A listener cannot be turned into a borrowed value")
|
||||
}
|
||||
AttributeValue::Any(value) => {
|
||||
let value = value.borrow();
|
||||
BorrowedAttributeValue::Any(std::cell::Ref::map(value, |value| {
|
||||
&**value.as_ref().unwrap()
|
||||
}))
|
||||
}
|
||||
AttributeValue::None => BorrowedAttributeValue::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for BorrowedAttributeValue<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Text(arg0) => f.debug_tuple("Text").field(arg0).finish(),
|
||||
Self::Float(arg0) => f.debug_tuple("Float").field(arg0).finish(),
|
||||
Self::Int(arg0) => f.debug_tuple("Int").field(arg0).finish(),
|
||||
Self::Bool(arg0) => f.debug_tuple("Bool").field(arg0).finish(),
|
||||
Self::Any(_) => f.debug_tuple("Any").field(&"...").finish(),
|
||||
Self::None => write!(f, "None"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for BorrowedAttributeValue<'_> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::Text(l0), Self::Text(r0)) => l0 == r0,
|
||||
(Self::Float(l0), Self::Float(r0)) => l0 == r0,
|
||||
(Self::Int(l0), Self::Int(r0)) => l0 == r0,
|
||||
(Self::Bool(l0), Self::Bool(r0)) => l0 == r0,
|
||||
(Self::Any(l0), Self::Any(r0)) => l0.any_cmp(&**r0),
|
||||
_ => core::mem::discriminant(self) == core::mem::discriminant(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
fn serialize_any_value<S>(_: &std::cell::Ref<'_, dyn AnyValue>, _: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
panic!("Any cannot be serialized")
|
||||
}
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
fn deserialize_any_value<'de, 'a, D>(_: D) -> Result<std::cell::Ref<'a, dyn AnyValue>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
panic!("Any cannot be deserialized")
|
||||
}
|
||||
|
||||
impl<'a> std::fmt::Debug for AttributeValue<'a> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
@@ -383,29 +640,36 @@ impl<'a> PartialEq for AttributeValue<'a> {
|
||||
(Self::Int(l0), Self::Int(r0)) => l0 == r0,
|
||||
(Self::Bool(l0), Self::Bool(r0)) => l0 == r0,
|
||||
(Self::Listener(_), Self::Listener(_)) => true,
|
||||
(Self::Any(l0), Self::Any(r0)) => l0.any_cmp(r0.as_ref()),
|
||||
_ => core::mem::discriminant(self) == core::mem::discriminant(other),
|
||||
(Self::Any(l0), Self::Any(r0)) => {
|
||||
let l0 = l0.borrow();
|
||||
let r0 = r0.borrow();
|
||||
l0.as_ref().unwrap().any_cmp(&**r0.as_ref().unwrap())
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub trait AnyValue {
|
||||
pub trait AnyValue: 'static {
|
||||
fn any_cmp(&self, other: &dyn AnyValue) -> bool;
|
||||
fn our_typeid(&self) -> TypeId;
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
fn type_id(&self) -> TypeId {
|
||||
self.as_any().type_id()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialEq + Any> AnyValue for T {
|
||||
impl<T: Any + PartialEq + 'static> AnyValue for T {
|
||||
fn any_cmp(&self, other: &dyn AnyValue) -> bool {
|
||||
if self.type_id() != other.our_typeid() {
|
||||
return false;
|
||||
if let Some(other) = other.as_any().downcast_ref() {
|
||||
self == other
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
self == unsafe { &*(other as *const _ as *const T) }
|
||||
}
|
||||
|
||||
fn our_typeid(&self) -> TypeId {
|
||||
self.type_id()
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,7 +680,10 @@ pub trait ComponentReturn<'a, A = ()> {
|
||||
|
||||
impl<'a> ComponentReturn<'a> for Element<'a> {
|
||||
fn into_return(self, _cx: &ScopeState) -> RenderReturn<'a> {
|
||||
RenderReturn::Sync(self)
|
||||
match self {
|
||||
Some(node) => RenderReturn::Ready(node),
|
||||
None => RenderReturn::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -428,7 +695,7 @@ where
|
||||
{
|
||||
fn into_return(self, cx: &'a ScopeState) -> RenderReturn<'a> {
|
||||
let f: &mut dyn Future<Output = Element<'a>> = cx.bump().alloc(self);
|
||||
RenderReturn::Async(unsafe { BumpBox::from_raw(f) })
|
||||
RenderReturn::Pending(unsafe { BumpBox::from_raw(f) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -490,15 +757,21 @@ impl<'a, 'b> IntoDynNode<'a> for LazyNodes<'a, 'b> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoDynNode<'_> for &'a str {
|
||||
fn into_vnode(self, cx: &ScopeState) -> DynamicNode {
|
||||
cx.text_node(format_args!("{}", self))
|
||||
impl<'a, 'b> IntoDynNode<'b> for &'a str {
|
||||
fn into_vnode(self, cx: &'b ScopeState) -> DynamicNode<'b> {
|
||||
DynamicNode::Text(VText {
|
||||
value: bumpalo::collections::String::from_str_in(self, cx.bump()).into_bump_str(),
|
||||
id: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDynNode<'_> for String {
|
||||
fn into_vnode(self, cx: &ScopeState) -> DynamicNode {
|
||||
cx.text_node(format_args!("{}", self))
|
||||
DynamicNode::Text(VText {
|
||||
value: cx.bump().alloc(self),
|
||||
id: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -512,8 +785,8 @@ impl<'a> IntoDynNode<'a> for &'a VNode<'a> {
|
||||
fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> {
|
||||
DynamicNode::Fragment(_cx.bump().alloc([VNode {
|
||||
parent: self.parent,
|
||||
template: self.template,
|
||||
root_ids: self.root_ids,
|
||||
template: self.template.clone(),
|
||||
root_ids: self.root_ids.clone(),
|
||||
key: self.key,
|
||||
dynamic_nodes: self.dynamic_nodes,
|
||||
dynamic_attrs: self.dynamic_attrs,
|
||||
@@ -544,7 +817,6 @@ impl<'a, 'b> IntoTemplate<'a> for LazyNodes<'a, 'b> {
|
||||
}
|
||||
|
||||
// Note that we're using the E as a generic but this is never crafted anyways.
|
||||
#[doc(hidden)]
|
||||
pub struct FromNodeIterator;
|
||||
impl<'a, T, I> IntoDynNode<'a, FromNodeIterator> for T
|
||||
where
|
||||
@@ -569,26 +841,36 @@ pub trait IntoAttributeValue<'a> {
|
||||
fn into_value(self, bump: &'a Bump) -> AttributeValue<'a>;
|
||||
}
|
||||
|
||||
impl<'a> IntoAttributeValue<'a> for AttributeValue<'a> {
|
||||
fn into_value(self, _: &'a Bump) -> AttributeValue<'a> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoAttributeValue<'a> for &'a str {
|
||||
fn into_value(self, _: &'a Bump) -> AttributeValue<'a> {
|
||||
AttributeValue::Text(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoAttributeValue<'a> for f64 {
|
||||
fn into_value(self, _: &'a Bump) -> AttributeValue<'a> {
|
||||
AttributeValue::Float(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoAttributeValue<'a> for i64 {
|
||||
fn into_value(self, _: &'a Bump) -> AttributeValue<'a> {
|
||||
AttributeValue::Int(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoAttributeValue<'a> for bool {
|
||||
fn into_value(self, _: &'a Bump) -> AttributeValue<'a> {
|
||||
AttributeValue::Bool(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoAttributeValue<'a> for Arguments<'_> {
|
||||
fn into_value(self, bump: &'a Bump) -> AttributeValue<'a> {
|
||||
use bumpalo::core_alloc::fmt::Write;
|
||||
@@ -597,3 +879,18 @@ impl<'a> IntoAttributeValue<'a> for Arguments<'_> {
|
||||
AttributeValue::Text(str_buf.into_bump_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoAttributeValue<'a> for BumpBox<'a, dyn AnyValue> {
|
||||
fn into_value(self, _: &'a Bump) -> AttributeValue<'a> {
|
||||
AttributeValue::Any(RefCell::new(Some(self)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: IntoAttributeValue<'a>> IntoAttributeValue<'a> for Option<T> {
|
||||
fn into_value(self, bump: &'a Bump) -> AttributeValue<'a> {
|
||||
match self {
|
||||
Some(val) => val.into_value(bump),
|
||||
None => AttributeValue::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,9 @@ use slab::Slab;
|
||||
mod suspense;
|
||||
mod task;
|
||||
mod wait;
|
||||
mod waker;
|
||||
|
||||
pub use suspense::*;
|
||||
pub use task::*;
|
||||
pub use waker::RcWake;
|
||||
|
||||
/// The type of message that can be sent to the scheduler.
|
||||
///
|
||||
@@ -31,10 +29,10 @@ pub(crate) struct Scheduler {
|
||||
pub sender: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
|
||||
|
||||
/// Tasks created with cx.spawn
|
||||
pub tasks: RefCell<Slab<Rc<LocalTask>>>,
|
||||
pub tasks: RefCell<Slab<LocalTask>>,
|
||||
|
||||
/// Async components
|
||||
pub leaves: RefCell<Slab<Rc<SuspenseLeaf>>>,
|
||||
pub leaves: RefCell<Slab<SuspenseLeaf>>,
|
||||
}
|
||||
|
||||
impl Scheduler {
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
use super::{waker::RcWake, SchedulerMsg};
|
||||
use futures_util::task::ArcWake;
|
||||
|
||||
use super::SchedulerMsg;
|
||||
use crate::ElementId;
|
||||
use crate::{innerlude::Mutations, Element, ScopeId};
|
||||
use std::future::Future;
|
||||
use std::sync::Arc;
|
||||
use std::task::Waker;
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
collections::HashSet,
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
/// An ID representing an ongoing suspended component
|
||||
@@ -35,16 +38,19 @@ impl SuspenseContext {
|
||||
}
|
||||
|
||||
pub(crate) struct SuspenseLeaf {
|
||||
pub(crate) id: SuspenseId,
|
||||
pub(crate) scope_id: ScopeId,
|
||||
pub(crate) tx: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
|
||||
pub(crate) notified: Cell<bool>,
|
||||
pub(crate) task: *mut dyn Future<Output = Element<'static>>,
|
||||
pub(crate) waker: Waker,
|
||||
}
|
||||
|
||||
impl RcWake for SuspenseLeaf {
|
||||
fn wake_by_ref(arc_self: &Rc<Self>) {
|
||||
arc_self.notified.set(true);
|
||||
pub struct SuspenseHandle {
|
||||
pub(crate) id: SuspenseId,
|
||||
pub(crate) tx: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
|
||||
}
|
||||
|
||||
impl ArcWake for SuspenseHandle {
|
||||
fn wake_by_ref(arc_self: &Arc<Self>) {
|
||||
_ = arc_self
|
||||
.tx
|
||||
.unbounded_send(SchedulerMsg::SuspenseNotified(arc_self.id));
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
use super::{waker::RcWake, Scheduler, SchedulerMsg};
|
||||
use futures_util::task::ArcWake;
|
||||
|
||||
use super::{Scheduler, SchedulerMsg};
|
||||
use crate::ScopeId;
|
||||
use std::cell::RefCell;
|
||||
use std::future::Future;
|
||||
use std::{pin::Pin, rc::Rc};
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::task::Waker;
|
||||
|
||||
/// A task's unique identifier.
|
||||
///
|
||||
@@ -16,8 +20,7 @@ pub struct TaskId(pub usize);
|
||||
pub(crate) struct LocalTask {
|
||||
pub scope: ScopeId,
|
||||
pub(super) task: RefCell<Pin<Box<dyn Future<Output = ()> + 'static>>>,
|
||||
id: TaskId,
|
||||
tx: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
|
||||
pub waker: Waker,
|
||||
}
|
||||
|
||||
impl Scheduler {
|
||||
@@ -32,15 +35,20 @@ impl Scheduler {
|
||||
/// will only occur when the VirtuaalDom itself has been dropped.
|
||||
pub fn spawn(&self, scope: ScopeId, task: impl Future<Output = ()> + 'static) -> TaskId {
|
||||
let mut tasks = self.tasks.borrow_mut();
|
||||
|
||||
let entry = tasks.vacant_entry();
|
||||
let task_id = TaskId(entry.key());
|
||||
|
||||
entry.insert(Rc::new(LocalTask {
|
||||
id: task_id,
|
||||
tx: self.sender.clone(),
|
||||
let task = LocalTask {
|
||||
task: RefCell::new(Box::pin(task)),
|
||||
scope,
|
||||
}));
|
||||
waker: futures_util::task::waker(Arc::new(LocalTaskHandle {
|
||||
id: task_id,
|
||||
tx: self.sender.clone(),
|
||||
})),
|
||||
};
|
||||
|
||||
entry.insert(task);
|
||||
|
||||
self.sender
|
||||
.unbounded_send(SchedulerMsg::TaskNotified(task_id))
|
||||
@@ -51,15 +59,21 @@ impl Scheduler {
|
||||
|
||||
/// Drop the future with the given TaskId
|
||||
///
|
||||
/// This does nto abort the task, so you'll want to wrap it in an aborthandle if that's important to you
|
||||
/// This does not abort the task, so you'll want to wrap it in an aborthandle if that's important to you
|
||||
pub fn remove(&self, id: TaskId) {
|
||||
self.tasks.borrow_mut().remove(id.0);
|
||||
self.tasks.borrow_mut().try_remove(id.0);
|
||||
}
|
||||
}
|
||||
|
||||
impl RcWake for LocalTask {
|
||||
fn wake_by_ref(arc_self: &Rc<Self>) {
|
||||
_ = arc_self
|
||||
pub struct LocalTaskHandle {
|
||||
id: TaskId,
|
||||
tx: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
|
||||
}
|
||||
|
||||
impl ArcWake for LocalTaskHandle {
|
||||
fn wake_by_ref(arc_self: &Arc<Self>) {
|
||||
// This can fail if the scheduler has been dropped while the application is shutting down
|
||||
let _ = arc_self
|
||||
.tx
|
||||
.unbounded_send(SchedulerMsg::TaskNotified(arc_self.id));
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::{
|
||||
ScopeId, TaskId, VNode, VirtualDom,
|
||||
};
|
||||
|
||||
use super::{waker::RcWake, SuspenseId};
|
||||
use super::SuspenseId;
|
||||
|
||||
impl VirtualDom {
|
||||
/// Handle notifications by tasks inside the scheduler
|
||||
@@ -22,42 +22,37 @@ impl VirtualDom {
|
||||
|
||||
let task = match tasks.get(id.0) {
|
||||
Some(task) => task,
|
||||
// The task was removed from the scheduler, so we can just ignore it
|
||||
None => return,
|
||||
};
|
||||
|
||||
let waker = task.waker();
|
||||
let mut cx = Context::from_waker(&waker);
|
||||
let mut cx = Context::from_waker(&task.waker);
|
||||
|
||||
// If the task completes...
|
||||
if task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() {
|
||||
// Remove it from the scope so we dont try to double drop it when the scope dropes
|
||||
self.scopes[task.scope.0].spawned_tasks.remove(&id);
|
||||
let scope = &self.scopes[task.scope];
|
||||
scope.spawned_tasks.borrow_mut().remove(&id);
|
||||
|
||||
// Remove it from the scheduler
|
||||
tasks.remove(id.0);
|
||||
tasks.try_remove(id.0);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn acquire_suspense_boundary(&self, id: ScopeId) -> Rc<SuspenseContext> {
|
||||
self.scopes[id.0]
|
||||
self.scopes[id]
|
||||
.consume_context::<Rc<SuspenseContext>>()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn handle_suspense_wakeup(&mut self, id: SuspenseId) {
|
||||
let leaf = self
|
||||
.scheduler
|
||||
.leaves
|
||||
.borrow_mut()
|
||||
.get(id.0)
|
||||
.unwrap()
|
||||
.clone();
|
||||
let leaves = self.scheduler.leaves.borrow_mut();
|
||||
let leaf = leaves.get(id.0).unwrap();
|
||||
|
||||
let scope_id = leaf.scope_id;
|
||||
|
||||
// todo: cache the waker
|
||||
let waker = leaf.waker();
|
||||
let mut cx = Context::from_waker(&waker);
|
||||
let mut cx = Context::from_waker(&leaf.waker);
|
||||
|
||||
// Safety: the future is always pinned to the bump arena
|
||||
let mut pinned = unsafe { std::pin::Pin::new_unchecked(&mut *leaf.task) };
|
||||
@@ -67,18 +62,21 @@ impl VirtualDom {
|
||||
// we should attach them to that component and then render its children
|
||||
// continue rendering the tree until we hit yet another suspended component
|
||||
if let Poll::Ready(new_nodes) = as_pinned_mut.poll_unpin(&mut cx) {
|
||||
// safety: we're not going to modify the suspense context but we don't want to make a clone of it
|
||||
let fiber = self.acquire_suspense_boundary(leaf.scope_id);
|
||||
|
||||
let scope = &mut self.scopes[scope_id.0];
|
||||
let scope = &self.scopes[scope_id];
|
||||
let arena = scope.current_frame();
|
||||
|
||||
let ret = arena.bump.alloc(RenderReturn::Sync(new_nodes));
|
||||
let ret = arena.bump().alloc(match new_nodes {
|
||||
Some(new) => RenderReturn::Ready(new),
|
||||
None => RenderReturn::default(),
|
||||
});
|
||||
|
||||
arena.node.set(ret);
|
||||
|
||||
fiber.waiting_on.borrow_mut().remove(&id);
|
||||
|
||||
if let RenderReturn::Sync(Some(template)) = ret {
|
||||
if let RenderReturn::Ready(template) = ret {
|
||||
let mutations_ref = &mut fiber.mutations.borrow_mut();
|
||||
let mutations = &mut **mutations_ref;
|
||||
let template: &VNode = unsafe { std::mem::transmute(template) };
|
||||
@@ -88,6 +86,9 @@ impl VirtualDom {
|
||||
|
||||
let place_holder_id = scope.placeholder.get().unwrap();
|
||||
self.scope_stack.push(scope_id);
|
||||
|
||||
drop(leaves);
|
||||
|
||||
let created = self.create(template);
|
||||
self.scope_stack.pop();
|
||||
mutations.push(Mutation::ReplaceWith {
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
use std::task::{RawWaker, RawWakerVTable, Waker};
|
||||
use std::{mem, rc::Rc};
|
||||
|
||||
pub trait RcWake: Sized {
|
||||
/// Create a waker from this self-wakening object
|
||||
fn waker(self: &Rc<Self>) -> Waker {
|
||||
unsafe fn rc_vtable<T: RcWake>() -> &'static RawWakerVTable {
|
||||
&RawWakerVTable::new(
|
||||
|data| {
|
||||
let arc = mem::ManuallyDrop::new(Rc::<T>::from_raw(data.cast::<T>()));
|
||||
let _rc_clone: mem::ManuallyDrop<_> = arc.clone();
|
||||
RawWaker::new(data, rc_vtable::<T>())
|
||||
},
|
||||
|data| Rc::from_raw(data.cast::<T>()).wake(),
|
||||
|data| {
|
||||
let arc = mem::ManuallyDrop::new(Rc::<T>::from_raw(data.cast::<T>()));
|
||||
RcWake::wake_by_ref(&arc);
|
||||
},
|
||||
|data| drop(Rc::<T>::from_raw(data.cast::<T>())),
|
||||
)
|
||||
}
|
||||
|
||||
unsafe {
|
||||
Waker::from_raw(RawWaker::new(
|
||||
Rc::into_raw(self.clone()).cast(),
|
||||
rc_vtable::<Self>(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn wake_by_ref(arc_self: &Rc<Self>);
|
||||
|
||||
fn wake(self: Rc<Self>) {
|
||||
Self::wake_by_ref(&self)
|
||||
}
|
||||
}
|
||||
@@ -2,18 +2,16 @@ use crate::{
|
||||
any_props::AnyProps,
|
||||
bump_frame::BumpFrame,
|
||||
innerlude::DirtyScope,
|
||||
innerlude::{SuspenseId, SuspenseLeaf},
|
||||
innerlude::{SuspenseHandle, SuspenseId, SuspenseLeaf},
|
||||
nodes::RenderReturn,
|
||||
scheduler::RcWake,
|
||||
scopes::{ScopeId, ScopeState},
|
||||
virtual_dom::VirtualDom,
|
||||
};
|
||||
use bumpalo::Bump;
|
||||
use futures_util::FutureExt;
|
||||
use std::{
|
||||
mem,
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
@@ -26,9 +24,9 @@ impl VirtualDom {
|
||||
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) };
|
||||
let id = ScopeId(entry.key());
|
||||
let id = entry.key();
|
||||
|
||||
entry.insert(Box::new(ScopeState {
|
||||
entry.insert(ScopeState {
|
||||
parent,
|
||||
id,
|
||||
height,
|
||||
@@ -45,15 +43,14 @@ impl VirtualDom {
|
||||
hook_idx: Default::default(),
|
||||
shared_contexts: Default::default(),
|
||||
borrowed_props: Default::default(),
|
||||
listeners: Default::default(),
|
||||
}))
|
||||
attributes_to_drop: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
fn acquire_current_scope_raw(&mut self) -> Option<*mut ScopeState> {
|
||||
self.scope_stack
|
||||
.last()
|
||||
.copied()
|
||||
.and_then(|id| self.scopes.get_mut(id.0).map(|f| f.as_mut() as *mut _))
|
||||
fn acquire_current_scope_raw(&self) -> Option<*const ScopeState> {
|
||||
let id = self.scope_stack.last().copied()?;
|
||||
let scope = self.scopes.get(id)?;
|
||||
Some(scope)
|
||||
}
|
||||
|
||||
pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> &RenderReturn {
|
||||
@@ -63,42 +60,37 @@ impl VirtualDom {
|
||||
self.ensure_drop_safety(scope_id);
|
||||
|
||||
let mut new_nodes = unsafe {
|
||||
let scope = self.scopes[scope_id.0].as_mut();
|
||||
self.scopes[scope_id].previous_frame().bump_mut().reset();
|
||||
|
||||
// if this frame hasn't been intialized yet, we can guess the size of the next frame to be more efficient
|
||||
if scope.previous_frame().bump.allocated_bytes() == 0 {
|
||||
scope.previous_frame_mut().bump =
|
||||
Bump::with_capacity(scope.current_frame().bump.allocated_bytes());
|
||||
} else {
|
||||
scope.previous_frame_mut().bump.reset();
|
||||
}
|
||||
let scope = &self.scopes[scope_id];
|
||||
|
||||
// Make sure to reset the hook counter so we give out hooks in the right order
|
||||
scope.hook_idx.set(0);
|
||||
|
||||
// safety: due to how we traverse the tree, we know that the scope is not currently aliased
|
||||
let props: &dyn AnyProps = scope.props.as_ref().unwrap().as_ref();
|
||||
let props: &dyn AnyProps = mem::transmute(props);
|
||||
|
||||
props.render(scope).extend_lifetime()
|
||||
};
|
||||
|
||||
// immediately resolve futures that can be resolved
|
||||
if let RenderReturn::Async(task) = &mut new_nodes {
|
||||
if let RenderReturn::Pending(task) = &mut new_nodes {
|
||||
let mut leaves = self.scheduler.leaves.borrow_mut();
|
||||
|
||||
let entry = leaves.vacant_entry();
|
||||
let suspense_id = SuspenseId(entry.key());
|
||||
|
||||
let leaf = Rc::new(SuspenseLeaf {
|
||||
let leaf = SuspenseLeaf {
|
||||
scope_id,
|
||||
task: task.as_mut(),
|
||||
id: suspense_id,
|
||||
tx: self.scheduler.sender.clone(),
|
||||
notified: Default::default(),
|
||||
});
|
||||
waker: futures_util::task::waker(Arc::new(SuspenseHandle {
|
||||
id: suspense_id,
|
||||
tx: self.scheduler.sender.clone(),
|
||||
})),
|
||||
};
|
||||
|
||||
let waker = leaf.waker();
|
||||
let mut cx = Context::from_waker(&waker);
|
||||
let mut cx = Context::from_waker(&leaf.waker);
|
||||
|
||||
// safety: the task is already pinned in the bump arena
|
||||
let mut pinned = unsafe { Pin::new_unchecked(task.as_mut()) };
|
||||
@@ -108,7 +100,11 @@ impl VirtualDom {
|
||||
match pinned.poll_unpin(&mut cx) {
|
||||
// If nodes are produced, then set it and we can break
|
||||
Poll::Ready(nodes) => {
|
||||
new_nodes = RenderReturn::Sync(nodes);
|
||||
new_nodes = match nodes {
|
||||
Some(nodes) => RenderReturn::Ready(nodes),
|
||||
None => RenderReturn::default(),
|
||||
};
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -131,13 +127,13 @@ impl VirtualDom {
|
||||
}
|
||||
};
|
||||
|
||||
let scope = &self.scopes[scope_id.0];
|
||||
let scope = &self.scopes[scope_id];
|
||||
|
||||
// We write on top of the previous frame and then make it the current by pushing the generation forward
|
||||
let frame = scope.previous_frame();
|
||||
|
||||
// set the new head of the bump frame
|
||||
let allocated = &*frame.bump.alloc(new_nodes);
|
||||
let allocated = &*frame.bump().alloc(new_nodes);
|
||||
frame.node.set(allocated);
|
||||
|
||||
// And move the render generation forward by one
|
||||
@@ -150,6 +146,6 @@ impl VirtualDom {
|
||||
});
|
||||
|
||||
// rebind the lifetime now that its stored internally
|
||||
unsafe { mem::transmute(allocated) }
|
||||
unsafe { allocated.extend_lifetime_ref() }
|
||||
}
|
||||
}
|
||||
+144
-31
@@ -4,18 +4,21 @@ use crate::{
|
||||
arena::ElementId,
|
||||
bump_frame::BumpFrame,
|
||||
innerlude::{DynamicNode, EventHandler, VComponent, VText},
|
||||
innerlude::{Scheduler, SchedulerMsg},
|
||||
innerlude::{ErrorBoundary, Scheduler, SchedulerMsg},
|
||||
lazynodes::LazyNodes,
|
||||
nodes::{ComponentReturn, IntoAttributeValue, IntoDynNode, RenderReturn},
|
||||
Attribute, AttributeValue, Element, Event, Properties, TaskId,
|
||||
AnyValue, Attribute, AttributeValue, Element, Event, Properties, TaskId,
|
||||
};
|
||||
use bumpalo::{boxed::Box as BumpBox, Bump};
|
||||
use bumpslab::{BumpSlab, Slot};
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use slab::{Slab, VacantEntry};
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
cell::{Cell, RefCell},
|
||||
fmt::Arguments,
|
||||
fmt::{Arguments, Debug},
|
||||
future::Future,
|
||||
ops::{Index, IndexMut},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
@@ -63,6 +66,95 @@ impl<'a, T> std::ops::Deref for Scoped<'a, T> {
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
|
||||
pub struct ScopeId(pub usize);
|
||||
|
||||
/// A thin wrapper around a BumpSlab that uses ids to index into the slab.
|
||||
pub(crate) struct ScopeSlab {
|
||||
slab: BumpSlab<ScopeState>,
|
||||
// a slab of slots of stable pointers to the ScopeState in the bump slab
|
||||
entries: Slab<Slot<'static, ScopeState>>,
|
||||
}
|
||||
|
||||
impl Drop for ScopeSlab {
|
||||
fn drop(&mut self) {
|
||||
// Bump slab doesn't drop its contents, so we need to do it manually
|
||||
for slot in self.entries.drain() {
|
||||
self.slab.remove(slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ScopeSlab {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
slab: BumpSlab::new(),
|
||||
entries: Slab::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ScopeSlab {
|
||||
pub(crate) fn get(&self, id: ScopeId) -> Option<&ScopeState> {
|
||||
self.entries.get(id.0).map(|slot| unsafe { &*slot.ptr() })
|
||||
}
|
||||
|
||||
pub(crate) fn get_mut(&mut self, id: ScopeId) -> Option<&mut ScopeState> {
|
||||
self.entries
|
||||
.get(id.0)
|
||||
.map(|slot| unsafe { &mut *slot.ptr_mut() })
|
||||
}
|
||||
|
||||
pub(crate) fn vacant_entry(&mut self) -> ScopeSlabEntry {
|
||||
let entry = self.entries.vacant_entry();
|
||||
ScopeSlabEntry {
|
||||
slab: &mut self.slab,
|
||||
entry,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn remove(&mut self, id: ScopeId) {
|
||||
self.slab.remove(self.entries.remove(id.0));
|
||||
}
|
||||
|
||||
pub(crate) fn contains(&self, id: ScopeId) -> bool {
|
||||
self.entries.contains(id.0)
|
||||
}
|
||||
|
||||
pub(crate) fn iter(&self) -> impl Iterator<Item = &ScopeState> {
|
||||
self.entries.iter().map(|(_, slot)| unsafe { &*slot.ptr() })
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ScopeSlabEntry<'a> {
|
||||
slab: &'a mut BumpSlab<ScopeState>,
|
||||
entry: VacantEntry<'a, Slot<'static, ScopeState>>,
|
||||
}
|
||||
|
||||
impl<'a> ScopeSlabEntry<'a> {
|
||||
pub(crate) fn key(&self) -> ScopeId {
|
||||
ScopeId(self.entry.key())
|
||||
}
|
||||
|
||||
pub(crate) fn insert(self, scope: ScopeState) -> &'a ScopeState {
|
||||
let slot = self.slab.push(scope);
|
||||
// this is safe because the slot is only ever accessed with the lifetime of the borrow of the slab
|
||||
let slot = unsafe { std::mem::transmute(slot) };
|
||||
let entry = self.entry.insert(slot);
|
||||
unsafe { &*entry.ptr() }
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<ScopeId> for ScopeSlab {
|
||||
type Output = ScopeState;
|
||||
fn index(&self, id: ScopeId) -> &Self::Output {
|
||||
self.get(id).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl IndexMut<ScopeId> for ScopeSlab {
|
||||
fn index_mut(&mut self, id: ScopeId) -> &mut Self::Output {
|
||||
self.get_mut(id).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// A component's state separate from its props.
|
||||
///
|
||||
/// This struct exists to provide a common interface for all scopes without relying on generics.
|
||||
@@ -73,7 +165,7 @@ pub struct ScopeState {
|
||||
pub(crate) node_arena_1: BumpFrame,
|
||||
pub(crate) node_arena_2: BumpFrame,
|
||||
|
||||
pub(crate) parent: Option<*mut ScopeState>,
|
||||
pub(crate) parent: Option<*const ScopeState>,
|
||||
pub(crate) id: ScopeId,
|
||||
|
||||
pub(crate) height: u32,
|
||||
@@ -85,10 +177,10 @@ pub struct ScopeState {
|
||||
pub(crate) shared_contexts: RefCell<FxHashMap<TypeId, Box<dyn Any>>>,
|
||||
|
||||
pub(crate) tasks: Rc<Scheduler>,
|
||||
pub(crate) spawned_tasks: FxHashSet<TaskId>,
|
||||
pub(crate) spawned_tasks: RefCell<FxHashSet<TaskId>>,
|
||||
|
||||
pub(crate) borrowed_props: RefCell<Vec<*const VComponent<'static>>>,
|
||||
pub(crate) listeners: RefCell<Vec<*const Attribute<'static>>>,
|
||||
pub(crate) attributes_to_drop: RefCell<Vec<*const Attribute<'static>>>,
|
||||
|
||||
pub(crate) props: Option<Box<dyn AnyProps<'static>>>,
|
||||
pub(crate) placeholder: Cell<Option<ElementId>>,
|
||||
@@ -111,14 +203,6 @@ impl<'src> ScopeState {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn previous_frame_mut(&mut self) -> &mut BumpFrame {
|
||||
match self.render_cnt.get() % 2 {
|
||||
1 => &mut self.node_arena_1,
|
||||
0 => &mut self.node_arena_2,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the name of this component
|
||||
pub fn name(&self) -> &str {
|
||||
self.name
|
||||
@@ -139,7 +223,7 @@ impl<'src> ScopeState {
|
||||
/// If you need to allocate items that need to be dropped, use bumpalo's box.
|
||||
pub fn bump(&self) -> &Bump {
|
||||
// note that this is actually the previous frame since we use that as scratch space while the component is rendering
|
||||
&self.previous_frame().bump
|
||||
self.previous_frame().bump()
|
||||
}
|
||||
|
||||
/// Get a handle to the currently active head node arena for this Scope
|
||||
@@ -289,17 +373,12 @@ impl<'src> ScopeState {
|
||||
None
|
||||
}
|
||||
|
||||
/// Expose state to children further down the [`crate::VirtualDom`] Tree. Does not require `clone` on the context,
|
||||
/// though we do recommend it.
|
||||
/// Expose state to children further down the [`crate::VirtualDom`] Tree. Requires `Clone` on the context to allow getting values down the tree.
|
||||
///
|
||||
/// 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_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
|
||||
/// behavior might change in the future.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust, ignore
|
||||
@@ -327,7 +406,9 @@ impl<'src> ScopeState {
|
||||
|
||||
/// Pushes the future onto the poll queue to be polled after the component renders.
|
||||
pub fn push_future(&self, fut: impl Future<Output = ()> + 'static) -> TaskId {
|
||||
self.tasks.spawn(self.id, fut)
|
||||
let id = self.tasks.spawn(self.id, fut);
|
||||
self.spawned_tasks.borrow_mut().insert(id);
|
||||
id
|
||||
}
|
||||
|
||||
/// Spawns the future but does not return the [`TaskId`]
|
||||
@@ -348,6 +429,8 @@ impl<'src> ScopeState {
|
||||
.unbounded_send(SchedulerMsg::TaskNotified(id))
|
||||
.expect("Scheduler should exist");
|
||||
|
||||
self.spawned_tasks.borrow_mut().insert(id);
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
@@ -374,19 +457,25 @@ impl<'src> ScopeState {
|
||||
pub fn render(&'src self, rsx: LazyNodes<'src, '_>) -> Element<'src> {
|
||||
let element = rsx.call(self);
|
||||
|
||||
let mut listeners = self.listeners.borrow_mut();
|
||||
let mut listeners = self.attributes_to_drop.borrow_mut();
|
||||
for attr in element.dynamic_attrs {
|
||||
if let AttributeValue::Listener(_) = attr.value {
|
||||
let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) };
|
||||
listeners.push(unbounded);
|
||||
match attr.value {
|
||||
AttributeValue::Any(_) | AttributeValue::Listener(_) => {
|
||||
let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) };
|
||||
listeners.push(unbounded);
|
||||
}
|
||||
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
let mut props = self.borrowed_props.borrow_mut();
|
||||
for node in element.dynamic_nodes {
|
||||
if let DynamicNode::Component(comp) = node {
|
||||
let unbounded = unsafe { std::mem::transmute(comp as *const VComponent) };
|
||||
props.push(unbounded);
|
||||
if !comp.static_props {
|
||||
let unbounded = unsafe { std::mem::transmute(comp as *const VComponent) };
|
||||
props.push(unbounded);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -499,9 +588,9 @@ impl<'src> ScopeState {
|
||||
BumpBox::from_raw(self.bump().alloc(move |event: Event<dyn Any>| {
|
||||
if let Ok(data) = event.data.downcast::<T>() {
|
||||
callback(Event {
|
||||
propogates: event.propogates,
|
||||
propagates: event.propagates,
|
||||
data,
|
||||
})
|
||||
});
|
||||
}
|
||||
}))
|
||||
};
|
||||
@@ -509,6 +598,30 @@ impl<'src> ScopeState {
|
||||
AttributeValue::Listener(RefCell::new(Some(boxed)))
|
||||
}
|
||||
|
||||
/// Create a new [`AttributeValue`] with a value that implements [`AnyValue`]
|
||||
pub fn any_value<T: AnyValue>(&'src self, value: T) -> AttributeValue<'src> {
|
||||
// safety: there's no other way to create a dynamicly-dispatched bump box other than alloc + from-raw
|
||||
// This is the suggested way to build a bumpbox
|
||||
//
|
||||
// In theory, we could just use regular boxes
|
||||
let boxed: BumpBox<'src, dyn AnyValue> =
|
||||
unsafe { BumpBox::from_raw(self.bump().alloc(value)) };
|
||||
AttributeValue::Any(RefCell::new(Some(boxed)))
|
||||
}
|
||||
|
||||
/// Inject an error into the nearest error boundary and quit rendering
|
||||
///
|
||||
/// The error doesn't need to implement Error or any specific traits since the boundary
|
||||
/// itself will downcast the error into a trait object.
|
||||
pub fn throw(&self, error: impl Debug + 'static) -> Option<()> {
|
||||
if let Some(cx) = self.consume_context::<Rc<ErrorBoundary>>() {
|
||||
cx.insert_error(self.scope_id(), Box::new(error));
|
||||
}
|
||||
|
||||
// Always return none during a throw
|
||||
None
|
||||
}
|
||||
|
||||
/// Store a value between renders. The foundational hook for all other hooks.
|
||||
///
|
||||
/// Accepts an `initializer` closure, which is run on the first use of the hook (typically the initial render). The return value of this closure is stored for the lifetime of the component, and a mutable reference to it is provided on every render as the return value of `use_hook`.
|
||||
@@ -528,7 +641,7 @@ impl<'src> ScopeState {
|
||||
#[allow(clippy::mut_from_ref)]
|
||||
pub fn use_hook<State: 'static>(&self, initializer: impl FnOnce() -> State) -> &mut State {
|
||||
let cur_hook = self.hook_idx.get();
|
||||
let mut hook_list = self.hook_list.borrow_mut();
|
||||
let mut hook_list = self.hook_list.try_borrow_mut().expect("The hook list is already borrowed: This error is likely caused by trying to use a hook inside a hook which violates the rules of hooks.");
|
||||
|
||||
if cur_hook >= hook_list.len() {
|
||||
hook_list.push(self.hook_arena.alloc(initializer()));
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
use crate::{
|
||||
any_props::VProps,
|
||||
arena::{ElementId, ElementRef},
|
||||
innerlude::{DirtyScope, ErrorBoundary, Mutations, Scheduler, SchedulerMsg},
|
||||
innerlude::{DirtyScope, ErrorBoundary, Mutations, Scheduler, SchedulerMsg, ScopeSlab},
|
||||
mutations::Mutation,
|
||||
nodes::RenderReturn,
|
||||
nodes::{Template, TemplateId},
|
||||
@@ -24,7 +24,9 @@ use std::{any::Any, borrow::BorrowMut, cell::Cell, collections::BTreeSet, future
|
||||
///
|
||||
/// Components are defined as simple functions that take [`Scope`] and return an [`Element`].
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// ```rust
|
||||
/// # use dioxus::prelude::*;
|
||||
///
|
||||
/// #[derive(Props, PartialEq)]
|
||||
/// struct AppProps {
|
||||
/// title: String
|
||||
@@ -39,7 +41,17 @@ use std::{any::Any, borrow::BorrowMut, cell::Cell, collections::BTreeSet, future
|
||||
///
|
||||
/// Components may be composed to make complex apps.
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// ```rust
|
||||
/// # #![allow(unused)]
|
||||
/// # use dioxus::prelude::*;
|
||||
///
|
||||
/// # #[derive(Props, PartialEq)]
|
||||
/// # struct AppProps {
|
||||
/// # title: String
|
||||
/// # }
|
||||
///
|
||||
/// static ROUTES: &str = "";
|
||||
///
|
||||
/// fn App(cx: Scope<AppProps>) -> Element {
|
||||
/// cx.render(rsx!(
|
||||
/// NavBar { routes: ROUTES }
|
||||
@@ -47,12 +59,33 @@ use std::{any::Any, borrow::BorrowMut, cell::Cell, collections::BTreeSet, future
|
||||
/// Footer {}
|
||||
/// ))
|
||||
/// }
|
||||
///
|
||||
/// #[inline_props]
|
||||
/// fn NavBar(cx: Scope, routes: &'static str) -> Element {
|
||||
/// cx.render(rsx! {
|
||||
/// div { "Routes: {routes}" }
|
||||
/// })
|
||||
/// }
|
||||
///
|
||||
/// fn Footer(cx: Scope) -> Element {
|
||||
/// cx.render(rsx! { div { "Footer" } })
|
||||
/// }
|
||||
///
|
||||
/// #[inline_props]
|
||||
/// fn Title<'a>(cx: Scope<'a>, children: Element<'a>) -> Element {
|
||||
/// cx.render(rsx! {
|
||||
/// div { id: "title", children }
|
||||
/// })
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// To start an app, create a [`VirtualDom`] and call [`VirtualDom::rebuild`] to get the list of edits required to
|
||||
/// draw the UI.
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// ```rust
|
||||
/// # use dioxus::prelude::*;
|
||||
/// # fn App(cx: Scope) -> Element { cx.render(rsx! { div {} }) }
|
||||
///
|
||||
/// let mut vdom = VirtualDom::new(App);
|
||||
/// let edits = vdom.rebuild();
|
||||
/// ```
|
||||
@@ -142,8 +175,9 @@ use std::{any::Any, borrow::BorrowMut, cell::Cell, collections::BTreeSet, future
|
||||
/// }
|
||||
/// ```
|
||||
pub struct VirtualDom {
|
||||
pub(crate) templates: FxHashMap<TemplateId, Template<'static>>,
|
||||
pub(crate) scopes: Slab<Box<ScopeState>>,
|
||||
// Maps a template path to a map of byteindexes to templates
|
||||
pub(crate) templates: FxHashMap<TemplateId, FxHashMap<usize, Template<'static>>>,
|
||||
pub(crate) scopes: ScopeSlab,
|
||||
pub(crate) dirty_scopes: BTreeSet<DirtyScope>,
|
||||
pub(crate) scheduler: Rc<Scheduler>,
|
||||
|
||||
@@ -224,7 +258,7 @@ impl VirtualDom {
|
||||
rx,
|
||||
scheduler: Scheduler::new(tx),
|
||||
templates: Default::default(),
|
||||
scopes: Slab::default(),
|
||||
scopes: Default::default(),
|
||||
elements: Default::default(),
|
||||
scope_stack: Vec::new(),
|
||||
dirty_scopes: BTreeSet::new(),
|
||||
@@ -248,7 +282,7 @@ impl VirtualDom {
|
||||
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());
|
||||
dom.elements.insert(ElementRef::none());
|
||||
|
||||
dom
|
||||
}
|
||||
@@ -257,14 +291,14 @@ impl VirtualDom {
|
||||
///
|
||||
/// This is useful for inserting or removing contexts from a scope, or rendering out its root node
|
||||
pub fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> {
|
||||
self.scopes.get(id.0).map(|f| f.as_ref())
|
||||
self.scopes.get(id)
|
||||
}
|
||||
|
||||
/// Get the single scope at the top of the VirtualDom tree that will always be around
|
||||
///
|
||||
/// This scope has a ScopeId of 0 and is the root of the tree
|
||||
pub fn base_scope(&self) -> &ScopeState {
|
||||
self.scopes.get(0).unwrap()
|
||||
self.scopes.get(ScopeId(0)).unwrap()
|
||||
}
|
||||
|
||||
/// Build the virtualdom with a global context inserted into the base scope
|
||||
@@ -279,7 +313,7 @@ impl VirtualDom {
|
||||
///
|
||||
/// Whenever the VirtualDom "works", it will re-render this scope
|
||||
pub fn mark_dirty(&mut self, id: ScopeId) {
|
||||
if let Some(scope) = self.scopes.get(id.0) {
|
||||
if let Some(scope) = self.scopes.get(id) {
|
||||
let height = scope.height;
|
||||
self.dirty_scopes.insert(DirtyScope { height, id });
|
||||
}
|
||||
@@ -290,7 +324,7 @@ impl VirtualDom {
|
||||
/// This does not mean the scope is waiting on its own futures, just that the tree that the scope exists in is
|
||||
/// currently suspended.
|
||||
pub fn is_scope_suspended(&self, id: ScopeId) -> bool {
|
||||
!self.scopes[id.0]
|
||||
!self.scopes[id]
|
||||
.consume_context::<Rc<SuspenseContext>>()
|
||||
.unwrap()
|
||||
.waiting_on
|
||||
@@ -346,22 +380,24 @@ impl VirtualDom {
|
||||
|
||||
// We will clone this later. The data itself is wrapped in RC to be used in callbacks if required
|
||||
let uievent = Event {
|
||||
propogates: Rc::new(Cell::new(bubbles)),
|
||||
propagates: Rc::new(Cell::new(bubbles)),
|
||||
data,
|
||||
};
|
||||
|
||||
// Loop through each dynamic attribute in this template before moving up to the template's parent.
|
||||
while let Some(el_ref) = parent_path {
|
||||
// safety: we maintain references of all vnodes in the element slab
|
||||
let template = unsafe { &*el_ref.template };
|
||||
let template = unsafe { el_ref.template.unwrap().as_ref() };
|
||||
let node_template = template.template.get();
|
||||
let target_path = el_ref.path;
|
||||
|
||||
for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
|
||||
let this_path = template.template.attr_paths[idx];
|
||||
let this_path = node_template.attr_paths[idx];
|
||||
|
||||
// listeners are required to be prefixed with "on", but they come back to the virtualdom with that missing
|
||||
// we should fix this so that we look for "onclick" instead of "click"
|
||||
if &attr.name[2..] == name && target_path.is_ascendant(&this_path) {
|
||||
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
|
||||
if attr.name.trim_start_matches("on") == name
|
||||
&& target_path.is_decendant(&this_path)
|
||||
{
|
||||
listeners.push(&attr.value);
|
||||
|
||||
// Break if the event doesn't bubble anyways
|
||||
@@ -386,7 +422,7 @@ impl VirtualDom {
|
||||
cb(uievent.clone());
|
||||
}
|
||||
|
||||
if !uievent.propogates.get() {
|
||||
if !uievent.propagates.get() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -454,6 +490,30 @@ impl VirtualDom {
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace a template at runtime. This will re-render all components that use this template.
|
||||
/// This is the primitive that enables hot-reloading.
|
||||
///
|
||||
/// The caller must ensure that the template refrences the same dynamic attributes and nodes as the original template.
|
||||
///
|
||||
/// This will only replace the the parent template, not any nested templates.
|
||||
pub fn replace_template(&mut self, template: Template<'static>) {
|
||||
self.register_template_first_byte_index(template);
|
||||
// iterating a slab is very inefficient, but this is a rare operation that will only happen during development so it's fine
|
||||
for scope in self.scopes.iter() {
|
||||
if let Some(RenderReturn::Ready(sync)) = scope.try_root_node() {
|
||||
if sync.template.get().name.rsplit_once(':').unwrap().0
|
||||
== template.name.rsplit_once(':').unwrap().0
|
||||
{
|
||||
let height = scope.height;
|
||||
self.dirty_scopes.insert(DirtyScope {
|
||||
height,
|
||||
id: scope.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom from scratch.
|
||||
///
|
||||
/// The mutations item expects the RealDom's stack to be the root of the application.
|
||||
@@ -477,7 +537,7 @@ impl VirtualDom {
|
||||
pub fn rebuild(&mut self) -> Mutations {
|
||||
match unsafe { self.run_scope(ScopeId(0)).extend_lifetime_ref() } {
|
||||
// Rebuilding implies we append the created elements to the root
|
||||
RenderReturn::Sync(Some(node)) => {
|
||||
RenderReturn::Ready(node) => {
|
||||
let m = self.create_scope(ScopeId(0), node);
|
||||
self.mutations.edits.push(Mutation::AppendChildren {
|
||||
id: ElementId(0),
|
||||
@@ -485,8 +545,8 @@ impl VirtualDom {
|
||||
});
|
||||
}
|
||||
// If an error occurs, we should try to render the default error component and context where the error occured
|
||||
RenderReturn::Sync(None) => panic!("Cannot catch errors during rebuild"),
|
||||
RenderReturn::Async(_) => unreachable!("Root scope cannot be an async component"),
|
||||
RenderReturn::Aborted(_placeholder) => panic!("Cannot catch errors during rebuild"),
|
||||
RenderReturn::Pending(_) => unreachable!("Root scope cannot be an async component"),
|
||||
}
|
||||
|
||||
self.finalize()
|
||||
@@ -523,7 +583,7 @@ impl VirtualDom {
|
||||
loop {
|
||||
// first, unload any complete suspense trees
|
||||
for finished_fiber in self.finished_fibers.drain(..) {
|
||||
let scope = &mut self.scopes[finished_fiber.0];
|
||||
let scope = &self.scopes[finished_fiber];
|
||||
let context = scope.has_context::<Rc<SuspenseContext>>().unwrap();
|
||||
|
||||
self.mutations
|
||||
@@ -547,7 +607,7 @@ impl VirtualDom {
|
||||
self.dirty_scopes.remove(&dirty);
|
||||
|
||||
// If the scope doesn't exist for whatever reason, then we should skip it
|
||||
if !self.scopes.contains(dirty.id.0) {
|
||||
if !self.scopes.contains(dirty.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -566,7 +626,7 @@ impl VirtualDom {
|
||||
// If suspended leaves are present, then we should find the boundary for this scope and attach things
|
||||
// No placeholder necessary since this is a diff
|
||||
if !self.collected_leaves.is_empty() {
|
||||
let mut boundary = self.scopes[dirty.id.0]
|
||||
let mut boundary = self.scopes[dirty.id]
|
||||
.consume_context::<Rc<SuspenseContext>>()
|
||||
.unwrap();
|
||||
|
||||
@@ -615,16 +675,13 @@ impl VirtualDom {
|
||||
|
||||
/// Swap the current mutations with a new
|
||||
fn finalize(&mut self) -> Mutations {
|
||||
// todo: make this a routine
|
||||
let mut out = Mutations::default();
|
||||
std::mem::swap(&mut self.mutations, &mut out);
|
||||
out
|
||||
std::mem::take(&mut self.mutations)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for VirtualDom {
|
||||
fn drop(&mut self) {
|
||||
// Simply drop this scope which drops all of its children
|
||||
self.drop_scope(ScopeId(0));
|
||||
self.drop_scope(ScopeId(0), true);
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,10 @@
|
||||
//!
|
||||
//! This tests to ensure we clean it up
|
||||
|
||||
use bumpalo::Bump;
|
||||
use dioxus::core::{ElementId, Mutation::*};
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_core::BorrowedAttributeValue;
|
||||
|
||||
#[test]
|
||||
fn attrs_cycle() {
|
||||
@@ -22,6 +24,8 @@ fn attrs_cycle() {
|
||||
}
|
||||
});
|
||||
|
||||
let bump = Bump::new();
|
||||
|
||||
assert_eq!(
|
||||
dom.rebuild().santize().edits,
|
||||
[
|
||||
@@ -36,8 +40,18 @@ fn attrs_cycle() {
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
|
||||
AssignId { path: &[0,], id: ElementId(3,) },
|
||||
SetAttribute { name: "class", value: "1", id: ElementId(3,), ns: None },
|
||||
SetAttribute { name: "id", value: "1", id: ElementId(3,), ns: None },
|
||||
SetAttribute {
|
||||
name: "class",
|
||||
value: (&*bump.alloc("1".into_value(&bump))).into(),
|
||||
id: ElementId(3,),
|
||||
ns: None
|
||||
},
|
||||
SetAttribute {
|
||||
name: "id",
|
||||
value: (&*bump.alloc("1".into_value(&bump))).into(),
|
||||
id: ElementId(3,),
|
||||
ns: None
|
||||
},
|
||||
ReplaceWith { id: ElementId(1,), m: 1 },
|
||||
]
|
||||
);
|
||||
@@ -57,8 +71,18 @@ fn attrs_cycle() {
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(2) },
|
||||
AssignId { path: &[0], id: ElementId(3) },
|
||||
SetAttribute { name: "class", value: "3", id: ElementId(3), ns: None },
|
||||
SetAttribute { name: "id", value: "3", id: ElementId(3), ns: None },
|
||||
SetAttribute {
|
||||
name: "class",
|
||||
value: BorrowedAttributeValue::Text("3"),
|
||||
id: ElementId(3),
|
||||
ns: None
|
||||
},
|
||||
SetAttribute {
|
||||
name: "id",
|
||||
value: BorrowedAttributeValue::Text("3"),
|
||||
id: ElementId(3),
|
||||
ns: None
|
||||
},
|
||||
ReplaceWith { id: ElementId(1), m: 1 }
|
||||
]
|
||||
);
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
use bumpalo::Bump;
|
||||
use dioxus::core::{ElementId, Mutation::*};
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn bool_test() {
|
||||
let mut app = VirtualDom::new(|cx| cx.render(rsx!(div { hidden: false })));
|
||||
let bump = Bump::new();
|
||||
|
||||
assert_eq!(
|
||||
app.rebuild().santize().edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
|
||||
SetBoolAttribute { name: "hidden", value: false, id: ElementId(1,) },
|
||||
SetAttribute {
|
||||
name: "hidden",
|
||||
value: (&*bump.alloc(false.into_value(&bump))).into(),
|
||||
id: ElementId(1,),
|
||||
ns: None
|
||||
},
|
||||
AppendChildren { m: 1, id: ElementId(0) },
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -18,7 +18,7 @@ fn test_borrowed_state() {
|
||||
ReplacePlaceholder { path: &[0,], m: 1 },
|
||||
AppendChildren { m: 1, id: ElementId(0) },
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
fn Parent(cx: Scope) -> Element {
|
||||
|
||||
@@ -20,7 +20,9 @@ fn app(cx: Scope) -> Element {
|
||||
fn bubbles_error() {
|
||||
let mut dom = VirtualDom::new(app);
|
||||
|
||||
let _edits = dom.rebuild().santize();
|
||||
{
|
||||
let _edits = dom.rebuild().santize();
|
||||
}
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
|
||||
|
||||
@@ -12,14 +12,16 @@ fn cycling_elements() {
|
||||
})
|
||||
});
|
||||
|
||||
let edits = dom.rebuild().santize();
|
||||
assert_eq!(
|
||||
edits.edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
|
||||
AppendChildren { m: 1, id: ElementId(0) },
|
||||
]
|
||||
);
|
||||
{
|
||||
let edits = dom.rebuild().santize();
|
||||
assert_eq!(
|
||||
edits.edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
|
||||
AppendChildren { m: 1, id: ElementId(0) },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
|
||||
@@ -59,19 +59,21 @@ fn component_swap() {
|
||||
}
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
let edits = dom.rebuild().santize();
|
||||
assert_eq!(
|
||||
edits.edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(2) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(3) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(4) },
|
||||
ReplacePlaceholder { path: &[1], m: 3 },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(5) },
|
||||
AppendChildren { m: 2, id: ElementId(0) }
|
||||
]
|
||||
);
|
||||
{
|
||||
let edits = dom.rebuild().santize();
|
||||
assert_eq!(
|
||||
edits.edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(2) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(3) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(4) },
|
||||
ReplacePlaceholder { path: &[1], m: 3 },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(5) },
|
||||
AppendChildren { m: 2, id: ElementId(0) }
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
|
||||
@@ -20,22 +20,24 @@ fn keyed_diffing_out_of_order() {
|
||||
cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" }))))
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
dom.rebuild().santize().edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(3,) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(4,) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(5,) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(6,) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(7,) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(8,) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(9,) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(10,) },
|
||||
AppendChildren { m: 10, id: ElementId(0) },
|
||||
]
|
||||
);
|
||||
{
|
||||
assert_eq!(
|
||||
dom.rebuild().santize().edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(3,) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(4,) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(5,) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(6,) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(7,) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(8,) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(9,) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(10,) },
|
||||
AppendChildren { m: 10, id: ElementId(0) },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
assert_eq!(
|
||||
@@ -44,7 +46,7 @@ fn keyed_diffing_out_of_order() {
|
||||
PushRoot { id: ElementId(7,) },
|
||||
InsertBefore { id: ElementId(5,), m: 1 },
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// Should result in moves only
|
||||
@@ -70,7 +72,7 @@ fn keyed_diffing_out_of_order_adds() {
|
||||
PushRoot { id: ElementId(4,) },
|
||||
InsertBefore { id: ElementId(1,), m: 2 },
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// Should result in moves only
|
||||
|
||||
@@ -315,66 +315,76 @@ fn remove_many() {
|
||||
})
|
||||
});
|
||||
|
||||
let edits = dom.rebuild().santize();
|
||||
assert!(edits.templates.is_empty());
|
||||
assert_eq!(
|
||||
edits.edits,
|
||||
[
|
||||
CreatePlaceholder { id: ElementId(1,) },
|
||||
AppendChildren { id: ElementId(0), m: 1 },
|
||||
]
|
||||
);
|
||||
{
|
||||
let edits = dom.rebuild().santize();
|
||||
assert!(edits.templates.is_empty());
|
||||
assert_eq!(
|
||||
edits.edits,
|
||||
[
|
||||
CreatePlaceholder { id: ElementId(1,) },
|
||||
AppendChildren { id: ElementId(0), m: 1 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
let edits = dom.render_immediate().santize();
|
||||
assert_eq!(
|
||||
edits.edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
|
||||
HydrateText { path: &[0,], value: "hello 0", id: ElementId(3,) },
|
||||
ReplaceWith { id: ElementId(1,), m: 1 },
|
||||
]
|
||||
);
|
||||
{
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
let edits = dom.render_immediate().santize();
|
||||
assert_eq!(
|
||||
edits.edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
|
||||
HydrateText { path: &[0,], value: "hello 0", id: ElementId(3,) },
|
||||
ReplaceWith { id: ElementId(1,), m: 1 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
let edits = dom.render_immediate().santize();
|
||||
assert_eq!(
|
||||
edits.edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
|
||||
HydrateText { path: &[0,], value: "hello 1", id: ElementId(4,) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(5,) },
|
||||
HydrateText { path: &[0,], value: "hello 2", id: ElementId(6,) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(7,) },
|
||||
HydrateText { path: &[0,], value: "hello 3", id: ElementId(8,) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(9,) },
|
||||
HydrateText { path: &[0,], value: "hello 4", id: ElementId(10,) },
|
||||
InsertAfter { id: ElementId(2,), m: 4 },
|
||||
]
|
||||
);
|
||||
{
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
let edits = dom.render_immediate().santize();
|
||||
assert_eq!(
|
||||
edits.edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(1,) },
|
||||
HydrateText { path: &[0,], value: "hello 1", id: ElementId(4,) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(5,) },
|
||||
HydrateText { path: &[0,], value: "hello 2", id: ElementId(6,) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(7,) },
|
||||
HydrateText { path: &[0,], value: "hello 3", id: ElementId(8,) },
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(9,) },
|
||||
HydrateText { path: &[0,], value: "hello 4", id: ElementId(10,) },
|
||||
InsertAfter { id: ElementId(2,), m: 4 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
let edits = dom.render_immediate().santize();
|
||||
assert_eq!(
|
||||
edits.edits,
|
||||
[
|
||||
CreatePlaceholder { id: ElementId(11,) },
|
||||
Remove { id: ElementId(9,) },
|
||||
Remove { id: ElementId(7,) },
|
||||
Remove { id: ElementId(5,) },
|
||||
Remove { id: ElementId(1,) },
|
||||
ReplaceWith { id: ElementId(2,), m: 1 },
|
||||
]
|
||||
);
|
||||
{
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
let edits = dom.render_immediate().santize();
|
||||
assert_eq!(
|
||||
edits.edits,
|
||||
[
|
||||
CreatePlaceholder { id: ElementId(11,) },
|
||||
Remove { id: ElementId(9,) },
|
||||
Remove { id: ElementId(7,) },
|
||||
Remove { id: ElementId(5,) },
|
||||
Remove { id: ElementId(1,) },
|
||||
ReplaceWith { id: ElementId(2,), m: 1 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
let edits = dom.render_immediate().santize();
|
||||
assert_eq!(
|
||||
edits.edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
|
||||
HydrateText { path: &[0,], value: "hello 0", id: ElementId(3,) },
|
||||
ReplaceWith { id: ElementId(11,), m: 1 },
|
||||
]
|
||||
)
|
||||
{
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
let edits = dom.render_immediate().santize();
|
||||
assert_eq!(
|
||||
edits.edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
|
||||
HydrateText { path: &[0,], value: "hello 0", id: ElementId(3,) },
|
||||
ReplaceWith { id: ElementId(11,), m: 1 },
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn catches_panic() {
|
||||
let mut dom = VirtualDom::new(app);
|
||||
_ = dom.rebuild();
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
h1 { "Title" }
|
||||
|
||||
NoneChild {}
|
||||
ThrowChild {}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn NoneChild(_cx: Scope) -> Element {
|
||||
None
|
||||
}
|
||||
|
||||
fn ThrowChild(cx: Scope) -> Element {
|
||||
cx.throw(std::io::Error::new(std::io::ErrorKind::AddrInUse, "asd"))?;
|
||||
|
||||
let _g: i32 = "123123".parse().throw(cx)?;
|
||||
|
||||
cx.render(rsx! {
|
||||
div {}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
use dioxus::prelude::Props;
|
||||
use dioxus_core::*;
|
||||
use std::cell::Cell;
|
||||
|
||||
fn random_ns() -> Option<&'static str> {
|
||||
let namespace = rand::random::<u8>() % 2;
|
||||
match namespace {
|
||||
0 => None,
|
||||
1 => Some(Box::leak(
|
||||
format!("ns{}", rand::random::<usize>()).into_boxed_str(),
|
||||
)),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn create_random_attribute(attr_idx: &mut usize) -> TemplateAttribute<'static> {
|
||||
match rand::random::<u8>() % 2 {
|
||||
0 => TemplateAttribute::Static {
|
||||
name: Box::leak(format!("attr{}", rand::random::<usize>()).into_boxed_str()),
|
||||
value: Box::leak(format!("value{}", rand::random::<usize>()).into_boxed_str()),
|
||||
namespace: random_ns(),
|
||||
},
|
||||
1 => TemplateAttribute::Dynamic {
|
||||
id: {
|
||||
let old_idx = *attr_idx;
|
||||
*attr_idx += 1;
|
||||
old_idx
|
||||
},
|
||||
},
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn create_random_template_node(
|
||||
dynamic_node_types: &mut Vec<DynamicNodeType>,
|
||||
template_idx: &mut usize,
|
||||
attr_idx: &mut usize,
|
||||
depth: usize,
|
||||
) -> TemplateNode<'static> {
|
||||
match rand::random::<u8>() % 4 {
|
||||
0 => {
|
||||
let attrs = {
|
||||
let attrs: Vec<_> = (0..(rand::random::<usize>() % 10))
|
||||
.map(|_| create_random_attribute(attr_idx))
|
||||
.collect();
|
||||
Box::leak(attrs.into_boxed_slice())
|
||||
};
|
||||
TemplateNode::Element {
|
||||
tag: Box::leak(format!("tag{}", rand::random::<usize>()).into_boxed_str()),
|
||||
namespace: random_ns(),
|
||||
attrs,
|
||||
children: {
|
||||
if depth > 4 {
|
||||
&[]
|
||||
} else {
|
||||
let children: Vec<_> = (0..(rand::random::<usize>() % 3))
|
||||
.map(|_| {
|
||||
create_random_template_node(
|
||||
dynamic_node_types,
|
||||
template_idx,
|
||||
attr_idx,
|
||||
depth + 1,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
Box::leak(children.into_boxed_slice())
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
1 => TemplateNode::Text {
|
||||
text: Box::leak(format!("{}", rand::random::<usize>()).into_boxed_str()),
|
||||
},
|
||||
2 => TemplateNode::DynamicText {
|
||||
id: {
|
||||
let old_idx = *template_idx;
|
||||
*template_idx += 1;
|
||||
dynamic_node_types.push(DynamicNodeType::Text);
|
||||
old_idx
|
||||
},
|
||||
},
|
||||
3 => TemplateNode::Dynamic {
|
||||
id: {
|
||||
let old_idx = *template_idx;
|
||||
*template_idx += 1;
|
||||
dynamic_node_types.push(DynamicNodeType::Other);
|
||||
old_idx
|
||||
},
|
||||
},
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_paths(
|
||||
node: &TemplateNode<'static>,
|
||||
current_path: &[u8],
|
||||
node_paths: &mut Vec<Vec<u8>>,
|
||||
attr_paths: &mut Vec<Vec<u8>>,
|
||||
) {
|
||||
match node {
|
||||
TemplateNode::Element { children, attrs, .. } => {
|
||||
for attr in *attrs {
|
||||
match attr {
|
||||
TemplateAttribute::Static { .. } => {}
|
||||
TemplateAttribute::Dynamic { .. } => {
|
||||
attr_paths.push(current_path.to_vec());
|
||||
}
|
||||
}
|
||||
}
|
||||
for (i, child) in children.iter().enumerate() {
|
||||
let mut current_path = current_path.to_vec();
|
||||
current_path.push(i as u8);
|
||||
generate_paths(child, ¤t_path, node_paths, attr_paths);
|
||||
}
|
||||
}
|
||||
TemplateNode::Text { .. } => {}
|
||||
TemplateNode::DynamicText { .. } => {
|
||||
node_paths.push(current_path.to_vec());
|
||||
}
|
||||
TemplateNode::Dynamic { .. } => {
|
||||
node_paths.push(current_path.to_vec());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum DynamicNodeType {
|
||||
Text,
|
||||
Other,
|
||||
}
|
||||
|
||||
fn create_random_template(name: &'static str) -> (Template<'static>, Vec<DynamicNodeType>) {
|
||||
let mut dynamic_node_type = Vec::new();
|
||||
let mut template_idx = 0;
|
||||
let mut attr_idx = 0;
|
||||
let roots = (0..(1 + rand::random::<usize>() % 5))
|
||||
.map(|_| {
|
||||
create_random_template_node(&mut dynamic_node_type, &mut template_idx, &mut attr_idx, 0)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert!(!roots.is_empty());
|
||||
let roots = Box::leak(roots.into_boxed_slice());
|
||||
let mut node_paths = Vec::new();
|
||||
let mut attr_paths = Vec::new();
|
||||
for (i, root) in roots.iter().enumerate() {
|
||||
generate_paths(root, &[i as u8], &mut node_paths, &mut attr_paths);
|
||||
}
|
||||
let node_paths = Box::leak(
|
||||
node_paths
|
||||
.into_iter()
|
||||
.map(|v| &*Box::leak(v.into_boxed_slice()))
|
||||
.collect::<Vec<_>>()
|
||||
.into_boxed_slice(),
|
||||
);
|
||||
let attr_paths = Box::leak(
|
||||
attr_paths
|
||||
.into_iter()
|
||||
.map(|v| &*Box::leak(v.into_boxed_slice()))
|
||||
.collect::<Vec<_>>()
|
||||
.into_boxed_slice(),
|
||||
);
|
||||
(
|
||||
Template { name, roots, node_paths, attr_paths },
|
||||
dynamic_node_type,
|
||||
)
|
||||
}
|
||||
|
||||
fn create_random_dynamic_node(cx: &ScopeState, depth: usize) -> DynamicNode {
|
||||
let range = if depth > 3 { 1 } else { 3 };
|
||||
match rand::random::<u8>() % range {
|
||||
0 => DynamicNode::Placeholder(Default::default()),
|
||||
1 => cx.make_node((0..(rand::random::<u8>() % 5)).map(|_| VNode {
|
||||
key: None,
|
||||
parent: Default::default(),
|
||||
template: Cell::new(Template {
|
||||
name: concat!(file!(), ":", line!(), ":", column!(), ":0"),
|
||||
roots: &[TemplateNode::Dynamic { id: 0 }],
|
||||
node_paths: &[&[0]],
|
||||
attr_paths: &[],
|
||||
}),
|
||||
root_ids: Default::default(),
|
||||
dynamic_nodes: cx.bump().alloc([cx.component(
|
||||
create_random_element,
|
||||
DepthProps { depth, root: false },
|
||||
"create_random_element",
|
||||
)]),
|
||||
dynamic_attrs: &[],
|
||||
})),
|
||||
2 => cx.component(
|
||||
create_random_element,
|
||||
DepthProps { depth, root: false },
|
||||
"create_random_element",
|
||||
),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute {
|
||||
let value = match rand::random::<u8>() % 6 {
|
||||
0 => AttributeValue::Text(Box::leak(
|
||||
format!("{}", rand::random::<usize>()).into_boxed_str(),
|
||||
)),
|
||||
1 => AttributeValue::Float(rand::random()),
|
||||
2 => AttributeValue::Int(rand::random()),
|
||||
3 => AttributeValue::Bool(rand::random()),
|
||||
4 => cx.any_value(rand::random::<usize>()),
|
||||
5 => AttributeValue::None,
|
||||
// Listener(RefCell<Option<ListenerCb<'a>>>),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
Attribute {
|
||||
name: Box::leak(format!("attr{}", rand::random::<usize>()).into_boxed_str()),
|
||||
value,
|
||||
namespace: random_ns(),
|
||||
mounted_element: Default::default(),
|
||||
volatile: rand::random(),
|
||||
}
|
||||
}
|
||||
|
||||
static mut TEMPLATE_COUNT: usize = 0;
|
||||
|
||||
#[derive(PartialEq, Props)]
|
||||
struct DepthProps {
|
||||
depth: usize,
|
||||
root: bool,
|
||||
}
|
||||
|
||||
fn create_random_element(cx: Scope<DepthProps>) -> Element {
|
||||
cx.needs_update();
|
||||
let range = if cx.props.root { 2 } else { 3 };
|
||||
let node = match rand::random::<usize>() % range {
|
||||
0 | 1 => {
|
||||
let (template, dynamic_node_types) = create_random_template(Box::leak(
|
||||
format!(
|
||||
"{}{}",
|
||||
concat!(file!(), ":", line!(), ":", column!(), ":"),
|
||||
{
|
||||
unsafe {
|
||||
let old = TEMPLATE_COUNT;
|
||||
TEMPLATE_COUNT += 1;
|
||||
old
|
||||
}
|
||||
}
|
||||
)
|
||||
.into_boxed_str(),
|
||||
));
|
||||
println!("{template:#?}");
|
||||
let node = VNode {
|
||||
key: None,
|
||||
parent: None,
|
||||
template: Cell::new(template),
|
||||
root_ids: Default::default(),
|
||||
dynamic_nodes: {
|
||||
let dynamic_nodes: Vec<_> = dynamic_node_types
|
||||
.iter()
|
||||
.map(|ty| match ty {
|
||||
DynamicNodeType::Text => DynamicNode::Text(VText {
|
||||
value: Box::leak(
|
||||
format!("{}", rand::random::<usize>()).into_boxed_str(),
|
||||
),
|
||||
id: Default::default(),
|
||||
}),
|
||||
DynamicNodeType::Other => {
|
||||
create_random_dynamic_node(cx, cx.props.depth + 1)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
cx.bump().alloc(dynamic_nodes)
|
||||
},
|
||||
dynamic_attrs: cx.bump().alloc(
|
||||
(0..template.attr_paths.len())
|
||||
.map(|_| create_random_dynamic_attr(cx))
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
};
|
||||
Some(node)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
println!("{node:#?}");
|
||||
node
|
||||
}
|
||||
|
||||
// test for panics when creating random nodes and templates
|
||||
#[test]
|
||||
fn create() {
|
||||
for _ in 0..100 {
|
||||
let mut vdom =
|
||||
VirtualDom::new_with_props(create_random_element, DepthProps { depth: 0, root: true });
|
||||
let _ = vdom.rebuild();
|
||||
}
|
||||
}
|
||||
|
||||
// test for panics when diffing random nodes
|
||||
// This test will change the template every render which is not very realistic, but it helps stress the system
|
||||
#[test]
|
||||
fn diff() {
|
||||
for _ in 0..10 {
|
||||
let mut vdom =
|
||||
VirtualDom::new_with_props(create_random_element, DepthProps { depth: 0, root: true });
|
||||
let _ = vdom.rebuild();
|
||||
for _ in 0..10 {
|
||||
let _ = vdom.render_immediate();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use bumpalo::Bump;
|
||||
use dioxus::core::{ElementId, Mutation};
|
||||
use dioxus::prelude::*;
|
||||
|
||||
@@ -26,17 +27,22 @@ fn basic_syntax_is_a_template(cx: Scope) -> Element {
|
||||
#[test]
|
||||
fn dual_stream() {
|
||||
let mut dom = VirtualDom::new(basic_syntax_is_a_template);
|
||||
let bump = Bump::new();
|
||||
let edits = dom.rebuild().santize();
|
||||
|
||||
use Mutation::*;
|
||||
assert_eq!(
|
||||
edits.edits,
|
||||
assert_eq!(edits.edits, {
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
|
||||
SetAttribute { name: "class", value: "123", id: ElementId(1), ns: None },
|
||||
SetAttribute {
|
||||
name: "class",
|
||||
value: (&*bump.alloc("123".into_value(&bump))).into(),
|
||||
id: ElementId(1),
|
||||
ns: None,
|
||||
},
|
||||
NewEventListener { name: "click", id: ElementId(1) },
|
||||
HydrateText { path: &[0, 0], value: "123", id: ElementId(2) },
|
||||
AppendChildren { id: ElementId(0), m: 1 }
|
||||
],
|
||||
);
|
||||
AppendChildren { id: ElementId(0), m: 1 },
|
||||
]
|
||||
});
|
||||
}
|
||||
@@ -137,213 +137,78 @@ fn free_works_on_root_hooks() {
|
||||
assert_eq!(Rc::strong_count(&ptr), 1);
|
||||
}
|
||||
|
||||
// #[test]
|
||||
// fn old_props_arent_stale() {
|
||||
// fn app(cx: Scope) -> Element {
|
||||
// dbg!("rendering parent");
|
||||
// let cnt = cx.use_hook(|| 0);
|
||||
// *cnt += 1;
|
||||
#[test]
|
||||
fn supports_async() {
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
|
||||
// if *cnt == 1 {
|
||||
// render!(div { Child { a: "abcdef".to_string() } })
|
||||
// } else {
|
||||
// render!(div { Child { a: "abcdef".to_string() } })
|
||||
// }
|
||||
// }
|
||||
fn app(cx: Scope) -> Element {
|
||||
let colors = use_state(cx, || vec!["green", "blue", "red"]);
|
||||
let padding = use_state(cx, || 10);
|
||||
|
||||
// #[derive(Props, PartialEq)]
|
||||
// struct ChildProps {
|
||||
// a: String,
|
||||
// }
|
||||
// fn Child(cx: Scope<ChildProps>) -> Element {
|
||||
// dbg!("rendering child", &cx.props.a);
|
||||
// render!(div { "child {cx.props.a}" })
|
||||
// }
|
||||
use_effect(cx, colors, |colors| async move {
|
||||
sleep(Duration::from_millis(1000)).await;
|
||||
colors.with_mut(|colors| colors.reverse());
|
||||
});
|
||||
|
||||
// let mut dom = new_dom(app, ());
|
||||
// let _ = dom.rebuild();
|
||||
use_effect(cx, padding, |padding| async move {
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
padding.with_mut(|padding| {
|
||||
if *padding < 65 {
|
||||
*padding += 1;
|
||||
} else {
|
||||
*padding = 5;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
||||
// dom.work_with_deadline(|| false);
|
||||
let big = colors[0];
|
||||
let mid = colors[1];
|
||||
let small = colors[2];
|
||||
|
||||
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
||||
// dom.work_with_deadline(|| false);
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
background: "{big}",
|
||||
height: "stretch",
|
||||
width: "stretch",
|
||||
padding: "50",
|
||||
label {
|
||||
"hello",
|
||||
}
|
||||
div {
|
||||
background: "{mid}",
|
||||
height: "auto",
|
||||
width: "stretch",
|
||||
padding: "{padding}",
|
||||
label {
|
||||
"World",
|
||||
}
|
||||
div {
|
||||
background: "{small}",
|
||||
height: "auto",
|
||||
width: "stretch",
|
||||
padding: "20",
|
||||
label {
|
||||
"ddddddd",
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
||||
// dom.work_with_deadline(|| false);
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_time()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// dbg!("forcing update to child");
|
||||
rt.block_on(async {
|
||||
let mut dom = VirtualDom::new(app);
|
||||
let _ = dom.rebuild();
|
||||
|
||||
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
|
||||
// dom.work_with_deadline(|| false);
|
||||
|
||||
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
|
||||
// dom.work_with_deadline(|| false);
|
||||
|
||||
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
|
||||
// dom.work_with_deadline(|| false);
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn basic() {
|
||||
// fn app(cx: Scope) -> Element {
|
||||
// render!(div {
|
||||
// Child { a: "abcdef".to_string() }
|
||||
// })
|
||||
// }
|
||||
|
||||
// #[derive(Props, PartialEq)]
|
||||
// struct ChildProps {
|
||||
// a: String,
|
||||
// }
|
||||
|
||||
// fn Child(cx: Scope<ChildProps>) -> Element {
|
||||
// dbg!("rendering child", &cx.props.a);
|
||||
// render!(div { "child {cx.props.a}" })
|
||||
// }
|
||||
|
||||
// let mut dom = new_dom(app, ());
|
||||
// let _ = dom.rebuild();
|
||||
|
||||
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
||||
// dom.work_with_deadline(|| false);
|
||||
|
||||
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
||||
// dom.work_with_deadline(|| false);
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn leak_thru_children() {
|
||||
// fn app(cx: Scope) -> Element {
|
||||
// cx.render(rsx! {
|
||||
// Child {
|
||||
// name: "asd".to_string(),
|
||||
// }
|
||||
// });
|
||||
// cx.render(rsx! {
|
||||
// div {}
|
||||
// })
|
||||
// }
|
||||
|
||||
// #[inline_props]
|
||||
// fn Child(cx: Scope, name: String) -> Element {
|
||||
// render!(div { "child {name}" })
|
||||
// }
|
||||
|
||||
// let mut dom = new_dom(app, ());
|
||||
// let _ = dom.rebuild();
|
||||
|
||||
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
||||
// dom.work_with_deadline(|| false);
|
||||
|
||||
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
||||
// dom.work_with_deadline(|| false);
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_pass_thru() {
|
||||
// #[inline_props]
|
||||
// fn NavContainer<'a>(cx: Scope, children: Element<'a>) -> Element {
|
||||
// cx.render(rsx! {
|
||||
// header {
|
||||
// nav { children }
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
// fn NavMenu(cx: Scope) -> Element {
|
||||
// render!( NavBrand {}
|
||||
// div {
|
||||
// NavStart {}
|
||||
// NavEnd {}
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
|
||||
// fn NavBrand(cx: Scope) -> Element {
|
||||
// render!(div {})
|
||||
// }
|
||||
|
||||
// fn NavStart(cx: Scope) -> Element {
|
||||
// render!(div {})
|
||||
// }
|
||||
|
||||
// fn NavEnd(cx: Scope) -> Element {
|
||||
// render!(div {})
|
||||
// }
|
||||
|
||||
// #[inline_props]
|
||||
// fn MainContainer<'a>(
|
||||
// cx: Scope,
|
||||
// nav: Element<'a>,
|
||||
// body: Element<'a>,
|
||||
// footer: Element<'a>,
|
||||
// ) -> Element {
|
||||
// cx.render(rsx! {
|
||||
// div {
|
||||
// class: "columns is-mobile",
|
||||
// div {
|
||||
// class: "column is-full",
|
||||
// nav,
|
||||
// body,
|
||||
// footer,
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
// fn app(cx: Scope) -> Element {
|
||||
// let nav = cx.render(rsx! {
|
||||
// NavContainer {
|
||||
// NavMenu {}
|
||||
// }
|
||||
// });
|
||||
// let body = cx.render(rsx! {
|
||||
// div {}
|
||||
// });
|
||||
// let footer = cx.render(rsx! {
|
||||
// div {}
|
||||
// });
|
||||
|
||||
// cx.render(rsx! {
|
||||
// MainContainer {
|
||||
// nav: nav,
|
||||
// body: body,
|
||||
// footer: footer,
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
// let mut dom = new_dom(app, ());
|
||||
// let _ = dom.rebuild();
|
||||
|
||||
// for _ in 0..40 {
|
||||
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
||||
// dom.work_with_deadline(|| false);
|
||||
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
||||
// dom.work_with_deadline(|| false);
|
||||
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
|
||||
// dom.work_with_deadline(|| false);
|
||||
|
||||
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
|
||||
// dom.work_with_deadline(|| false);
|
||||
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
|
||||
// dom.work_with_deadline(|| false);
|
||||
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
|
||||
// dom.work_with_deadline(|| false);
|
||||
|
||||
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(2)));
|
||||
// dom.work_with_deadline(|| false);
|
||||
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(2)));
|
||||
// dom.work_with_deadline(|| false);
|
||||
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(2)));
|
||||
// dom.work_with_deadline(|| false);
|
||||
|
||||
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(3)));
|
||||
// dom.work_with_deadline(|| false);
|
||||
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(3)));
|
||||
// dom.work_with_deadline(|| false);
|
||||
// dom.handle_message(SchedulerMsg::Immediate(ScopeId(3)));
|
||||
// dom.work_with_deadline(|| false);
|
||||
// }
|
||||
// }
|
||||
for _ in 0..10 {
|
||||
dom.wait_for_work().await;
|
||||
let _edits = dom.render_immediate();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -5,40 +5,48 @@ use std::future::IntoFuture;
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::test]
|
||||
async fn it_works() {
|
||||
let mut dom = VirtualDom::new(app);
|
||||
|
||||
let mutations = dom.rebuild().santize();
|
||||
|
||||
// We should at least get the top-level template in before pausing for the children
|
||||
// note: we dont test template edits anymore
|
||||
// assert_eq!(
|
||||
// mutations.templates,
|
||||
// [
|
||||
// CreateElement { name: "div" },
|
||||
// CreateStaticText { value: "Waiting for child..." },
|
||||
// CreateStaticPlaceholder,
|
||||
// AppendChildren { m: 2 },
|
||||
// SaveTemplate { name: "template", m: 1 }
|
||||
// ]
|
||||
// );
|
||||
|
||||
// And we should load it in and assign the placeholder properly
|
||||
assert_eq!(
|
||||
mutations.edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
|
||||
// hmmmmmmmmm.... with suspense how do we guarantee that IDs increase linearly?
|
||||
// can we even?
|
||||
AssignId { path: &[1], id: ElementId(3) },
|
||||
AppendChildren { m: 1, id: ElementId(0) },
|
||||
]
|
||||
);
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
// wait just a moment, not enough time for the boundary to resolve
|
||||
|
||||
dom.wait_for_work().await;
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_time()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block_on(async {
|
||||
let mut dom = VirtualDom::new(app);
|
||||
|
||||
{
|
||||
let mutations = dom.rebuild().santize();
|
||||
|
||||
// We should at least get the top-level template in before pausing for the children
|
||||
// note: we dont test template edits anymore
|
||||
// assert_eq!(
|
||||
// mutations.templates,
|
||||
// [
|
||||
// CreateElement { name: "div" },
|
||||
// CreateStaticText { value: "Waiting for child..." },
|
||||
// CreateStaticPlaceholder,
|
||||
// AppendChildren { m: 2 },
|
||||
// SaveTemplate { name: "template", m: 1 }
|
||||
// ]
|
||||
// );
|
||||
|
||||
// And we should load it in and assign the placeholder properly
|
||||
assert_eq!(
|
||||
mutations.edits,
|
||||
[
|
||||
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
|
||||
// hmmmmmmmmm.... with suspense how do we guarantee that IDs increase linearly?
|
||||
// can we even?
|
||||
AssignId { path: &[1], id: ElementId(3) },
|
||||
AppendChildren { m: 1, id: ElementId(0) },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
dom.wait_for_work().await;
|
||||
});
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
# dioxus webview arch
|
||||
|
||||
|
||||
dioxus webview represents a usecase of dioxus to stream edits from one dom to another. In this particular case, we stream edits from a native process into the webview's runtime. WebView is really portable, so we can leverage the UI of iOS, Android, and all desktop platforms while also maintaining direct access to platform-specific APIs. It's the best of both worlds!
|
||||
|
||||
|
||||
For this to work properly, we need to ship a universal client that receives Dioxus edits from a remote process. To make our lives (as developers) a bit easier, the client code is currently implemented in JS (subject to change!).
|
||||
|
||||
The provider (the thing that talks to the client on the other side of the network) is still written in Rust, and is supplied through the dioxus-webview crate. The client is much more "dumb" than the provider - the provider must handle NodeID generation, flushing changes, priorities, managing events, and more.
|
||||
|
||||
|
||||
## update; wry!
|
||||
|
||||
Wry is an effort by the Tauri team to make fully native
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "dioxus-desktop"
|
||||
version = "0.2.3"
|
||||
version = "0.3.0"
|
||||
authors = ["Jonathan Kelley"]
|
||||
edition = "2018"
|
||||
description = "Dioxus VirtualDOM renderer for a remote webview instance"
|
||||
@@ -8,19 +8,20 @@ license = "MIT/Apache-2.0"
|
||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||
homepage = "https://dioxuslabs.com"
|
||||
documentation = "https://dioxuslabs.com"
|
||||
keywords = ["dom", "ui", "gui", "react", "wasm"]
|
||||
keywords = ["dom", "ui", "gui", "react"]
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus-core = { path = "../core", version = "^0.2.1", features = ["serialize"] }
|
||||
dioxus-html = { path = "../html", features = ["serialize"], version = "^0.2.1" }
|
||||
dioxus-interpreter-js = { path = "../interpreter", version = "^0.2.1" }
|
||||
dioxus-core = { path = "../core", version = "^0.3.0", features = ["serialize"] }
|
||||
dioxus-html = { path = "../html", features = ["serialize"], version = "^0.3.0" }
|
||||
dioxus-interpreter-js = { path = "../interpreter", version = "^0.3.0" }
|
||||
dioxus-hot-reload = { path = "../hot-reload", optional = true }
|
||||
|
||||
serde = "1.0.136"
|
||||
serde_json = "1.0.79"
|
||||
thiserror = "1.0.30"
|
||||
log = "0.4.14"
|
||||
wry = { version = "0.23.4" }
|
||||
wry = { version = "0.27.2" }
|
||||
futures-channel = "0.3.21"
|
||||
tokio = { version = "1.16.1", features = [
|
||||
"sync",
|
||||
@@ -32,8 +33,8 @@ tokio = { version = "1.16.1", features = [
|
||||
webbrowser = "0.8.0"
|
||||
infer = "0.11.0"
|
||||
dunce = "1.0.2"
|
||||
slab = "0.4"
|
||||
|
||||
interprocess = { version = "1.1.1", optional = true}
|
||||
futures-util = "0.3.25"
|
||||
|
||||
[target.'cfg(target_os = "ios")'.dependencies]
|
||||
@@ -49,7 +50,7 @@ tokio_runtime = ["tokio"]
|
||||
fullscreen = ["wry/fullscreen"]
|
||||
transparent = ["wry/transparent"]
|
||||
tray = ["wry/tray"]
|
||||
hot-reload = ["interprocess"]
|
||||
hot-reload = ["dioxus-hot-reload"]
|
||||
|
||||
[dev-dependencies]
|
||||
dioxus-core-macro = { path = "../core-macro" }
|
||||
|
||||
@@ -1,5 +1,57 @@
|
||||
# Dioxus-Desktop
|
||||
# Dioxus Desktop (webview)
|
||||
|
||||
Because the host VirtualDOM is running in its own native process, native applications can unlock their full potential. Dioxus-Desktop is designed to be a 100% rust alternative to ElectronJS without the memory overhead or bloat of ElectronJS apps.
|
||||
|
||||
By bridging the native process, desktop apps can access full multithreading power, peripheral support, hardware access, and native filesystem controls without the hassle of web technologies. Our goal with this desktop crate is to make it easy to ship both a web and native application, and quickly see large performance boosts without having to re-write the whole stack. As the dioxus ecosystem grows, we hope to see 3rd parties providing wrappers for storage, offline mode, etc that supports both web and native technologies.
|
||||
[![Crates.io][crates-badge]][crates-url]
|
||||
[![MIT licensed][mit-badge]][mit-url]
|
||||
[![Build Status][actions-badge]][actions-url]
|
||||
[![Discord chat][discord-badge]][discord-url]
|
||||
|
||||
[crates-badge]: https://img.shields.io/crates/v/dioxus-desktop.svg
|
||||
[crates-url]: https://crates.io/crates/dioxus-desktop
|
||||
|
||||
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
|
||||
|
||||
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
|
||||
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
|
||||
|
||||
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
|
||||
[discord-url]: https://discord.gg/XgGxMSkvUM
|
||||
|
||||
[Website](https://dioxuslabs.com) |
|
||||
[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) |
|
||||
[API Docs](https://docs.rs/dioxus-desktop/latest/dioxus_desktop) |
|
||||
[Chat](https://discord.gg/XgGxMSkvUM)
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
`dioxus-desktop` provides a webview-based desktop renderer for the Dioxus VirtualDom.
|
||||
|
||||
This requires that webview is installed on the target system. WebView is installed by default on macOS and iOS devices, but might not come preinstalled on Windows or Linux devices. To fix these issues, follow the [instructions in the guide](guide-url).
|
||||
|
||||
[guide-url]: https://dioxuslabs.com/docs/0.3/guide/en/getting_started/desktop.html
|
||||
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- Simple, one-line launch for desktop apps
|
||||
- Dioxus VirtualDom running on a native thread
|
||||
- Full HTML/CSS support via `wry` and `tao`
|
||||
- Exposed `window` and `Proxy` types from tao for direct window manipulation
|
||||
- Helpful hooks for accessing the window, WebView, and running javascript.
|
||||
|
||||
## Contributing
|
||||
|
||||
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
|
||||
- Join the discord and ask questions!
|
||||
|
||||
## License
|
||||
This project is licensed under the [MIT license].
|
||||
|
||||
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in Dioxus by you shall be licensed as MIT without any additional
|
||||
terms or conditions.
|
||||
+13
-12
@@ -1,3 +1,4 @@
|
||||
use std::borrow::Cow;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use wry::application::window::Icon;
|
||||
@@ -16,9 +17,9 @@ pub struct Config {
|
||||
pub(crate) file_drop_handler: Option<DropHandler>,
|
||||
pub(crate) protocols: Vec<WryProtocol>,
|
||||
pub(crate) pre_rendered: Option<String>,
|
||||
// pub(crate) event_handler: Option<Box<DynEventHandlerFn>>,
|
||||
pub(crate) disable_context_menu: bool,
|
||||
pub(crate) resource_dir: Option<PathBuf>,
|
||||
pub(crate) data_dir: Option<PathBuf>,
|
||||
pub(crate) custom_head: Option<String>,
|
||||
pub(crate) custom_index: Option<String>,
|
||||
pub(crate) root_name: String,
|
||||
@@ -28,7 +29,7 @@ type DropHandler = Box<dyn Fn(&Window, FileDropEvent) -> bool>;
|
||||
|
||||
pub(crate) type WryProtocol = (
|
||||
String,
|
||||
Box<dyn Fn(&HttpRequest<Vec<u8>>) -> WryResult<HttpResponse<Vec<u8>>> + 'static>,
|
||||
Box<dyn Fn(&HttpRequest<Vec<u8>>) -> WryResult<HttpResponse<Cow<'static, [u8]>>> + 'static>,
|
||||
);
|
||||
|
||||
impl Config {
|
||||
@@ -45,6 +46,7 @@ impl Config {
|
||||
pre_rendered: None,
|
||||
disable_context_menu: !cfg!(debug_assertions),
|
||||
resource_dir: None,
|
||||
data_dir: None,
|
||||
custom_head: None,
|
||||
custom_index: None,
|
||||
root_name: "main".to_string(),
|
||||
@@ -57,6 +59,14 @@ impl Config {
|
||||
self
|
||||
}
|
||||
|
||||
/// set the directory where data will be stored in release mode.
|
||||
///
|
||||
/// > Note: This **must** be set when bundling on Windows.
|
||||
pub fn with_data_directory(mut self, path: impl Into<PathBuf>) -> Self {
|
||||
self.data_dir = Some(path.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set whether or not the right-click context menu should be disabled.
|
||||
pub fn with_disable_context_menu(mut self, disable: bool) -> Self {
|
||||
self.disable_context_menu = disable;
|
||||
@@ -77,15 +87,6 @@ impl Config {
|
||||
self
|
||||
}
|
||||
|
||||
// /// Set a custom event handler
|
||||
// pub fn with_event_handler(
|
||||
// mut self,
|
||||
// handler: impl Fn(&mut EventLoop<()>, &mut WebView) + 'static,
|
||||
// ) -> Self {
|
||||
// self.event_handler = Some(Box::new(handler));
|
||||
// self
|
||||
// }
|
||||
|
||||
/// Set a file drop handler
|
||||
pub fn with_file_drop_handler(
|
||||
mut self,
|
||||
@@ -98,7 +99,7 @@ impl Config {
|
||||
/// Set a custom protocol
|
||||
pub fn with_custom_protocol<F>(mut self, name: String, handler: F) -> Self
|
||||
where
|
||||
F: Fn(&HttpRequest<Vec<u8>>) -> WryResult<HttpResponse<Vec<u8>>> + 'static,
|
||||
F: Fn(&HttpRequest<Vec<u8>>) -> WryResult<HttpResponse<Cow<'static, [u8]>>> + 'static,
|
||||
{
|
||||
self.protocols.push((name, Box::new(handler)));
|
||||
self
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
use crate::desktop_context::{DesktopContext, UserWindowEvent};
|
||||
use dioxus_core::*;
|
||||
use dioxus_html::HtmlEvent;
|
||||
use futures_channel::mpsc::{unbounded, UnboundedSender};
|
||||
use futures_util::StreamExt;
|
||||
#[cfg(target_os = "ios")]
|
||||
use objc::runtime::Object;
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::Arc,
|
||||
sync::{atomic::AtomicBool, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
use wry::{
|
||||
self,
|
||||
application::{event_loop::ControlFlow, event_loop::EventLoopProxy, window::WindowId},
|
||||
webview::WebView,
|
||||
};
|
||||
|
||||
pub(super) struct DesktopController {
|
||||
pub(super) webviews: HashMap<WindowId, WebView>,
|
||||
pub(super) eval_sender: tokio::sync::mpsc::UnboundedSender<Value>,
|
||||
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>,
|
||||
}
|
||||
|
||||
impl DesktopController {
|
||||
// Launch the virtualdom on its own thread managed by tokio
|
||||
// returns the desktop state
|
||||
pub(super) fn new_on_tokio<P: Send + 'static>(
|
||||
root: Component<P>,
|
||||
props: P,
|
||||
proxy: EventLoopProxy<UserWindowEvent>,
|
||||
) -> 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();
|
||||
let (eval_sender, eval_reciever) = tokio::sync::mpsc::unbounded_channel::<Value>();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
// We create the runtime as multithreaded, so you can still "tokio::spawn" onto multiple threads
|
||||
// I'd personally not require tokio to be built-in to Dioxus-Desktop, but the DX is worse without it
|
||||
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
runtime.block_on(async move {
|
||||
let mut dom = VirtualDom::new_with_props(root, props)
|
||||
.with_root_context(DesktopContext::new(desktop_context_proxy, eval_reciever));
|
||||
{
|
||||
let edits = dom.rebuild();
|
||||
let mut queue = edit_queue.lock().unwrap();
|
||||
queue.push(serde_json::to_string(&edits).unwrap());
|
||||
proxy.send_event(UserWindowEvent::EditsReady).unwrap();
|
||||
}
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = dom.wait_for_work() => {}
|
||||
Some(json_value) = event_rx.next() => {
|
||||
if let Ok(value) = serde_json::from_value::<HtmlEvent>(json_value) {
|
||||
let HtmlEvent {
|
||||
name,
|
||||
element,
|
||||
bubbles,
|
||||
data
|
||||
} = value;
|
||||
dom.handle_event(&name, data.into_any(), element, bubbles);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let muts = dom
|
||||
.render_with_deadline(tokio::time::sleep(Duration::from_millis(16)))
|
||||
.await;
|
||||
|
||||
edit_queue
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(serde_json::to_string(&muts).unwrap());
|
||||
let _ = proxy.send_event(UserWindowEvent::EditsReady);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
Self {
|
||||
pending_edits,
|
||||
eval_sender,
|
||||
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![],
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn close_window(&mut self, window_id: WindowId, control_flow: &mut ControlFlow) {
|
||||
self.webviews.remove(&window_id);
|
||||
|
||||
if self.webviews.is_empty() && self.quit_app_on_close {
|
||||
*control_flow = ControlFlow::Exit;
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn try_load_ready_webviews(&mut self) {
|
||||
if self.is_ready.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
let mut new_queue = Vec::new();
|
||||
|
||||
{
|
||||
let mut queue = self.pending_edits.lock().unwrap();
|
||||
std::mem::swap(&mut new_queue, &mut *queue);
|
||||
}
|
||||
|
||||
let (_id, view) = self.webviews.iter_mut().next().unwrap();
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,32 @@
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::rc::Weak;
|
||||
|
||||
use crate::controller::DesktopController;
|
||||
use crate::create_new_window;
|
||||
use crate::eval::EvalResult;
|
||||
use crate::events::IpcMessage;
|
||||
use crate::shortcut::IntoKeyCode;
|
||||
use crate::shortcut::IntoModifersState;
|
||||
use crate::shortcut::ShortcutId;
|
||||
use crate::shortcut::ShortcutRegistry;
|
||||
use crate::shortcut::ShortcutRegistryError;
|
||||
use crate::Config;
|
||||
use crate::WebviewHandler;
|
||||
use dioxus_core::ScopeState;
|
||||
use serde::de::Error;
|
||||
use dioxus_core::VirtualDom;
|
||||
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||
use dioxus_hot_reload::HotReloadMsg;
|
||||
use serde_json::Value;
|
||||
use std::future::Future;
|
||||
use std::future::IntoFuture;
|
||||
use std::pin::Pin;
|
||||
use wry::application::event_loop::ControlFlow;
|
||||
use slab::Slab;
|
||||
use wry::application::event::Event;
|
||||
use wry::application::event_loop::EventLoopProxy;
|
||||
use wry::application::event_loop::EventLoopWindowTarget;
|
||||
#[cfg(target_os = "ios")]
|
||||
use wry::application::platform::ios::WindowExtIOS;
|
||||
use wry::application::window::Fullscreen as WryFullscreen;
|
||||
|
||||
use UserWindowEvent::*;
|
||||
use wry::application::window::Window;
|
||||
use wry::application::window::WindowId;
|
||||
use wry::webview::WebView;
|
||||
|
||||
pub type ProxyType = EventLoopProxy<UserWindowEvent>;
|
||||
|
||||
@@ -24,6 +37,8 @@ pub fn use_window(cx: &ScopeState) -> &DesktopContext {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub(crate) type WebviewQueue = Rc<RefCell<Vec<WebviewHandler>>>;
|
||||
|
||||
/// An imperative interface to the current window.
|
||||
///
|
||||
/// To get a handle to the current window, use the [`use_window`] hook.
|
||||
@@ -39,21 +54,92 @@ pub fn use_window(cx: &ScopeState) -> &DesktopContext {
|
||||
#[derive(Clone)]
|
||||
pub struct DesktopContext {
|
||||
/// The wry/tao proxy to the current window
|
||||
pub webview: Rc<WebView>,
|
||||
|
||||
/// The proxy to the event loop
|
||||
pub proxy: ProxyType,
|
||||
pub(super) eval_reciever: Rc<tokio::sync::Mutex<tokio::sync::mpsc::UnboundedReceiver<Value>>>,
|
||||
|
||||
/// The receiver for eval results since eval is async
|
||||
pub(super) eval: tokio::sync::broadcast::Sender<Value>,
|
||||
|
||||
pub(super) pending_windows: WebviewQueue,
|
||||
|
||||
pub(crate) event_loop: EventLoopWindowTarget<UserWindowEvent>,
|
||||
|
||||
pub(crate) event_handlers: WindowEventHandlers,
|
||||
|
||||
pub(crate) shortcut_manager: ShortcutRegistry,
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
pub(crate) views: Rc<RefCell<Vec<*mut objc::runtime::Object>>>,
|
||||
}
|
||||
|
||||
/// A smart pointer to the current window.
|
||||
impl std::ops::Deref for DesktopContext {
|
||||
type Target = Window;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.webview.window()
|
||||
}
|
||||
}
|
||||
|
||||
impl DesktopContext {
|
||||
pub(crate) fn new(
|
||||
webview: Rc<WebView>,
|
||||
proxy: ProxyType,
|
||||
eval_reciever: tokio::sync::mpsc::UnboundedReceiver<Value>,
|
||||
event_loop: EventLoopWindowTarget<UserWindowEvent>,
|
||||
webviews: WebviewQueue,
|
||||
event_handlers: WindowEventHandlers,
|
||||
shortcut_manager: ShortcutRegistry,
|
||||
) -> Self {
|
||||
Self {
|
||||
webview,
|
||||
proxy,
|
||||
eval_reciever: Rc::new(tokio::sync::Mutex::new(eval_reciever)),
|
||||
event_loop,
|
||||
eval: tokio::sync::broadcast::channel(8).0,
|
||||
pending_windows: webviews,
|
||||
event_handlers,
|
||||
shortcut_manager,
|
||||
#[cfg(target_os = "ios")]
|
||||
views: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new window using the props and window builder
|
||||
///
|
||||
/// Returns the webview handle for the new window.
|
||||
///
|
||||
/// You can use this to control other windows from the current window.
|
||||
///
|
||||
/// Be careful to not create a cycle of windows, or you might leak memory.
|
||||
pub fn new_window(&self, dom: VirtualDom, cfg: Config) -> Weak<WebView> {
|
||||
let window = create_new_window(
|
||||
cfg,
|
||||
&self.event_loop,
|
||||
&self.proxy,
|
||||
dom,
|
||||
&self.pending_windows,
|
||||
&self.event_handlers,
|
||||
self.shortcut_manager.clone(),
|
||||
);
|
||||
|
||||
let id = window.webview.window().id();
|
||||
|
||||
self.proxy
|
||||
.send_event(UserWindowEvent(EventData::NewWindow, id))
|
||||
.unwrap();
|
||||
|
||||
self.proxy
|
||||
.send_event(UserWindowEvent(EventData::Poll, id))
|
||||
.unwrap();
|
||||
|
||||
let webview = window.webview.clone();
|
||||
|
||||
self.pending_windows.borrow_mut().push(window);
|
||||
|
||||
Rc::downgrade(&webview)
|
||||
}
|
||||
|
||||
/// trigger the drag-window event
|
||||
///
|
||||
/// Moves the window with the left mouse button until the button is released.
|
||||
@@ -63,284 +149,187 @@ impl DesktopContext {
|
||||
/// onmousedown: move |_| { desktop.drag_window(); }
|
||||
/// ```
|
||||
pub fn drag(&self) {
|
||||
let _ = self.proxy.send_event(DragWindow);
|
||||
let window = self.webview.window();
|
||||
|
||||
// if the drag_window has any errors, we don't do anything
|
||||
if window.fullscreen().is_none() {
|
||||
window.drag_window().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// set window minimize state
|
||||
pub fn set_minimized(&self, minimized: bool) {
|
||||
let _ = self.proxy.send_event(Minimize(minimized));
|
||||
}
|
||||
|
||||
/// set window maximize state
|
||||
pub fn set_maximized(&self, maximized: bool) {
|
||||
let _ = self.proxy.send_event(Maximize(maximized));
|
||||
}
|
||||
|
||||
/// toggle window maximize state
|
||||
/// Toggle whether the window is maximized or not
|
||||
pub fn toggle_maximized(&self) {
|
||||
let _ = self.proxy.send_event(MaximizeToggle);
|
||||
}
|
||||
let window = self.webview.window();
|
||||
|
||||
/// set window visible or not
|
||||
pub fn set_visible(&self, visible: bool) {
|
||||
let _ = self.proxy.send_event(Visible(visible));
|
||||
window.set_maximized(!window.is_maximized())
|
||||
}
|
||||
|
||||
/// close window
|
||||
pub fn close(&self) {
|
||||
let _ = self.proxy.send_event(CloseWindow);
|
||||
let _ = self
|
||||
.proxy
|
||||
.send_event(UserWindowEvent(EventData::CloseWindow, self.id()));
|
||||
}
|
||||
|
||||
/// set window to focus
|
||||
pub fn focus(&self) {
|
||||
let _ = self.proxy.send_event(FocusWindow);
|
||||
/// close window
|
||||
pub fn close_window(&self, id: WindowId) {
|
||||
let _ = self
|
||||
.proxy
|
||||
.send_event(UserWindowEvent(EventData::CloseWindow, id));
|
||||
}
|
||||
|
||||
/// change window to fullscreen
|
||||
pub fn set_fullscreen(&self, fullscreen: bool) {
|
||||
let _ = self.proxy.send_event(Fullscreen(fullscreen));
|
||||
}
|
||||
|
||||
/// set resizable state
|
||||
pub fn set_resizable(&self, resizable: bool) {
|
||||
let _ = self.proxy.send_event(Resizable(resizable));
|
||||
}
|
||||
|
||||
/// set the window always on top
|
||||
pub fn set_always_on_top(&self, top: bool) {
|
||||
let _ = self.proxy.send_event(AlwaysOnTop(top));
|
||||
}
|
||||
|
||||
/// set cursor visible or not
|
||||
pub fn set_cursor_visible(&self, visible: bool) {
|
||||
let _ = self.proxy.send_event(CursorVisible(visible));
|
||||
}
|
||||
|
||||
/// set cursor grab
|
||||
pub fn set_cursor_grab(&self, grab: bool) {
|
||||
let _ = self.proxy.send_event(CursorGrab(grab));
|
||||
}
|
||||
|
||||
/// set window title
|
||||
pub fn set_title(&self, title: &str) {
|
||||
let _ = self.proxy.send_event(SetTitle(String::from(title)));
|
||||
}
|
||||
|
||||
/// change window to borderless
|
||||
pub fn set_decorations(&self, decoration: bool) {
|
||||
let _ = self.proxy.send_event(SetDecorations(decoration));
|
||||
}
|
||||
|
||||
/// set window zoom level
|
||||
pub fn set_zoom_level(&self, scale_factor: f64) {
|
||||
let _ = self.proxy.send_event(SetZoomLevel(scale_factor));
|
||||
if let Some(handle) = self.webview.window().current_monitor() {
|
||||
self.webview
|
||||
.window()
|
||||
.set_fullscreen(fullscreen.then_some(WryFullscreen::Borderless(Some(handle))));
|
||||
}
|
||||
}
|
||||
|
||||
/// launch print modal
|
||||
pub fn print(&self) {
|
||||
let _ = self.proxy.send_event(Print);
|
||||
if let Err(e) = self.webview.print() {
|
||||
log::warn!("Open print modal failed: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the zoom level of the webview
|
||||
pub fn set_zoom_level(&self, level: f64) {
|
||||
self.webview.zoom(level);
|
||||
}
|
||||
|
||||
/// opens DevTool window
|
||||
pub fn devtool(&self) {
|
||||
let _ = self.proxy.send_event(DevTool);
|
||||
#[cfg(debug_assertions)]
|
||||
self.webview.open_devtools();
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
log::warn!("Devtools are disabled in release builds");
|
||||
}
|
||||
|
||||
/// run (evaluate) a script in the WebView context
|
||||
pub fn eval(&self, script: impl std::string::ToString) {
|
||||
let _ = self.proxy.send_event(Eval(script.to_string()));
|
||||
/// Evaluate a javascript expression
|
||||
pub fn eval(&self, code: &str) -> EvalResult {
|
||||
// Embed the return of the eval in a function so we can send it back to the main thread
|
||||
let script = format!(
|
||||
r#"
|
||||
window.ipc.postMessage(
|
||||
JSON.stringify({{
|
||||
"method":"eval_result",
|
||||
"params": (
|
||||
function(){{
|
||||
{code}
|
||||
}}
|
||||
)()
|
||||
}})
|
||||
);
|
||||
"#
|
||||
);
|
||||
|
||||
if let Err(e) = self.webview.evaluate_script(&script) {
|
||||
// send an error to the eval receiver
|
||||
log::warn!("Eval script error: {e}");
|
||||
}
|
||||
|
||||
EvalResult::new(self.eval.clone())
|
||||
}
|
||||
|
||||
/// Push view
|
||||
/// Create a wry event handler that listens for wry events.
|
||||
/// This event handler is scoped to the currently active window and will only recieve events that are either global or related to the current window.
|
||||
///
|
||||
/// The id this function returns can be used to remove the event handler with [`DesktopContext::remove_wry_event_handler`]
|
||||
pub fn create_wry_event_handler(
|
||||
&self,
|
||||
handler: impl FnMut(&Event<UserWindowEvent>, &EventLoopWindowTarget<UserWindowEvent>) + 'static,
|
||||
) -> WryEventHandlerId {
|
||||
self.event_handlers.add(self.id(), handler)
|
||||
}
|
||||
|
||||
/// Remove a wry event handler created with [`DesktopContext::create_wry_event_handler`]
|
||||
pub fn remove_wry_event_handler(&self, id: WryEventHandlerId) {
|
||||
self.event_handlers.remove(id)
|
||||
}
|
||||
|
||||
/// Create a global shortcut
|
||||
///
|
||||
/// Linux: Only works on x11. See [this issue](https://github.com/tauri-apps/tao/issues/331) for more information.
|
||||
pub fn create_shortcut(
|
||||
&self,
|
||||
key: impl IntoKeyCode,
|
||||
modifiers: impl IntoModifersState,
|
||||
callback: impl FnMut() + 'static,
|
||||
) -> Result<ShortcutId, ShortcutRegistryError> {
|
||||
self.shortcut_manager.add_shortcut(
|
||||
modifiers.into_modifiers_state(),
|
||||
key.into_key_code(),
|
||||
Box::new(callback),
|
||||
)
|
||||
}
|
||||
|
||||
/// Remove a global shortcut
|
||||
pub fn remove_shortcut(&self, id: ShortcutId) {
|
||||
self.shortcut_manager.remove_shortcut(id)
|
||||
}
|
||||
|
||||
/// Remove all global shortcuts
|
||||
pub fn remove_all_shortcuts(&self) {
|
||||
self.shortcut_manager.remove_all()
|
||||
}
|
||||
|
||||
/// Push an objc view to the window
|
||||
#[cfg(target_os = "ios")]
|
||||
pub fn push_view(&self, view: objc_id::ShareId<objc::runtime::Object>) {
|
||||
let _ = self.proxy.send_event(PushView(view));
|
||||
let window = self.webview.window();
|
||||
|
||||
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];
|
||||
|
||||
let ui_view_controller = window.ui_view_controller() as *mut Object;
|
||||
let _: () = msg_send![ui_view_controller, setView: view];
|
||||
self.views.borrow_mut().push(ui_view);
|
||||
}
|
||||
}
|
||||
|
||||
/// Push view
|
||||
/// Pop an objc view from the window
|
||||
#[cfg(target_os = "ios")]
|
||||
pub fn pop_view(&self) {
|
||||
let _ = self.proxy.send_event(PopView);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum UserWindowEvent {
|
||||
EditsReady,
|
||||
Initialize,
|
||||
|
||||
CloseWindow,
|
||||
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),
|
||||
MaximizeToggle,
|
||||
Resizable(bool),
|
||||
AlwaysOnTop(bool),
|
||||
Fullscreen(bool),
|
||||
|
||||
CursorVisible(bool),
|
||||
CursorGrab(bool),
|
||||
|
||||
SetTitle(String),
|
||||
SetDecorations(bool),
|
||||
|
||||
SetZoomLevel(f64),
|
||||
|
||||
Print,
|
||||
DevTool,
|
||||
|
||||
Eval(String),
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
PushView(objc_id::ShareId<objc::runtime::Object>),
|
||||
#[cfg(target_os = "ios")]
|
||||
PopView,
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
let window = webview.window();
|
||||
|
||||
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());
|
||||
}
|
||||
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))));
|
||||
}
|
||||
}
|
||||
FocusWindow => window.set_focus(),
|
||||
Resizable(state) => window.set_resizable(state),
|
||||
AlwaysOnTop(state) => window.set_always_on_top(state),
|
||||
|
||||
Eval(code) => {
|
||||
let script = format!(
|
||||
r#"window.ipc.postMessage(JSON.stringify({{"method":"eval_result", params: (function(){{
|
||||
{}
|
||||
}})()}}));"#,
|
||||
code
|
||||
);
|
||||
if let Err(e) = webview.evaluate_script(&script) {
|
||||
// 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);
|
||||
}
|
||||
|
||||
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}");
|
||||
}
|
||||
}
|
||||
DevTool => {
|
||||
#[cfg(debug_assertions)]
|
||||
webview.open_devtools();
|
||||
#[cfg(not(debug_assertions))]
|
||||
log::warn!("Devtools are disabled in release builds");
|
||||
}
|
||||
|
||||
#[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];
|
||||
let window = self.webview.window();
|
||||
|
||||
unsafe {
|
||||
use objc::runtime::Object;
|
||||
use objc::*;
|
||||
assert!(is_main_thread());
|
||||
if let Some(view) = self.views.borrow_mut().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];
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a closure that executes any JavaScript in the WebView context.
|
||||
pub fn use_eval<S: std::string::ToString>(cx: &ScopeState) -> &dyn Fn(S) -> EvalResult {
|
||||
let desktop = use_window(cx).clone();
|
||||
cx.use_hook(|| {
|
||||
move |script| {
|
||||
desktop.eval(script);
|
||||
let recv = desktop.eval_reciever.clone();
|
||||
EvalResult { reciever: recv }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// A future that resolves to the result of a JavaScript evaluation.
|
||||
pub struct EvalResult {
|
||||
reciever: Rc<tokio::sync::Mutex<tokio::sync::mpsc::UnboundedReceiver<serde_json::Value>>>,
|
||||
}
|
||||
|
||||
impl IntoFuture for EvalResult {
|
||||
type Output = Result<serde_json::Value, serde_json::Error>;
|
||||
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Result<serde_json::Value, serde_json::Error>>>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(async move {
|
||||
let mut reciever = self.reciever.lock().await;
|
||||
match reciever.recv().await {
|
||||
Some(result) => Ok(result),
|
||||
None => Err(serde_json::Error::custom("No result returned")),
|
||||
}
|
||||
}) as Pin<Box<dyn Future<Output = Result<serde_json::Value, serde_json::Error>>>>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UserWindowEvent(pub EventData, pub WindowId);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum EventData {
|
||||
Poll,
|
||||
|
||||
Ipc(IpcMessage),
|
||||
|
||||
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||
HotReloadEvent(HotReloadMsg),
|
||||
|
||||
NewWindow,
|
||||
|
||||
CloseWindow,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
fn is_main_thread() -> bool {
|
||||
use objc::runtime::{Class, BOOL, NO};
|
||||
@@ -350,3 +339,114 @@ fn is_main_thread() -> bool {
|
||||
let result: BOOL = unsafe { msg_send![cls, isMainThread] };
|
||||
result != NO
|
||||
}
|
||||
|
||||
/// The unique identifier of a window event handler. This can be used to later remove the handler.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct WryEventHandlerId(usize);
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct WindowEventHandlers {
|
||||
handlers: Rc<RefCell<Slab<WryWindowEventHandlerInner>>>,
|
||||
}
|
||||
|
||||
impl WindowEventHandlers {
|
||||
pub(crate) fn add(
|
||||
&self,
|
||||
window_id: WindowId,
|
||||
handler: impl FnMut(&Event<UserWindowEvent>, &EventLoopWindowTarget<UserWindowEvent>) + 'static,
|
||||
) -> WryEventHandlerId {
|
||||
WryEventHandlerId(
|
||||
self.handlers
|
||||
.borrow_mut()
|
||||
.insert(WryWindowEventHandlerInner {
|
||||
window_id,
|
||||
handler: Box::new(handler),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn remove(&self, id: WryEventHandlerId) {
|
||||
self.handlers.borrow_mut().try_remove(id.0);
|
||||
}
|
||||
|
||||
pub(crate) fn apply_event(
|
||||
&self,
|
||||
event: &Event<UserWindowEvent>,
|
||||
target: &EventLoopWindowTarget<UserWindowEvent>,
|
||||
) {
|
||||
for (_, handler) in self.handlers.borrow_mut().iter_mut() {
|
||||
handler.apply_event(event, target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WryWindowEventHandlerInner {
|
||||
window_id: WindowId,
|
||||
handler: WryEventHandlerCallback,
|
||||
}
|
||||
|
||||
type WryEventHandlerCallback =
|
||||
Box<dyn FnMut(&Event<UserWindowEvent>, &EventLoopWindowTarget<UserWindowEvent>) + 'static>;
|
||||
|
||||
impl WryWindowEventHandlerInner {
|
||||
fn apply_event(
|
||||
&mut self,
|
||||
event: &Event<UserWindowEvent>,
|
||||
target: &EventLoopWindowTarget<UserWindowEvent>,
|
||||
) {
|
||||
// if this event does not apply to the window this listener cares about, return
|
||||
match event {
|
||||
Event::WindowEvent { window_id, .. }
|
||||
| Event::MenuEvent {
|
||||
window_id: Some(window_id),
|
||||
..
|
||||
} => {
|
||||
if *window_id != self.window_id {
|
||||
return;
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
(self.handler)(event, target)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a closure that executes any JavaScript in the WebView context.
|
||||
pub fn use_wry_event_handler(
|
||||
cx: &ScopeState,
|
||||
handler: impl FnMut(&Event<UserWindowEvent>, &EventLoopWindowTarget<UserWindowEvent>) + 'static,
|
||||
) -> &WryEventHandler {
|
||||
let desktop = use_window(cx);
|
||||
cx.use_hook(move || {
|
||||
let desktop = desktop.clone();
|
||||
|
||||
let id = desktop.create_wry_event_handler(handler);
|
||||
|
||||
WryEventHandler {
|
||||
handlers: desktop.event_handlers,
|
||||
id,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// A wry event handler that is scoped to the current component and window. The event handler will only receive events for the window it was created for and global events.
|
||||
///
|
||||
/// This will automatically be removed when the component is unmounted.
|
||||
pub struct WryEventHandler {
|
||||
handlers: WindowEventHandlers,
|
||||
/// The unique identifier of the event handler.
|
||||
pub id: WryEventHandlerId,
|
||||
}
|
||||
|
||||
impl WryEventHandler {
|
||||
/// Remove the event handler.
|
||||
pub fn remove(&self) {
|
||||
self.handlers.remove(self.id);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for WryEventHandler {
|
||||
fn drop(&mut self) {
|
||||
self.handlers.remove(self.id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::use_window;
|
||||
use dioxus_core::ScopeState;
|
||||
use serde::de::Error;
|
||||
use std::future::Future;
|
||||
use std::future::IntoFuture;
|
||||
use std::pin::Pin;
|
||||
|
||||
/// A future that resolves to the result of a JavaScript evaluation.
|
||||
pub struct EvalResult {
|
||||
pub(crate) broadcast: tokio::sync::broadcast::Sender<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl EvalResult {
|
||||
pub(crate) fn new(sender: tokio::sync::broadcast::Sender<serde_json::Value>) -> Self {
|
||||
Self { broadcast: sender }
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoFuture for EvalResult {
|
||||
type Output = Result<serde_json::Value, serde_json::Error>;
|
||||
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Result<serde_json::Value, serde_json::Error>>>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(async move {
|
||||
let mut reciever = self.broadcast.subscribe();
|
||||
match reciever.recv().await {
|
||||
Ok(result) => Ok(result),
|
||||
Err(_) => Err(serde_json::Error::custom("No result returned")),
|
||||
}
|
||||
}) as Pin<Box<dyn Future<Output = Result<serde_json::Value, serde_json::Error>>>>
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a closure that executes any JavaScript in the WebView context.
|
||||
pub fn use_eval(cx: &ScopeState) -> &Rc<dyn Fn(String) -> EvalResult> {
|
||||
let desktop = use_window(cx);
|
||||
&*cx.use_hook(|| {
|
||||
let desktop = desktop.clone();
|
||||
|
||||
Rc::new(move |script: String| desktop.eval(&script)) as Rc<dyn Fn(String) -> EvalResult>
|
||||
})
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub(crate) struct IpcMessage {
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct IpcMessage {
|
||||
method: String,
|
||||
params: serde_json::Value,
|
||||
}
|
||||
@@ -17,13 +17,3 @@ impl IpcMessage {
|
||||
self.params
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_ipc_message(payload: &str) -> Option<IpcMessage> {
|
||||
match serde_json::from_str(payload) {
|
||||
Ok(message) => Some(message),
|
||||
Err(e) => {
|
||||
log::error!("could not parse IPC message, error: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use dioxus_core::VirtualDom;
|
||||
|
||||
use interprocess::local_socket::{LocalSocketListener, LocalSocketStream};
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::time::Duration;
|
||||
use std::{sync::Arc, sync::Mutex};
|
||||
|
||||
fn handle_error(connection: std::io::Result<LocalSocketStream>) -> Option<LocalSocketStream> {
|
||||
connection
|
||||
.map_err(|error| eprintln!("Incoming connection failed: {}", error))
|
||||
.ok()
|
||||
}
|
||||
|
||||
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 || {
|
||||
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 || {
|
||||
loop {
|
||||
if let Some(conn) = &mut *latest_in_connection.lock().unwrap() {
|
||||
let mut buf = String::new();
|
||||
match conn.read_line(&mut buf) {
|
||||
Ok(_) => {
|
||||
todo!()
|
||||
// let msg: SetTemplateMsg = serde_json::from_str(&buf).unwrap();
|
||||
// channel
|
||||
// .start_send(SchedulerMsg::SetTemplate(Box::new(msg)))
|
||||
// .unwrap();
|
||||
}
|
||||
Err(err) => {
|
||||
if err.kind() != std::io::ErrorKind::WouldBlock {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// give the error handler time to take the mutex
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
});
|
||||
}
|
||||
+229
-150
@@ -4,36 +4,40 @@
|
||||
#![deny(missing_docs)]
|
||||
|
||||
mod cfg;
|
||||
mod controller;
|
||||
mod desktop_context;
|
||||
mod escape;
|
||||
mod eval;
|
||||
mod events;
|
||||
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, EvalResult};
|
||||
use futures_channel::mpsc::UnboundedSender;
|
||||
pub use wry;
|
||||
pub use wry::application as tao;
|
||||
mod shortcut;
|
||||
mod waker;
|
||||
mod webview;
|
||||
|
||||
pub use cfg::Config;
|
||||
use controller::DesktopController;
|
||||
pub use desktop_context::{
|
||||
use_window, use_wry_event_handler, DesktopContext, WryEventHandler, WryEventHandlerId,
|
||||
};
|
||||
use desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers};
|
||||
use dioxus_core::*;
|
||||
use events::parse_ipc_message;
|
||||
use dioxus_html::HtmlEvent;
|
||||
pub use eval::{use_eval, EvalResult};
|
||||
use futures_util::{pin_mut, FutureExt};
|
||||
use shortcut::ShortcutRegistry;
|
||||
pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError};
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
use std::task::Waker;
|
||||
pub use tao::dpi::{LogicalSize, PhysicalSize};
|
||||
use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget};
|
||||
pub use tao::window::WindowBuilder;
|
||||
use tao::{
|
||||
event::{Event, StartCause, WindowEvent},
|
||||
event_loop::{ControlFlow, EventLoop},
|
||||
window::Window,
|
||||
};
|
||||
use wry::webview::WebViewBuilder;
|
||||
pub use wry;
|
||||
pub use wry::application as tao;
|
||||
use wry::webview::WebView;
|
||||
use wry::{application::window::WindowId, webview::WebContext};
|
||||
|
||||
/// Launch the WebView and run the event loop.
|
||||
///
|
||||
@@ -81,7 +85,7 @@ pub fn launch_cfg(root: Component, config_builder: Config) {
|
||||
|
||||
/// Launch the WebView and run the event loop, with configuration and root props.
|
||||
///
|
||||
/// This function will start a multithreaded Tokio runtime as well the WebView event loop.
|
||||
/// This function will start a multithreaded Tokio runtime as well the WebView event loop. This will block the current thread.
|
||||
///
|
||||
/// You can configure the WebView window with a configuration closure
|
||||
///
|
||||
@@ -89,7 +93,7 @@ pub fn launch_cfg(root: Component, config_builder: Config) {
|
||||
/// use dioxus::prelude::*;
|
||||
///
|
||||
/// fn main() {
|
||||
/// dioxus_desktop::launch_cfg(app, AppProps { name: "asd" }, |c| c);
|
||||
/// dioxus_desktop::launch_with_props(app, AppProps { name: "asd" }, Config::default());
|
||||
/// }
|
||||
///
|
||||
/// struct AppProps {
|
||||
@@ -102,162 +106,237 @@ 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());
|
||||
pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config) {
|
||||
let event_loop = EventLoop::<UserWindowEvent>::with_user_event();
|
||||
|
||||
let proxy = event_loop.create_proxy();
|
||||
|
||||
// Intialize hot reloading if it is enabled
|
||||
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||
{
|
||||
let proxy = proxy.clone();
|
||||
dioxus_hot_reload::connect(move |template| {
|
||||
let _ = proxy.send_event(UserWindowEvent(
|
||||
EventData::HotReloadEvent(template),
|
||||
unsafe { WindowId::dummy() },
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
// We start the tokio runtime *on this thread*
|
||||
// Any future we poll later will use this runtime to spawn tasks and for IO
|
||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// We enter the runtime but we poll futures manually, circumventing the per-task runtime budget
|
||||
let _guard = rt.enter();
|
||||
|
||||
// We only have one webview right now, but we'll have more later
|
||||
// Store them in a hashmap so we can remove them when they're closed
|
||||
let mut webviews = HashMap::<WindowId, WebviewHandler>::new();
|
||||
|
||||
// We use this to allow dynamically adding and removing window event handlers
|
||||
let event_handlers = WindowEventHandlers::default();
|
||||
|
||||
let queue = WebviewQueue::default();
|
||||
|
||||
let shortcut_manager = ShortcutRegistry::new(&event_loop);
|
||||
|
||||
// By default, we'll create a new window when the app starts
|
||||
queue.borrow_mut().push(create_new_window(
|
||||
cfg,
|
||||
&event_loop,
|
||||
&proxy,
|
||||
VirtualDom::new_with_props(root, props),
|
||||
&queue,
|
||||
&event_handlers,
|
||||
shortcut_manager.clone(),
|
||||
));
|
||||
|
||||
event_loop.run(move |window_event, event_loop, control_flow| {
|
||||
*control_flow = ControlFlow::Wait;
|
||||
|
||||
match window_event {
|
||||
Event::NewEvents(StartCause::Init) => desktop.start(&mut cfg, event_loop),
|
||||
event_handlers.apply_event(&window_event, event_loop);
|
||||
|
||||
match window_event {
|
||||
Event::WindowEvent {
|
||||
event, window_id, ..
|
||||
} => match event {
|
||||
WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
|
||||
WindowEvent::Destroyed { .. } => desktop.close_window(window_id, control_flow),
|
||||
WindowEvent::CloseRequested => {
|
||||
webviews.remove(&window_id);
|
||||
|
||||
if webviews.is_empty() {
|
||||
*control_flow = ControlFlow::Exit
|
||||
}
|
||||
}
|
||||
WindowEvent::Destroyed { .. } => {
|
||||
webviews.remove(&window_id);
|
||||
|
||||
if webviews.is_empty() {
|
||||
*control_flow = ControlFlow::Exit;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
|
||||
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.eval_sender.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>,
|
||||
eval_sender: tokio::sync::mpsc::UnboundedSender<serde_json::Value>,
|
||||
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();
|
||||
let root_name = cfg.root_name.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() {
|
||||
window.set_window_icon(Some(
|
||||
tao::window::Icon::from_rgba(
|
||||
include_bytes!("./assets/default_icon.bin").to_vec(),
|
||||
460,
|
||||
460,
|
||||
)
|
||||
.expect("image parse failed"),
|
||||
));
|
||||
}
|
||||
|
||||
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| {
|
||||
let message = match parse_ipc_message(&payload) {
|
||||
Some(message) => message,
|
||||
None => {
|
||||
log::error!("Failed to parse IPC message: {}", payload);
|
||||
return;
|
||||
Event::NewEvents(StartCause::Init)
|
||||
| Event::UserEvent(UserWindowEvent(EventData::NewWindow, _)) => {
|
||||
for handler in queue.borrow_mut().drain(..) {
|
||||
let id = handler.webview.window().id();
|
||||
webviews.insert(id, handler);
|
||||
_ = proxy.send_event(UserWindowEvent(EventData::Poll, id));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
match message.method() {
|
||||
"eval_result" => {
|
||||
let result = message.params();
|
||||
eval_sender.send(result).unwrap();
|
||||
Event::UserEvent(event) => match event.0 {
|
||||
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||
EventData::HotReloadEvent(msg) => match msg {
|
||||
dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => {
|
||||
for webview in webviews.values_mut() {
|
||||
webview.dom.replace_template(template);
|
||||
|
||||
poll_vdom(webview);
|
||||
}
|
||||
}
|
||||
dioxus_hot_reload::HotReloadMsg::Shutdown => {
|
||||
*control_flow = ControlFlow::Exit;
|
||||
}
|
||||
},
|
||||
|
||||
EventData::CloseWindow => {
|
||||
webviews.remove(&event.1);
|
||||
|
||||
if webviews.is_empty() {
|
||||
*control_flow = ControlFlow::Exit
|
||||
}
|
||||
}
|
||||
"user_event" => {
|
||||
_ = event_tx.unbounded_send(message.params());
|
||||
|
||||
EventData::Poll => {
|
||||
if let Some(view) = webviews.get_mut(&event.1) {
|
||||
poll_vdom(view);
|
||||
}
|
||||
}
|
||||
"initialize" => {
|
||||
is_ready.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
let _ = proxy.send_event(UserWindowEvent::EditsReady);
|
||||
|
||||
EventData::Ipc(msg) if msg.method() == "user_event" => {
|
||||
let evt = match serde_json::from_value::<HtmlEvent>(msg.params()) {
|
||||
Ok(value) => value,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let view = webviews.get_mut(&event.1).unwrap();
|
||||
|
||||
view.dom
|
||||
.handle_event(&evt.name, evt.data.into_any(), evt.element, evt.bubbles);
|
||||
|
||||
send_edits(view.dom.render_immediate(), &view.webview);
|
||||
}
|
||||
"browser_open" => {
|
||||
let data = message.params();
|
||||
log::trace!("Open browser: {:?}", data);
|
||||
if let Some(temp) = data.as_object() {
|
||||
|
||||
EventData::Ipc(msg) if msg.method() == "initialize" => {
|
||||
let view = webviews.get_mut(&event.1).unwrap();
|
||||
send_edits(view.dom.rebuild(), &view.webview);
|
||||
}
|
||||
|
||||
// When the webview chirps back with the result of the eval, we send it to the active receiver
|
||||
//
|
||||
// This currently doesn't perform any targeting to the callsite, so if you eval multiple times at once,
|
||||
// you might the wrong result. This should be fixed
|
||||
EventData::Ipc(msg) if msg.method() == "eval_result" => {
|
||||
webviews[&event.1]
|
||||
.dom
|
||||
.base_scope()
|
||||
.consume_context::<DesktopContext>()
|
||||
.unwrap()
|
||||
.eval
|
||||
.send(msg.params())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
EventData::Ipc(msg) if msg.method() == "browser_open" => {
|
||||
if let Some(temp) = msg.params().as_object() {
|
||||
if temp.contains_key("href") {
|
||||
let url = temp.get("href").unwrap().as_str().unwrap();
|
||||
if let Err(e) = webbrowser::open(url) {
|
||||
let open = webbrowser::open(temp["href"].as_str().unwrap());
|
||||
if let Err(e) = open {
|
||||
log::error!("Open Browser error: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
})
|
||||
.with_custom_protocol(String::from("dioxus"), move |r| {
|
||||
protocol::desktop_handler(
|
||||
r,
|
||||
resource_dir.clone(),
|
||||
custom_head.clone(),
|
||||
index_file.clone(),
|
||||
&root_name,
|
||||
)
|
||||
})
|
||||
.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)
|
||||
_ => {}
|
||||
},
|
||||
Event::GlobalShortcutEvent(id) => shortcut_manager.call_handlers(id),
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn create_new_window(
|
||||
mut cfg: Config,
|
||||
event_loop: &EventLoopWindowTarget<UserWindowEvent>,
|
||||
proxy: &EventLoopProxy<UserWindowEvent>,
|
||||
dom: VirtualDom,
|
||||
queue: &WebviewQueue,
|
||||
event_handlers: &WindowEventHandlers,
|
||||
shortcut_manager: ShortcutRegistry,
|
||||
) -> WebviewHandler {
|
||||
let (webview, web_context) = webview::build(&mut cfg, event_loop, proxy.clone());
|
||||
|
||||
dom.base_scope().provide_context(DesktopContext::new(
|
||||
webview.clone(),
|
||||
proxy.clone(),
|
||||
event_loop.clone(),
|
||||
queue.clone(),
|
||||
event_handlers.clone(),
|
||||
shortcut_manager,
|
||||
));
|
||||
|
||||
let id = webview.window().id();
|
||||
|
||||
// We want to poll the virtualdom and the event loop at the same time, so the waker will be connected to both
|
||||
WebviewHandler {
|
||||
webview,
|
||||
dom,
|
||||
waker: waker::tao_waker(proxy, id),
|
||||
web_context,
|
||||
}
|
||||
}
|
||||
|
||||
struct WebviewHandler {
|
||||
dom: VirtualDom,
|
||||
webview: Rc<wry::webview::WebView>,
|
||||
waker: Waker,
|
||||
web_context: WebContext,
|
||||
}
|
||||
|
||||
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();
|
||||
}, false);
|
||||
} else {
|
||||
document.attachEvent('oncontextmenu', function() {
|
||||
window.event.returnValue = false;
|
||||
});
|
||||
}
|
||||
"#,
|
||||
)
|
||||
} else {
|
||||
// in debug, we are okay with the reload menu showing and dev tool
|
||||
webview = webview.with_devtools(true);
|
||||
/// Poll the virtualdom until it's pending
|
||||
///
|
||||
/// The waker we give it is connected to the event loop, so it will wake up the event loop when it's ready to be polled again
|
||||
///
|
||||
/// All IO is done on the tokio runtime we started earlier
|
||||
fn poll_vdom(view: &mut WebviewHandler) {
|
||||
let mut cx = std::task::Context::from_waker(&view.waker);
|
||||
|
||||
loop {
|
||||
{
|
||||
let fut = view.dom.wait_for_work();
|
||||
pin_mut!(fut);
|
||||
|
||||
match fut.poll_unpin(&mut cx) {
|
||||
std::task::Poll::Ready(_) => {}
|
||||
std::task::Poll::Pending => break,
|
||||
}
|
||||
}
|
||||
|
||||
send_edits(view.dom.render_immediate(), &view.webview);
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a list of mutations to the webview
|
||||
fn send_edits(edits: Mutations, webview: &WebView) {
|
||||
let serialized = serde_json::to_string(&edits).unwrap();
|
||||
|
||||
webview.build().unwrap()
|
||||
// todo: use SSE and binary data to send the edits with lower overhead
|
||||
_ = webview.evaluate_script(&format!("window.interpreter.handleEdits({serialized})"));
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
use dioxus_interpreter_js::INTERPRETER_JS;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use wry::{
|
||||
http::{status::StatusCode, Request, Response},
|
||||
Result,
|
||||
@@ -11,87 +14,75 @@ fn module_loader(root_name: &str) -> String {
|
||||
<script>
|
||||
{INTERPRETER_JS}
|
||||
|
||||
let rootname = "{}";
|
||||
let rootname = "{root_name}";
|
||||
let root = window.document.getElementById(rootname);
|
||||
if (root != null) {{
|
||||
window.interpreter = new Interpreter(root);
|
||||
window.ipc.postMessage(serializeIpcMessage("initialize"));
|
||||
}}
|
||||
</script>
|
||||
"#,
|
||||
root_name
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn desktop_handler(
|
||||
request: &Request<Vec<u8>>,
|
||||
asset_root: Option<PathBuf>,
|
||||
custom_head: Option<String>,
|
||||
custom_index: Option<String>,
|
||||
root_name: &str,
|
||||
) -> Result<Response<Vec<u8>>> {
|
||||
// Any content that uses the `dioxus://` scheme will be shuttled through this handler as a "special case".
|
||||
// For now, we only serve two pieces of content which get included as bytes into the final binary.
|
||||
let path = request.uri().to_string().replace("dioxus://", "");
|
||||
|
||||
// all assets should be called from index.html
|
||||
let trimmed = path.trim_start_matches("index.html/");
|
||||
|
||||
if trimmed.is_empty() {
|
||||
) -> Result<Response<Cow<'static, [u8]>>> {
|
||||
// If the request is for the root, we'll serve the index.html file.
|
||||
if request.uri().path() == "/" {
|
||||
// If a custom index is provided, just defer to that, expecting the user to know what they're doing.
|
||||
// we'll look for the closing </body> tag and insert our little module loader there.
|
||||
if let Some(custom_index) = custom_index {
|
||||
let rendered = custom_index
|
||||
let body = match custom_index {
|
||||
Some(custom_index) => custom_index
|
||||
.replace("</body>", &format!("{}</body>", module_loader(root_name)))
|
||||
.into_bytes();
|
||||
Response::builder()
|
||||
.header("Content-Type", "text/html")
|
||||
.body(rendered)
|
||||
.map_err(From::from)
|
||||
} else {
|
||||
// Otherwise, we'll serve the default index.html and apply a custom head if that's specified.
|
||||
let mut template = include_str!("./index.html").to_string();
|
||||
if let Some(custom_head) = custom_head {
|
||||
template = template.replace("<!-- CUSTOM HEAD -->", &custom_head);
|
||||
.into_bytes(),
|
||||
|
||||
None => {
|
||||
// Otherwise, we'll serve the default index.html and apply a custom head if that's specified.
|
||||
let mut template = include_str!("./index.html").to_string();
|
||||
|
||||
if let Some(custom_head) = custom_head {
|
||||
template = template.replace("<!-- CUSTOM HEAD -->", &custom_head);
|
||||
}
|
||||
|
||||
template
|
||||
.replace("<!-- MODULE LOADER -->", &module_loader(root_name))
|
||||
.into_bytes()
|
||||
}
|
||||
template = template.replace("<!-- MODULE LOADER -->", &module_loader(root_name));
|
||||
};
|
||||
|
||||
Response::builder()
|
||||
.header("Content-Type", "text/html")
|
||||
.body(template.into_bytes())
|
||||
.map_err(From::from)
|
||||
}
|
||||
} else if trimmed == "index.js" {
|
||||
Response::builder()
|
||||
.header("Content-Type", "text/javascript")
|
||||
.body(dioxus_interpreter_js::INTERPRETER_JS.as_bytes().to_vec())
|
||||
.map_err(From::from)
|
||||
} else {
|
||||
let asset_root = asset_root
|
||||
.unwrap_or_else(|| get_asset_root().unwrap_or_else(|| Path::new(".").to_path_buf()))
|
||||
.canonicalize()?;
|
||||
|
||||
let asset = asset_root.join(trimmed).canonicalize()?;
|
||||
|
||||
if !asset.starts_with(asset_root) {
|
||||
return Response::builder()
|
||||
.status(StatusCode::FORBIDDEN)
|
||||
.body(String::from("Forbidden").into_bytes())
|
||||
.map_err(From::from);
|
||||
}
|
||||
|
||||
if !asset.exists() {
|
||||
return Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body(String::from("Not Found").into_bytes())
|
||||
.map_err(From::from);
|
||||
}
|
||||
|
||||
Response::builder()
|
||||
.header("Content-Type", get_mime_from_path(trimmed)?)
|
||||
.body(std::fs::read(asset)?)
|
||||
.map_err(From::from)
|
||||
return Response::builder()
|
||||
.header("Content-Type", "text/html")
|
||||
.body(Cow::from(body))
|
||||
.map_err(From::from);
|
||||
}
|
||||
|
||||
// Else, try to serve a file from the filesystem.
|
||||
let path = PathBuf::from(request.uri().path().trim_start_matches('/'));
|
||||
|
||||
// If the path is relative, we'll try to serve it from the assets directory.
|
||||
let mut asset = get_asset_root()
|
||||
.unwrap_or_else(|| Path::new(".").to_path_buf())
|
||||
.join(&path);
|
||||
|
||||
if !asset.exists() {
|
||||
asset = PathBuf::from("/").join(path);
|
||||
}
|
||||
|
||||
if asset.exists() {
|
||||
return Response::builder()
|
||||
.header("Content-Type", get_mime_from_path(&asset)?)
|
||||
.body(Cow::from(std::fs::read(asset)?))
|
||||
.map_err(From::from);
|
||||
}
|
||||
|
||||
Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body(Cow::from(String::from("Not Found").into_bytes()))
|
||||
.map_err(From::from)
|
||||
}
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
@@ -128,7 +119,7 @@ fn get_asset_root() -> Option<PathBuf> {
|
||||
}
|
||||
|
||||
/// Get the mime type from a path-like string
|
||||
fn get_mime_from_path(trimmed: &str) -> Result<&str> {
|
||||
fn get_mime_from_path(trimmed: &Path) -> Result<&'static str> {
|
||||
if trimmed.ends_with(".svg") {
|
||||
return Ok("image/svg+xml");
|
||||
}
|
||||
@@ -143,9 +134,8 @@ fn get_mime_from_path(trimmed: &str) -> Result<&str> {
|
||||
}
|
||||
|
||||
/// Get the mime type from a URI using its extension
|
||||
fn get_mime_by_ext(trimmed: &str) -> &str {
|
||||
let suffix = trimmed.split('.').last();
|
||||
match suffix {
|
||||
fn get_mime_by_ext(trimmed: &Path) -> &'static str {
|
||||
match trimmed.extension().and_then(|e| e.to_str()) {
|
||||
Some("bin") => "application/octet-stream",
|
||||
Some("css") => "text/css",
|
||||
Some("csv") => "text/csv",
|
||||
|
||||
@@ -6,7 +6,7 @@ Render the Dioxus VirtualDom using the platform's native WebView implementation.
|
||||
|
||||
One of Dioxus' flagship features is the ability to quickly build a native desktop app that looks and feels the same across platforms. Apps built with Dioxus are typically <5mb in size and use existing system resources, so they won't hog extreme amounts of RAM or memory.
|
||||
|
||||
Dioxus Desktop is built off Tauri. Right now there aren't any Dioxus abstractions over keyboard shortcuts, menubar, handling, etc, so you'll want to leverage Tauri - mostly [Wry](http://github.com/tauri-apps/wry/) and [Tao](http://github.com/tauri-apps/tao) directly. An upcoming release of Dioxus-Desktop will include components and hooks for notifications, global shortcuts, menubar, etc.
|
||||
Dioxus Desktop is built off Tauri. Right now there aren't any Dioxus abstractions over the menubar, handling, etc, so you'll want to leverage Tauri - mostly [Wry](http://github.com/tauri-apps/wry/) and [Tao](http://github.com/tauri-apps/tao) directly. An upcoming release of Dioxus-Desktop will include components and hooks for notifications, global shortcuts, menubar, etc.
|
||||
|
||||
|
||||
## Getting Set up
|
||||
@@ -18,10 +18,11 @@ $ cargo new --bin demo
|
||||
$ cd app
|
||||
```
|
||||
|
||||
Add Dioxus with the `desktop` feature:
|
||||
Add Dioxus and the `desktop` renderer feature:
|
||||
|
||||
```shell
|
||||
$ cargo add dioxus --features desktop
|
||||
$ cargo add dioxus
|
||||
$ cargo add dioxus-desktop
|
||||
```
|
||||
|
||||
Edit your `main.rs`:
|
||||
@@ -48,4 +49,4 @@ To configure the webview, menubar, and other important desktop-specific features
|
||||
|
||||
## Future Steps
|
||||
|
||||
Make sure to read the [Dioxus Guide](https://dioxuslabs.com/guide) if you already haven't!
|
||||
Make sure to read the [Dioxus Guide](https://dioxuslabs.com/docs/0.3/guide/en) if you already haven't!
|
||||
@@ -0,0 +1,332 @@
|
||||
use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
||||
|
||||
use dioxus_core::ScopeState;
|
||||
use dioxus_html::input_data::keyboard_types::Modifiers;
|
||||
use slab::Slab;
|
||||
use wry::application::{
|
||||
accelerator::{Accelerator, AcceleratorId},
|
||||
event_loop::EventLoopWindowTarget,
|
||||
global_shortcut::{GlobalShortcut, ShortcutManager, ShortcutManagerError},
|
||||
keyboard::{KeyCode, ModifiersState},
|
||||
};
|
||||
|
||||
use crate::{use_window, DesktopContext};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ShortcutRegistry {
|
||||
manager: Rc<RefCell<ShortcutManager>>,
|
||||
shortcuts: ShortcutMap,
|
||||
}
|
||||
|
||||
type ShortcutMap = Rc<RefCell<HashMap<AcceleratorId, Shortcut>>>;
|
||||
|
||||
struct Shortcut {
|
||||
shortcut: GlobalShortcut,
|
||||
callbacks: Slab<Box<dyn FnMut()>>,
|
||||
}
|
||||
|
||||
impl Shortcut {
|
||||
fn insert(&mut self, callback: Box<dyn FnMut()>) -> usize {
|
||||
self.callbacks.insert(callback)
|
||||
}
|
||||
|
||||
fn remove(&mut self, id: usize) {
|
||||
let _ = self.callbacks.remove(id);
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.callbacks.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl ShortcutRegistry {
|
||||
pub fn new<T>(target: &EventLoopWindowTarget<T>) -> Self {
|
||||
let myself = Self {
|
||||
manager: Rc::new(RefCell::new(ShortcutManager::new(target))),
|
||||
shortcuts: Rc::new(RefCell::new(HashMap::new())),
|
||||
};
|
||||
// prevent CTRL+R from reloading the page which breaks apps
|
||||
let _ = myself.add_shortcut(
|
||||
Some(ModifiersState::CONTROL),
|
||||
KeyCode::KeyR,
|
||||
Box::new(|| {}),
|
||||
);
|
||||
myself
|
||||
}
|
||||
|
||||
pub(crate) fn call_handlers(&self, id: AcceleratorId) {
|
||||
if let Some(Shortcut { callbacks, .. }) = self.shortcuts.borrow_mut().get_mut(&id) {
|
||||
for (_, callback) in callbacks.iter_mut() {
|
||||
(callback)();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn add_shortcut(
|
||||
&self,
|
||||
modifiers: impl Into<Option<ModifiersState>>,
|
||||
key: KeyCode,
|
||||
callback: Box<dyn FnMut()>,
|
||||
) -> Result<ShortcutId, ShortcutRegistryError> {
|
||||
let accelerator = Accelerator::new(modifiers, key);
|
||||
let accelerator_id = accelerator.clone().id();
|
||||
let mut shortcuts = self.shortcuts.borrow_mut();
|
||||
Ok(
|
||||
if let Some(callbacks) = shortcuts.get_mut(&accelerator_id) {
|
||||
let id = callbacks.insert(callback);
|
||||
ShortcutId {
|
||||
id: accelerator_id,
|
||||
number: id,
|
||||
}
|
||||
} else {
|
||||
match self.manager.borrow_mut().register(accelerator) {
|
||||
Ok(global_shortcut) => {
|
||||
let mut slab = Slab::new();
|
||||
let id = slab.insert(callback);
|
||||
let shortcut = Shortcut {
|
||||
shortcut: global_shortcut,
|
||||
callbacks: slab,
|
||||
};
|
||||
shortcuts.insert(accelerator_id, shortcut);
|
||||
ShortcutId {
|
||||
id: accelerator_id,
|
||||
number: id,
|
||||
}
|
||||
}
|
||||
Err(ShortcutManagerError::InvalidAccelerator(shortcut)) => {
|
||||
return Err(ShortcutRegistryError::InvalidShortcut(shortcut))
|
||||
}
|
||||
Err(err) => return Err(ShortcutRegistryError::Other(Box::new(err))),
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn remove_shortcut(&self, id: ShortcutId) {
|
||||
let mut shortcuts = self.shortcuts.borrow_mut();
|
||||
if let Some(callbacks) = shortcuts.get_mut(&id.id) {
|
||||
callbacks.remove(id.number);
|
||||
if callbacks.is_empty() {
|
||||
if let Some(shortcut) = shortcuts.remove(&id.id) {
|
||||
let _ = self.manager.borrow_mut().unregister(shortcut.shortcut);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn remove_all(&self) {
|
||||
let mut shortcuts = self.shortcuts.borrow_mut();
|
||||
shortcuts.clear();
|
||||
let _ = self.manager.borrow_mut().unregister_all();
|
||||
// prevent CTRL+R from reloading the page which breaks apps
|
||||
let _ = self.add_shortcut(
|
||||
Some(ModifiersState::CONTROL),
|
||||
KeyCode::KeyR,
|
||||
Box::new(|| {}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug)]
|
||||
/// An error that can occur when registering a shortcut.
|
||||
pub enum ShortcutRegistryError {
|
||||
/// The shortcut is invalid.
|
||||
InvalidShortcut(String),
|
||||
/// An unknown error occurred.
|
||||
Other(Box<dyn std::error::Error>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
/// An global id for a shortcut.
|
||||
pub struct ShortcutId {
|
||||
id: AcceleratorId,
|
||||
number: usize,
|
||||
}
|
||||
|
||||
/// A global shortcut. This will be automatically removed when it is dropped.
|
||||
pub struct ShortcutHandle {
|
||||
desktop: DesktopContext,
|
||||
/// The id of the shortcut
|
||||
pub shortcut_id: ShortcutId,
|
||||
}
|
||||
|
||||
/// Get a closure that executes any JavaScript in the WebView context.
|
||||
pub fn use_global_shortcut(
|
||||
cx: &ScopeState,
|
||||
key: impl IntoKeyCode,
|
||||
modifiers: impl IntoModifersState,
|
||||
handler: impl FnMut() + 'static,
|
||||
) -> &Result<ShortcutHandle, ShortcutRegistryError> {
|
||||
let desktop = use_window(cx);
|
||||
cx.use_hook(move || {
|
||||
let desktop = desktop.clone();
|
||||
|
||||
let id = desktop.create_shortcut(
|
||||
key.into_key_code(),
|
||||
modifiers.into_modifiers_state(),
|
||||
handler,
|
||||
);
|
||||
|
||||
Ok(ShortcutHandle {
|
||||
desktop,
|
||||
shortcut_id: id?,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
impl ShortcutHandle {
|
||||
/// Remove the shortcut.
|
||||
pub fn remove(&self) {
|
||||
self.desktop.remove_shortcut(self.shortcut_id);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ShortcutHandle {
|
||||
fn drop(&mut self) {
|
||||
self.remove()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IntoModifersState {
|
||||
fn into_modifiers_state(self) -> ModifiersState;
|
||||
}
|
||||
|
||||
impl IntoModifersState for ModifiersState {
|
||||
fn into_modifiers_state(self) -> ModifiersState {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoModifersState for Modifiers {
|
||||
fn into_modifiers_state(self) -> ModifiersState {
|
||||
let mut state = ModifiersState::empty();
|
||||
if self.contains(Modifiers::SHIFT) {
|
||||
state |= ModifiersState::SHIFT
|
||||
}
|
||||
if self.contains(Modifiers::CONTROL) {
|
||||
state |= ModifiersState::CONTROL
|
||||
}
|
||||
if self.contains(Modifiers::ALT) {
|
||||
state |= ModifiersState::ALT
|
||||
}
|
||||
if self.contains(Modifiers::META) || self.contains(Modifiers::SUPER) {
|
||||
state |= ModifiersState::SUPER
|
||||
}
|
||||
state
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IntoKeyCode {
|
||||
fn into_key_code(self) -> KeyCode;
|
||||
}
|
||||
|
||||
impl IntoKeyCode for KeyCode {
|
||||
fn into_key_code(self) -> KeyCode {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoKeyCode for dioxus_html::KeyCode {
|
||||
fn into_key_code(self) -> KeyCode {
|
||||
match self {
|
||||
dioxus_html::KeyCode::Backspace => KeyCode::Backspace,
|
||||
dioxus_html::KeyCode::Tab => KeyCode::Tab,
|
||||
dioxus_html::KeyCode::Clear => KeyCode::NumpadClear,
|
||||
dioxus_html::KeyCode::Enter => KeyCode::Enter,
|
||||
dioxus_html::KeyCode::Shift => KeyCode::ShiftLeft,
|
||||
dioxus_html::KeyCode::Ctrl => KeyCode::ControlLeft,
|
||||
dioxus_html::KeyCode::Alt => KeyCode::AltLeft,
|
||||
dioxus_html::KeyCode::Pause => KeyCode::Pause,
|
||||
dioxus_html::KeyCode::CapsLock => KeyCode::CapsLock,
|
||||
dioxus_html::KeyCode::Escape => KeyCode::Escape,
|
||||
dioxus_html::KeyCode::Space => KeyCode::Space,
|
||||
dioxus_html::KeyCode::PageUp => KeyCode::PageUp,
|
||||
dioxus_html::KeyCode::PageDown => KeyCode::PageDown,
|
||||
dioxus_html::KeyCode::End => KeyCode::End,
|
||||
dioxus_html::KeyCode::Home => KeyCode::Home,
|
||||
dioxus_html::KeyCode::LeftArrow => KeyCode::ArrowLeft,
|
||||
dioxus_html::KeyCode::UpArrow => KeyCode::ArrowUp,
|
||||
dioxus_html::KeyCode::RightArrow => KeyCode::ArrowRight,
|
||||
dioxus_html::KeyCode::DownArrow => KeyCode::ArrowDown,
|
||||
dioxus_html::KeyCode::Insert => KeyCode::Insert,
|
||||
dioxus_html::KeyCode::Delete => KeyCode::Delete,
|
||||
dioxus_html::KeyCode::Num0 => KeyCode::Numpad0,
|
||||
dioxus_html::KeyCode::Num1 => KeyCode::Numpad1,
|
||||
dioxus_html::KeyCode::Num2 => KeyCode::Numpad2,
|
||||
dioxus_html::KeyCode::Num3 => KeyCode::Numpad3,
|
||||
dioxus_html::KeyCode::Num4 => KeyCode::Numpad4,
|
||||
dioxus_html::KeyCode::Num5 => KeyCode::Numpad5,
|
||||
dioxus_html::KeyCode::Num6 => KeyCode::Numpad6,
|
||||
dioxus_html::KeyCode::Num7 => KeyCode::Numpad7,
|
||||
dioxus_html::KeyCode::Num8 => KeyCode::Numpad8,
|
||||
dioxus_html::KeyCode::Num9 => KeyCode::Numpad9,
|
||||
dioxus_html::KeyCode::A => KeyCode::KeyA,
|
||||
dioxus_html::KeyCode::B => KeyCode::KeyB,
|
||||
dioxus_html::KeyCode::C => KeyCode::KeyC,
|
||||
dioxus_html::KeyCode::D => KeyCode::KeyD,
|
||||
dioxus_html::KeyCode::E => KeyCode::KeyE,
|
||||
dioxus_html::KeyCode::F => KeyCode::KeyF,
|
||||
dioxus_html::KeyCode::G => KeyCode::KeyG,
|
||||
dioxus_html::KeyCode::H => KeyCode::KeyH,
|
||||
dioxus_html::KeyCode::I => KeyCode::KeyI,
|
||||
dioxus_html::KeyCode::J => KeyCode::KeyJ,
|
||||
dioxus_html::KeyCode::K => KeyCode::KeyK,
|
||||
dioxus_html::KeyCode::L => KeyCode::KeyL,
|
||||
dioxus_html::KeyCode::M => KeyCode::KeyM,
|
||||
dioxus_html::KeyCode::N => KeyCode::KeyN,
|
||||
dioxus_html::KeyCode::O => KeyCode::KeyO,
|
||||
dioxus_html::KeyCode::P => KeyCode::KeyP,
|
||||
dioxus_html::KeyCode::Q => KeyCode::KeyQ,
|
||||
dioxus_html::KeyCode::R => KeyCode::KeyR,
|
||||
dioxus_html::KeyCode::S => KeyCode::KeyS,
|
||||
dioxus_html::KeyCode::T => KeyCode::KeyT,
|
||||
dioxus_html::KeyCode::U => KeyCode::KeyU,
|
||||
dioxus_html::KeyCode::V => KeyCode::KeyV,
|
||||
dioxus_html::KeyCode::W => KeyCode::KeyW,
|
||||
dioxus_html::KeyCode::X => KeyCode::KeyX,
|
||||
dioxus_html::KeyCode::Y => KeyCode::KeyY,
|
||||
dioxus_html::KeyCode::Z => KeyCode::KeyZ,
|
||||
dioxus_html::KeyCode::Numpad0 => KeyCode::Numpad0,
|
||||
dioxus_html::KeyCode::Numpad1 => KeyCode::Numpad1,
|
||||
dioxus_html::KeyCode::Numpad2 => KeyCode::Numpad2,
|
||||
dioxus_html::KeyCode::Numpad3 => KeyCode::Numpad3,
|
||||
dioxus_html::KeyCode::Numpad4 => KeyCode::Numpad4,
|
||||
dioxus_html::KeyCode::Numpad5 => KeyCode::Numpad5,
|
||||
dioxus_html::KeyCode::Numpad6 => KeyCode::Numpad6,
|
||||
dioxus_html::KeyCode::Numpad7 => KeyCode::Numpad7,
|
||||
dioxus_html::KeyCode::Numpad8 => KeyCode::Numpad8,
|
||||
dioxus_html::KeyCode::Numpad9 => KeyCode::Numpad9,
|
||||
dioxus_html::KeyCode::Multiply => KeyCode::NumpadMultiply,
|
||||
dioxus_html::KeyCode::Add => KeyCode::NumpadAdd,
|
||||
dioxus_html::KeyCode::Subtract => KeyCode::NumpadSubtract,
|
||||
dioxus_html::KeyCode::DecimalPoint => KeyCode::NumpadDecimal,
|
||||
dioxus_html::KeyCode::Divide => KeyCode::NumpadDivide,
|
||||
dioxus_html::KeyCode::F1 => KeyCode::F1,
|
||||
dioxus_html::KeyCode::F2 => KeyCode::F2,
|
||||
dioxus_html::KeyCode::F3 => KeyCode::F3,
|
||||
dioxus_html::KeyCode::F4 => KeyCode::F4,
|
||||
dioxus_html::KeyCode::F5 => KeyCode::F5,
|
||||
dioxus_html::KeyCode::F6 => KeyCode::F6,
|
||||
dioxus_html::KeyCode::F7 => KeyCode::F7,
|
||||
dioxus_html::KeyCode::F8 => KeyCode::F8,
|
||||
dioxus_html::KeyCode::F9 => KeyCode::F9,
|
||||
dioxus_html::KeyCode::F10 => KeyCode::F10,
|
||||
dioxus_html::KeyCode::F11 => KeyCode::F11,
|
||||
dioxus_html::KeyCode::F12 => KeyCode::F12,
|
||||
dioxus_html::KeyCode::NumLock => KeyCode::NumLock,
|
||||
dioxus_html::KeyCode::ScrollLock => KeyCode::ScrollLock,
|
||||
dioxus_html::KeyCode::Semicolon => KeyCode::Semicolon,
|
||||
dioxus_html::KeyCode::EqualSign => KeyCode::Equal,
|
||||
dioxus_html::KeyCode::Comma => KeyCode::Comma,
|
||||
dioxus_html::KeyCode::Period => KeyCode::Period,
|
||||
dioxus_html::KeyCode::ForwardSlash => KeyCode::Slash,
|
||||
dioxus_html::KeyCode::GraveAccent => KeyCode::Backquote,
|
||||
dioxus_html::KeyCode::OpenBracket => KeyCode::BracketLeft,
|
||||
dioxus_html::KeyCode::BackSlash => KeyCode::Backslash,
|
||||
dioxus_html::KeyCode::CloseBraket => KeyCode::BracketRight,
|
||||
dioxus_html::KeyCode::SingleQuote => KeyCode::Quote,
|
||||
key => panic!("Failed to convert {:?} to tao::keyboard::KeyCode, try using tao::keyboard::KeyCode directly", key),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
use crate::desktop_context::{EventData, UserWindowEvent};
|
||||
use futures_util::task::ArcWake;
|
||||
use std::sync::Arc;
|
||||
use wry::application::{event_loop::EventLoopProxy, window::WindowId};
|
||||
|
||||
/// Create a waker that will send a poll event to the event loop.
|
||||
///
|
||||
/// This lets the VirtualDom "come up for air" and process events while the main thread is blocked by the WebView.
|
||||
///
|
||||
/// All other IO lives in the Tokio runtime,
|
||||
pub fn tao_waker(proxy: &EventLoopProxy<UserWindowEvent>, id: WindowId) -> std::task::Waker {
|
||||
struct DomHandle {
|
||||
proxy: EventLoopProxy<UserWindowEvent>,
|
||||
id: WindowId,
|
||||
}
|
||||
|
||||
// this should be implemented by most platforms, but ios is missing this until
|
||||
// https://github.com/tauri-apps/wry/issues/830 is resolved
|
||||
unsafe impl Send for DomHandle {}
|
||||
unsafe impl Sync for DomHandle {}
|
||||
|
||||
impl ArcWake for DomHandle {
|
||||
fn wake_by_ref(arc_self: &Arc<Self>) {
|
||||
_ = arc_self
|
||||
.proxy
|
||||
.send_event(UserWindowEvent(EventData::Poll, arc_self.id));
|
||||
}
|
||||
}
|
||||
|
||||
futures_util::task::waker(Arc::new(DomHandle {
|
||||
id,
|
||||
proxy: proxy.clone(),
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::desktop_context::EventData;
|
||||
use crate::protocol;
|
||||
use crate::{desktop_context::UserWindowEvent, Config};
|
||||
use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget};
|
||||
pub use wry;
|
||||
pub use wry::application as tao;
|
||||
use wry::application::window::Window;
|
||||
use wry::webview::{WebContext, WebView, WebViewBuilder};
|
||||
|
||||
pub fn build(
|
||||
cfg: &mut Config,
|
||||
event_loop: &EventLoopWindowTarget<UserWindowEvent>,
|
||||
proxy: EventLoopProxy<UserWindowEvent>,
|
||||
) -> (Rc<WebView>, WebContext) {
|
||||
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 index_file = cfg.custom_index.clone();
|
||||
let root_name = cfg.root_name.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() {
|
||||
window.set_window_icon(Some(
|
||||
tao::window::Icon::from_rgba(
|
||||
include_bytes!("./assets/default_icon.bin").to_vec(),
|
||||
460,
|
||||
460,
|
||||
)
|
||||
.expect("image parse failed"),
|
||||
));
|
||||
}
|
||||
|
||||
let mut web_context = WebContext::new(cfg.data_dir.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| {
|
||||
// defer the event to the main thread
|
||||
if let Ok(message) = serde_json::from_str(&payload) {
|
||||
_ = proxy.send_event(UserWindowEvent(EventData::Ipc(message), window.id()));
|
||||
}
|
||||
})
|
||||
.with_custom_protocol(String::from("dioxus"), move |r| {
|
||||
protocol::desktop_handler(r, custom_head.clone(), index_file.clone(), &root_name)
|
||||
})
|
||||
.with_file_drop_handler(move |window, evet| {
|
||||
file_handler
|
||||
.as_ref()
|
||||
.map(|handler| handler(window, evet))
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.with_web_context(&mut web_context);
|
||||
|
||||
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 (document.addEventListener) {
|
||||
document.addEventListener('contextmenu', function(e) {
|
||||
e.preventDefault();
|
||||
}, false);
|
||||
} else {
|
||||
document.attachEvent('oncontextmenu', function() {
|
||||
window.event.returnValue = false;
|
||||
});
|
||||
}
|
||||
"#,
|
||||
)
|
||||
} else {
|
||||
// in debug, we are okay with the reload menu showing and dev tool
|
||||
webview = webview.with_devtools(true);
|
||||
}
|
||||
|
||||
(Rc::new(webview.build().unwrap()), web_context)
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,40 @@
|
||||
[package]
|
||||
name = "dioxus-tui"
|
||||
version = "0.2.2"
|
||||
authors = ["Jonathan Kelley, @dementhos"]
|
||||
edition = "2021"
|
||||
description = "TUI-based renderer for Dioxus"
|
||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||
homepage = "https://dioxuslabs.com"
|
||||
documentation = "https://dioxuslabs.com"
|
||||
keywords = ["dom", "ui", "gui", "react", "terminal"]
|
||||
license = "MIT/Apache-2.0"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus = { path = "../dioxus", version = "^0.3.0" }
|
||||
dioxus-core = { path = "../core", version = "^0.3.0", features = ["serialize"] }
|
||||
dioxus-html = { path = "../html", version = "^0.3.0" }
|
||||
dioxus-native-core = { path = "../native-core", version = "^0.2.0", features = ["dioxus"] }
|
||||
dioxus-native-core-macro = { path = "../native-core-macro", version = "^0.3.0" }
|
||||
dioxus-hot-reload = { path = "../hot-reload", optional = true }
|
||||
rink = { path = "../rink" }
|
||||
|
||||
crossterm = "0.23.0"
|
||||
tokio = { version = "1.15.0", features = ["full"] }
|
||||
futures = "0.3.19"
|
||||
taffy = "0.2.1"
|
||||
|
||||
[dev-dependencies]
|
||||
dioxus = { path = "../dioxus" }
|
||||
tokio = { version = "1" }
|
||||
criterion = "0.3.5"
|
||||
|
||||
[[bench]]
|
||||
name = "update"
|
||||
harness = false
|
||||
|
||||
[features]
|
||||
default = ["hot-reload"]
|
||||
hot-reload = ["dioxus-hot-reload"]
|
||||
@@ -1,5 +1,5 @@
|
||||
<div align="center">
|
||||
<h1>Rink</h1>
|
||||
<h1>Dioxus TUI</h1>
|
||||
<p>
|
||||
<strong>Beautiful terminal user interfaces in Rust with <a href="https://dioxuslabs.com/">Dioxus </a>.</strong>
|
||||
</p>
|
||||
@@ -37,7 +37,6 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
<br/>
|
||||
|
||||
Leverage React-like patterns, CSS, HTML, and Rust to build beautiful, portable, terminal user interfaces with Dioxus.
|
||||
@@ -61,34 +60,36 @@ fn app(cx: Scope) -> Element {
|
||||
|
||||
## Background
|
||||
|
||||
You can use Html-like semantics with stylesheets, inline styles, tree hierarchy, components, and more in your [`text-based user interface (TUI)`](https://en.wikipedia.org/wiki/Text-based_user_interface) application.
|
||||
You can use Html-like semantics with inline styles, tree hierarchy, components, and more in your [`text-based user interface (TUI)`](https://en.wikipedia.org/wiki/Text-based_user_interface) application.
|
||||
|
||||
Rink is basically a port of [Ink](https://github.com/vadimdemedes/ink) but for [`Rust`](https://www.rust-lang.org/) and [`Dioxus`](https://dioxuslabs.com/). Rink doesn't depend on Node.js or any other JavaScript runtime, so your binaries are portable and beautiful.
|
||||
Dioxus TUI is essentially a port of [Ink](https://github.com/vadimdemedes/ink) but for [`Rust`](https://www.rust-lang.org/) and [`Dioxus`](https://dioxuslabs.com/). Dioxus TUI doesn't depend on Node.js or any other JavaScript runtime, so your binaries are portable and beautiful.
|
||||
|
||||
## Limitations
|
||||
|
||||
- **Subset of Html**
|
||||
Terminals can only render a subset of HTML. We support as much as we can.
|
||||
Terminals can only render a subset of HTML. We support as much as we can.
|
||||
- **Particular frontend design**
|
||||
Terminals and browsers are and look different. Therefore, the same design might not be the best to cover both renderers.
|
||||
|
||||
Terminals and browsers are and look different. Therefore, the same design might not be the best to cover both renderers.
|
||||
|
||||
## Status
|
||||
|
||||
**WARNING: Rink is currently under construction!**
|
||||
**WARNING: Dioxus TUI is currently under construction!**
|
||||
|
||||
Rendering a VirtualDom works fine, but the ecosystem of hooks is not yet ready. Additionally, some bugs in the flexbox implementation might be quirky at times.
|
||||
|
||||
## Features
|
||||
|
||||
Rink features:
|
||||
- [x] Flexbox based layout system
|
||||
Dioxus TUI features:
|
||||
|
||||
- [x] Flexbox-based layout system
|
||||
- [ ] CSS selectors
|
||||
- [x] inline CSS support
|
||||
- [x] Built-in focusing system
|
||||
- [ ] high-quality keyboard support
|
||||
* [ ] Support for events, hooks, and callbacks<sup>1</sup>
|
||||
* [ ] Html tags<sup>2</sup>
|
||||
|
||||
<sup>1</sup> Basic keyboard and mouse events are implemented.
|
||||
<sup>2</sup> Currently, HTML tags don't translate into any meaning inside of rink. So an `input` won't really mean anything nor does it have any additional functionality.
|
||||
* [x] Widgets<sup>1</sup>
|
||||
* [ ] Support for events, hooks, and callbacks<sup>2</sup>
|
||||
* [ ] Html tags<sup>3</sup>
|
||||
|
||||
<sup>1</sup> Currently only a subset of the input element is implemented as a component (not an element). The `Input` component supports sliders, text, numbers, passwords, buttons, and checkboxes.
|
||||
<sup>2</sup> Basic keyboard, mouse, and focus events are implemented.
|
||||
<sup>3</sup> Currently, most HTML tags don't translate into any meaning inside of Dioxus TUI. So an `input` _element_ won't mean anything nor does it have any additional functionality.
|
||||
@@ -0,0 +1,165 @@
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_tui::{Config, TuiContext};
|
||||
|
||||
criterion_group!(mbenches, tui_update);
|
||||
criterion_main!(mbenches);
|
||||
|
||||
/// This benchmarks the cache performance of the TUI for small edits by changing one box at a time.
|
||||
fn tui_update(c: &mut Criterion) {
|
||||
{
|
||||
let mut group = c.benchmark_group("Update boxes");
|
||||
|
||||
for size in 1..=20usize {
|
||||
let parameter_string = format!("{}", (size).pow(2));
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("size", parameter_string),
|
||||
&size,
|
||||
|b, size| {
|
||||
b.iter(|| {
|
||||
dioxus_tui::launch_cfg_with_props(
|
||||
app,
|
||||
GridProps {
|
||||
size: *size,
|
||||
update_count: 1,
|
||||
},
|
||||
Config::default().with_headless(),
|
||||
)
|
||||
})
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut group = c.benchmark_group("Update many boxes");
|
||||
|
||||
for update_count in 1..=20usize {
|
||||
let update_count = update_count * 20;
|
||||
let parameter_string = update_count.to_string();
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("update count", parameter_string),
|
||||
&update_count,
|
||||
|b, update_count| {
|
||||
b.iter(|| {
|
||||
dioxus_tui::launch_cfg_with_props(
|
||||
app,
|
||||
GridProps {
|
||||
size: 20,
|
||||
update_count: *update_count,
|
||||
},
|
||||
Config::default().with_headless(),
|
||||
)
|
||||
})
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Props, PartialEq)]
|
||||
struct BoxProps {
|
||||
x: usize,
|
||||
y: usize,
|
||||
hue: f32,
|
||||
alpha: f32,
|
||||
}
|
||||
#[allow(non_snake_case)]
|
||||
fn Box(cx: Scope<BoxProps>) -> Element {
|
||||
let count = use_state(cx, || 0);
|
||||
|
||||
let x = cx.props.x * 2;
|
||||
let y = cx.props.y * 2;
|
||||
let hue = cx.props.hue;
|
||||
let display_hue = cx.props.hue as u32 / 10;
|
||||
let count = count.get();
|
||||
let alpha = cx.props.alpha + (count % 100) as f32;
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
left: "{x}%",
|
||||
top: "{y}%",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
background_color: "hsl({hue}, 100%, 50%, {alpha}%)",
|
||||
align_items: "center",
|
||||
p{"{display_hue:03}"}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Props, PartialEq)]
|
||||
struct GridProps {
|
||||
size: usize,
|
||||
update_count: usize,
|
||||
}
|
||||
#[allow(non_snake_case)]
|
||||
fn Grid(cx: Scope<GridProps>) -> Element {
|
||||
let size = cx.props.size;
|
||||
let count = use_state(cx, || 0);
|
||||
let counts = use_ref(cx, || vec![0; size * size]);
|
||||
|
||||
let ctx: TuiContext = cx.consume_context().unwrap();
|
||||
if *count.get() + cx.props.update_count >= (size * size) {
|
||||
ctx.quit();
|
||||
} else {
|
||||
for _ in 0..cx.props.update_count {
|
||||
counts.with_mut(|c| {
|
||||
let i = *count.current();
|
||||
c[i] += 1;
|
||||
c[i] %= 360;
|
||||
});
|
||||
count.with_mut(|i| {
|
||||
*i += 1;
|
||||
*i %= size * size;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render! {
|
||||
div{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
flex_direction: "column",
|
||||
(0..size).map(|x|
|
||||
{
|
||||
rsx! {
|
||||
div{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
flex_direction: "row",
|
||||
(0..size).map(|y|
|
||||
{
|
||||
let alpha = y as f32*100.0/size as f32 + counts.read()[x*size + y] as f32;
|
||||
let key = format!("{}-{}", x, y);
|
||||
rsx! {
|
||||
Box{
|
||||
x: x,
|
||||
y: y,
|
||||
alpha: 100.0,
|
||||
hue: alpha,
|
||||
key: "{key}",
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn app(cx: Scope<GridProps>) -> Element {
|
||||
cx.render(rsx! {
|
||||
div{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
Grid{
|
||||
size: cx.props.size,
|
||||
update_count: cx.props.update_count,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+7
-5
@@ -1,6 +1,7 @@
|
||||
use dioxus::core::RenderReturn;
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_tui::query::Query;
|
||||
use dioxus_tui::DioxusElementToNodeId;
|
||||
use dioxus_tui::Query;
|
||||
use dioxus_tui::Size;
|
||||
|
||||
fn main() {
|
||||
@@ -10,16 +11,17 @@ fn main() {
|
||||
fn app(cx: Scope) -> Element {
|
||||
let hue = use_state(cx, || 0.0);
|
||||
let brightness = use_state(cx, || 0.0);
|
||||
let tui_query: &Query = cx.consume_context().unwrap();
|
||||
let tui_query: Query = cx.consume_context().unwrap();
|
||||
let mapping: DioxusElementToNodeId = cx.consume_context().unwrap();
|
||||
// disable templates so that every node has an id and can be queried
|
||||
cx.render(rsx! {
|
||||
div{
|
||||
width: "100%",
|
||||
background_color: "hsl({hue}, 70%, {brightness}%)",
|
||||
onmousemove: move |evt| {
|
||||
if let RenderReturn::Sync(Some(node)) = cx.root_node() {
|
||||
if let Some(id) = node.root_ids[0].get() {
|
||||
let node = tui_query.get(id);
|
||||
if let RenderReturn::Ready(node) = cx.root_node() {
|
||||
if let Some(id) = node.root_ids.get(0){
|
||||
let node = tui_query.get(mapping.get_node_id(id).unwrap());
|
||||
let Size{width, height} = node.size().unwrap();
|
||||
let pos = evt.inner().element_coordinates();
|
||||
hue.set((pos.x as f32/width as f32)*255.0);
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loaded 100 of 343 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user