Merge branch 'main' into unwind-into-error-boundary

This commit is contained in:
Jonathan Kelley 2024-03-15 12:15:55 -07:00 committed by GitHub
commit 1b8f7023f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
151 changed files with 2370 additions and 18857 deletions

View file

@ -3,8 +3,8 @@
"[toml]": {
"editor.formatOnSave": false
},
"rust-analyzer.check.workspace": false,
// "rust-analyzer.check.workspace": true,
"rust-analyzer.check.workspace": false,
"rust-analyzer.check.features": "all",
"rust-analyzer.cargo.features": "all",
"rust-analyzer.check.allTargets": true

1225
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -23,10 +23,6 @@ members = [
"packages/autofmt",
"packages/check",
"packages/rsx",
"packages/dioxus-tui",
"packages/plasmo",
"packages/native-core",
"packages/native-core-macro",
"packages/rsx-rosetta",
"packages/generational-box",
"packages/signals",
@ -48,7 +44,7 @@ members = [
"packages/playwright-tests/web",
"packages/playwright-tests/fullstack",
]
exclude = ["examples/mobile_demo", "examples/openid_connect_demo",]
exclude = ["examples/mobile_demo", "examples/openid_connect_demo"]
[workspace.package]
version = "0.5.0-alpha.0"
@ -74,17 +70,13 @@ dioxus-liveview = { path = "packages/liveview", version = "0.5.0-alpha.0" }
dioxus-autofmt = { path = "packages/autofmt", version = "0.5.0-alpha.0" }
dioxus-check = { path = "packages/check", version = "0.5.0-alpha.0" }
dioxus-rsx = { path = "packages/rsx", version = "0.5.0-alpha.0" }
dioxus-tui = { path = "packages/dioxus-tui", version = "0.5.0-alpha.0" }
plasmo = { path = "packages/plasmo", version = "0.5.0-alpha.0" }
dioxus-native-core = { path = "packages/native-core", version = "0.5.0-alpha.0" }
dioxus-native-core-macro = { path = "packages/native-core-macro", version = "0.5.0-alpha.0" }
rsx-rosetta = { path = "packages/rsx-rosetta", version = "0.5.0-alpha.0" }
dioxus-signals = { path = "packages/signals", version = "0.5.0-alpha.0" }
dioxus-cli-config = { path = "packages/cli-config", version = "0.5.0-alpha.0" }
generational-box = { path = "packages/generational-box", version = "0.5.0-alpha.0" }
dioxus-hot-reload = { path = "packages/hot-reload", version = "0.5.0-alpha.0" }
dioxus-fullstack = { path = "packages/fullstack", version = "0.5.0-alpha.0" }
dioxus_server_macro = { path = "packages/server-macro", version = "0.5.0-alpha.0", default-features = false}
dioxus_server_macro = { path = "packages/server-macro", version = "0.5.0-alpha.0", default-features = false }
dioxus-ext = { path = "packages/extension", version = "0.4.0" }
tracing = "0.1.37"
tracing-futures = "0.2.5"
@ -105,10 +97,13 @@ manganis-cli-support = { version = "0.2.1", features = [
] }
manganis = { version = "0.2.1" }
interprocess = { version = "1.2.1" }
# interprocess = { git = "https://github.com/kotauskas/interprocess" }
lru = "0.12.2"
async-trait = "0.1.77"
axum = "0.7.0"
axum-server = {version = "0.6.0", default-features = false}
axum-server = { version = "0.6.0", default-features = false }
tower = "0.4.13"
http = "1.0.0"
tower-http = "0.5.1"
@ -116,10 +111,24 @@ hyper = "1.0.0"
hyper-rustls = "0.26.0"
serde_json = "1.0.61"
serde = "1.0.61"
syn = "2.0"
quote = "1.0"
proc-macro2 = "1.0"
axum_session = "0.12.1"
axum_session_auth = "0.12.1"
axum-extra = "0.9.2"
reqwest = "0.11.24"
owo-colors = "4.0.0"
# Enable a small amount of optimization in debug mode
[profile.cli-dev]
inherits = "dev"
opt-level = 1
# Enable high optimizations for dependencies (incl. Bevy), but not for our code:
[profile.cli-dev.package."*"]
opt-level = 3
# This is a "virtual package"
# It is not meant to be published, but is used so "cargo run --example XYZ" works properly
@ -138,9 +147,9 @@ rust-version = "1.60.0"
publish = false
[dependencies]
manganis = { workspace = true, optional = true}
reqwest = { version = "0.11.9", features = ["json"], optional = true}
http-range = {version = "0.1.5", optional = true }
manganis = { workspace = true, optional = true }
reqwest = { version = "0.11.9", features = ["json"], optional = true }
http-range = { version = "0.1.5", optional = true }
[dev-dependencies]
dioxus = { workspace = true, features = ["router"] }
@ -154,7 +163,13 @@ form_urlencoded = "1.2.0"
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
getrandom = { version = "0.2.12", features = ["js"] }
tokio = { version = "1.16.1", default-features = false, features = ["sync", "macros", "io-util", "rt", "time"] }
tokio = { version = "1.16.1", default-features = false, features = [
"sync",
"macros",
"io-util",
"rt",
"time",
] }
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
tokio = { version = "1.16.1", features = ["full"] }

View file

@ -13,8 +13,8 @@ keywords = ["dom", "ui", "gui", "react"]
[dependencies]
dioxus-rsx = { workspace = true }
proc-macro2 = { version = "1.0.6", features = ["span-locations"] }
quote = "1.0"
syn = { version = "2.0", features = ["full", "extra-traits", "visit"] }
quote = { workspace = true }
syn = { workspace = true, features = ["full", "extra-traits", "visit"] }
serde = { version = "1.0.136", features = ["derive"] }
prettyplease = { workspace = true }

View file

@ -11,10 +11,10 @@ keywords = ["dom", "ui", "gui", "react"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
proc-macro2 = { version = "1.0.6", features = ["span-locations"] }
quote = "1.0"
syn = { version = "1.0.11", features = ["full", "extra-traits", "visit"] }
owo-colors = { version = "3.5.0", features = ["supports-colors"] }
proc-macro2 = { workspace = true, features = ["span-locations"] }
quote = {workspace = true }
syn = { workspace = true, features = ["full", "extra-traits", "visit"] }
owo-colors = { workspace = true, features = ["supports-colors"] }
[dev-dependencies]
indoc = "2.0.3"

View file

@ -77,8 +77,8 @@ fn is_component_fn(item_fn: &syn::ItemFn) -> bool {
fn get_closure_hook_body(local: &syn::Local) -> Option<&syn::Expr> {
if let Pat::Ident(ident) = &local.pat {
if is_hook_ident(&ident.ident) {
if let Some((_, expr)) = &local.init {
if let syn::Expr::Closure(closure) = &**expr {
if let Some(init) = &local.init {
if let syn::Expr::Closure(closure) = init.expr.as_ref() {
return Some(&closure.body);
}
}

View file

@ -7,12 +7,17 @@ use std::path::PathBuf;
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Debug)]
#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
pub enum Platform {
/// Targeting the web platform using WASM
#[cfg_attr(feature = "cli", clap(name = "web"))]
#[serde(rename = "web")]
Web,
/// Targeting the desktop platform using Tao/Wry-based webview
#[cfg_attr(feature = "cli", clap(name = "desktop"))]
#[serde(rename = "desktop")]
Desktop,
/// Targeting the server platform using Axum and Dioxus-Fullstack
#[cfg_attr(feature = "cli", clap(name = "fullstack"))]
#[serde(rename = "fullstack")]
Fullstack,
@ -220,10 +225,13 @@ impl Default for DioxusConfig {
pub struct ApplicationConfig {
#[serde(default = "default_name")]
pub name: String,
#[serde(default = "default_platform")]
pub default_platform: Platform,
#[serde(default = "out_dir_default")]
pub out_dir: PathBuf,
#[serde(default = "asset_dir_default")]
pub asset_dir: PathBuf,
@ -301,8 +309,10 @@ pub struct WebProxyConfig {
pub struct WebWatcherConfig {
#[serde(default = "watch_path_default")]
pub watch_path: Vec<PathBuf>,
#[serde(default)]
pub reload_html: bool,
#[serde(default = "true_bool")]
pub index_on_404: bool,
}
@ -531,6 +541,34 @@ impl CrateConfig {
self.cargo_args = cargo_args;
self
}
pub fn add_features(&mut self, feature: Vec<String>) -> &mut Self {
if let Some(features) = &mut self.features {
features.extend(feature);
} else {
self.features = Some(feature);
}
self
}
#[cfg(feature = "cli")]
pub fn extend_with_platform(&mut self, platform: Platform) -> &mut Self {
let manifest = &self.manifest;
let features = match platform {
Platform::Web if manifest.features.contains_key("web") => {
vec!["web".to_string()]
}
Platform::Desktop if manifest.features.contains_key("desktop") => {
vec!["desktop".to_string()]
}
_ => {
// fullstack has its own feature insertion - we use a different featureset for the client and server
vec![]
}
};
self.add_features(features);
self
}
}
fn true_bool() -> bool {

View file

@ -10,7 +10,7 @@ keywords = ["react", "gui", "cli", "dioxus", "wasm"]
[dependencies]
# cli core
clap = { version = "4.2", features = ["derive"] }
clap = { version = "4.2", features = ["derive", "cargo"] }
thiserror = { workspace = true }
wasm-bindgen-cli-support = "0.2"
colored = "2.0.0"
@ -21,10 +21,10 @@ log = "0.4.14"
fern = { version = "0.6.0", features = ["colored"] }
serde = { version = "1.0.136", features = ["derive"] }
serde_json = "1.0.79"
toml = {workspace = true}
toml = { workspace = true }
fs_extra = "1.2.0"
cargo_toml = "0.18.0"
futures-util = { workspace = true }
futures-util = { workspace = true, features = ["async-await-macro"] }
notify = { version = "5.0.0-pre.16", features = ["serde"] }
html_parser = { workspace = true }
cargo_metadata = "0.18.1"
@ -78,7 +78,7 @@ toml_edit = "0.21.0"
tauri-bundler = { version = "=1.4.*", features = ["native-tls-vendored"] }
# formatting
syn = { version = "2.0" }
syn = { workspace = true }
prettyplease = { workspace = true }
manganis-cli-support = { workspace = true, features = ["webp", "html"] }
@ -90,8 +90,10 @@ dioxus-rsx = { workspace = true }
dioxus-html = { workspace = true, features = ["hot-reload-context"] }
dioxus-core = { workspace = true, features = ["serialize"] }
dioxus-hot-reload = { workspace = true }
interprocess-docfix = { version = "1.2.2" }
interprocess = { workspace = true }
# interprocess-docfix = { version = "1.2.2" }
ignore = "0.4.22"
env_logger = "0.11.3"
[features]
default = []

View file

@ -1,45 +0,0 @@
//! Construct version in the `commit-hash date channel` format
use std::{env, path::PathBuf, process::Command};
fn main() {
set_rerun();
set_commit_info();
}
fn set_rerun() {
let mut manifest_dir = PathBuf::from(
env::var("CARGO_MANIFEST_DIR").expect("`CARGO_MANIFEST_DIR` is always set by cargo."),
);
while manifest_dir.parent().is_some() {
let head_ref = manifest_dir.join(".git/HEAD");
if head_ref.exists() {
println!("cargo:rerun-if-changed={}", head_ref.display());
return;
}
manifest_dir.pop();
}
println!("cargo:warning=Could not find `.git/HEAD` from manifest dir!");
}
fn set_commit_info() {
let output = match Command::new("git")
.arg("log")
.arg("-1")
.arg("--date=short")
.arg("--format=%H %h %cd")
.output()
{
Ok(output) if output.status.success() => output,
_ => return,
};
let stdout = String::from_utf8(output.stdout).unwrap();
let mut parts = stdout.split_whitespace();
let mut next = || parts.next().unwrap();
println!("cargo:rustc-env=RA_COMMIT_HASH={}", next());
println!("cargo:rustc-env=RA_COMMIT_SHORT_HASH={}", next());
println!("cargo:rustc-env=RA_COMMIT_DATE={}", next())
}

View file

@ -0,0 +1 @@
imports_granularity = "Crate"

View file

@ -5,6 +5,7 @@
var protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
var url = protocol + '//' + window.location.host + '/_dioxus/ws';
var poll_interval = 8080;
var reload_upon_connect = () => {
window.setTimeout(
() => {
@ -16,10 +17,14 @@
};
var ws = new WebSocket(url);
ws.onmessage = (ev) => {
console.log("Received message: ", ev, ev.data);
if (ev.data == "reload") {
window.location.reload();
}
};
ws.onclose = reload_upon_connect;
})()

View file

@ -3,10 +3,9 @@ use crate::{
error::{Error, Result},
tools::Tool,
};
use anyhow::Context;
use cargo_metadata::{diagnostic::Diagnostic, Message};
use dioxus_cli_config::crate_root;
use dioxus_cli_config::CrateConfig;
use dioxus_cli_config::ExecutableType;
use dioxus_cli_config::{crate_root, CrateConfig, ExecutableType};
use indicatif::{ProgressBar, ProgressStyle};
use lazy_static::lazy_static;
use manganis_cli_support::{AssetManifest, ManganisSupportGuard};
@ -16,6 +15,7 @@ use std::{
io::Read,
panic,
path::PathBuf,
process::Command,
time::Duration,
};
use wasm_bindgen_cli_support::Bindgen;
@ -65,9 +65,8 @@ impl ExecWithRustFlagsSetter for subprocess::Exec {
/// Build client (WASM).
/// Note: `rust_flags` argument is only used for the fullstack platform.
pub fn build(
pub fn build_web(
config: &CrateConfig,
_: bool,
skip_assets: bool,
rust_flags: Option<String>,
) -> Result<BuildResult> {
@ -100,16 +99,18 @@ pub fn build(
// [1] Build the .wasm module
log::info!("🚅 Running build command...");
let wasm_check_command = std::process::Command::new("rustup")
.args(["show"])
.output()?;
// If the user has rustup, we can check if the wasm32-unknown-unknown target is installed
// Otherwise we can just assume it is installed - which i snot great...
// Eventually we can poke at the errors and let the user know they need to install the target
if let Ok(wasm_check_command) = Command::new("rustup").args(["show"]).output() {
let wasm_check_output = String::from_utf8(wasm_check_command.stdout).unwrap();
if !wasm_check_output.contains("wasm32-unknown-unknown") {
log::info!("wasm32-unknown-unknown target not detected, installing..");
let _ = std::process::Command::new("rustup")
let _ = Command::new("rustup")
.args(["target", "add", "wasm32-unknown-unknown"])
.output()?;
}
}
let cmd = subprocess::Exec::cmd("cargo")
.set_rust_flags(rust_flags)
@ -163,9 +164,10 @@ pub fn build(
let input_path = warning_messages
.output_location
.as_ref()
.unwrap()
.context("No output location found")?
.with_extension("wasm");
log::info!("Running wasm-bindgen");
let bindgen_result = panic::catch_unwind(move || {
// [3] Bindgen the final binary for use easy linking
let mut bindgen_builder = Bindgen::new();
@ -183,11 +185,13 @@ pub fn build(
.generate(&bindgen_outdir)
.unwrap();
});
if bindgen_result.is_err() {
return Err(Error::BuildFailed("Bindgen build failed! \nThis is probably due to the Bindgen version, dioxus-cli using `0.2.81` Bindgen crate.".to_string()));
}
// check binaryen:wasm-opt tool
log::info!("Running optimization with wasm-opt...");
let dioxus_tools = dioxus_config.application.tools.clone();
if dioxus_tools.contains_key("binaryen") {
let info = dioxus_tools.get("binaryen").unwrap();
@ -221,6 +225,8 @@ pub fn build(
"Binaryen tool not found, you can use `dx tool add binaryen` to install it."
);
}
} else {
log::info!("Skipping optimization with wasm-opt, binaryen tool not found.");
}
// [5][OPTIONAL] If tailwind is enabled and installed we run it to generate the CSS
@ -271,6 +277,8 @@ pub fn build(
content_only: false,
depth: 0,
};
log::info!("Copying public assets to the output directory...");
if asset_dir.is_dir() {
for entry in std::fs::read_dir(config.asset_dir())?.flatten() {
let path = entry.path();
@ -294,6 +302,7 @@ pub fn build(
}
}
log::info!("Processing assets");
let assets = if !skip_assets {
let assets = asset_manifest(executable.executable(), config);
process_assets(config, &assets)?;

View file

@ -1,5 +1,4 @@
use crate::assets::AssetConfigDropGuard;
use crate::server::fullstack;
use crate::{assets::AssetConfigDropGuard, server::fullstack};
use dioxus_cli_config::Platform;
use super::*;
@ -51,6 +50,7 @@ impl Build {
}
crate_config.set_cargo_args(self.build.cargo_args.clone());
crate_config.extend_with_platform(platform);
// #[cfg(feature = "plugin")]
// let _ = crate::plugin::PluginManager::on_build_start(&crate_config, &platform);
@ -58,7 +58,7 @@ impl Build {
let build_result = match platform {
Platform::Web => {
// `rust_flags` are used by fullstack's client build.
crate::builder::build(&crate_config, false, self.build.skip_assets, rust_flags)?
crate::builder::build_web(&crate_config, self.build.skip_assets, rust_flags)?
}
Platform::Desktop => {
// Since desktop platform doesn't use `rust_flags`, this
@ -81,9 +81,8 @@ impl Build {
}
None => web_config.features = Some(vec![web_feature]),
};
crate::builder::build(
crate::builder::build_web(
&web_config,
false,
self.build.skip_assets,
Some(client_rust_flags),
)?;

View file

@ -82,6 +82,9 @@ impl Bundle {
}
crate_config.set_cargo_args(self.build.cargo_args);
if let Some(platform) = self.build.platform {
crate_config.extend_with_platform(platform);
}
// build the desktop app
// Since the `bundle()` function is only run for the desktop platform,

View file

@ -85,8 +85,8 @@ pub struct ConfigOptsServe {
#[clap(default_value_t = 8080)]
pub port: u16,
/// Open the app in the default browser [default: false]
#[clap(long)]
/// Open the app in the default browser [default: true]
#[clap(long, default_value_t = true)]
#[serde(default)]
pub open: bool,
@ -178,7 +178,7 @@ pub struct ConfigOptsBundle {
/// Build platform: support Web & Desktop [default: "default_platform"]
#[clap(long)]
pub platform: Option<String>,
pub platform: Option<Platform>,
/// Space separated list of features to activate
#[clap(long)]

View file

@ -2,7 +2,7 @@ use super::*;
use cargo_generate::{GenerateArgs, TemplatePath};
#[derive(Clone, Debug, Default, Deserialize, Parser)]
#[clap(name = "create")]
#[clap(name = "new")]
pub struct Create {
/// Template path
#[clap(default_value = "gh:dioxuslabs/dioxus-template", long)]

View file

@ -10,7 +10,6 @@ pub mod init;
pub mod plugin;
pub mod serve;
pub mod translate;
pub mod version;
use crate::{
cfg::{ConfigOptsBuild, ConfigOptsServe},
@ -57,10 +56,11 @@ pub enum Commands {
/// Build, watch & serve the Rust WASM app and all of its assets.
Serve(serve::Serve),
/// Create a new project for Dioxus.
Create(create::Create),
/// Create a new project for Dioxus.a
New(create::Create),
/// Init a new project for Dioxus
/// Init a new project for Dioxus in an existing directory.
/// Will attempt to keep your project in a good state
Init(init::Init),
/// Clean output artifacts.
@ -69,10 +69,6 @@ pub enum Commands {
/// Bundle the Rust desktop app and all of its assets.
Bundle(bundle::Bundle),
/// Print the version of this extension
#[clap(name = "version")]
Version(version::Version),
/// Format some rsx
#[clap(name = "fmt")]
Autoformat(autoformat::Autoformat),
@ -97,11 +93,10 @@ impl Display for Commands {
Commands::Build(_) => write!(f, "build"),
Commands::Translate(_) => write!(f, "translate"),
Commands::Serve(_) => write!(f, "serve"),
Commands::Create(_) => write!(f, "create"),
Commands::New(_) => write!(f, "create"),
Commands::Init(_) => write!(f, "init"),
Commands::Clean(_) => write!(f, "clean"),
Commands::Config(_) => write!(f, "config"),
Commands::Version(_) => write!(f, "version"),
Commands::Autoformat(_) => write!(f, "fmt"),
Commands::Check(_) => write!(f, "check"),
Commands::Bundle(_) => write!(f, "bundle"),

View file

@ -60,25 +60,16 @@ impl Serve {
}
let platform = platform.unwrap_or(crate_config.dioxus_config.application.default_platform);
crate_config.extend_with_platform(platform);
match platform {
Platform::Web => {
// start the develop server
server::web::startup(
self.serve.port,
crate_config.clone(),
self.serve.open,
self.serve.skip_assets,
)
.await?;
}
Platform::Desktop => {
server::desktop::startup(crate_config.clone(), &serve_cfg).await?;
}
Platform::Fullstack => {
server::fullstack::startup(crate_config.clone(), &serve_cfg).await?;
}
use server::{desktop, fullstack, web};
match platform {
Platform::Web => web::startup(crate_config.clone(), &serve_cfg).await?,
Platform::Desktop => desktop::startup(crate_config.clone(), &serve_cfg).await?,
Platform::Fullstack => fullstack::startup(crate_config.clone(), &serve_cfg).await?,
}
Ok(())
}

View file

@ -1,76 +0,0 @@
use super::*;
/// Print the version of this extension
#[derive(Clone, Debug, Parser)]
#[clap(name = "version")]
pub struct Version {}
impl Version {
pub fn version(self) -> VersionInfo {
version()
}
}
use std::fmt;
/// Information about the git repository where rust-analyzer was built from.
pub struct CommitInfo {
pub short_commit_hash: &'static str,
pub commit_hash: &'static str,
pub commit_date: &'static str,
}
/// Cargo's version.
pub struct VersionInfo {
/// rust-analyzer's version, such as "1.57.0", "1.58.0-beta.1", "1.59.0-nightly", etc.
pub version: &'static str,
/// The release channel we were built for (stable/beta/nightly/dev).
///
/// `None` if not built via rustbuild.
pub release_channel: Option<&'static str>,
/// Information about the Git repository we may have been built from.
///
/// `None` if not built from a git repo.
pub commit_info: Option<CommitInfo>,
}
impl fmt::Display for VersionInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.version)?;
if let Some(ci) = &self.commit_info {
write!(f, " ({} {})", ci.short_commit_hash, ci.commit_date)?;
};
Ok(())
}
}
/// Returns information about cargo's version.
pub const fn version() -> VersionInfo {
let version = match option_env!("CARGO_PKG_VERSION") {
Some(x) => x,
None => "0.0.0",
};
let release_channel = option_env!("CFG_RELEASE_CHANNEL");
let commit_info = match (
option_env!("RA_COMMIT_SHORT_HASH"),
option_env!("RA_COMMIT_HASH"),
option_env!("RA_COMMIT_DATE"),
) {
(Some(short_commit_hash), Some(commit_hash), Some(commit_date)) => Some(CommitInfo {
short_commit_hash,
commit_hash,
commit_date,
}),
_ => None,
};
VersionInfo {
version,
release_channel,
commit_info,
}
}

View file

@ -2,8 +2,6 @@
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
pub const DIOXUS_CLI_VERSION: &str = "0.4.1";
mod assets;
pub mod builder;
pub mod server;

View file

@ -7,47 +7,21 @@ use dioxus_cli::*;
use Commands::*;
fn get_bin(bin: Option<String>) -> Result<PathBuf> {
let metadata = cargo_metadata::MetadataCommand::new()
.exec()
.map_err(Error::CargoMetadata)?;
let package = if let Some(bin) = bin {
metadata
.workspace_packages()
.into_iter()
.find(|p| p.name == bin)
.ok_or(Error::CargoError(format!("no such package: {}", bin)))?
} else {
metadata
.root_package()
.ok_or(Error::CargoError("no root package?".to_string()))?
};
let crate_dir = package
.manifest_path
.parent()
.ok_or(Error::CargoError("couldn't take parent dir".to_string()))?;
Ok(crate_dir.into())
}
/// Simplifies error messages that use the same pattern.
fn error_wrapper(message: &str) -> String {
format!("🚫 {message}:")
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let args = Cli::parse();
set_up_logging();
#[cfg(debug_assertions)]
env_logger::init();
// set_up_logging();
match args.action {
Translate(opts) => opts
.translate()
.context(error_wrapper("Translation of HTML into RSX failed")),
Create(opts) => opts
New(opts) => opts
.create()
.context(error_wrapper("Creating new project failed")),
@ -74,12 +48,6 @@ async fn main() -> anyhow::Result<()> {
.await
.context(error_wrapper("Error checking RSX")),
Version(opt) => {
let version = opt.version();
println!("{}", version);
Ok(())
}
action => {
let bin = get_bin(args.bin)?;
let _dioxus_config = DioxusConfig::load(Some(bin.clone()))
@ -119,3 +87,32 @@ async fn main() -> anyhow::Result<()> {
}
}
}
fn get_bin(bin: Option<String>) -> Result<PathBuf> {
let metadata = cargo_metadata::MetadataCommand::new()
.exec()
.map_err(Error::CargoMetadata)?;
let package = if let Some(bin) = bin {
metadata
.workspace_packages()
.into_iter()
.find(|p| p.name == bin)
.ok_or(Error::CargoError(format!("no such package: {}", bin)))?
} else {
metadata
.root_package()
.ok_or(Error::CargoError("no root package?".to_string()))?
};
let crate_dir = package
.manifest_path
.parent()
.ok_or(Error::CargoError("couldn't take parent dir".to_string()))?;
Ok(crate_dir.into())
}
/// Simplifies error messages that use the same pattern.
fn error_wrapper(message: &str) -> String {
format!("🚫 {message}:")
}

View file

@ -1,20 +1,18 @@
use crate::server::Platform;
use crate::{
cfg::ConfigOptsServe,
server::{
output::{print_console_info, PrettierOptions},
setup_file_watcher,
setup_file_watcher, Platform,
},
BuildResult, Result,
};
use dioxus_cli_config::CrateConfig;
use dioxus_hot_reload::HotReloadMsg;
use dioxus_html::HtmlCtx;
use dioxus_rsx::hot_reload::*;
use interprocess_docfix::local_socket::LocalSocketListener;
use std::fs::create_dir_all;
use interprocess::local_socket::LocalSocketListener;
use std::{
fs::create_dir_all,
process::{Child, Command},
sync::{Arc, Mutex, RwLock},
};
@ -33,13 +31,7 @@ pub(crate) async fn startup_with_platform<P: Platform + Send + 'static>(
config: CrateConfig,
serve_cfg: &ConfigOptsServe,
) -> Result<()> {
// ctrl-c shutdown checker
let _crate_config = config.clone();
let _ = ctrlc::set_handler(move || {
#[cfg(feature = "plugin")]
let _ = PluginManager::on_serve_shutdown(&_crate_config);
std::process::exit(0);
});
set_ctrl_c(&config);
let hot_reload_state = match config.hot_reload {
true => {
@ -67,6 +59,16 @@ pub(crate) async fn startup_with_platform<P: Platform + Send + 'static>(
Ok(())
}
fn set_ctrl_c(config: &CrateConfig) {
// ctrl-c shutdown checker
let _crate_config = config.clone();
let _ = ctrlc::set_handler(move || {
#[cfg(feature = "plugin")]
let _ = PluginManager::on_serve_shutdown(&_crate_config);
std::process::exit(0);
});
}
/// Start the server without hot reload
async fn serve<P: Platform + Send + 'static>(
config: CrateConfig,
@ -136,7 +138,7 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()
.unwrap()
.map
.values()
.filter_map(|(_, template_slot)| *template_slot)
.flat_map(|v| v.templates.values().copied())
.collect()
};
for template in templates {
@ -169,12 +171,13 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()
let mut hot_reload_rx = hot_reload_state.messages.subscribe();
while let Ok(template) = hot_reload_rx.recv().await {
while let Ok(msg) = hot_reload_rx.recv().await {
let channels = &mut *channels.lock().unwrap();
let mut i = 0;
while i < channels.len() {
let channel = &mut channels[i];
if send_msg(HotReloadMsg::UpdateTemplate(template), channel) {
if send_msg(msg.clone(), channel) {
i += 1;
} else {
channels.remove(i);

View file

@ -3,10 +3,15 @@ use dioxus_cli_config::CrateConfig;
use cargo_metadata::diagnostic::Diagnostic;
use dioxus_core::Template;
use dioxus_hot_reload::HotReloadMsg;
use dioxus_html::HtmlCtx;
use dioxus_rsx::hot_reload::*;
use fs_extra::dir::CopyOptions;
use notify::{RecommendedWatcher, Watcher};
use std::sync::{Arc, Mutex};
use std::{
path::PathBuf,
sync::{Arc, Mutex},
};
use tokio::sync::broadcast::{self};
mod output;
@ -15,7 +20,31 @@ pub mod desktop;
pub mod fullstack;
pub mod web;
/// Sets up a file watcher
#[derive(Clone)]
pub struct HotReloadState {
/// Pending hotreload updates to be sent to all connected clients
pub messages: broadcast::Sender<HotReloadMsg>,
/// The file map that tracks the state of the projecta
pub file_map: SharedFileMap,
}
type SharedFileMap = Arc<Mutex<FileMap<HtmlCtx>>>;
impl HotReloadState {
pub fn all_templates(&self) -> Vec<Template> {
self.file_map
.lock()
.unwrap()
.map
.values()
.flat_map(|v| v.templates.values().copied())
.collect()
}
}
/// Sets up a file watcher.
///
/// Will attempt to hotreload HTML, RSX (.rs), and CSS
async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
build_with: F,
config: &CrateConfig,
@ -25,63 +54,155 @@ async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
let mut last_update_time = chrono::Local::now().timestamp();
// file watcher: check file change
let allow_watch_path = config.dioxus_config.web.watcher.watch_path.clone();
let mut allow_watch_path = config.dioxus_config.web.watcher.watch_path.clone();
// Extend the watch path to include the assets directory - this is so we can hotreload CSS and other assets
allow_watch_path.push(config.dioxus_config.application.asset_dir.clone());
// Create the file watcher
let mut watcher = notify::recommended_watcher({
let watcher_config = config.clone();
let mut watcher = notify::recommended_watcher(move |info: notify::Result<notify::Event>| {
let config = watcher_config.clone();
if let Ok(e) = info {
match e.kind {
notify::EventKind::Create(_)
| notify::EventKind::Remove(_)
| notify::EventKind::Modify(_) => {
if chrono::Local::now().timestamp() > last_update_time {
let mut needs_full_rebuild;
move |info: notify::Result<notify::Event>| {
let Ok(e) = info else {
return;
};
watch_event(
e,
&mut last_update_time,
&hot_reload,
&watcher_config,
&build_with,
&web_info,
);
}
})
.expect("Failed to create file watcher - please ensure you have the required permissions to watch the specified directories.");
// Watch the specified paths
for sub_path in allow_watch_path {
let path = &config.crate_dir.join(sub_path);
let mode = notify::RecursiveMode::Recursive;
if let Err(err) = watcher.watch(path, mode) {
log::warn!("Failed to watch path: {}", err);
}
}
Ok(watcher)
}
fn watch_event<F>(
event: notify::Event,
last_update_time: &mut i64,
hot_reload: &Option<HotReloadState>,
config: &CrateConfig,
build_with: &F,
web_info: &Option<WebServerInfo>,
) where
F: Fn() -> Result<BuildResult> + Send + 'static,
{
// Ensure that we're tracking only modifications
if !matches!(
event.kind,
notify::EventKind::Create(_) | notify::EventKind::Remove(_) | notify::EventKind::Modify(_)
) {
return;
}
// Ensure that we're not rebuilding too frequently
if chrono::Local::now().timestamp() <= *last_update_time {
return;
}
// By default we want to not do a full rebuild, and instead let the hot reload system invalidate it
let mut needs_full_rebuild = false;
if let Some(hot_reload) = &hot_reload {
// find changes to the rsx in the file
let mut rsx_file_map = hot_reload.file_map.lock().unwrap();
let mut messages: Vec<Template> = Vec::new();
// In hot reload mode, we only need to rebuild if non-rsx code is changed
needs_full_rebuild = false;
for path in &e.paths {
// if this is not a rust file, rebuild the whole project
let path_extension = path.extension().and_then(|p| p.to_str());
if path_extension != Some("rs") {
needs_full_rebuild = true;
// if backup file generated will impact normal hot-reload, so ignore it
if path_extension == Some("rs~") {
needs_full_rebuild = false;
}
break;
}
// Workaround for notify and vscode-like editor:
// when edit & save a file in vscode, there will be two notifications,
// the first one is a file with empty content.
// filter the empty file notification to avoid false rebuild during hot-reload
if let Ok(metadata) = fs::metadata(path) {
if metadata.len() == 0 {
continue;
}
}
match rsx_file_map.update_rsx(path, &config.crate_dir) {
Ok(UpdateResult::UpdatedRsx(msgs)) => {
messages.extend(msgs);
needs_full_rebuild = false;
}
Ok(UpdateResult::NeedsRebuild) => {
needs_full_rebuild = true;
}
Err(err) => {
log::error!("{}", err);
}
}
hotreload_files(hot_reload, &mut needs_full_rebuild, &event, config);
}
if needs_full_rebuild {
full_rebuild(build_with, last_update_time, config, event, web_info);
}
}
fn full_rebuild<F>(
build_with: &F,
last_update_time: &mut i64,
config: &CrateConfig,
event: notify::Event,
web_info: &Option<WebServerInfo>,
) where
F: Fn() -> Result<BuildResult> + Send + 'static,
{
match build_with() {
Ok(res) => {
*last_update_time = chrono::Local::now().timestamp();
#[allow(clippy::redundant_clone)]
print_console_info(
config,
PrettierOptions {
changed: event.paths.clone(),
warnings: res.warnings,
elapsed_time: res.elapsed_time,
},
web_info.clone(),
);
}
Err(e) => {
*last_update_time = chrono::Local::now().timestamp();
log::error!("{:?}", e);
}
}
}
fn hotreload_files(
hot_reload: &HotReloadState,
needs_full_rebuild: &mut bool,
event: &notify::Event,
config: &CrateConfig,
) {
// find changes to the rsx in the file
let mut rsx_file_map = hot_reload.file_map.lock().unwrap();
let mut messages: Vec<HotReloadMsg> = Vec::new();
for path in &event.paths {
// Attempt to hotreload this file
let is_potentially_reloadable = hotreload_file(
path,
config,
&rsx_file_map,
&mut messages,
needs_full_rebuild,
);
// If the file was not hotreloaded, continue
if is_potentially_reloadable.is_none() {
continue;
}
// If the file was hotreloaded, update the file map in place
match rsx_file_map.update_rsx(path, &config.crate_dir) {
Ok(UpdateResult::UpdatedRsx(msgs)) => {
messages.extend(msgs.into_iter().map(HotReloadMsg::UpdateTemplate));
}
// If the file was not updated, we need to do a full rebuild
Ok(UpdateResult::NeedsRebuild) => {
log::trace!("Needs full rebuild because file changed: {:?}", path);
*needs_full_rebuild = true;
}
// Not necessarily a fatal error, but we should log it
Err(err) => log::error!("{}", err),
}
}
// If full rebuild, extend the file map with the new file map
// This will wipe away any previous cached changed templates
if *needs_full_rebuild {
// Reset the file map to the new state of the project
let FileMapBuildResult {
map: new_file_map,
@ -93,54 +214,103 @@ async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
}
*rsx_file_map = new_file_map;
} else {
return;
}
for msg in messages {
let _ = hot_reload.messages.send(msg);
}
}
fn hotreload_file(
path: &Path,
config: &CrateConfig,
rsx_file_map: &std::sync::MutexGuard<'_, FileMap<HtmlCtx>>,
messages: &mut Vec<HotReloadMsg>,
needs_full_rebuild: &mut bool,
) -> Option<()> {
// for various assets that might be linked in, we just try to hotreloading them forcefully
// That is, unless they appear in an include! macro, in which case we need to a full rebuild....
let ext = path.extension().and_then(|v| v.to_str())?;
// Workaround for notify and vscode-like editor:
// when edit & save a file in vscode, there will be two notifications,
// the first one is a file with empty content.
// filter the empty file notification to avoid false rebuild during hot-reload
if let Ok(metadata) = fs::metadata(path) {
if metadata.len() == 0 {
return None;
}
} else {
needs_full_rebuild = true;
}
if needs_full_rebuild {
match build_with() {
Ok(res) => {
last_update_time = chrono::Local::now().timestamp();
// If the extension is a backup file, or a hidden file, ignore it completely (no rebuilds)
if is_backup_file(path) {
log::trace!("Ignoring backup file: {:?}", path);
return None;
}
#[allow(clippy::redundant_clone)]
print_console_info(
&config,
PrettierOptions {
changed: e.paths.clone(),
warnings: res.warnings,
elapsed_time: res.elapsed_time,
},
web_info.clone(),
// Attempt to hotreload css in the asset directory
// Currently no other assets are hotreloaded, but in theory we could hotreload pngs/jpegs, etc
//
// All potential hotreloadable mime types:
// "bin" |"css" | "csv" | "html" | "ico" | "js" | "json" | "jsonld" | "mjs" | "rtf" | "svg" | "mp4"
if ext == "css" {
let asset_dir = config
.crate_dir
.join(&config.dioxus_config.application.asset_dir);
// Only if the CSS is in the asset directory, and we're tracking it, do we hotreload it
// Otherwise, we need to do a full rebuild since the user might be doing an include_str! on it
if attempt_css_reload(path, asset_dir, rsx_file_map, config, messages).is_none() {
*needs_full_rebuild = true;
}
return None;
}
// If the file is not rsx or css and we've already not needed a full rebuild, return
if ext != "rs" && ext != "css" {
*needs_full_rebuild = true;
return None;
}
Some(())
}
fn attempt_css_reload(
path: &Path,
asset_dir: PathBuf,
rsx_file_map: &std::sync::MutexGuard<'_, FileMap<HtmlCtx>>,
config: &CrateConfig,
messages: &mut Vec<HotReloadMsg>,
) -> Option<()> {
// If the path is not in the asset directory, return
if !path.starts_with(asset_dir) {
return None;
}
// Get the local path of the asset (ie var.css or some_dir/var.css as long as the dir is under the asset dir)
let local_path = local_path_of_asset(path)?;
// Make sure we're actually tracking this asset...
_ = rsx_file_map.is_tracking_asset(&local_path)?;
// copy the asset over to the output directory
// todo this whole css hotreloading shouldbe less hacky and more robust
_ = fs_extra::copy_items(
&[path],
config.out_dir(),
&CopyOptions::new().overwrite(true),
);
}
Err(e) => {
last_update_time = chrono::Local::now().timestamp();
log::error!("{:?}", e);
}
}
}
}
}
_ => {}
}
}
})
.unwrap();
for sub_path in allow_watch_path {
if let Err(err) = watcher.watch(
&config.crate_dir.join(sub_path),
notify::RecursiveMode::Recursive,
) {
log::warn!("Failed to watch path: {}", err);
}
}
Ok(watcher)
messages.push(HotReloadMsg::UpdateAsset(local_path));
Some(())
}
fn local_path_of_asset(path: &Path) -> Option<PathBuf> {
path.file_name()?.to_str()?.to_string().parse().ok()
}
pub(crate) trait Platform {
@ -150,8 +320,38 @@ pub(crate) trait Platform {
fn rebuild(&mut self, config: &CrateConfig) -> Result<BuildResult>;
}
#[derive(Clone)]
pub struct HotReloadState {
pub messages: broadcast::Sender<Template>,
pub file_map: Arc<Mutex<FileMap<HtmlCtx>>>,
fn is_backup_file(path: &Path) -> bool {
// If there's a tilde at the end of the file, it's a backup file
if let Some(name) = path.file_name() {
if let Some(name) = name.to_str() {
if name.ends_with('~') {
return true;
}
}
}
// if the file is hidden, it's a backup file
if let Some(name) = path.file_name() {
if let Some(name) = name.to_str() {
if name.starts_with('.') {
return true;
}
}
}
false
}
#[test]
fn test_is_backup_file() {
assert!(is_backup_file(&PathBuf::from("examples/test.rs~")));
assert!(is_backup_file(&PathBuf::from("examples/.back")));
assert!(is_backup_file(&PathBuf::from("test.rs~")));
assert!(is_backup_file(&PathBuf::from(".back")));
assert!(!is_backup_file(&PathBuf::from("val.rs")));
assert!(!is_backup_file(&PathBuf::from(
"/Users/jonkelley/Development/Tinkering/basic_05_example/src/lib.rs"
)));
assert!(!is_backup_file(&PathBuf::from("exmaples/val.rs")));
}

View file

@ -1,9 +1,7 @@
use crate::server::Diagnostic;
use colored::Colorize;
use dioxus_cli_config::crate_root;
use dioxus_cli_config::CrateConfig;
use std::path::PathBuf;
use std::process::Command;
use dioxus_cli_config::{crate_root, CrateConfig};
use std::{path::PathBuf, process::Command};
#[derive(Debug, Default)]
pub struct PrettierOptions {
@ -48,7 +46,7 @@ pub fn print_console_info(
let custom_html_file = if crate_root.join("index.html").is_file() {
"Custom [index.html]"
} else {
"Default"
"None"
};
let url_rewrite = if config.dioxus_config.web.watcher.index_on_404 {
"True"
@ -60,9 +58,9 @@ pub fn print_console_info(
if options.changed.is_empty() {
println!(
"{} @ v{} [{}] \n",
"{} @ v{} [{}]",
"Dioxus".bold().green(),
crate::DIOXUS_CLI_VERSION,
clap::crate_version!(),
chrono::Local::now().format("%H:%M:%S").to_string().dimmed()
);
} else {
@ -81,40 +79,67 @@ pub fn print_console_info(
if let Some(WebServerInfo { ip, port }) = web_info {
if config.dioxus_config.web.https.enabled == Some(true) {
println!(
"\t> Local : {}",
" > Local address: {}",
format!("https://localhost:{}/", port).blue()
);
println!(
"\t> Network : {}",
" > Network address: {}",
format!("https://{}:{}/", ip, port).blue()
);
println!("\t> HTTPS : {}", "Enabled".to_string().green());
println!(" > HTTPS: {}", "Enabled".to_string().green());
} else {
println!(
"\t> Local : {}",
" > Local address: {}",
format!("http://localhost:{}/", port).blue()
);
println!(
"\t> Network : {}",
" > Network address: {}",
format!("http://{}:{}/", ip, port).blue()
);
println!("\t> HTTPS : {}", "Disabled".to_string().red());
println!(" > HTTPS status: {}", "Disabled".to_string().red());
}
}
println!();
println!("\t> Profile : {}", profile.green());
println!("\t> Hot Reload : {}", hot_reload.cyan());
println!(" > Hot Reload Mode: {}", hot_reload.cyan());
println!(
" > Watching: [ {} ]",
config
.dioxus_config
.web
.watcher
.watch_path
.iter()
.cloned()
.chain(Some(config.dioxus_config.application.asset_dir.clone()))
.map(|f| f.display().to_string())
.collect::<Vec<String>>()
.join(", ")
.cyan()
);
if !proxies.is_empty() {
println!("\t> Proxies :");
println!(" > Proxies :");
for proxy in proxies {
println!("\t\t- {}", proxy.backend.blue());
println!(" - {}", proxy.backend.blue());
}
}
println!("\t> Index Template : {}", custom_html_file.green());
println!("\t> URL Rewrite [index_on_404] : {}", url_rewrite.purple());
println!(" > Custom index.html: {}", custom_html_file.green());
println!(" > Serve index.html on 404: {}", url_rewrite.purple());
println!();
println!(
"\t> Build Time Use : {} millis",
" > Build Features: [ {} ]",
config
.features
.clone()
.unwrap_or_default()
.join(", ")
.green()
);
println!(" > Build Profile: {}", profile.green());
println!(
" > Build took: {} millis",
options.elapsed_time.to_string().green().bold()
);
println!();

View file

@ -1,54 +1,75 @@
use crate::server::HotReloadState;
use axum::{
extract::{ws::Message, WebSocketUpgrade},
extract::{
ws::{Message, WebSocket},
WebSocketUpgrade,
},
response::IntoResponse,
Extension,
};
use dioxus_hot_reload::HotReloadMsg;
use futures_util::{pin_mut, FutureExt};
pub async fn hot_reload_handler(
ws: WebSocketUpgrade,
Extension(state): Extension<HotReloadState>,
) -> impl IntoResponse {
ws.on_upgrade(|mut socket| async move {
log::info!("🔥 Hot Reload WebSocket connected");
{
// update any rsx calls that changed before the websocket connected.
{
log::info!("🔮 Finding updates since last compile...");
let templates: Vec<_> = {
state
.file_map
.lock()
.unwrap()
.map
.values()
.filter_map(|(_, template_slot)| *template_slot)
.collect()
};
for template in templates {
if socket
.send(Message::Text(serde_json::to_string(&template).unwrap()))
.await
.is_err()
{
return;
}
}
}
log::info!("finished");
}
ws.on_upgrade(|socket| async move {
let err = hotreload_loop(socket, state).await;
let mut rx = state.messages.subscribe();
loop {
if let Ok(rsx) = rx.recv().await {
if socket
.send(Message::Text(serde_json::to_string(&rsx).unwrap()))
.await
.is_err()
{
break;
};
}
if let Err(err) = err {
log::error!("Hotreload receiver failed: {}", err);
}
})
}
async fn hotreload_loop(mut socket: WebSocket, state: HotReloadState) -> anyhow::Result<()> {
log::info!("🔥 Hot Reload WebSocket connected");
// update any rsx calls that changed before the websocket connected.
// These templates will be sent down immediately so the page is in sync with the hotreloaded version
// The compiled version will be different from the one we actually want to present
for template in state.all_templates() {
socket
.send(Message::Text(serde_json::to_string(&template).unwrap()))
.await?;
}
let mut rx = state.messages.subscribe();
loop {
let msg = {
// Poll both the receiver and the socket
//
// This shuts us down if the connection is closed.
let mut _socket = socket.recv().fuse();
let mut _rx = rx.recv().fuse();
pin_mut!(_socket, _rx);
let msg = futures_util::select! {
msg = _rx => msg,
_ = _socket => break,
};
let Ok(msg) = msg else { break };
match msg {
HotReloadMsg::UpdateTemplate(template) => {
Message::Text(serde_json::to_string(&template).unwrap())
}
HotReloadMsg::UpdateAsset(asset) => {
Message::Text(format!("reload-asset: {}", asset.display()))
}
HotReloadMsg::Shutdown => {
log::info!("🔥 Hot Reload WebSocket shutting down");
break;
}
}
};
socket.send(msg).await?;
}
Ok(())
}

View file

@ -1,5 +1,6 @@
use crate::{
builder,
cfg::ConfigOptsServe,
serve::Serve,
server::{
output::{print_console_info, PrettierOptions, WebServerInfo},
@ -7,111 +8,51 @@ use crate::{
},
BuildResult, Result,
};
use axum::{
body::Body,
extract::{ws::Message, Extension, WebSocketUpgrade},
http::{
self,
header::{HeaderName, HeaderValue},
Method, Response, StatusCode,
},
response::IntoResponse,
routing::{get, get_service},
Router,
};
use axum_server::tls_rustls::RustlsConfig;
use dioxus_cli_config::CrateConfig;
use dioxus_cli_config::WebHttpsConfig;
use dioxus_html::HtmlCtx;
use dioxus_rsx::hot_reload::*;
use std::{
net::UdpSocket,
process::Command,
net::{SocketAddr, UdpSocket},
sync::{Arc, Mutex},
};
use tokio::sync::broadcast::{self, Sender};
use tower::ServiceBuilder;
use tower_http::services::fs::{ServeDir, ServeFileSystemResponseBody};
use tower_http::{
cors::{Any, CorsLayer},
ServiceBuilderExt,
};
#[cfg(feature = "plugin")]
use crate::plugin::PluginManager;
mod proxy;
use tokio::sync::broadcast;
mod hot_reload;
use hot_reload::*;
mod proxy;
mod server;
struct WsReloadState {
use server::*;
pub struct WsReloadState {
update: broadcast::Sender<()>,
}
pub async fn startup(
port: u16,
config: CrateConfig,
start_browser: bool,
skip_assets: bool,
) -> Result<()> {
// ctrl-c shutdown checker
let _crate_config = config.clone();
let _ = ctrlc::set_handler(move || {
#[cfg(feature = "plugin")]
let _ = PluginManager::on_serve_shutdown(&_crate_config);
std::process::exit(0);
});
pub async fn startup(config: CrateConfig, serve_cfg: &ConfigOptsServe) -> Result<()> {
set_ctrlc_handler(&config);
let ip = get_ip().unwrap_or(String::from("0.0.0.0"));
let hot_reload_state = match config.hot_reload {
true => {
let FileMapBuildResult { map, errors } =
FileMap::<HtmlCtx>::create(config.crate_dir.clone()).unwrap();
let mut hot_reload_state = None;
for err in errors {
log::error!("{}", err);
if config.hot_reload {
hot_reload_state = Some(build_hotreload_filemap(&config));
}
let file_map = Arc::new(Mutex::new(map));
let hot_reload_tx = broadcast::channel(100).0;
Some(HotReloadState {
messages: hot_reload_tx.clone(),
file_map: file_map.clone(),
})
}
false => None,
};
serve(
ip,
port,
config,
start_browser,
skip_assets,
hot_reload_state,
)
.await?;
Ok(())
serve(ip, config, hot_reload_state, serve_cfg).await
}
/// Start the server without hot reload
pub async fn serve(
ip: String,
port: u16,
config: CrateConfig,
start_browser: bool,
skip_assets: bool,
hot_reload_state: Option<HotReloadState>,
opts: &ConfigOptsServe,
) -> Result<()> {
let skip_assets = opts.skip_assets;
let port = opts.port;
// Since web platform doesn't use `rust_flags`, this argument is explicitly
// set to `None`.
let first_build_result = crate::builder::build(&config, false, skip_assets, None)?;
let first_build_result = crate::builder::build_web(&config, skip_assets, None)?;
// generate dev-index page
Serve::regen_dev_page(&config, first_build_result.assets.as_ref())?;
@ -154,7 +95,7 @@ pub async fn serve(
warnings: first_build_result.warnings,
elapsed_time: first_build_result.elapsed_time,
},
Some(crate::server::output::WebServerInfo {
Some(WebServerInfo {
ip: ip.clone(),
port,
}),
@ -164,230 +105,43 @@ pub async fn serve(
let router = setup_router(config.clone(), ws_reload_state, hot_reload_state).await?;
// Start server
start_server(port, router, start_browser, rustls_config, &config).await?;
start_server(port, router, opts.open, rustls_config, &config).await?;
Ok(())
}
const DEFAULT_KEY_PATH: &str = "ssl/key.pem";
const DEFAULT_CERT_PATH: &str = "ssl/cert.pem";
/// Returns an enum of rustls config and a bool if mkcert isn't installed
async fn get_rustls(config: &CrateConfig) -> Result<Option<RustlsConfig>> {
let web_config = &config.dioxus_config.web.https;
if web_config.enabled != Some(true) {
return Ok(None);
}
let (cert_path, key_path) = if let Some(true) = web_config.mkcert {
// mkcert, use it
get_rustls_with_mkcert(web_config)?
} else {
// if mkcert not specified or false, don't use it
get_rustls_without_mkcert(web_config)?
};
Ok(Some(
RustlsConfig::from_pem_file(cert_path, key_path).await?,
))
}
fn get_rustls_with_mkcert(web_config: &WebHttpsConfig) -> Result<(String, String)> {
// Get paths to store certs, otherwise use ssl/item.pem
let key_path = web_config
.key_path
.clone()
.unwrap_or(DEFAULT_KEY_PATH.to_string());
let cert_path = web_config
.cert_path
.clone()
.unwrap_or(DEFAULT_CERT_PATH.to_string());
// Create ssl directory if using defaults
if key_path == DEFAULT_KEY_PATH && cert_path == DEFAULT_CERT_PATH {
_ = fs::create_dir("ssl");
}
let cmd = Command::new("mkcert")
.args([
"-install",
"-key-file",
&key_path,
"-cert-file",
&cert_path,
"localhost",
"::1",
"127.0.0.1",
])
.spawn();
match cmd {
Err(e) => {
match e.kind() {
io::ErrorKind::NotFound => log::error!("mkcert is not installed. See https://github.com/FiloSottile/mkcert#installation for installation instructions."),
e => log::error!("an error occured while generating mkcert certificates: {}", e.to_string()),
};
return Err("failed to generate mkcert certificates".into());
}
Ok(mut cmd) => {
cmd.wait()?;
}
}
Ok((cert_path, key_path))
}
fn get_rustls_without_mkcert(web_config: &WebHttpsConfig) -> Result<(String, String)> {
// get paths to cert & key
if let (Some(key), Some(cert)) = (web_config.key_path.clone(), web_config.cert_path.clone()) {
Ok((cert, key))
} else {
// missing cert or key
Err("https is enabled but cert or key path is missing".into())
}
}
/// Sets up and returns a router
async fn setup_router(
config: CrateConfig,
ws_reload: Arc<WsReloadState>,
hot_reload: Option<HotReloadState>,
) -> Result<Router> {
// Setup cors
let cors = CorsLayer::new()
// allow `GET` and `POST` when accessing the resource
.allow_methods([Method::GET, Method::POST])
// allow requests from any origin
.allow_origin(Any)
.allow_headers(Any);
let (coep, coop) = if config.cross_origin_policy {
(
HeaderValue::from_static("require-corp"),
HeaderValue::from_static("same-origin"),
)
} else {
(
HeaderValue::from_static("unsafe-none"),
HeaderValue::from_static("unsafe-none"),
)
};
// Create file service
let file_service_config = config.clone();
let file_service = ServiceBuilder::new()
.override_response_header(
HeaderName::from_static("cross-origin-embedder-policy"),
coep,
)
.override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop)
.and_then(
move |response: Response<ServeFileSystemResponseBody>| async move {
let mut response = if file_service_config.dioxus_config.web.watcher.index_on_404
&& response.status() == StatusCode::NOT_FOUND
{
let body = Body::from(
// TODO: Cache/memoize this.
std::fs::read_to_string(file_service_config.out_dir().join("index.html"))
.ok()
.unwrap(),
);
Response::builder()
.status(StatusCode::OK)
.body(body)
.unwrap()
} else {
response.into_response()
};
let headers = response.headers_mut();
headers.insert(
http::header::CACHE_CONTROL,
HeaderValue::from_static("no-cache"),
);
headers.insert(http::header::PRAGMA, HeaderValue::from_static("no-cache"));
headers.insert(http::header::EXPIRES, HeaderValue::from_static("0"));
Ok(response)
},
)
.service(ServeDir::new(config.out_dir()));
// Setup websocket
let mut router = Router::new().route("/_dioxus/ws", get(ws_handler));
// Setup proxy
for proxy_config in config.dioxus_config.web.proxy {
router = proxy::add_proxy(router, &proxy_config)?;
}
// Route file service
router = router.fallback(get_service(file_service).handle_error(
|error: std::convert::Infallible| async move {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Unhandled internal error: {}", error),
)
},
));
router = if let Some(base_path) = config.dioxus_config.web.app.base_path.clone() {
let base_path = format!("/{}", base_path.trim_matches('/'));
Router::new()
.route(&base_path, axum::routing::any_service(router))
.fallback(get(move || {
let base_path = base_path.clone();
async move { format!("Outside of the base path: {}", base_path) }
}))
} else {
router
};
// Setup routes
router = router
.route("/_dioxus/hot_reload", get(hot_reload_handler))
.layer(cors)
.layer(Extension(ws_reload));
if let Some(hot_reload) = hot_reload {
router = router.layer(Extension(hot_reload))
}
Ok(router)
}
/// Starts dx serve with no hot reload
async fn start_server(
port: u16,
router: Router,
router: axum::Router,
start_browser: bool,
rustls: Option<RustlsConfig>,
rustls: Option<axum_server::tls_rustls::RustlsConfig>,
_config: &CrateConfig,
) -> Result<()> {
// If plugins, call on_serve_start event
#[cfg(feature = "plugin")]
PluginManager::on_serve_start(_config)?;
crate::plugin::PluginManager::on_serve_start(_config)?;
// Bind the server to `[::]` and it will LISTEN for both IPv4 and IPv6. (required IPv6 dual stack)
let addr = format!("[::]:{}", port).parse().unwrap();
let addr: SocketAddr = format!("0.0.0.0:{}", port).parse().unwrap();
// Open the browser
if start_browser {
match rustls {
Some(_) => _ = open::that(format!("https://{}", addr)),
None => _ = open::that(format!("http://{}", addr)),
Some(_) => _ = open::that(format!("https://localhost:{port}")),
None => _ = open::that(format!("http://localhost:{port}")),
}
}
let svc = router.into_make_service();
// Start the server with or without rustls
match rustls {
Some(rustls) => {
axum_server::bind_rustls(addr, rustls)
.serve(router.into_make_service())
.await?
}
Some(rustls) => axum_server::bind_rustls(addr, rustls).serve(svc).await?,
None => {
// Create a TCP listener bound to the address
let listener = tokio::net::TcpListener::bind(&addr).await?;
axum::serve(listener, router.into_make_service()).await?
axum::serve(listener, svc).await?
}
}
@ -412,43 +166,50 @@ fn get_ip() -> Option<String> {
}
}
/// Handle websockets
async fn ws_handler(
ws: WebSocketUpgrade,
Extension(state): Extension<Arc<WsReloadState>>,
) -> impl IntoResponse {
ws.on_upgrade(|mut socket| async move {
let mut rx = state.update.subscribe();
let reload_watcher = tokio::spawn(async move {
loop {
rx.recv().await.unwrap();
// ignore the error
if socket
.send(Message::Text(String::from("reload")))
.await
.is_err()
{
break;
}
// flush the errors after recompling
rx = rx.resubscribe();
}
});
reload_watcher.await.unwrap();
})
}
fn build(config: &CrateConfig, reload_tx: &Sender<()>, skip_assets: bool) -> Result<BuildResult> {
fn build(
config: &CrateConfig,
reload_tx: &broadcast::Sender<()>,
skip_assets: bool,
) -> Result<BuildResult> {
// Since web platform doesn't use `rust_flags`, this argument is explicitly
// set to `None`.
let result = builder::build(config, true, skip_assets, None)?;
let result = std::panic::catch_unwind(|| builder::build_web(config, skip_assets, None))
.map_err(|e| anyhow::anyhow!("Build failed: {e:?}"))?;
// change the websocket reload state to true;
// the page will auto-reload.
if config.dioxus_config.web.watcher.reload_html {
let _ = Serve::regen_dev_page(config, result.assets.as_ref());
if let Ok(assets) = result.as_ref().map(|x| x.assets.as_ref()) {
let _ = Serve::regen_dev_page(config, assets);
}
}
let _ = reload_tx.send(());
Ok(result)
result
}
fn set_ctrlc_handler(config: &CrateConfig) {
// ctrl-c shutdown checker
let _crate_config = config.clone();
let _ = ctrlc::set_handler(move || {
#[cfg(feature = "plugin")]
let _ = crate::plugin::PluginManager::on_serve_shutdown(&_crate_config);
std::process::exit(0);
});
}
fn build_hotreload_filemap(config: &CrateConfig) -> HotReloadState {
let FileMapBuildResult { map, errors } = FileMap::create(config.crate_dir.clone()).unwrap();
for err in errors {
log::error!("{}", err);
}
HotReloadState {
messages: broadcast::channel(100).0.clone(),
file_map: Arc::new(Mutex::new(map)).clone(),
}
}

View file

@ -0,0 +1,243 @@
use super::{hot_reload::*, WsReloadState};
use crate::{server::HotReloadState, Result};
use axum::{
body::Body,
extract::{
ws::{Message, WebSocket},
Extension, WebSocketUpgrade,
},
http::{
self,
header::{HeaderName, HeaderValue},
Method, Response, StatusCode,
},
response::IntoResponse,
routing::{get, get_service},
Router,
};
use axum_server::tls_rustls::RustlsConfig;
use dioxus_cli_config::{CrateConfig, WebHttpsConfig};
use std::{fs, io, process::Command, sync::Arc};
use tower::ServiceBuilder;
use tower_http::{
cors::{Any, CorsLayer},
services::fs::{ServeDir, ServeFileSystemResponseBody},
ServiceBuilderExt,
};
/// Sets up and returns a router
pub async fn setup_router(
config: CrateConfig,
ws_reload: Arc<WsReloadState>,
hot_reload: Option<HotReloadState>,
) -> Result<Router> {
// Setup cors
let cors = CorsLayer::new()
// allow `GET` and `POST` when accessing the resource
.allow_methods([Method::GET, Method::POST])
// allow requests from any origin
.allow_origin(Any)
.allow_headers(Any);
let (coep, coop) = if config.cross_origin_policy {
(
HeaderValue::from_static("require-corp"),
HeaderValue::from_static("same-origin"),
)
} else {
(
HeaderValue::from_static("unsafe-none"),
HeaderValue::from_static("unsafe-none"),
)
};
// Create file service
let file_service_config = config.clone();
let file_service = ServiceBuilder::new()
.override_response_header(
HeaderName::from_static("cross-origin-embedder-policy"),
coep,
)
.override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop)
.and_then(move |response| async move { Ok(no_cache(file_service_config, response)) })
.service(ServeDir::new(config.out_dir()));
// Setup websocket
let mut router = Router::new().route("/_dioxus/ws", get(ws_handler));
// Setup proxy
for proxy_config in config.dioxus_config.web.proxy {
router = super::proxy::add_proxy(router, &proxy_config)?;
}
// Route file service
router = router.fallback(get_service(file_service).handle_error(
|error: std::convert::Infallible| async move {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Unhandled internal error: {}", error),
)
},
));
router = if let Some(base_path) = config.dioxus_config.web.app.base_path.clone() {
let base_path = format!("/{}", base_path.trim_matches('/'));
Router::new()
.route(&base_path, axum::routing::any_service(router))
.fallback(get(move || {
let base_path = base_path.clone();
async move { format!("Outside of the base path: {}", base_path) }
}))
} else {
router
};
// Setup routes
router = router
.route("/_dioxus/hot_reload", get(hot_reload_handler))
.layer(cors)
.layer(Extension(ws_reload));
if let Some(hot_reload) = hot_reload {
router = router.layer(Extension(hot_reload))
}
Ok(router)
}
fn no_cache(
file_service_config: CrateConfig,
response: Response<ServeFileSystemResponseBody>,
) -> Response<Body> {
let mut response = if file_service_config.dioxus_config.web.watcher.index_on_404
&& response.status() == StatusCode::NOT_FOUND
{
let body = Body::from(
// TODO: Cache/memoize this.
std::fs::read_to_string(file_service_config.out_dir().join("index.html"))
.ok()
.unwrap(),
);
Response::builder()
.status(StatusCode::OK)
.body(body)
.unwrap()
} else {
response.into_response()
};
let headers = response.headers_mut();
headers.insert(
http::header::CACHE_CONTROL,
HeaderValue::from_static("no-cache"),
);
headers.insert(http::header::PRAGMA, HeaderValue::from_static("no-cache"));
headers.insert(http::header::EXPIRES, HeaderValue::from_static("0"));
response
}
/// Handle websockets
async fn ws_handler(
ws: WebSocketUpgrade,
Extension(state): Extension<Arc<WsReloadState>>,
) -> impl IntoResponse {
ws.on_upgrade(move |socket| ws_reload_handler(socket, state))
}
async fn ws_reload_handler(mut socket: WebSocket, state: Arc<WsReloadState>) {
let mut rx = state.update.subscribe();
let reload_watcher = tokio::spawn(async move {
loop {
rx.recv().await.unwrap();
let _ = socket.send(Message::Text(String::from("reload"))).await;
// ignore the error
println!("forcing reload");
// flush the errors after recompling
rx = rx.resubscribe();
}
});
reload_watcher.await.unwrap();
}
const DEFAULT_KEY_PATH: &str = "ssl/key.pem";
const DEFAULT_CERT_PATH: &str = "ssl/cert.pem";
/// Returns an enum of rustls config and a bool if mkcert isn't installed
pub async fn get_rustls(config: &CrateConfig) -> Result<Option<RustlsConfig>> {
let web_config = &config.dioxus_config.web.https;
if web_config.enabled != Some(true) {
return Ok(None);
}
let (cert_path, key_path) = if let Some(true) = web_config.mkcert {
// mkcert, use it
get_rustls_with_mkcert(web_config)?
} else {
// if mkcert not specified or false, don't use it
get_rustls_without_mkcert(web_config)?
};
Ok(Some(
RustlsConfig::from_pem_file(cert_path, key_path).await?,
))
}
pub fn get_rustls_with_mkcert(web_config: &WebHttpsConfig) -> Result<(String, String)> {
// Get paths to store certs, otherwise use ssl/item.pem
let key_path = web_config
.key_path
.clone()
.unwrap_or(DEFAULT_KEY_PATH.to_string());
let cert_path = web_config
.cert_path
.clone()
.unwrap_or(DEFAULT_CERT_PATH.to_string());
// Create ssl directory if using defaults
if key_path == DEFAULT_KEY_PATH && cert_path == DEFAULT_CERT_PATH {
_ = fs::create_dir("ssl");
}
let cmd = Command::new("mkcert")
.args([
"-install",
"-key-file",
&key_path,
"-cert-file",
&cert_path,
"localhost",
"::1",
"127.0.0.1",
])
.spawn();
match cmd {
Err(e) => {
match e.kind() {
io::ErrorKind::NotFound => log::error!("mkcert is not installed. See https://github.com/FiloSottile/mkcert#installation for installation instructions."),
e => log::error!("an error occured while generating mkcert certificates: {}", e.to_string()),
};
return Err("failed to generate mkcert certificates".into());
}
Ok(mut cmd) => {
cmd.wait()?;
}
}
Ok((cert_path, key_path))
}
pub fn get_rustls_without_mkcert(web_config: &WebHttpsConfig) -> Result<(String, String)> {
// get paths to cert & key
if let (Some(key), Some(cert)) = (web_config.key_path.clone(), web_config.cert_path.clone()) {
Ok((cert, key))
} else {
// missing cert or key
Err("https is enabled but cert or key path is missing".into())
}
}

View file

@ -0,0 +1,4 @@
//! Test that autoformatting works on files/folders/etc
#[tokio::test]
async fn formats() {}

View file

@ -14,7 +14,7 @@ proc-macro = true
[dependencies]
proc-macro2 = { version = "1.0" }
quote = "1.0"
quote = { workspace = true }
[features]
default = []
@ -24,4 +24,3 @@ mobile = []
web = []
ssr = []
liveview = []
tui = []

View file

@ -23,7 +23,7 @@ pub fn server_only(input: TokenStream) -> TokenStream {
#[proc_macro]
pub fn client(input: TokenStream) -> TokenStream {
if cfg!(any(feature = "desktop", feature = "web", feature = "tui")) {
if cfg!(any(feature = "desktop", feature = "web")) {
let input = TokenStream2::from(input);
quote! {
#input
@ -110,18 +110,3 @@ pub fn liveview(input: TokenStream) -> TokenStream {
}
.into()
}
#[proc_macro]
pub fn tui(input: TokenStream) -> TokenStream {
if cfg!(feature = "tui") {
let input = TokenStream2::from(input);
quote! {
#input
}
} else {
quote! {
|| {}
}
}
.into()
}

View file

@ -14,8 +14,8 @@ proc-macro = true
[dependencies]
proc-macro2 = { version = "1.0" }
quote = "1.0"
syn = { version = "2.0", features = ["full", "extra-traits", "visit"] }
quote = { workspace = true }
syn = { workspace = true, features = ["full", "extra-traits", "visit"] }
dioxus-rsx = { workspace = true }
constcat = "0.3.0"
convert_case = "^0.6.0"

View file

@ -2,20 +2,19 @@
//!
//! This module provides the primary mechanics to create a hook-based, concurrent VDOM for Rust.
use crate::innerlude::{DirtyTasks, ScopeOrder};
use crate::Task;
use crate::{
any_props::AnyProps,
arena::ElementId,
innerlude::{
ElementRef, ErrorBoundary, NoOpMutations, SchedulerMsg, ScopeState, VNodeMount, VProps,
WriteMutations,
DirtyTasks, ElementRef, ErrorBoundary, NoOpMutations, SchedulerMsg, ScopeOrder, ScopeState,
VNodeMount, VProps, WriteMutations,
},
nodes::RenderReturn,
nodes::{Template, TemplateId},
runtime::{Runtime, RuntimeGuard},
scopes::ScopeId,
AttributeValue, ComponentFunction, Element, Event, Mutations,
AttributeValue, ComponentFunction, Element, Event, Mutations, VNode,
};
use futures_util::StreamExt;
use rustc_hash::FxHashMap;
@ -534,17 +533,38 @@ impl VirtualDom {
#[instrument(skip(self), level = "trace", name = "VirtualDom::replace_template")]
pub fn replace_template(&mut self, template: Template) {
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
let mut dirty = Vec::new();
for (id, scope) in self.scopes.iter() {
// Recurse into the dynamic nodes of the existing mounted node to see if the template is alive in the tree
fn check_node_for_templates(node: &VNode, template: Template) -> bool {
let this_template_name = node.template.get().name.rsplit_once(':').unwrap().0;
if this_template_name == template.name.rsplit_once(':').unwrap().0 {
return true;
}
for dynamic in node.dynamic_nodes.iter() {
if let crate::DynamicNode::Fragment(nodes) = dynamic {
for node in nodes {
if check_node_for_templates(node, template) {
return true;
}
}
}
}
false
}
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
{
if check_node_for_templates(sync, template) {
dirty.push(ScopeId(id));
}
}
}
for dirty in dirty {
self.mark_dirty(dirty);
}

View file

@ -277,6 +277,12 @@ impl App {
dioxus_hot_reload::HotReloadMsg::Shutdown => {
self.control_flow = ControlFlow::Exit;
}
dioxus_hot_reload::HotReloadMsg::UpdateAsset(_) => {
for webview in self.webviews.values_mut() {
webview.kick_stylsheets();
}
}
}
}

View file

@ -221,7 +221,10 @@ fn get_asset_root() -> Option<PathBuf> {
// If running under cargo, there's no bundle!
// There might be a smarter/more resilient way of doing this
if std::env::var_os("CARGO").is_some() {
return None;
return dioxus_cli_config::CURRENT_CONFIG
.as_ref()
.map(|c| c.out_dir())
.ok();
}
#[cfg(target_os = "macos")]

View file

@ -221,4 +221,12 @@ impl WebviewInstance {
self.desktop_context.send_edits();
}
}
pub fn kick_stylsheets(&self) {
// run eval in the webview to kick the stylesheets by appending a query string
// we should do something less clunky than this
_ = self.desktop_context
.webview
.evaluate_script("document.querySelectorAll('link[rel=\"stylesheet\"]').forEach((el) => el.href = el.href + \"?\" + Math.random());");
}
}

View file

@ -1,2 +0,0 @@
/target
Cargo.lock

View file

@ -1,2 +0,0 @@
esque
Tui

View file

@ -1,36 +0,0 @@
[package]
name = "dioxus-tui"
version = { workspace = true }
authors = ["Jonathan Kelley, Evan Almloff"]
edition = "2021"
description = "TUI-based renderer for Dioxus"
repository = "https://github.com/DioxusLabs/dioxus/"
homepage = "https://dioxuslabs.com/learn/0.4/getting_started/tui"
keywords = ["dom", "ui", "gui", "react", "terminal"]
license = "MIT OR Apache-2.0"
[dependencies]
dioxus-core = { workspace = true, features = ["serialize"] }
dioxus-html = { workspace = true }
dioxus-native-core = { workspace = true, features = ["dioxus"] }
dioxus-native-core-macro = { workspace = true }
dioxus-hot-reload = { workspace = true, optional = true }
plasmo = { workspace = true }
crossterm = "0.26.0"
tokio = { workspace = true, features = ["full"] }
futures = "0.3.19"
taffy = "0.3.12"
[dev-dependencies]
dioxus = { workspace = true }
tokio = { version = "1" }
criterion = "0.3.5"
[[bench]]
name = "update"
harness = false
[features]
default = ["hot-reload"]
hot-reload = ["dioxus-hot-reload"]

View file

@ -1,95 +0,0 @@
<div align="center">
<h1>Dioxus TUI</h1>
<p>
<strong>Beautiful terminal user interfaces in Rust with <a href="https://dioxuslabs.com/">Dioxus </a>.</strong>
</p>
</div>
<div align="center">
<!-- Crates version -->
<a href="https://crates.io/crates/dioxus">
<img src="https://img.shields.io/crates/v/dioxus.svg?style=flat-square"
alt="Crates.io version" />
</a>
<!-- Downloads -->
<a href="https://crates.io/crates/dioxus">
<img src="https://img.shields.io/crates/d/dioxus.svg?style=flat-square"
alt="Download" />
</a>
<!-- docs -->
<a href="https://docs.rs/dioxus">
<img src="https://img.shields.io/badge/docs-latest-blue.svg?style=flat-square"
alt="docs.rs docs" />
</a>
<!-- CI -->
<a href="https://github.com/jkelleyrtp/dioxus/actions">
<img src="https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg"
alt="CI status" />
</a>
<!--Awesome -->
<a href="https://github.com/dioxuslabs/awesome-dioxus">
<img src="https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg" alt="Awesome Page" />
</a>
<!-- Discord -->
<a href="https://discord.gg/XgGxMSkvUM">
<img src="https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square" alt="Discord Link" />
</a>
</div>
<br/>
Leverage React-like patterns, CSS, HTML, and Rust to build beautiful, portable, terminal user interfaces with Dioxus.
```rust
fn app() -> Element {
rsx!{
div {
width: "100%",
height: "10px",
background_color: "red",
justify_content: "center",
align_items: "center",
"Hello world!"
}
})
}
```
![demo app](examples/example.png)
## Background
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.
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.
- **Particular frontend design**
Terminals and browsers are and look different. Therefore, the same design might not be the best to cover both renderers.
## Status
**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
Dioxus TUI features:
- [x] Flexbox-based layout system
- [ ] CSS selectors
- [x] inline CSS support
- [x] Built-in focusing system
* [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.

View file

@ -1,155 +0,0 @@
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, Clone)]
struct BoxProps {
x: usize,
y: usize,
hue: f32,
alpha: f32,
}
#[allow(non_snake_case)]
fn Box(props: BoxProps) -> Element {
let count = use_signal(|| 0);
let x = props.x * 2;
let y = props.y * 2;
let hue = props.hue;
let display_hue = props.hue as u32 / 10;
let count = count();
let alpha = props.alpha + (count % 100) as f32;
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, Clone)]
struct GridProps {
size: usize,
update_count: usize,
}
#[allow(non_snake_case)]
fn Grid(props: GridProps) -> Element {
let size = props.size;
let mut count = use_signal(|| 0);
let mut counts = use_signal(|| vec![0; size * size]);
let ctx: TuiContext = consume_context();
if count() + props.update_count >= (size * size) {
ctx.quit();
} else {
for _ in 0..props.update_count {
counts.with_mut(|c| {
let i = count();
c[i] += 1;
c[i] %= 360;
});
count.with_mut(|i| {
*i += 1;
*i %= size * size;
});
}
}
rsx! {
div{
width: "100%",
height: "100%",
flex_direction: "column",
for x in 0..size {
div {
width: "100%",
height: "100%",
flex_direction: "row",
for y in 0..size {
Box {
key: "{x}-{y}",
x: x,
y: y,
alpha: 100.0,
hue: y as f32*100.0/size as f32 + counts.read()[x*size + y] as f32,
}
}
}
}
}
}
}
fn app(props: GridProps) -> Element {
rsx! {
div{
width: "100%",
height: "100%",
Grid{
size: props.size,
update_count: props.update_count,
}
}
}
}

View file

@ -1,64 +0,0 @@
use dioxus::prelude::*;
use std::{fmt::Debug, rc::Rc};
fn main() {
dioxus_tui::launch(app);
}
const MAX_EVENTS: usize = 8;
fn app() -> Element {
let mut events = use_signal(|| Vec::new() as Vec<Rc<dyn Debug>>);
let mut log_event = move |event: Rc<dyn Debug>| events.write().push(event);
rsx! {
div { width: "100%", height: "100%", flex_direction: "column",
div {
width: "80%",
height: "50%",
border_width: "1px",
justify_content: "center",
align_items: "center",
background_color: "hsl(248, 53%, 58%)",
// Mosue
onmousemove: move |event| log_event(event.data()),
onclick: move |event| log_event(event.data()),
ondoubleclick: move |event| log_event(event.data()),
onmousedown: move |event| log_event(event.data()),
onmouseup: move |event| log_event(event.data()),
// Scroll
onwheel: move |event| log_event(event.data()),
// Keyboard
onkeydown: move |event| log_event(event.data()),
onkeyup: move |event| log_event(event.data()),
onkeypress: move |event| log_event(event.data()),
// Focus
onfocusin: move |event| log_event(event.data()),
onfocusout: move |event| log_event(event.data()),
"Hover, click, type or scroll to see the info down below"
}
div { width: "80%", height: "50%", flex_direction: "column",
// A trailing iterator of the last MAX_EVENTS events
// The index actually is a fine key here, since events are append-only and therefore stable
for (index, event) in events.read().iter().enumerate().rev().take(MAX_EVENTS).rev() {
p { key: "{index}",
{
// TUI panics if text overflows (https://github.com/DioxusLabs/dioxus/issues/371)
// temporary hack: just trim the strings (and make sure viewport is big enough)
// todo: remove
let mut trimmed = format!("{event:?}");
trimmed.truncate(200);
trimmed
}
}
}
}
}
}
}

View file

@ -1,27 +0,0 @@
use dioxus::prelude::*;
fn main() {
dioxus_tui::launch(app);
}
fn app() -> Element {
let mut radius = use_signal(|| 0);
rsx! {
div {
width: "100%",
height: "100%",
justify_content: "center",
align_items: "center",
background_color: "hsl(248, 53%, 58%)",
onwheel: move |w| radius.with_mut(|r| *r = (*r + w.delta().strip_units().y as i8).abs()),
border_style: "solid none solid double",
border_width: "thick",
border_radius: "{radius}px",
border_color: "#0000FF #FF00FF #FF0000 #00FF00",
"{radius}"
}
}
}

View file

@ -1,72 +0,0 @@
use dioxus::prelude::*;
use dioxus_html::input_data::keyboard_types::Code;
fn main() {
dioxus_tui::launch(app);
}
#[component]
fn Button(color_offset: u32, layer: u16) -> Element {
let mut toggle = use_signal(|| false);
let mut hovered = use_signal(|| false);
let hue = color_offset % 255;
let saturation = if toggle() { 50 } else { 25 } + if hovered() { 50 } else { 25 };
let brightness = saturation / 2;
let color = format!("hsl({hue}, {saturation}, {brightness})");
rsx! {
div{
width: "100%",
height: "100%",
background_color: "{color}",
tabindex: "{layer}",
onkeydown: move |e| {
if let Code::Space = e.code() {
toggle.toggle();
}
},
onclick: move |_| toggle.toggle(),
onmouseenter: move |_| hovered.set(true),
onmouseleave: move |_| hovered.set(false),
justify_content: "center",
align_items: "center",
display: "flex",
flex_direction: "column",
p { "tabindex: {layer}" }
}
}
}
fn app() -> Element {
rsx! {
div {
display: "flex",
flex_direction: "column",
width: "100%",
height: "100%",
for y in 1..8 {
div {
display: "flex",
flex_direction: "row",
width: "100%",
height: "100%",
for x in 1..8 {
if (x + y) % 2 == 0 {
div {
width: "100%",
height: "100%",
background_color: "rgb(100, 100, 100)",
}
} else {
Button {
color_offset: x * y,
layer: ((x + y) % 3) as u16,
}
}
}
}
}
}
}
}

View file

@ -1,38 +0,0 @@
use dioxus::prelude::*;
fn main() {
dioxus_tui::launch_cfg(
app,
dioxus_tui::Config::default().with_rendering_mode(dioxus_tui::RenderingMode::Ansi),
);
}
fn app() -> Element {
let steps = 50;
rsx! {
div{
width: "100%",
height: "100%",
flex_direction: "column",
for x in 0..=steps {
div { width: "100%", height: "100%", flex_direction: "row",
for y in 0..=steps {
{
let hue = x as f32*360.0/steps as f32;
let alpha = y as f32*100.0/steps as f32;
rsx! {
div {
left: "{x}px",
top: "{y}px",
width: "10%",
height: "100%",
background_color: "hsl({hue}, 100%, 50%, {alpha}%)",
}
}
}
}
}
}
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

View file

@ -1,82 +0,0 @@
use dioxus::prelude::*;
fn main() {
dioxus_tui::launch(app);
}
fn app() -> Element {
rsx! {
div {
width: "100%",
height: "100%",
flex_direction: "column",
// justify_content: "center",
// align_items: "center",
// flex_direction: "row",
// background_color: "red",
p {
background_color: "black",
flex_direction: "column",
justify_content: "center",
align_items: "center",
// height: "10%",
"hi"
"hi"
"hi"
}
li {
background_color: "red",
flex_direction: "column",
justify_content: "center",
align_items: "center",
// height: "10%",
"bib"
"bib"
"bib"
"bib"
"bib"
"bib"
"bib"
"bib"
}
li {
background_color: "blue",
flex_direction: "column",
justify_content: "center",
align_items: "center",
// height: "10%",
"zib"
"zib"
"zib"
"zib"
"zib"
"zib"
"zib"
"zib"
"zib"
"zib"
"zib"
"zib"
"zib"
}
p {
background_color: "yellow",
"asd"
}
p {
background_color: "green",
"asd"
}
p {
background_color: "white",
"asd"
}
p {
background_color: "cyan",
"asd"
}
}
}
}

View file

@ -1,125 +0,0 @@
use dioxus::{events::MouseData, prelude::*};
use dioxus_core::Event;
use std::convert::TryInto;
use std::fmt::Write;
use std::rc::Rc;
fn main() {
dioxus_tui::launch(app);
}
fn app() -> Element {
fn to_str(c: &[i32; 3]) -> String {
let mut result = String::new();
result += "#";
for c in c.iter() {
write!(result, "{c:02X?}").unwrap();
}
result
}
fn get_brightness(m: Rc<MouseData>) -> i32 {
let b: i32 = m.held_buttons().len().try_into().unwrap();
127 * b
}
let mut q1_color = use_signal(|| [200; 3]);
let mut q2_color = use_signal(|| [200; 3]);
let mut q3_color = use_signal(|| [200; 3]);
let mut q4_color = use_signal(|| [200; 3]);
let mut page_coordinates = use_signal(|| "".to_string());
let mut element_coordinates = use_signal(|| "".to_string());
let mut buttons = use_signal(|| "".to_string());
let mut modifiers = use_signal(|| "".to_string());
let update_data = move |event: Event<MouseData>| {
page_coordinates.set(format!("{:?}", event.page_coordinates()));
element_coordinates.set(format!("{:?}", event.element_coordinates()));
// Note: client coordinates are also available, but they would be the same as the page coordinates in this example, because there is no scrolling.
// There are also screen coordinates, but they are currently the same as client coordinates due to technical limitations
buttons.set(format!("{:?}", event.held_buttons()));
modifiers.set(format!("{:?}", event.modifiers()));
};
rsx! {
div {
width: "100%",
height: "100%",
flex_direction: "column",
div {
width: "100%",
height: "50%",
flex_direction: "row",
div {
border_width: "1px",
width: "50%",
height: "100%",
justify_content: "center",
align_items: "center",
background_color: to_str(&q1_color()),
onmouseenter: move |m| q1_color.set([get_brightness(m.data()), 0, 0]),
onmousedown: move |m| q1_color.set([get_brightness(m.data()), 0, 0]),
onmouseup: move |m| q1_color.set([get_brightness(m.data()), 0, 0]),
onwheel: move |w| q1_color.set([q1_color()[0] + (10.0 * w.delta().strip_units().y) as i32, 0, 0]),
onmouseleave: move |_| q1_color.set([200; 3]),
onmousemove: update_data,
"click me"
}
div {
width: "50%",
height: "100%",
justify_content: "center",
align_items: "center",
background_color: to_str(&q2_color()),
onmouseenter: move |m| q2_color.set([get_brightness(m.data()); 3]),
onmousedown: move |m| q2_color.set([get_brightness(m.data()); 3]),
onmouseup: move |m| q2_color.set([get_brightness(m.data()); 3]),
onwheel: move |w| q2_color.set([q2_color()[0] + (10.0 * w.delta().strip_units().y) as i32; 3]),
onmouseleave: move |_| q2_color.set([200; 3]),
onmousemove: update_data,
"click me"
}
}
div {
width: "100%",
height: "50%",
flex_direction: "row",
div {
width: "50%",
height: "100%",
justify_content: "center",
align_items: "center",
background_color: to_str(&q3_color()),
onmouseenter: move |m| q3_color.set([0, get_brightness(m.data()), 0]),
onmousedown: move |m| q3_color.set([0, get_brightness(m.data()), 0]),
onmouseup: move |m| q3_color.set([0, get_brightness(m.data()), 0]),
onwheel: move |w| q3_color.set([0, q3_color()[1] + (10.0 * w.delta().strip_units().y) as i32, 0]),
onmouseleave: move |_| q3_color.set([200; 3]),
onmousemove: update_data,
"click me"
}
div {
width: "50%",
height: "100%",
justify_content: "center",
align_items: "center",
background_color: to_str(&q4_color()),
onmouseenter: move |m| q4_color.set([0, 0, get_brightness(m.data())]),
onmousedown: move |m| q4_color.set([0, 0, get_brightness(m.data())]),
onmouseup: move |m| q4_color.set([0, 0, get_brightness(m.data())]),
onwheel: move |w| q4_color.set([0, 0, q4_color()[2] + (10.0 * w.delta().strip_units().y) as i32]),
onmouseleave: move |_| q4_color.set([200; 3]),
onmousemove: update_data,
"click me"
}
}
div { "Page coordinates: {page_coordinates}" }
div { "Element coordinates: {element_coordinates}" }
div { "Buttons: {buttons}" }
div { "Modifiers: {modifiers}" }
}
}
}

View file

@ -1,28 +0,0 @@
use dioxus::prelude::*;
fn main() {
dioxus_tui::launch(app);
}
fn app() -> Element {
rsx! {
div {
width: "100%",
height: "100%",
flex_direction: "column",
border_width: "1px",
h1 { height: "2px", color: "green",
"that's awesome!"
}
ul {
flex_direction: "column",
padding_left: "3px",
for i in 0..10 {
"> hello {i}"
}
}
}
}
}

View file

@ -1,96 +0,0 @@
use dioxus::prelude::*;
use dioxus_tui::{Config, TuiContext};
/// This benchmarks the cache performance of the TUI for small edits by changing one box at a time.
fn main() {
for size in 1..=20usize {
for _ in 0..10 {
let dom = VirtualDom::new(app).with_root_context(size);
dioxus_tui::launch_vdom_cfg(dom, Config::default().with_headless())
}
}
}
fn app() -> Element {
let size = use_context::<usize>();
rsx! {
div { width: "100%", height: "100%", Grid { size } }
}
}
#[component]
fn Box(x: usize, y: usize, hue: f32, alpha: f32) -> Element {
let count = use_signal(|| 0);
let x = x * 2;
let y = y * 2;
let hue = hue;
let display_hue = hue as u32 / 10;
let alpha = alpha + (count() % 100) as f32;
rsx! {
div {
left: "{x}%",
top: "{y}%",
width: "100%",
height: "100%",
background_color: "hsl({hue}, 100%, 50%, {alpha}%)",
align_items: "center",
p{"{display_hue:03}"}
}
}
}
#[component]
fn Grid(size: usize) -> Element {
let size = size;
let mut count = use_signal(|| 0);
let mut counts = use_signal(|| vec![0; size * size]);
let ctx: TuiContext = consume_context();
if count() + 1 >= (size * size) {
ctx.quit();
} else {
counts.with_mut(|c| {
let i = count();
c[i] += 1;
c[i] %= 360;
});
count.with_mut(|i| {
*i += 1;
*i %= size * size;
});
}
rsx! {
div{
width: "100%",
height: "100%",
flex_direction: "column",
for x in 0..size {
div{
width: "100%",
height: "100%",
flex_direction: "row",
for y in 0..size {
{
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}",
}
}
}
}
}
}
}
}
}

View file

@ -1,50 +0,0 @@
use dioxus::prelude::*;
fn main() {
dioxus_tui::launch(app);
}
fn app() -> Element {
rsx! {
div {
width: "100%",
height: "100%",
flex_direction: "column",
background_color: "black",
margin_right: "10px",
div {
width: "70%",
height: "70%",
background_color: "green",
margin_left: "4px",
div {
width: "100%",
height: "100%",
margin_top: "2px",
margin_bottom: "2px",
margin_left: "2px",
margin_right: "2px",
flex_shrink: "0",
background_color: "red",
justify_content: "center",
align_items: "center",
flex_direction: "column",
padding_top: "2px",
padding_bottom: "2px",
padding_left: "4px",
padding_right: "4px",
"[A]"
"[A]"
"[A]"
"[A]"
}
}
}
}
}

View file

@ -1,58 +0,0 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
fn main() {
dioxus_tui::launch_cfg(app, Default::default());
}
#[component]
fn Quadrant(color: String, text: String) -> Element {
rsx! {
div {
border_width: "1px",
width: "50%",
height: "100%",
justify_content: "center",
align_items: "center",
background_color: "{color}",
"{text}"
}
}
}
fn app() -> Element {
rsx! {
div {
width: "100%",
height: "100%",
flex_direction: "column",
div {
width: "100%",
height: "50%",
flex_direction: "row",
Quadrant {
color: "red".to_string(),
text: "[A]".to_string()
},
Quadrant {
color: "black".to_string(),
text: "[B]".to_string()
}
}
div {
width: "100%",
height: "50%",
flex_direction: "row",
Quadrant {
color: "green".to_string(),
text: "[C]".to_string()
},
Quadrant {
color: "blue".to_string(),
text: "[D]".to_string()
}
}
}
}
}

View file

@ -1,19 +0,0 @@
use dioxus::prelude::*;
fn main() {
dioxus_tui::launch(app);
}
fn app() -> Element {
rsx! {
div {
width: "100%",
height: "10px",
background_color: "red",
justify_content: "center",
align_items: "center",
"Hello world!"
}
}
}

View file

@ -1,28 +0,0 @@
use dioxus::prelude::*;
fn main() {
dioxus_tui::launch(app);
}
fn app() -> Element {
let mut count = use_signal(|| 0);
use_future(move || async move {
loop {
count += 1;
tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
schedule_update();
}
});
rsx! {
div { width: "100%",
div { width: "50%", height: "5px", background_color: "blue", justify_content: "center", align_items: "center",
"Hello {count}!"
}
div { width: "50%", height: "10px", background_color: "red", justify_content: "center", align_items: "center",
"Hello {count}!"
}
}
}
}

View file

@ -1,113 +0,0 @@
use dioxus::prelude::*;
fn main() {
dioxus_tui::launch(app);
}
fn app() -> Element {
let mut alpha = use_signal(|| 100);
rsx! {
div {
width: "100%",
height: "100%",
flex_direction: "column",
onwheel: move |evt| alpha.set((alpha() + evt.delta().strip_units().y as i64).clamp(0, 100)),
p {
background_color: "black",
flex_direction: "column",
justify_content: "center",
align_items: "center",
color: "green",
"hi"
"hi"
"hi"
}
li {
background_color: "red",
flex_direction: "column",
justify_content: "center",
align_items: "center",
"bib"
"bib"
"bib"
"bib"
"bib"
"bib"
"bib"
"bib"
}
li {
background_color: "blue",
flex_direction: "column",
justify_content: "center",
align_items: "center",
"zib"
"zib"
"zib"
"zib"
"zib"
"zib"
}
p {
background_color: "yellow",
"asd"
}
p {
background_color: "green",
"asd"
}
p {
background_color: "white",
"asd"
}
p {
background_color: "cyan",
"asd"
}
div {
font_weight: "bold",
color: "#666666",
p {
"bold"
}
p {
font_weight: "normal",
" normal"
}
}
p {
font_style: "italic",
color: "red",
"italic"
}
p {
text_decoration: "underline",
color: "rgba(255, 255, 255)",
"underline"
}
p {
text_decoration: "line-through",
color: "hsla(10, 100%, 70%)",
"line-through"
}
div{
position: "absolute",
top: "1px",
background_color: "rgba(255, 0, 0, 50%)",
width: "100%",
p {
color: "rgba(255, 255, 255, {alpha}%)",
background_color: "rgba(100, 100, 100, {alpha}%)",
"rgba(255, 255, 255, {alpha}%)"
}
p {
color: "rgba(255, 255, 255, 100%)",
"rgba(255, 255, 255, 100%)"
}
}
}
}
}

View file

@ -1,90 +0,0 @@
use dioxus::prelude::*;
use dioxus_tui::Config;
fn main() {
dioxus_tui::launch_cfg(app, Config::new());
}
fn app() -> Element {
let mut bg_green = use_signal(|| false);
let color = if bg_green() { "green" } else { "red" };
rsx! {
div {
width: "100%",
background_color: "{color}",
flex_direction: "column",
align_items: "center",
justify_content: "center",
input {
oninput: move |data| if data.value() == "good" {
bg_green.set(true);
} else{
bg_green.set(false);
},
r#type: "checkbox",
value: "good",
width: "50%",
height: "10%",
checked: "true",
}
input {
oninput: move |data| if &data.value() == "hello world"{
bg_green.set(true);
} else {
bg_green.set(false);
},
width: "50%",
height: "10%",
maxlength: "11",
}
input {
oninput: move |data| {
if (data.value().parse::<f32>().unwrap() - 40.0).abs() < 5.0 {
bg_green.set(true);
} else{
bg_green.set(false);
}
},
r#type: "range",
width: "50%",
height: "10%",
min: "20",
max: "80",
}
input {
oninput: move |data| {
if data.value() == "10"{
bg_green.set(true);
} else {
bg_green.set(false);
}
},
r#type: "number",
width: "50%",
height: "10%",
maxlength: "4",
}
input {
oninput: move |data| {
if data.value() == "hello world"{
bg_green.set(true);
} else{
bg_green.set(false);
}
},
r#type: "password",
width: "50%",
height: "10%",
maxlength: "11",
}
input {
oninput: move |_| { bg_green.set(true) },
r#type: "button",
value: "green",
width: "50%",
height: "10%",
}
}
}
}

View file

@ -1,158 +0,0 @@
use std::{
any::Any,
fmt::{Display, Formatter},
};
use dioxus_core::{ElementId, WriteMutations};
use dioxus_html::{
geometry::euclid::{Point2D, Rect, Size2D},
MountedData, MountedError, RenderedElementBacking,
};
use dioxus_native_core::{dioxus::DioxusNativeCoreMutationWriter, NodeId};
use plasmo::query::{ElementRef, Query};
pub(crate) struct DioxusTUIMutationWriter<'a> {
pub(crate) query: Query,
pub(crate) events: &'a mut Vec<(ElementId, &'static str, Box<dyn Any>, bool)>,
pub(crate) native_core_writer: DioxusNativeCoreMutationWriter<'a>,
}
impl WriteMutations for DioxusTUIMutationWriter<'_> {
fn register_template(&mut self, template: dioxus_core::prelude::Template) {
self.native_core_writer.register_template(template)
}
fn append_children(&mut self, id: ElementId, m: usize) {
self.native_core_writer.append_children(id, m)
}
fn assign_node_id(&mut self, path: &'static [u8], id: ElementId) {
self.native_core_writer.assign_node_id(path, id)
}
fn create_placeholder(&mut self, id: ElementId) {
self.native_core_writer.create_placeholder(id)
}
fn create_text_node(&mut self, value: &str, id: ElementId) {
self.native_core_writer.create_text_node(value, id)
}
fn hydrate_text_node(&mut self, path: &'static [u8], value: &str, id: ElementId) {
self.native_core_writer.hydrate_text_node(path, value, id)
}
fn load_template(&mut self, name: &'static str, index: usize, id: ElementId) {
self.native_core_writer.load_template(name, index, id)
}
fn replace_node_with(&mut self, id: ElementId, m: usize) {
self.native_core_writer.replace_node_with(id, m)
}
fn replace_placeholder_with_nodes(&mut self, path: &'static [u8], m: usize) {
self.native_core_writer
.replace_placeholder_with_nodes(path, m)
}
fn insert_nodes_after(&mut self, id: ElementId, m: usize) {
self.native_core_writer.insert_nodes_after(id, m)
}
fn insert_nodes_before(&mut self, id: ElementId, m: usize) {
self.native_core_writer.insert_nodes_before(id, m)
}
fn set_attribute(
&mut self,
name: &'static str,
ns: Option<&'static str>,
value: &dioxus_core::AttributeValue,
id: ElementId,
) {
self.native_core_writer.set_attribute(name, ns, value, id)
}
fn set_node_text(&mut self, value: &str, id: ElementId) {
self.native_core_writer.set_node_text(value, id)
}
fn create_event_listener(&mut self, name: &'static str, id: ElementId) {
if name == "mounted" {
let element = TuiElement {
query: self.query.clone(),
id: self.native_core_writer.state.element_to_node_id(id),
};
self.events
.push((id, "mounted", Box::new(MountedData::new(element)), false));
} else {
self.native_core_writer.create_event_listener(name, id)
}
}
fn remove_event_listener(&mut self, name: &'static str, id: ElementId) {
self.native_core_writer.remove_event_listener(name, id)
}
fn remove_node(&mut self, id: ElementId) {
self.native_core_writer.remove_node(id)
}
fn push_root(&mut self, id: ElementId) {
self.native_core_writer.push_root(id)
}
}
#[derive(Clone)]
pub(crate) struct TuiElement {
query: Query,
id: NodeId,
}
impl TuiElement {
pub(crate) fn element(&self) -> ElementRef {
self.query.get(self.id)
}
}
impl RenderedElementBacking for TuiElement {
fn get_client_rect(
&self,
) -> std::pin::Pin<
Box<
dyn futures::Future<
Output = dioxus_html::MountedResult<dioxus_html::geometry::euclid::Rect<f64, f64>>,
>,
>,
> {
let layout = self.element().layout();
Box::pin(async move {
match layout {
Some(layout) => {
let x = layout.location.x as f64;
let y = layout.location.y as f64;
let width = layout.size.width as f64;
let height = layout.size.height as f64;
Ok(Rect::new(Point2D::new(x, y), Size2D::new(width, height)))
}
None => Err(MountedError::OperationFailed(Box::new(TuiElementNotFound))),
}
})
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
#[derive(Debug)]
struct TuiElementNotFound;
impl Display for TuiElementNotFound {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "TUI element not found")
}
}
impl std::error::Error for TuiElementNotFound {}

View file

@ -1,108 +0,0 @@
use core::panic;
use dioxus_html::*;
use crate::element::TuiElement;
fn downcast(event: &PlatformEventData) -> plasmo::EventData {
event
.downcast::<plasmo::EventData>()
.expect("event should be of type EventData")
.clone()
}
pub(crate) struct SerializedHtmlEventConverter;
impl HtmlEventConverter for SerializedHtmlEventConverter {
fn convert_animation_data(&self, _: &PlatformEventData) -> AnimationData {
panic!("animation events not supported")
}
fn convert_clipboard_data(&self, _: &PlatformEventData) -> ClipboardData {
panic!("clipboard events not supported")
}
fn convert_composition_data(&self, _: &PlatformEventData) -> CompositionData {
panic!("composition events not supported")
}
fn convert_drag_data(&self, _: &PlatformEventData) -> DragData {
panic!("drag events not supported")
}
fn convert_focus_data(&self, event: &PlatformEventData) -> FocusData {
if let plasmo::EventData::Focus(event) = downcast(event) {
FocusData::new(event)
} else {
panic!("event should be of type Focus")
}
}
fn convert_form_data(&self, event: &PlatformEventData) -> FormData {
if let plasmo::EventData::Form(event) = downcast(event) {
FormData::new(event)
} else {
panic!("event should be of type Form")
}
}
fn convert_image_data(&self, _: &PlatformEventData) -> ImageData {
panic!("image events not supported")
}
fn convert_keyboard_data(&self, event: &PlatformEventData) -> KeyboardData {
if let plasmo::EventData::Keyboard(event) = downcast(event) {
KeyboardData::new(event)
} else {
panic!("event should be of type Keyboard")
}
}
fn convert_media_data(&self, _: &PlatformEventData) -> MediaData {
panic!("media events not supported")
}
fn convert_mounted_data(&self, event: &PlatformEventData) -> MountedData {
event.downcast::<TuiElement>().cloned().unwrap().into()
}
fn convert_mouse_data(&self, event: &PlatformEventData) -> MouseData {
if let plasmo::EventData::Mouse(event) = downcast(event) {
MouseData::new(event)
} else {
panic!("event should be of type Mouse")
}
}
fn convert_pointer_data(&self, _: &PlatformEventData) -> PointerData {
panic!("pointer events not supported")
}
fn convert_scroll_data(&self, _: &PlatformEventData) -> ScrollData {
panic!("scroll events not supported")
}
fn convert_selection_data(&self, _: &PlatformEventData) -> SelectionData {
panic!("selection events not supported")
}
fn convert_toggle_data(&self, _: &PlatformEventData) -> ToggleData {
panic!("toggle events not supported")
}
fn convert_touch_data(&self, _: &PlatformEventData) -> TouchData {
panic!("touch events not supported")
}
fn convert_transition_data(&self, _: &PlatformEventData) -> TransitionData {
panic!("transition events not supported")
}
fn convert_wheel_data(&self, event: &PlatformEventData) -> WheelData {
if let plasmo::EventData::Wheel(event) = downcast(event) {
WheelData::new(event)
} else {
panic!("event should be of type Wheel")
}
}
}

View file

@ -1,208 +0,0 @@
#![doc = include_str!("../README.md")]
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
mod element;
mod events;
use std::{
any::Any,
ops::Deref,
rc::Rc,
sync::{Arc, RwLock},
};
use dioxus_core::{Element, ElementId, ScopeId, VirtualDom};
use dioxus_html::PlatformEventData;
use dioxus_native_core::dioxus::{DioxusState, NodeImmutableDioxusExt};
use dioxus_native_core::prelude::*;
use element::DioxusTUIMutationWriter;
pub use plasmo::{query::Query, Config, RenderingMode, Size, TuiContext};
use plasmo::{render, Driver};
pub mod launch {
use super::*;
pub type Config = super::Config;
/// Launches the WebView and runs the event loop, with configuration and root props.
pub fn launch(
root: fn() -> Element,
contexts: Vec<Box<dyn Fn() -> Box<dyn Any>>>,
platform_config: Config,
) {
let mut virtual_dom = VirtualDom::new(root);
for context in contexts {
virtual_dom.insert_any_root_context(context());
}
launch_vdom_cfg(virtual_dom, platform_config)
}
}
pub fn launch(app: fn() -> Element) {
launch_cfg(app, Config::default())
}
pub fn launch_cfg(app: fn() -> Element, cfg: Config) {
launch_vdom_cfg(VirtualDom::new(app), cfg)
}
pub fn launch_cfg_with_props<P: Clone + 'static>(app: fn(P) -> Element, props: P, cfg: Config) {
launch_vdom_cfg(VirtualDom::new_with_props(app, props), cfg)
}
pub fn launch_vdom_cfg(vdom: VirtualDom, cfg: Config) {
dioxus_html::set_event_converter(Box::new(events::SerializedHtmlEventConverter));
render(cfg, |rdom, taffy, event_tx| {
let dioxus_state = {
let mut rdom = rdom.write().unwrap();
DioxusState::create(&mut rdom)
};
let dioxus_state = Rc::new(RwLock::new(dioxus_state));
let vdom = vdom
.with_root_context(TuiContext::new(event_tx))
.with_root_context(Query::new(rdom.clone(), taffy.clone()))
.with_root_context(DioxusElementToNodeId {
mapping: dioxus_state.clone(),
});
let queued_events = Vec::new();
let mut myself = DioxusRenderer {
vdom,
dioxus_state,
queued_events,
#[cfg(all(feature = "hot-reload", debug_assertions))]
hot_reload_rx: {
let (hot_reload_tx, hot_reload_rx) =
tokio::sync::mpsc::unbounded_channel::<dioxus_hot_reload::HotReloadMsg>();
dioxus_hot_reload::connect(move |msg| {
let _ = hot_reload_tx.send(msg);
});
hot_reload_rx
},
};
{
let mut rdom = rdom.write().unwrap();
let mut dioxus_state = myself.dioxus_state.write().unwrap();
let mut writer = DioxusTUIMutationWriter {
query: myself
.vdom
.in_runtime(|| ScopeId::ROOT.consume_context().unwrap()),
events: &mut myself.queued_events,
native_core_writer: dioxus_state.create_mutation_writer(&mut rdom),
};
// Find any mount events
myself.vdom.rebuild(&mut writer);
}
myself
})
.unwrap();
}
struct DioxusRenderer {
vdom: VirtualDom,
dioxus_state: Rc<RwLock<DioxusState>>,
// Events that are queued up to be sent to the vdom next time the vdom is polled
queued_events: Vec<(ElementId, &'static str, Box<dyn Any>, bool)>,
#[cfg(all(feature = "hot-reload", debug_assertions))]
hot_reload_rx: tokio::sync::mpsc::UnboundedReceiver<dioxus_hot_reload::HotReloadMsg>,
}
impl Driver for DioxusRenderer {
fn update(&mut self, rdom: &Arc<RwLock<RealDom>>) {
let mut rdom = rdom.write().unwrap();
let mut dioxus_state = self.dioxus_state.write().unwrap();
let mut writer = DioxusTUIMutationWriter {
query: self
.vdom
.in_runtime(|| ScopeId::ROOT.consume_context().unwrap()),
events: &mut self.queued_events,
native_core_writer: dioxus_state.create_mutation_writer(&mut rdom),
};
// Find any mount events
self.vdom.render_immediate(&mut writer);
}
fn handle_event(
&mut self,
rdom: &Arc<RwLock<RealDom>>,
id: NodeId,
event: &str,
value: Rc<plasmo::EventData>,
bubbles: bool,
) {
let id = { rdom.read().unwrap().get(id).unwrap().mounted_id() };
if let Some(id) = id {
let inner_value = value.deref().clone();
let boxed_event = Box::new(inner_value);
let platform_event = PlatformEventData::new(boxed_event);
self.vdom
.handle_event(event, Rc::new(platform_event), id, bubbles);
}
}
fn poll_async(&mut self) -> std::pin::Pin<Box<dyn futures::Future<Output = ()> + '_>> {
// Add any queued events
for (id, event, value, bubbles) in self.queued_events.drain(..) {
let platform_event = PlatformEventData::new(value);
self.vdom
.handle_event(event, Rc::new(platform_event), id, bubbles);
}
#[cfg(all(feature = "hot-reload", debug_assertions))]
return Box::pin(async {
let hot_reload_wait = self.hot_reload_rx.recv();
let mut hot_reload_msg = None;
let wait_for_work = self.vdom.wait_for_work();
tokio::select! {
Some(msg) = hot_reload_wait => {
#[cfg(all(feature = "hot-reload", debug_assertions))]
{
hot_reload_msg = Some(msg);
}
#[cfg(not(all(feature = "hot-reload", debug_assertions)))]
let () = msg;
}
_ = wait_for_work => {}
}
// if we have a new template, replace the old one
if let Some(msg) = hot_reload_msg {
match msg {
dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => {
self.vdom.replace_template(template);
}
dioxus_hot_reload::HotReloadMsg::Shutdown => {
std::process::exit(0);
}
}
}
});
#[cfg(not(all(feature = "hot-reload", debug_assertions)))]
Box::pin(self.vdom.wait_for_work())
}
}
#[derive(Clone)]
pub struct DioxusElementToNodeId {
mapping: Rc<RwLock<DioxusState>>,
}
impl DioxusElementToNodeId {
pub fn get_node_id(&self, element_id: ElementId) -> Option<NodeId> {
self.mapping
.read()
.unwrap()
.try_element_to_node_id(element_id)
}
}

View file

@ -1,73 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Test</title>
<style>
html,
body {
height: 100%;
}
.container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background-color: black;
/* justify-content: center;
align-items: center; */
/* margin: auto; */
}
.smaller {
height: 70%;
width: 70%;
background-color: green;
/* justify-content: center; */
/* align-items: center; */
}
.superinner {
height: 100%;
width: 100%;
/* display: flex; */
/* */
margin-top: 20px;
margin-bottom: 20px;
margin-left: 20px;
margin-right: 20px;
/* */
background-color: red;
justify-content: center;
align-items: center;
flex-direction: column;
/* margin: 20px; */
/* margin: 20px; */
}
</style>
</head>
<body>
<div class="container">
<div class="smaller">
<div class="superinner">
<h1>Hello World</h1>
<p>This is a test</p>
</div>
</div>
</div>
<!-- <div class="container">
<div class="smaller">
hello world
<div style="color: green; margin: 40px;">
goodbye
<div style="color:red;">
asdasdasd
</div>
</div>
</div>
</div> -->
</body>
</html>

View file

@ -1,381 +0,0 @@
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent};
use dioxus::prelude::*;
use dioxus_html::input_data::keyboard_types::Code;
use dioxus_tui::TuiContext;
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
/// The tui renderer will look for any event that has occured or any future that has resolved in a loop.
/// It will resolve at most one event per loop.
/// This future will resolve after a certain number of polls. If the number of polls is greater than the number of events triggered, and the event has not been recieved there is an issue with the event system.
struct PollN(usize);
impl PollN {
fn new(n: usize) -> Self {
PollN(n)
}
}
impl Future for PollN {
type Output = ();
fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Self::Output> {
if self.0 == 0 {
Poll::Ready(())
} else {
self.0 -= 1;
Poll::Pending
}
}
}
#[test]
fn key_down() {
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
fn app() -> Element {
let render_count = use_signal(|| 0);
let mut render_count_handle = render_count;
let tui_ctx: TuiContext = consume_context();
spawn(async move {
PollN::new(3).await;
render_count_handle.with_mut(|x| *x + 1);
});
if render_count() > 2 {
panic!("Event was not received");
}
// focus the element
tui_ctx.inject_event(Event::Key(KeyEvent {
code: KeyCode::Tab,
modifiers: KeyModifiers::NONE,
kind: crossterm::event::KeyEventKind::Press,
state: crossterm::event::KeyEventState::NONE,
}));
tui_ctx.inject_event(Event::Key(KeyEvent {
code: KeyCode::Char('a'),
modifiers: KeyModifiers::NONE,
kind: crossterm::event::KeyEventKind::Press,
state: crossterm::event::KeyEventState::NONE,
}));
rsx! {
div {
width: "100%",
height: "100%",
onkeydown: move |evt| {
assert_eq!(evt.data.code(), Code::KeyA);
tui_ctx.quit();
}
}
}
}
}
#[test]
fn mouse_down() {
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
fn app() -> Element {
let render_count = use_signal(|| 0);
let tui_ctx: TuiContext = consume_context();
let mut render_count_handle = render_count;
spawn(async move {
PollN::new(2).await;
render_count_handle.with_mut(|x| *x + 1);
});
if render_count() > 2 {
panic!("Event was not received");
}
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 0,
row: 0,
kind: crossterm::event::MouseEventKind::Down(MouseButton::Left),
modifiers: KeyModifiers::NONE,
}));
rsx! {
div {
width: "100%",
height: "100%",
onmousedown: move |evt| {
assert!(
evt.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary)
);
tui_ctx.quit();
}
}
}
}
}
#[test]
fn mouse_up() {
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
fn app() -> Element {
let render_count = use_signal(|| 0);
let tui_ctx: TuiContext = consume_context();
let mut render_count_handle = render_count;
spawn(async move {
PollN::new(3).await;
render_count_handle.with_mut(|x| *x + 1);
});
if render_count() > 2 {
panic!("Event was not received");
}
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 0,
row: 0,
kind: crossterm::event::MouseEventKind::Down(MouseButton::Left),
modifiers: KeyModifiers::NONE,
}));
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 0,
row: 0,
kind: crossterm::event::MouseEventKind::Up(MouseButton::Left),
modifiers: KeyModifiers::NONE,
}));
rsx! {
div {
width: "100%",
height: "100%",
onmouseup: move |_| {
tui_ctx.quit();
}
}
}
}
}
#[test]
fn mouse_enter() {
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
fn app() -> Element {
let render_count = use_signal(|| 0);
let mut render_count_handle = render_count;
let tui_ctx: TuiContext = consume_context();
spawn(async move {
PollN::new(3).await;
render_count_handle.with_mut(|x| *x + 1);
});
if render_count() > 2 {
panic!("Event was not received");
}
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 100,
row: 100,
kind: crossterm::event::MouseEventKind::Moved,
modifiers: KeyModifiers::NONE,
}));
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 0,
row: 0,
kind: crossterm::event::MouseEventKind::Moved,
modifiers: KeyModifiers::NONE,
}));
rsx! {
div {
width: "50%",
height: "50%",
onmouseenter: move |_| {
tui_ctx.quit();
}
}
}
}
}
#[test]
fn mouse_exit() {
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
fn app() -> Element {
let render_count = use_signal(|| 0);
let tui_ctx: TuiContext = consume_context();
let mut render_count_handle = render_count;
spawn(async move {
PollN::new(3).await;
render_count_handle.with_mut(|x| *x + 1);
});
if render_count() > 2 {
panic!("Event was not received");
}
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 0,
row: 0,
kind: crossterm::event::MouseEventKind::Moved,
modifiers: KeyModifiers::NONE,
}));
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 100,
row: 100,
kind: crossterm::event::MouseEventKind::Moved,
modifiers: KeyModifiers::NONE,
}));
rsx! {
div {
width: "50%",
height: "50%",
onmouseenter: move |_| {
tui_ctx.quit();
}
}
}
}
}
#[test]
fn mouse_move() {
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
fn app() -> Element {
let render_count = use_signal(|| 0);
let tui_ctx: TuiContext = consume_context();
let mut render_count_handle = render_count;
spawn(async move {
PollN::new(3).await;
render_count_handle.with_mut(|x| *x + 1);
});
if render_count() > 2 {
panic!("Event was not received");
}
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 40,
row: 40,
kind: crossterm::event::MouseEventKind::Moved,
modifiers: KeyModifiers::NONE,
}));
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 60,
row: 60,
kind: crossterm::event::MouseEventKind::Moved,
modifiers: KeyModifiers::NONE,
}));
rsx! {
div {
width: "100%",
height: "100%",
onmousemove: move |_| {
tui_ctx.quit();
}
}
}
}
}
#[test]
fn wheel() {
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
fn app() -> Element {
let render_count = use_signal(|| 0);
let tui_ctx: TuiContext = consume_context();
let mut render_count_handle = render_count;
spawn(async move {
PollN::new(3).await;
render_count_handle.with_mut(|x| *x + 1);
});
if render_count() > 2 {
panic!("Event was not received");
}
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 50,
row: 50,
kind: crossterm::event::MouseEventKind::Moved,
modifiers: KeyModifiers::NONE,
}));
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 50,
row: 50,
kind: crossterm::event::MouseEventKind::ScrollDown,
modifiers: KeyModifiers::NONE,
}));
rsx! {
div {
width: "100%",
height: "100%",
onwheel: move |evt| {
assert!(evt.data.delta().strip_units().y > 0.0);
tui_ctx.quit();
}
}
}
}
}
#[test]
fn click() {
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
fn app() -> Element {
let render_count = use_signal(|| 0);
let tui_ctx: TuiContext = consume_context();
let mut render_count_handle = render_count;
spawn(async move {
PollN::new(3).await;
render_count_handle.with_mut(|x| *x + 1);
});
if render_count() > 2 {
panic!("Event was not received");
}
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 50,
row: 50,
kind: crossterm::event::MouseEventKind::Down(MouseButton::Left),
modifiers: KeyModifiers::NONE,
}));
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 50,
row: 50,
kind: crossterm::event::MouseEventKind::Up(MouseButton::Left),
modifiers: KeyModifiers::NONE,
}));
rsx! {
div {
width: "100%",
height: "100%",
onclick: move |_| {
tui_ctx.quit();
}
}
}
}
}
#[test]
fn context_menu() {
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
fn app() -> Element {
let render_count = use_signal(|| 0);
let tui_ctx: TuiContext = consume_context();
let mut render_count_handle = render_count;
spawn(async move {
PollN::new(3).await;
render_count_handle.with_mut(|x| *x + 1);
});
if render_count() > 2 {
panic!("Event was not received");
}
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 50,
row: 50,
kind: crossterm::event::MouseEventKind::Down(MouseButton::Right),
modifiers: KeyModifiers::NONE,
}));
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 50,
row: 50,
kind: crossterm::event::MouseEventKind::Up(MouseButton::Right),
modifiers: KeyModifiers::NONE,
}));
rsx! {
div {
width: "100%",
height: "100%",
oncontextmenu: move |_| {
tui_ctx.quit();
}
}
}
}
}

View file

@ -24,7 +24,6 @@ dioxus-desktop = { workspace = true, optional = true }
dioxus-fullstack = { workspace = true, optional = true }
dioxus-liveview = { workspace = true, optional = true }
dioxus-ssr ={ workspace = true, optional = true }
dioxus-tui = { workspace = true, optional = true }
serde = { version = "1.0.136", optional = true }
@ -45,12 +44,11 @@ router = ["dioxus-router"]
# Platforms
fullstack = ["dioxus-fullstack", "dioxus-config-macro/fullstack", "serde", "dioxus-router?/fullstack"]
desktop = ["dioxus-desktop", "dioxus-fullstack?/desktop", "dioxus-config-macro/desktop"]
mobile = ["dioxus-mobile", "dioxus-fullstack?/mobile", "dioxus-config-macro/mobile"]
mobile = ["dioxus-mobile", "dioxus-desktop", "dioxus-fullstack?/mobile", "dioxus-config-macro/mobile"]
web = ["dioxus-web", "dioxus-fullstack?/web", "dioxus-config-macro/web", "dioxus-router?/web"]
ssr = ["dioxus-ssr", "dioxus-router?/ssr", "dioxus-config-macro/ssr"]
liveview = ["dioxus-liveview", "dioxus-config-macro/liveview", "dioxus-router?/liveview"]
axum = ["dioxus-fullstack?/axum", "dioxus-fullstack?/server", "ssr", "dioxus-liveview?/axum"]
tui = ["dioxus-tui", "dioxus-config-macro/tui"]
# This feature just disables the no-renderer-enabled warning
third-party-renderer = []

View file

@ -7,7 +7,7 @@ fn main() {
let liveview_renderers = ["liveview", "axum"];
let fullstack_renderers = ["axum"];
let client_renderers = ["desktop", "mobile", "web", "tui"];
let client_renderers = ["desktop", "mobile", "web"];
let client_renderer_selected = client_renderers
.iter()
.any(|renderer| feature_enabled(renderer));

View file

@ -80,17 +80,6 @@ impl LaunchBuilder {
}
}
#[cfg(feature = "tui")]
#[cfg_attr(docsrs, doc(cfg(feature = "tui")))]
/// Launch your tui application
pub fn tui() -> LaunchBuilder<dioxus_tui::Config, UnsendContext> {
LaunchBuilder {
launch_fn: dioxus_tui::launch::launch,
contexts: Vec::new(),
platform_config: None,
}
}
/// Provide a custom launch function for your application.
///
/// Useful for third party renderers to tap into the launch builder API without having to reimplement it.
@ -184,24 +173,11 @@ mod current_platform {
))]
pub use dioxus_liveview::launch::*;
#[cfg(all(
feature = "tui",
not(any(
feature = "liveview",
feature = "web",
feature = "desktop",
feature = "mobile",
feature = "fullstack"
))
))]
pub use dioxus_tui::launch::*;
#[cfg(not(any(
feature = "liveview",
feature = "desktop",
feature = "mobile",
feature = "web",
feature = "tui",
feature = "fullstack"
)))]
pub type Config = ();
@ -211,7 +187,6 @@ mod current_platform {
feature = "desktop",
feature = "mobile",
feature = "web",
feature = "tui",
feature = "fullstack"
)))]
pub fn launch(
@ -251,10 +226,3 @@ pub fn launch_desktop(app: fn() -> Element) {
pub fn launch_fullstack(app: fn() -> Element) {
LaunchBuilder::fullstack().launch(app)
}
#[cfg(feature = "tui")]
#[cfg_attr(docsrs, doc(cfg(feature = "tui")))]
/// Launch your tui application without any additional configuration. See [`LaunchBuilder`] for more options.
pub fn launch_tui(app: fn() -> Element) {
LaunchBuilder::tui().launch(app)
}

View file

@ -106,10 +106,6 @@ pub use dioxus_desktop as mobile;
#[cfg_attr(docsrs, doc(cfg(feature = "liveview")))]
pub use dioxus_liveview as liveview;
#[cfg(feature = "tui")]
#[cfg_attr(docsrs, doc(cfg(feature = "tui")))]
pub use dioxus_tui as tui;
#[cfg(feature = "ssr")]
#[cfg_attr(docsrs, doc(cfg(feature = "ssr")))]
pub use dioxus_ssr as ssr;

View file

@ -1,12 +1,12 @@
{
"name": "dioxus",
"version": "0.0.3",
"version": "0.4.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "dioxus",
"version": "0.0.3",
"version": "0.4.0",
"license": "MIT",
"dependencies": {
"dioxus-ext": "./pkg",
@ -1654,9 +1654,9 @@
"dev": true
},
"node_modules/follow-redirects": {
"version": "1.15.4",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
"integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"dev": true,
"funding": [
{
@ -4973,9 +4973,9 @@
"dev": true
},
"follow-redirects": {
"version": "1.15.4",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
"integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"dev": true
},
"fs-constants": {

View file

@ -36,8 +36,9 @@ async fn main() {
}
// Hydrate the page
#[cfg(all(feature = "web", not(feature = "server")))]
#[cfg(not(feature = "server"))]
fn main() {
#[cfg(all(feature = "web", not(feature = "server")))]
dioxus_web::launch_with_props(
dioxus_fullstack::router::RouteWithCfg::<Route>,
dioxus_fullstack::prelude::get_root_props_from_document()

View file

@ -63,6 +63,7 @@ where
}
}
#[cfg(feature = "web")]
#[inline]
fn kick_future<F, T>(user_fut: F)
where

View file

@ -35,6 +35,7 @@ impl Default for HotReloadState {
dioxus_hot_reload::HotReloadMsg::Shutdown => {
std::process::exit(0);
}
_ => {}
}
});

View file

@ -14,7 +14,7 @@ dioxus-rsx = { workspace = true }
dioxus-core = { workspace = true, features = ["serialize"] }
dioxus-html = { workspace = true, optional = true }
interprocess-docfix = { version = "1.2.2" }
interprocess = { workspace = true }
notify = { version = "5.0.0", optional = true }
chrono = { version = "0.4.24", default-features = false, features = ["clock"], optional = true }
serde_json = "1.0.91"

View file

@ -10,7 +10,7 @@ use dioxus_rsx::{
hot_reload::{FileMap, FileMapBuildResult, UpdateResult},
HotReloadingContext,
};
use interprocess_docfix::local_socket::LocalSocketListener;
use interprocess::local_socket::LocalSocketListener;
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
#[cfg(feature = "file_watcher")]
@ -109,17 +109,25 @@ impl<Ctx: HotReloadingContext> Config<Ctx> {
}
/// Initialize the hot reloading listener
///
/// This is designed to be called by hot_reload_Init!() which will pass in information about the project
///
/// Notes:
/// - We don't wannt to watch the
pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
let Config {
mut rebuild_with,
root_path,
listening_paths,
log,
mut rebuild_with,
excluded_paths,
phantom: _,
..
} = cfg;
if let Ok(crate_dir) = PathBuf::from_str(root_path) {
let Ok(crate_dir) = PathBuf::from_str(root_path) else {
return;
};
// try to find the gitignore file
let gitignore_file_path = crate_dir.join(".gitignore");
let (gitignore, _) = ignore::gitignore::Gitignore::new(gitignore_file_path);
@ -143,11 +151,13 @@ pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
.is_ignore()
})
.unwrap();
for err in errors {
if log {
println!("hot reloading failed to initialize:\n{err:?}");
}
}
let file_map = Arc::new(Mutex::new(file_map));
let target_dir = crate_dir.join("target");
@ -163,8 +173,14 @@ pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
}
}
match LocalSocketListener::bind(hot_reload_socket_path) {
Ok(local_socket_stream) => {
let local_socket_stream = match LocalSocketListener::bind(hot_reload_socket_path) {
Ok(local_socket_stream) => local_socket_stream,
Err(err) => {
println!("failed to connect to hot reloading\n{err}");
return;
}
};
let aborted = Arc::new(Mutex::new(false));
// listen for connections
@ -183,14 +199,12 @@ pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
.unwrap()
.map
.values()
.filter_map(|(_, template_slot)| *template_slot)
.flat_map(|v| v.templates.values().copied())
.collect()
};
for template in templates {
if !send_msg(
HotReloadMsg::UpdateTemplate(template),
&mut connection,
) {
if !send_msg(HotReloadMsg::UpdateTemplate(template), &mut connection) {
continue;
}
}
@ -212,16 +226,47 @@ pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
let (tx, rx) = std::sync::mpsc::channel();
let mut watcher =
RecommendedWatcher::new(tx, notify::Config::default()).unwrap();
let mut watcher = RecommendedWatcher::new(tx, notify::Config::default()).unwrap();
let mut listening_pathbufs = vec![];
// We're attempting to watch the root path... which contains a target directory...
// And on some platforms the target directory is really really large and can cause the watcher to crash
// since it runs out of file handles
// So we're going to iterate through its children and watch them instead of the root path, skipping the target
// directory.
//
// In reality, this whole approach of doing embedded file watching is kinda hairy since you want full knowledge
// of where rust code is. We could just use the filemap we generated above as an indication of where the rust
// code is in this project and deduce the subfolders under the root path from that.
//
// FIXME: use a more robust system here for embedded discovery
//
// https://github.com/DioxusLabs/dioxus/issues/1914
if listening_paths == [""] {
for entry in std::fs::read_dir(&crate_dir)
.expect("failed to read rust crate directory. Are you running with cargo?")
{
let entry = entry.expect("failed to read directory entry");
let path = entry.path();
if path.is_dir() {
if path == target_dir {
continue;
}
listening_pathbufs.push(path);
}
}
} else {
for path in listening_paths {
let full_path = crate_dir.join(path);
listening_pathbufs.push(full_path);
}
}
for full_path in listening_pathbufs {
if let Err(err) = watcher.watch(&full_path, RecursiveMode::Recursive) {
if log {
println!(
"hot reloading failed to start watching {full_path:?}:\n{err:?}",
);
println!("hot reloading failed to start watching {full_path:?}:\n{err:?}",);
}
}
}
@ -246,17 +291,21 @@ pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
return shutdown;
} else if log {
println!(
"Rebuild needed... shutting down hot reloading.\nManually rebuild the application to view further changes."
);
println!("Rebuild needed... shutting down hot reloading.\nManually rebuild the application to view further changes.");
}
true
}
};
for evt in rx {
if chrono::Local::now().timestamp_millis() >= last_update_time {
if let Ok(evt) = evt {
if chrono::Local::now().timestamp_millis() < last_update_time {
continue;
}
let Ok(evt) = evt else {
continue;
};
let real_paths = evt
.paths
.iter()
@ -283,26 +332,22 @@ pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
let mut channels = channels.lock().unwrap();
for path in real_paths {
// if this file type cannot be hot reloaded, rebuild the application
if path.extension().and_then(|p| p.to_str()) != Some("rs")
&& rebuild()
{
if path.extension().and_then(|p| p.to_str()) != Some("rs") && rebuild() {
return;
}
// find changes to the rsx in the file
match file_map
let changes = file_map
.lock()
.unwrap()
.update_rsx(path, crate_dir.as_path())
{
.update_rsx(path, crate_dir.as_path());
match changes {
Ok(UpdateResult::UpdatedRsx(msgs)) => {
for msg in msgs {
let mut i = 0;
while i < channels.len() {
let channel = &mut channels[i];
if send_msg(
HotReloadMsg::UpdateTemplate(msg),
channel,
) {
if send_msg(HotReloadMsg::UpdateTemplate(msg), channel) {
i += 1;
} else {
channels.remove(i);
@ -310,6 +355,7 @@ pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
}
}
}
Ok(UpdateResult::NeedsRebuild) => {
drop(channels);
if rebuild() {
@ -319,22 +365,15 @@ pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
}
Err(err) => {
if log {
println!(
"hot reloading failed to update rsx:\n{err:?}"
);
}
println!("hot reloading failed to update rsx:\n{err:?}");
}
}
}
}
last_update_time = chrono::Local::now().timestamp_millis();
}
}
});
}
Err(error) => println!("failed to connect to hot reloading\n{error}"),
}
}
}
fn send_msg(msg: HotReloadMsg, channel: &mut impl Write) -> bool {

View file

@ -6,7 +6,7 @@ use std::{
use dioxus_core::Template;
#[cfg(feature = "file_watcher")]
pub use dioxus_html::HtmlCtx;
use interprocess_docfix::local_socket::LocalSocketStream;
use interprocess::local_socket::LocalSocketStream;
use serde::{Deserialize, Serialize};
#[cfg(feature = "custom_file_watcher")]
@ -15,36 +15,48 @@ mod file_watcher;
pub use file_watcher::*;
/// A message the hot reloading server sends to the client
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(bound(deserialize = "'de: 'static"))]
pub enum HotReloadMsg {
/// A template has been updated
UpdateTemplate(Template),
/// An asset discovered by rsx! has been updated
UpdateAsset(PathBuf),
/// The program needs to be recompiled, and the client should shut down
Shutdown,
}
/// Connect to the hot reloading listener. The callback provided will be called every time a template change is detected
pub fn connect(mut f: impl FnMut(HotReloadMsg) + Send + 'static) {
pub fn connect(mut callback: impl FnMut(HotReloadMsg) + Send + 'static) {
std::thread::spawn(move || {
let path = PathBuf::from("./").join("target").join("dioxusin");
if let Ok(socket) = LocalSocketStream::connect(path) {
// There might be a socket since the we're not running under the hot reloading server
let Ok(socket) = LocalSocketStream::connect(path) else {
return;
};
let mut buf_reader = BufReader::new(socket);
loop {
let mut buf = String::new();
match buf_reader.read_line(&mut buf) {
Ok(_) => {
let template: HotReloadMsg =
serde_json::from_str(Box::leak(buf.into_boxed_str())).unwrap();
f(template);
}
Err(err) => {
if let Err(err) = buf_reader.read_line(&mut buf) {
if err.kind() != std::io::ErrorKind::WouldBlock {
break;
}
}
}
}
let Ok(template) = serde_json::from_str(Box::leak(buf.into_boxed_str())) else {
eprintln!(
"Could not parse hot reloading message - make sure your client is up to date"
);
continue;
};
callback(template);
}
});
}

View file

@ -13,7 +13,7 @@ description = "HTML function macros for Dioxus"
[dependencies]
proc-macro2 = "1.0.66"
syn = { version = "2", features = ["full"] }
syn = { workspace = true, features = ["full"] }
quote = "^1.0.26"
convert_case = "^0.6.0"

View file

@ -17,7 +17,7 @@ web-sys = { version = "0.3.56", optional = true, features = [
"Element",
"Node",
] }
sledgehammer_bindgen = { git = "https://github.com/ealmloff/sledgehammer_bindgen", default-features = false, optional = true }
sledgehammer_bindgen = { version = "0.5.0", default-features = false, optional = true }
sledgehammer_utils = { version = "0.2", optional = true }
serde = { version = "1.0", features = ["derive"], optional = true }

View file

@ -218,6 +218,8 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
dioxus_hot_reload::HotReloadMsg::UpdateTemplate(new_template) => {
vdom.replace_template(new_template);
}
// todo: enable hotreloading in liveview
dioxus_hot_reload::HotReloadMsg::UpdateAsset(_) => {}
dioxus_hot_reload::HotReloadMsg::Shutdown => {
std::process::exit(0);
},

View file

@ -1,2 +0,0 @@
/target
Cargo.lock

View file

@ -1,24 +0,0 @@
[package]
name = "dioxus-native-core-macro"
version = { workspace = true }
edition = "2021"
description = "Build natively rendered apps with Dioxus"
license = "MIT OR Apache-2.0"
repository = "https://github.com/DioxusLabs/dioxus/"
homepage = "https://dioxuslabs.com"
keywords = ["dom", "ui", "gui", "react"]
authors = ["Jonathan Kelley", "Evan Almloff"]
[lib]
proc-macro = true
[dependencies]
syn = { version = "2.0", features = ["extra-traits", "full"] }
quote = "1.0"
[dev-dependencies]
smallvec = "1.6"
rustc-hash = { workspace = true }
anymap = "0.12.1"
dioxus = { workspace = true }
dioxus-native-core = { workspace = true }

View file

@ -1,39 +0,0 @@
# Dioxus Native Core Macro
[![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-native-core-macro.svg
[crates-url]: https://crates.io/crates/dioxus-native-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/learn/0.4/) |
[API Docs](https://docs.rs/dioxus-native-core-macro/latest/dioxus_native_core_macro) |
[Chat](https://discord.gg/XgGxMSkvUM)
## Overview
`dioxus-native-core-macro` provides a handful of macros used by native-core for native renderers like TUI, Blitz, and Freya to derive their state.
## 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.

View file

@ -1,390 +0,0 @@
#![doc = include_str!("../README.md")]
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
extern crate proc_macro;
use std::collections::HashSet;
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{parse_macro_input, ItemImpl, Type, TypePath, TypeTuple};
/// A helper attribute for deriving `State` for a struct.
#[proc_macro_attribute]
pub fn partial_derive_state(_: TokenStream, input: TokenStream) -> TokenStream {
let impl_block: syn::ItemImpl = parse_macro_input!(input as syn::ItemImpl);
let has_create_fn = impl_block
.items
.iter()
.any(|item| matches!(item, syn::ImplItem::Fn(method) if method.sig.ident == "create"));
let parent_dependencies = impl_block
.items
.iter()
.find_map(|item| {
if let syn::ImplItem::Type(syn::ImplItemType { ident, ty, .. }) = item {
(ident == "ParentDependencies").then_some(ty)
} else {
None
}
})
.expect("ParentDependencies must be defined");
let child_dependencies = impl_block
.items
.iter()
.find_map(|item| {
if let syn::ImplItem::Type(syn::ImplItemType { ident, ty, .. }) = item {
(ident == "ChildDependencies").then_some(ty)
} else {
None
}
})
.expect("ChildDependencies must be defined");
let node_dependencies = impl_block
.items
.iter()
.find_map(|item| {
if let syn::ImplItem::Type(syn::ImplItemType { ident, ty, .. }) = item {
(ident == "NodeDependencies").then_some(ty)
} else {
None
}
})
.expect("NodeDependencies must be defined");
let this_type = &impl_block.self_ty;
let this_type = extract_type_path(this_type)
.unwrap_or_else(|| panic!("Self must be a type path, found {}", quote!(#this_type)));
let mut combined_dependencies = HashSet::new();
let self_path: TypePath = syn::parse_quote!(Self);
let parent_dependencies = match extract_tuple(parent_dependencies) {
Some(tuple) => {
let mut parent_dependencies = Vec::new();
for type_ in &tuple.elems {
let mut type_ = extract_type_path(type_).unwrap_or_else(|| {
panic!(
"ParentDependencies must be a tuple of type paths, found {}",
quote!(#type_)
)
});
if type_ == self_path {
type_ = this_type.clone();
}
combined_dependencies.insert(type_.clone());
parent_dependencies.push(type_);
}
parent_dependencies
}
_ => panic!(
"ParentDependencies must be a tuple, found {}",
quote!(#parent_dependencies)
),
};
let child_dependencies = match extract_tuple(child_dependencies) {
Some(tuple) => {
let mut child_dependencies = Vec::new();
for type_ in &tuple.elems {
let mut type_ = extract_type_path(type_).unwrap_or_else(|| {
panic!(
"ChildDependencies must be a tuple of type paths, found {}",
quote!(#type_)
)
});
if type_ == self_path {
type_ = this_type.clone();
}
combined_dependencies.insert(type_.clone());
child_dependencies.push(type_);
}
child_dependencies
}
_ => panic!(
"ChildDependencies must be a tuple, found {}",
quote!(#child_dependencies)
),
};
let node_dependencies = match extract_tuple(node_dependencies) {
Some(tuple) => {
let mut node_dependencies = Vec::new();
for type_ in &tuple.elems {
let mut type_ = extract_type_path(type_).unwrap_or_else(|| {
panic!(
"NodeDependencies must be a tuple of type paths, found {}",
quote!(#type_)
)
});
if type_ == self_path {
type_ = this_type.clone();
}
combined_dependencies.insert(type_.clone());
node_dependencies.push(type_);
}
node_dependencies
}
_ => panic!(
"NodeDependencies must be a tuple, found {}",
quote!(#node_dependencies)
),
};
combined_dependencies.insert(this_type.clone());
let combined_dependencies: Vec<_> = combined_dependencies.into_iter().collect();
let parent_dependancies_idxes: Vec<_> = parent_dependencies
.iter()
.filter_map(|ident| combined_dependencies.iter().position(|i| i == ident))
.collect();
let child_dependencies_idxes: Vec<_> = child_dependencies
.iter()
.filter_map(|ident| combined_dependencies.iter().position(|i| i == ident))
.collect();
let node_dependencies_idxes: Vec<_> = node_dependencies
.iter()
.filter_map(|ident| combined_dependencies.iter().position(|i| i == ident))
.collect();
let this_type_idx = combined_dependencies
.iter()
.enumerate()
.find_map(|(i, ident)| (this_type == *ident).then_some(i))
.unwrap();
let this_view = format_ident!("__data{}", this_type_idx);
let combined_dependencies_quote = combined_dependencies.iter().map(|ident| {
if ident == &this_type {
quote! {shipyard::ViewMut<#ident>}
} else {
quote! {shipyard::View<#ident>}
}
});
let combined_dependencies_quote = quote!((#(#combined_dependencies_quote,)*));
let ItemImpl {
attrs,
defaultness,
unsafety,
impl_token,
generics,
trait_,
self_ty,
items,
..
} = impl_block;
let for_ = trait_.as_ref().map(|t| t.2);
let trait_ = trait_.map(|t| t.1);
let split_views: Vec<_> = (0..combined_dependencies.len())
.map(|i| {
let ident = format_ident!("__data{}", i);
if i == this_type_idx {
quote! {mut #ident}
} else {
quote! {#ident}
}
})
.collect();
let node_view = node_dependencies_idxes
.iter()
.map(|i| format_ident!("__data{}", i))
.collect::<Vec<_>>();
let get_node_view = {
if node_dependencies.is_empty() {
quote! {
let raw_node = ();
}
} else {
let temps = (0..node_dependencies.len())
.map(|i| format_ident!("__temp{}", i))
.collect::<Vec<_>>();
quote! {
let raw_node: (#(*const #node_dependencies,)*) = {
let (#(#temps,)*) = (#(&#node_view,)*).get(id).unwrap_or_else(|err| panic!("Failed to get node view {:?}", err));
(#(#temps as *const _,)*)
};
}
}
};
let deref_node_view = {
if node_dependencies.is_empty() {
quote! {
let node = raw_node;
}
} else {
let indexes = (0..node_dependencies.len()).map(syn::Index::from);
quote! {
let node = unsafe { (#(dioxus_native_core::prelude::DependancyView::new(&*raw_node.#indexes),)*) };
}
}
};
let parent_view = parent_dependancies_idxes
.iter()
.map(|i| format_ident!("__data{}", i))
.collect::<Vec<_>>();
let get_parent_view = {
if parent_dependencies.is_empty() {
quote! {
let raw_parent = tree.parent_id_advanced(id, Self::TRAVERSE_SHADOW_DOM).map(|_| ());
}
} else {
let temps = (0..parent_dependencies.len())
.map(|i| format_ident!("__temp{}", i))
.collect::<Vec<_>>();
quote! {
let raw_parent = tree.parent_id_advanced(id, Self::TRAVERSE_SHADOW_DOM).and_then(|parent_id| {
let raw_parent: Option<(#(*const #parent_dependencies,)*)> = (#(&#parent_view,)*).get(parent_id).ok().map(|c| {
let (#(#temps,)*) = c;
(#(#temps as *const _,)*)
});
raw_parent
});
}
}
};
let deref_parent_view = {
if parent_dependencies.is_empty() {
quote! {
let parent = raw_parent;
}
} else {
let indexes = (0..parent_dependencies.len()).map(syn::Index::from);
quote! {
let parent = unsafe { raw_parent.map(|raw_parent| (#(dioxus_native_core::prelude::DependancyView::new(&*raw_parent.#indexes),)*)) };
}
}
};
let child_view = child_dependencies_idxes
.iter()
.map(|i| format_ident!("__data{}", i))
.collect::<Vec<_>>();
let get_child_view = {
if child_dependencies.is_empty() {
quote! {
let raw_children: Vec<_> = tree.children_ids_advanced(id, Self::TRAVERSE_SHADOW_DOM).into_iter().map(|_| ()).collect();
}
} else {
let temps = (0..child_dependencies.len())
.map(|i| format_ident!("__temp{}", i))
.collect::<Vec<_>>();
quote! {
let raw_children: Vec<_> = tree.children_ids_advanced(id, Self::TRAVERSE_SHADOW_DOM).into_iter().filter_map(|id| {
let raw_children: Option<(#(*const #child_dependencies,)*)> = (#(&#child_view,)*).get(id).ok().map(|c| {
let (#(#temps,)*) = c;
(#(#temps as *const _,)*)
});
raw_children
}).collect();
}
}
};
let deref_child_view = {
if child_dependencies.is_empty() {
quote! {
let children = raw_children;
}
} else {
let indexes = (0..child_dependencies.len()).map(syn::Index::from);
quote! {
let children = unsafe { raw_children.iter().map(|raw_children| (#(dioxus_native_core::prelude::DependancyView::new(&*raw_children.#indexes),)*)).collect::<Vec<_>>() };
}
}
};
let trait_generics = trait_
.as_ref()
.unwrap()
.segments
.last()
.unwrap()
.arguments
.clone();
// if a create function is defined, we don't generate one
// otherwise we generate a default one that uses the update function and the default constructor
let create_fn = (!has_create_fn).then(|| {
quote! {
fn create<'a>(
node_view: dioxus_native_core::prelude::NodeView # trait_generics,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &dioxus_native_core::prelude::SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
});
quote!(
#(#attrs)*
#defaultness #unsafety #impl_token #generics #trait_ #for_ #self_ty {
#create_fn
#(#items)*
fn workload_system(type_id: std::any::TypeId, dependants: std::sync::Arc<dioxus_native_core::prelude::Dependants>, pass_direction: dioxus_native_core::prelude::PassDirection) -> dioxus_native_core::exports::shipyard::WorkloadSystem {
use dioxus_native_core::exports::shipyard::{IntoWorkloadSystem, Get, AddComponent};
use dioxus_native_core::tree::TreeRef;
use dioxus_native_core::prelude::{NodeType, NodeView};
let node_mask = Self::NODE_MASK.build();
(move |data: #combined_dependencies_quote, run_view: dioxus_native_core::prelude::RunPassView #trait_generics| {
let (#(#split_views,)*) = data;
let tree = run_view.tree.clone();
let node_types = run_view.node_type.clone();
dioxus_native_core::prelude::run_pass(type_id, dependants.clone(), pass_direction, run_view, |id, context| {
let node_data: &NodeType<_> = node_types.get(id).unwrap_or_else(|err| panic!("Failed to get node type {:?}", err));
// get all of the states from the tree view
// Safety: No node has itself as a parent or child.
let raw_myself: Option<*mut Self> = (&mut #this_view).get(id).ok().map(|c| c as *mut _);
#get_node_view
#get_parent_view
#get_child_view
let myself: Option<&mut Self> = unsafe { raw_myself.map(|val| &mut *val) };
#deref_node_view
#deref_parent_view
#deref_child_view
let view = NodeView::new(id, node_data, &node_mask);
if let Some(myself) = myself {
myself
.update(view, node, parent, children, context)
}
else {
(&mut #this_view).add_component_unchecked(
id,
Self::create(view, node, parent, children, context));
true
}
})
}).into_workload_system().unwrap()
}
}
)
.into()
}
fn extract_tuple(ty: &Type) -> Option<TypeTuple> {
match ty {
Type::Tuple(tuple) => Some(tuple.clone()),
Type::Group(group) => extract_tuple(&group.elem),
_ => None,
}
}
fn extract_type_path(ty: &Type) -> Option<TypePath> {
match ty {
Type::Path(path) => Some(path.clone()),
Type::Group(group) => extract_type_path(&group.elem),
_ => None,
}
}

View file

@ -1,2 +0,0 @@
/target
Cargo.lock

View file

@ -1,39 +0,0 @@
[package]
name = "dioxus-native-core"
version = { workspace = true }
edition = "2021"
license = "MIT OR Apache-2.0"
repository = "https://github.com/DioxusLabs/dioxus/"
homepage = "https://dioxuslabs.com"
description = "Build natively rendered apps with Dioxus"
keywords = ["dom", "ui", "gui", "react"]
authors = ["Jonathan Kelley", "Evan Almloff"]
[dependencies]
dioxus-core = { workspace = true, optional = true }
keyboard-types = "0.7"
smallvec = "1.6"
rustc-hash = { workspace = true }
anymap = "1.0.0-beta.2"
parking_lot = { version = "0.12.1", features = ["send_guard"] }
dashmap = "5.4.0"
# for parsing attributes
taffy = { version = "0.3.12", optional = true }
lightningcss = { version = "1.0.0-alpha.39", optional = true }
shipyard = { version = "0.6.2", features = ["proc", "std"], default-features = false }
[dev-dependencies]
rand = "0.8.5"
dioxus = { workspace = true }
tokio = { workspace = true, features = ["full"] }
dioxus-native-core = { workspace = true, features = ["dioxus"] }
dioxus-native-core-macro = { workspace = true }
[features]
default = []
layout-attributes = ["dep:taffy", "dep:lightningcss"]
dioxus = ["dioxus-core"]
parallel = ["shipyard/parallel"]

View file

@ -1,45 +0,0 @@
# Dioxus Native Core
[![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-native-core.svg
[crates-url]: https://crates.io/crates/dioxus-native-core
[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/guide/) |
[API Docs](https://docs.rs/dioxus-native-core/latest/dioxus_native_core) |
[Chat](https://discord.gg/XgGxMSkvUM)
## Overview
`dioxus-native-core` provides several helpful utilities for lazily resolving computed values of the Dioxus VirtualDom to be used in conjunction with a native rendering engine.
The main "value-add" of this crate over implementing your native tree is that this tree is incrementally recomputed using the Dioxus VirtualDom's edit stream. Only parts of the tree that rely on each other will be redrawn - all else will be ignored.
## 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.

View file

@ -1,237 +0,0 @@
use dioxus::prelude::*;
use dioxus_native_core::prelude::*;
use dioxus_native_core_macro::partial_derive_state;
use shipyard::Component;
macro_rules! dep {
( child( $name:ty, $dep:ty ) ) => {
#[partial_derive_state]
impl State for $name {
type ParentDependencies = ();
type ChildDependencies = $dep;
type NodeDependencies = ();
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::ALL;
fn update<'a>(
&mut self,
_: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
self.0 += 1;
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
};
( parent( $name:ty, $dep:ty ) ) => {
#[partial_derive_state]
impl State for $name {
type ParentDependencies = $dep;
type ChildDependencies = ();
type NodeDependencies = ();
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::ALL;
fn update<'a>(
&mut self,
_: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
self.0 += 1;
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
};
( node( $name:ty, $dep:ty ) ) => {
#[partial_derive_state]
impl State for $name {
type ParentDependencies = $dep;
type ChildDependencies = ();
type NodeDependencies = ();
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::ALL;
fn update<'a>(
&mut self,
_: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
self.0 += 1;
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
};
}
macro_rules! test_state{
( state: ( $( $state:ty ),* ) ) => {
#[test]
fn state_reduce_initally_called_minimally() {
#[allow(non_snake_case)]
fn Base() -> Element {
rsx!{
div {
div{
div{
p{}
}
p{
"hello"
}
div{
h1{}
}
p{
"world"
}
}
}
}
}
let mut vdom = VirtualDom::new(Base);
let mutations = vdom.rebuild();
let mut dom: RealDom = RealDom::new([$( <$state>::to_type_erased() ),*]);
let mut dioxus_state = DioxusState::create(&mut dom);
dioxus_state.apply_mutations(&mut dom, mutations);
dom.update_state(SendAnyMap::new());
dom.traverse_depth_first_advanced(false, |n| {
$(
assert_eq!(n.get::<$state>().unwrap().0, 1);
)*
});
}
}
}
mod node_depends_on_child_and_parent {
use super::*;
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Node(i32);
dep!(node(Node, (Child, Parent)));
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Child(i32);
dep!(child(Child, (Child,)));
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Parent(i32);
dep!(parent(Parent, (Parent,)));
test_state!(state: (Child, Node, Parent));
}
mod child_depends_on_node_that_depends_on_parent {
use super::*;
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Node(i32);
dep!(node(Node, (Parent,)));
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Child(i32);
dep!(child(Child, (Node,)));
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Parent(i32);
dep!(parent(Parent, (Parent,)));
test_state!(state: (Child, Node, Parent));
}
mod parent_depends_on_node_that_depends_on_child {
use super::*;
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Node(i32);
dep!(node(Node, (Child,)));
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Child(i32);
dep!(child(Child, (Child,)));
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Parent(i32);
dep!(parent(Parent, (Node,)));
test_state!(state: (Child, Node, Parent));
}
mod node_depends_on_other_node_state {
use super::*;
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Node1(i32);
dep!(node(Node1, (Node2,)));
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Node2(i32);
dep!(node(Node2, ()));
test_state!(state: (Node1, Node2));
}
mod node_child_and_parent_state_depends_on_self {
use super::*;
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Node(i32);
dep!(node(Node, ()));
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Child(i32);
dep!(child(Child, (Child,)));
#[derive(Debug, Clone, Default, PartialEq, Component)]
struct Parent(i32);
dep!(parent(Parent, (Parent,)));
test_state!(state: (Child, Node, Parent));
}

View file

@ -1,389 +0,0 @@
use dioxus::prelude::*;
use dioxus_native_core::{custom_element::CustomElement, prelude::*};
use dioxus_native_core_macro::partial_derive_state;
use shipyard::Component;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Component)]
pub struct ColorState {
color: usize,
}
#[partial_derive_state]
impl State for ColorState {
type ParentDependencies = (Self,);
type ChildDependencies = ();
type NodeDependencies = ();
// The color state should not be effected by the shadow dom
const TRAVERSE_SHADOW_DOM: bool = false;
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new()
.with_attrs(AttributeMaskBuilder::Some(&["color"]))
.with_element();
fn update<'a>(
&mut self,
view: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
if let Some(size) = view
.attributes()
.into_iter()
.flatten()
.find(|attr| attr.attribute.name == "color")
{
self.color = size
.value
.as_float()
.or_else(|| size.value.as_int().map(|i| i as f64))
.or_else(|| size.value.as_text().and_then(|i| i.parse().ok()))
.unwrap_or(0.0) as usize;
} else if let Some((parent,)) = parent {
*self = *parent;
}
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Component)]
pub struct LayoutState {
size: usize,
}
#[partial_derive_state]
impl State for LayoutState {
type ParentDependencies = (Self,);
type ChildDependencies = ();
type NodeDependencies = ();
// The layout state should be effected by the shadow dom
const TRAVERSE_SHADOW_DOM: bool = true;
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new()
.with_attrs(AttributeMaskBuilder::Some(&["size"]))
.with_element();
fn update<'a>(
&mut self,
view: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
if let Some(size) = view
.attributes()
.into_iter()
.flatten()
.find(|attr| attr.attribute.name == "size")
{
self.size = size
.value
.as_float()
.or_else(|| size.value.as_int().map(|i| i as f64))
.or_else(|| size.value.as_text().and_then(|i| i.parse().ok()))
.unwrap_or(0.0) as usize;
} else if let Some((parent,)) = parent {
if parent.size > 0 {
self.size = parent.size - 1;
}
}
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
mod dioxus_elements {
macro_rules! builder_constructors {
(
$(
$(#[$attr:meta])*
$name:ident {
$(
$(#[$attr_method:meta])*
$fil:ident: $vil:ident,
)*
};
)*
) => {
$(
#[allow(non_camel_case_types)]
$(#[$attr])*
pub struct $name;
#[allow(non_upper_case_globals, unused)]
impl $name {
pub const TAG_NAME: &'static str = stringify!($name);
pub const NAME_SPACE: Option<&'static str> = None;
$(
pub const $fil: (&'static str, Option<&'static str>, bool) = (stringify!($fil), None, false);
)*
}
impl GlobalAttributes for $name {}
)*
}
}
pub trait GlobalAttributes {}
pub trait SvgAttributes {}
builder_constructors! {
customelementslot {
size: attr,
color: attr,
};
customelementnoslot {
size: attr,
color: attr,
};
testing132 {
color: attr,
};
}
}
struct CustomElementWithSlot {
root: NodeId,
slot: NodeId,
}
impl CustomElement for CustomElementWithSlot {
const NAME: &'static str = "customelementslot";
fn create(mut node: NodeMut<()>) -> Self {
let dom = node.real_dom_mut();
let child = dom.create_node(ElementNode {
tag: "div".into(),
namespace: None,
attributes: Default::default(),
listeners: Default::default(),
});
let slot_id = child.id();
let mut root = dom.create_node(ElementNode {
tag: "div".into(),
namespace: None,
attributes: Default::default(),
listeners: Default::default(),
});
root.add_child(slot_id);
Self {
root: root.id(),
slot: slot_id,
}
}
fn slot(&self) -> Option<NodeId> {
Some(self.slot)
}
fn roots(&self) -> Vec<NodeId> {
vec![self.root]
}
fn attributes_changed(
&mut self,
node: NodeMut<()>,
attributes: &dioxus_native_core::node_ref::AttributeMask,
) {
println!("attributes_changed");
println!("{:?}", attributes);
println!("{:?}: {:#?}", node.id(), &*node.node_type());
}
}
struct CustomElementWithNoSlot {
root: NodeId,
}
impl CustomElement for CustomElementWithNoSlot {
const NAME: &'static str = "customelementnoslot";
fn create(mut node: NodeMut<()>) -> Self {
let dom = node.real_dom_mut();
let root = dom.create_node(ElementNode {
tag: "div".into(),
namespace: None,
attributes: Default::default(),
listeners: Default::default(),
});
Self { root: root.id() }
}
fn roots(&self) -> Vec<NodeId> {
vec![self.root]
}
fn attributes_changed(
&mut self,
node: NodeMut<()>,
attributes: &dioxus_native_core::node_ref::AttributeMask,
) {
println!("attributes_changed");
println!("{:?}", attributes);
println!("{:?}: {:#?}", node.id(), &*node.node_type());
}
}
#[test]
fn custom_elements_work() {
fn app() -> Element {
let count = use_signal(|| 0);
use_future(|count| async move {
count.with_mut(|count| *count += 1);
});
rsx! {
customelementslot {
size: "{count}",
color: "1",
customelementslot {
testing132 {}
}
}
}
}
let rt = tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.unwrap();
rt.block_on(async {
let mut rdom = RealDom::new([LayoutState::to_type_erased(), ColorState::to_type_erased()]);
rdom.register_custom_element::<CustomElementWithSlot>();
let mut dioxus_state = DioxusState::create(&mut rdom);
let mut dom = VirtualDom::new(app);
let mutations = dom.rebuild();
dioxus_state.apply_mutations(&mut rdom, mutations);
let ctx = SendAnyMap::new();
rdom.update_state(ctx);
for i in 0..10usize {
dom.wait_for_work().await;
let mutations = dom.render_immediate();
dioxus_state.apply_mutations(&mut rdom, mutations);
let ctx = SendAnyMap::new();
rdom.update_state(ctx);
// render...
rdom.traverse_depth_first_advanced(true, |node| {
let node_type = &*node.node_type();
let height = node.height() as usize;
let indent = " ".repeat(height);
let color = *node.get::<ColorState>().unwrap();
let size = *node.get::<LayoutState>().unwrap();
let id = node.id();
println!("{indent}{id:?} {color:?} {size:?} {node_type:?}");
if let NodeType::Element(el) = node_type {
match el.tag.as_str() {
// the color should bubble up from customelementslot
"testing132" | "customelementslot" => {
assert_eq!(color.color, 1);
}
// the color of the light dom should not effect the color of the shadow dom, so the color of divs in the shadow dom should be 0
"div" => {
assert_eq!(color.color, 0);
}
_ => {}
}
if el.tag != "Root" {
assert_eq!(size.size, (i + 2).saturating_sub(height));
}
}
});
}
});
}
#[test]
#[should_panic]
fn slotless_custom_element_cant_have_children() {
fn app() -> Element {
rsx! {
customelementnoslot {
testing132 {}
}
}
}
let rt = tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.unwrap();
rt.block_on(async {
let mut rdom = RealDom::new([LayoutState::to_type_erased(), ColorState::to_type_erased()]);
rdom.register_custom_element::<CustomElementWithNoSlot>();
let mut dioxus_state = DioxusState::create(&mut rdom);
let mut dom = VirtualDom::new(app);
let mutations = dom.rebuild();
dioxus_state.apply_mutations(&mut rdom, mutations);
let ctx = SendAnyMap::new();
rdom.update_state(ctx);
});
}
#[test]
fn slotless_custom_element() {
fn app() -> Element {
rsx! {
customelementnoslot {
}
}
}
let rt = tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.unwrap();
rt.block_on(async {
let mut rdom = RealDom::new([LayoutState::to_type_erased(), ColorState::to_type_erased()]);
rdom.register_custom_element::<CustomElementWithNoSlot>();
let mut dioxus_state = DioxusState::create(&mut rdom);
let mut dom = VirtualDom::new(app);
let mutations = dom.rebuild();
dioxus_state.apply_mutations(&mut rdom, mutations);
let ctx = SendAnyMap::new();
rdom.update_state(ctx);
});
}

View file

@ -1,386 +0,0 @@
use std::cell::Cell;
use dioxus::prelude::Props;
use dioxus_core::*;
use dioxus_native_core::prelude::*;
use dioxus_native_core_macro::partial_derive_state;
use shipyard::Component;
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 {
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,
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, &current_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, 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(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(|_| {
cx.vnode(
None.into(),
Default::default(),
Cell::new(Template {
name: concat!(file!(), ":", line!(), ":", column!(), ":0"),
roots: &[TemplateNode::Dynamic { id: 0 }],
node_paths: &[&[0]],
attr_paths: &[],
}),
dioxus::dioxus_core::exports::bumpalo::collections::Vec::new_in(cx.bump()).into(),
cx.bump().alloc([cx.component(
create_random_element,
DepthProps { depth, root: false },
"create_random_element",
)]),
&[],
)
})),
2 => cx.component(
create_random_element,
DepthProps { depth, root: false },
"create_random_element",
),
_ => unreachable!(),
}
}
fn create_random_dynamic_attr() -> 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,
_ => unreachable!(),
};
Attribute::new(
Box::leak(format!("attr{}", rand::random::<usize>()).into_boxed_str()),
value,
random_ns(),
rand::random(),
)
}
static mut TEMPLATE_COUNT: usize = 0;
#[derive(PartialEq, Props, Component)]
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 = cx.vnode(
None.into(),
None,
Cell::new(template),
dioxus::dioxus_core::exports::bumpalo::collections::Vec::new_in(cx.bump()).into(),
{
let dynamic_nodes: Vec<_> = dynamic_node_types
.iter()
.map(|ty| match ty {
DynamicNodeType::Text => DynamicNode::Text(VText::new(Box::leak(
format!("{}", rand::random::<usize>()).into_boxed_str(),
))),
DynamicNodeType::Other => {
create_random_dynamic_node(cx.props.depth + 1)
}
})
.collect();
cx.bump().alloc(dynamic_nodes)
},
cx.bump()
.alloc(
(0..template.attr_paths.len())
.map(|_| create_random_dynamic_attr(cx).into())
.collect::<Vec<_>>(),
)
.as_slice(),
);
Some(node)
}
_ => None,
};
println!("{node:#?}");
node
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Component)]
pub struct BlablaState {
count: usize,
}
#[partial_derive_state]
impl State for BlablaState {
type ParentDependencies = (Self,);
type ChildDependencies = ();
type NodeDependencies = ();
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new()
.with_attrs(AttributeMaskBuilder::Some(&["blabla"]))
.with_element();
fn update<'a>(
&mut self,
_: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
if let Some((parent,)) = parent {
if parent.count != 0 {
self.count += 1;
}
}
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
// 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 mutations = vdom.rebuild();
let mut rdom: RealDom = RealDom::new([BlablaState::to_type_erased()]);
let mut dioxus_state = DioxusState::create(&mut rdom);
dioxus_state.apply_mutations(&mut rdom, mutations);
let ctx = SendAnyMap::new();
rdom.update_state(ctx);
}
}
// 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 mutations = vdom.rebuild();
let mut rdom: RealDom = RealDom::new([BlablaState::to_type_erased()]);
let mut dioxus_state = DioxusState::create(&mut rdom);
dioxus_state.apply_mutations(&mut rdom, mutations);
let ctx = SendAnyMap::new();
rdom.update_state(ctx);
for _ in 0..10 {
let mutations = vdom.render_immediate();
dioxus_state.apply_mutations(&mut rdom, mutations);
let ctx = SendAnyMap::new();
rdom.update_state(ctx);
}
}
}

View file

@ -1,154 +0,0 @@
use dioxus::prelude::*;
use dioxus_native_core::prelude::*;
use dioxus_native_core_macro::partial_derive_state;
use shipyard::Component;
use tokio::time::sleep;
#[derive(Debug, Clone, PartialEq, Eq, Default, Component)]
pub struct BlablaState {
count: usize,
}
#[partial_derive_state]
impl State for BlablaState {
type ParentDependencies = (Self,);
type ChildDependencies = ();
type NodeDependencies = ();
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new()
.with_attrs(AttributeMaskBuilder::Some(&["blabla"]))
.with_element();
fn update<'a>(
&mut self,
_: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
if let Some((parent,)) = parent {
if parent.count != 0 {
self.count += 1;
}
}
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
mod dioxus_elements {
macro_rules! builder_constructors {
(
$(
$(#[$attr:meta])*
$name:ident {
$(
$(#[$attr_method:meta])*
$fil:ident: $vil:ident,
)*
};
)*
) => {
$(
#[allow(non_camel_case_types)]
$(#[$attr])*
pub struct $name;
impl $name {
pub const TAG_NAME: &'static str = stringify!($name);
pub const NAME_SPACE: Option<&'static str> = None;
$(
pub const $fil: (&'static str, Option<&'static str>, bool) = (stringify!($fil), None, false);
)*
}
impl GlobalAttributes for $name {}
)*
}
}
pub trait GlobalAttributes {}
pub trait SvgAttributes {}
builder_constructors! {
blabla {
};
}
}
#[test]
fn native_core_is_okay() {
use std::sync::{Arc, Mutex};
use std::time::Duration;
fn app() -> Element {
let colors = use_signal(|| vec!["green", "blue", "red"]);
let padding = use_signal(|| 10);
use_effect(colors, |colors| async move {
sleep(Duration::from_millis(1000)).await;
colors.with_mut(|colors| colors.reverse());
});
use_effect(padding, |padding| async move {
sleep(Duration::from_millis(10)).await;
padding.with_mut(|padding| {
if *padding < 65 {
*padding += 1;
} else {
*padding = 5;
}
});
});
let _big = colors[0];
let _mid = colors[1];
let _small = colors[2];
rsx! {
blabla {}
}
}
let rt = tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.unwrap();
rt.block_on(async {
let rdom = Arc::new(Mutex::new(RealDom::new([BlablaState::to_type_erased()])));
let mut dioxus_state = DioxusState::create(&mut rdom.lock().unwrap());
let mut dom = VirtualDom::new(app);
let mutations = dom.rebuild();
dioxus_state.apply_mutations(&mut rdom.lock().unwrap(), mutations);
let ctx = SendAnyMap::new();
rdom.lock().unwrap().update_state(ctx);
for _ in 0..10 {
dom.wait_for_work().await;
let mutations = dom.render_immediate();
dioxus_state.apply_mutations(&mut rdom.lock().unwrap(), mutations);
let ctx = SendAnyMap::new();
rdom.lock().unwrap().update_state(ctx);
}
});
}

View file

@ -1,440 +0,0 @@
use dioxus_native_core::node::NodeType;
use dioxus_native_core::prelude::*;
use dioxus_native_core_macro::partial_derive_state;
use rustc_hash::{FxHashMap, FxHashSet};
use shipyard::Component;
fn create_blank_element() -> NodeType {
NodeType::Element(ElementNode {
tag: "div".to_owned(),
namespace: None,
attributes: FxHashMap::default(),
listeners: FxHashSet::default(),
})
}
#[test]
fn node_pass() {
#[derive(Debug, Default, Clone, PartialEq, Component)]
struct Number(i32);
#[partial_derive_state]
impl State for Number {
type ChildDependencies = ();
type NodeDependencies = ();
type ParentDependencies = ();
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new();
fn update<'a>(
&mut self,
_: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
self.0 += 1;
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
let mut tree: RealDom = RealDom::new([Number::to_type_erased()]);
tree.update_state(SendAnyMap::new());
assert_eq!(
tree.get(tree.root_id()).unwrap().get().as_deref(),
Some(&Number(1))
);
// mark the node as dirty
tree.get_mut(tree.root_id()).unwrap().get_mut::<Number>();
tree.update_state(SendAnyMap::new());
assert_eq!(
tree.get(tree.root_id()).unwrap().get().as_deref(),
Some(&Number(2))
);
}
#[test]
fn dependant_node_pass() {
#[derive(Debug, Default, Clone, PartialEq, Component)]
struct AddNumber(i32);
#[partial_derive_state]
impl State for AddNumber {
type ChildDependencies = ();
type NodeDependencies = (SubtractNumber,);
type ParentDependencies = ();
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new();
fn update<'a>(
&mut self,
_: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
self.0 += 1;
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
#[derive(Debug, Default, Clone, PartialEq, Component)]
struct SubtractNumber(i32);
#[partial_derive_state]
impl State for SubtractNumber {
type ChildDependencies = ();
type NodeDependencies = ();
type ParentDependencies = ();
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new();
fn update<'a>(
&mut self,
_: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
self.0 -= 1;
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
let mut tree: RealDom = RealDom::new([
AddNumber::to_type_erased(),
SubtractNumber::to_type_erased(),
]);
tree.update_state(SendAnyMap::new());
let root = tree.get(tree.root_id()).unwrap();
assert_eq!(root.get().as_deref(), Some(&AddNumber(1)));
assert_eq!(root.get().as_deref(), Some(&SubtractNumber(-1)));
// mark the subtract state as dirty, it should update the add state
tree.get_mut(tree.root_id())
.unwrap()
.get_mut::<SubtractNumber>();
tree.update_state(SendAnyMap::new());
let root = tree.get(tree.root_id()).unwrap();
assert_eq!(root.get().as_deref(), Some(&AddNumber(2)));
assert_eq!(root.get().as_deref(), Some(&SubtractNumber(-2)));
// mark the add state as dirty, it should ~not~ update the subtract state
tree.get_mut(tree.root_id()).unwrap().get_mut::<AddNumber>();
tree.update_state(SendAnyMap::new());
let root = tree.get(tree.root_id()).unwrap();
assert_eq!(root.get().as_deref(), Some(&AddNumber(3)));
assert_eq!(root.get().as_deref(), Some(&SubtractNumber(-2)));
}
#[test]
fn independant_node_pass() {
#[derive(Debug, Default, Clone, PartialEq, Component)]
struct AddNumber(i32);
#[partial_derive_state]
impl State for AddNumber {
type ChildDependencies = ();
type NodeDependencies = ();
type ParentDependencies = ();
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new();
fn update<'a>(
&mut self,
_: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
self.0 += 1;
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
#[derive(Debug, Default, Clone, PartialEq, Component)]
struct SubtractNumber(i32);
#[partial_derive_state]
impl State for SubtractNumber {
type ChildDependencies = ();
type NodeDependencies = ();
type ParentDependencies = ();
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new();
fn update<'a>(
&mut self,
_: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
self.0 -= 1;
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
let mut tree: RealDom = RealDom::new([
AddNumber::to_type_erased(),
SubtractNumber::to_type_erased(),
]);
tree.update_state(SendAnyMap::new());
let root = tree.get(tree.root_id()).unwrap();
assert_eq!(root.get().as_deref(), Some(&AddNumber(1)));
assert_eq!(root.get().as_deref(), Some(&SubtractNumber(-1)));
// mark the subtract state as dirty, it should ~not~ update the add state
tree.get_mut(tree.root_id())
.unwrap()
.get_mut::<SubtractNumber>();
tree.update_state(SendAnyMap::new());
let root = tree.get(tree.root_id()).unwrap();
assert_eq!(root.get().as_deref(), Some(&AddNumber(1)));
assert_eq!(root.get().as_deref(), Some(&SubtractNumber(-2)));
// mark the add state as dirty, it should ~not~ update the subtract state
tree.get_mut(tree.root_id()).unwrap().get_mut::<AddNumber>();
tree.update_state(SendAnyMap::new());
let root = tree.get(tree.root_id()).unwrap();
assert_eq!(root.get().as_deref(), Some(&AddNumber(2)));
assert_eq!(root.get().as_deref(), Some(&SubtractNumber(-2)));
}
#[test]
fn down_pass() {
#[derive(Debug, Clone, PartialEq, Component)]
struct AddNumber(i32);
impl Default for AddNumber {
fn default() -> Self {
Self(1)
}
}
#[partial_derive_state]
impl State for AddNumber {
type ChildDependencies = ();
type NodeDependencies = ();
type ParentDependencies = (AddNumber,);
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new();
fn update<'a>(
&mut self,
_: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
if let Some((parent,)) = parent {
self.0 += parent.0;
}
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self::default();
myself.update(node_view, node, parent, children, context);
myself
}
}
let mut tree: RealDom = RealDom::new([AddNumber::to_type_erased()]);
let grandchild1 = tree.create_node(create_blank_element());
let grandchild1 = grandchild1.id();
let mut child1 = tree.create_node(create_blank_element());
child1.add_child(grandchild1);
let child1 = child1.id();
let grandchild2 = tree.create_node(create_blank_element());
let grandchild2 = grandchild2.id();
let mut child2 = tree.create_node(create_blank_element());
child2.add_child(grandchild2);
let child2 = child2.id();
let mut parent = tree.get_mut(tree.root_id()).unwrap();
parent.add_child(child1);
parent.add_child(child2);
tree.update_state(SendAnyMap::new());
let root = tree.get(tree.root_id()).unwrap();
dbg!(root.id());
assert_eq!(root.get().as_deref(), Some(&AddNumber(1)));
let child1 = tree.get(child1).unwrap();
dbg!(child1.id());
assert_eq!(child1.get().as_deref(), Some(&AddNumber(2)));
let grandchild1 = tree.get(grandchild1).unwrap();
assert_eq!(grandchild1.get().as_deref(), Some(&AddNumber(3)));
let child2 = tree.get(child2).unwrap();
assert_eq!(child2.get().as_deref(), Some(&AddNumber(2)));
let grandchild2 = tree.get(grandchild2).unwrap();
assert_eq!(grandchild2.get().as_deref(), Some(&AddNumber(3)));
}
#[test]
fn up_pass() {
// Tree before:
// 1=\
// 1=\
// 1
// 1=\
// 1
// Tree after:
// 4=\
// 2=\
// 1
// 2=\
// 1
#[derive(Debug, Clone, PartialEq, Component)]
struct AddNumber(i32);
#[partial_derive_state]
impl State for AddNumber {
type ChildDependencies = (AddNumber,);
type NodeDependencies = ();
type ParentDependencies = ();
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new();
fn update<'a>(
&mut self,
_: NodeView,
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
self.0 += children.iter().map(|(i,)| i.0).sum::<i32>();
true
}
fn create<'a>(
node_view: NodeView<()>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self {
let mut myself = Self(1);
myself.update(node_view, node, parent, children, context);
myself
}
}
let mut tree: RealDom = RealDom::new([AddNumber::to_type_erased()]);
let grandchild1 = tree.create_node(create_blank_element());
let grandchild1 = grandchild1.id();
let mut child1 = tree.create_node(create_blank_element());
child1.add_child(grandchild1);
let child1 = child1.id();
let grandchild2 = tree.create_node(create_blank_element());
let grandchild2 = grandchild2.id();
let mut child2 = tree.create_node(create_blank_element());
child2.add_child(grandchild2);
let child2 = child2.id();
let mut parent = tree.get_mut(tree.root_id()).unwrap();
parent.add_child(child1);
parent.add_child(child2);
tree.update_state(SendAnyMap::new());
let root = tree.get(tree.root_id()).unwrap();
assert_eq!(root.get().as_deref(), Some(&AddNumber(5)));
let child1 = tree.get(child1).unwrap();
assert_eq!(child1.get().as_deref(), Some(&AddNumber(2)));
let grandchild1 = tree.get(grandchild1).unwrap();
assert_eq!(grandchild1.get().as_deref(), Some(&AddNumber(1)));
let child2 = tree.get(child2).unwrap();
assert_eq!(child2.get().as_deref(), Some(&AddNumber(2)));
let grandchild2 = tree.get(grandchild2).unwrap();
assert_eq!(grandchild2.get().as_deref(), Some(&AddNumber(1)));
}

View file

@ -1,231 +0,0 @@
use dioxus_native_core::exports::shipyard::Component;
use dioxus_native_core::node_ref::*;
use dioxus_native_core::prelude::*;
use dioxus_native_core::real_dom::NodeTypeMut;
use dioxus_native_core_macro::partial_derive_state;
struct FontSize(f64);
// All states need to derive Component
#[derive(Default, Debug, Copy, Clone, Component)]
struct Size(f64, f64);
#[derive(Default, Debug, Copy, Clone, Component)]
struct X;
impl FromAnyValue for X {
fn from_any_value(_: &dyn std::any::Any) -> Self {
X
}
}
/// Derive some of the boilerplate for the State implementation
#[partial_derive_state]
impl State<X> for Size {
type ParentDependencies = ();
// The size of the current node depends on the size of its children
type ChildDependencies = (Self,);
type NodeDependencies = ();
// Size only cares about the width, height, and text parts of the current node
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new()
// Get access to the width and height attributes
.with_attrs(AttributeMaskBuilder::Some(&["width", "height"]))
// Get access to the text of the node
.with_text();
fn update<'a>(
&mut self,
node_view: NodeView<X>,
_node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> bool {
let font_size = context.get::<FontSize>().unwrap().0;
let mut width;
let mut height;
if let Some(text) = node_view.text() {
// if the node has text, use the text to size our object
width = text.len() as f64 * font_size;
height = font_size;
} else {
// otherwise, the size is the maximum size of the children
width = children
.iter()
.map(|(item,)| item.0)
.reduce(|accum, item| if accum >= item { accum } else { item })
.unwrap_or(0.0);
height = children
.iter()
.map(|(item,)| item.1)
.reduce(|accum, item| if accum >= item { accum } else { item })
.unwrap_or(0.0);
}
// if the node contains a width or height attribute it overrides the other size
for a in node_view.attributes().into_iter().flatten() {
match &*a.attribute.name {
"width" => width = a.value.as_float().unwrap(),
"height" => height = a.value.as_float().unwrap(),
// because Size only depends on the width and height, no other attributes will be passed to the member
_ => panic!(),
}
}
// to determine what other parts of the dom need to be updated we return a boolean that marks if this member changed
let changed = (width != self.0) || (height != self.1);
*self = Self(width, height);
changed
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default, Component)]
struct TextColor {
r: u8,
g: u8,
b: u8,
}
#[partial_derive_state]
impl State<X> for TextColor {
// TextColor depends on the TextColor part of the parent
type ParentDependencies = (Self,);
type ChildDependencies = ();
type NodeDependencies = ();
// TextColor only cares about the color attribute of the current node
const NODE_MASK: NodeMaskBuilder<'static> =
// Get access to the color attribute
NodeMaskBuilder::new().with_attrs(AttributeMaskBuilder::Some(&["color"]));
fn update<'a>(
&mut self,
node_view: NodeView<X>,
_node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_context: &SendAnyMap,
) -> bool {
// TextColor only depends on the color tag, so getting the first tag is equivilent to looking through all tags
let new = match node_view
.attributes()
.and_then(|mut attrs| attrs.next())
.and_then(|attr| attr.value.as_text())
{
// if there is a color tag, translate it
Some("red") => TextColor { r: 255, g: 0, b: 0 },
Some("green") => TextColor { r: 0, g: 255, b: 0 },
Some("blue") => TextColor { r: 0, g: 0, b: 255 },
Some(color) => panic!("unknown color {color}"),
// otherwise check if the node has a parent and inherit that color
None => match parent {
Some((parent,)) => *parent,
None => Self::default(),
},
};
// check if the member has changed
let changed = new != *self;
*self = new;
changed
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default, Component)]
struct Border(bool);
#[partial_derive_state]
impl State<X> for Border {
// TextColor depends on the TextColor part of the parent
type ParentDependencies = (Self,);
type ChildDependencies = ();
type NodeDependencies = ();
// Border does not depended on any other member in the current node
const NODE_MASK: NodeMaskBuilder<'static> =
// Get access to the border attribute
NodeMaskBuilder::new().with_attrs(AttributeMaskBuilder::Some(&["border"]));
fn update<'a>(
&mut self,
node_view: NodeView<X>,
_node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_context: &SendAnyMap,
) -> bool {
// check if the node contians a border attribute
let new = Self(
node_view
.attributes()
.and_then(|mut attrs| attrs.next().map(|a| a.attribute.name == "border"))
.is_some(),
);
// check if the member has changed
let changed = new != *self;
*self = new;
changed
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut rdom: RealDom<X> = RealDom::new([
Border::to_type_erased(),
TextColor::to_type_erased(),
Size::to_type_erased(),
]);
let mut count = 0;
// intial render
let text_id = rdom.create_node(format!("Count: {count}")).id();
let mut root = rdom.get_mut(rdom.root_id()).unwrap();
// set the color to red
if let NodeTypeMut::Element(mut element) = root.node_type_mut() {
element.set_attribute(("color", "style"), "red".to_string());
}
root.add_child(text_id);
let mut ctx = SendAnyMap::new();
// set the font size to 3.3
ctx.insert(FontSize(3.3));
// update the State for nodes in the real_dom tree
let _to_rerender = rdom.update_state(ctx);
// we need to run the vdom in a async runtime
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?
.block_on(async {
loop {
// update the count
count += 1;
let mut text = rdom.get_mut(text_id).unwrap();
if let NodeTypeMut::Text(mut text) = text.node_type_mut() {
*text = format!("Count: {count}");
}
let mut ctx = SendAnyMap::new();
ctx.insert(FontSize(3.3));
let _to_rerender = rdom.update_state(ctx);
// render...
rdom.traverse_depth_first_advanced(true, |node| {
let indent = " ".repeat(node.height() as usize);
let color = *node.get::<TextColor>().unwrap();
let size = *node.get::<Size>().unwrap();
let border = *node.get::<Border>().unwrap();
let id = node.id();
println!("{indent}{id:?} {color:?} {size:?} {border:?}");
});
// wait 1 second
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
})
}

View file

@ -1,175 +0,0 @@
use dioxus_native_core::exports::shipyard::Component;
use dioxus_native_core::node_ref::*;
use dioxus_native_core::prelude::*;
use dioxus_native_core::real_dom::NodeTypeMut;
use dioxus_native_core_macro::partial_derive_state;
// All states need to derive Component
#[derive(Default, Debug, Copy, Clone, Component)]
struct Size(f64, f64);
/// Derive some of the boilerplate for the State implementation
#[partial_derive_state]
impl State for Size {
type ParentDependencies = ();
// The size of the current node depends on the size of its children
type ChildDependencies = (Self,);
type NodeDependencies = (FontSize,);
// Size only cares about the width, height, and text parts of the current node
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new()
// Get access to the width and height attributes
.with_attrs(AttributeMaskBuilder::Some(&["width", "height"]))
// Get access to the text of the node
.with_text();
fn update<'a>(
&mut self,
node_view: NodeView<()>,
(font_size,): <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_: &SendAnyMap,
) -> bool {
let font_size = font_size.size;
let mut width;
let mut height;
if let Some(text) = node_view.text() {
// if the node has text, use the text to size our object
width = text.len() as f64 * font_size;
height = font_size;
} else {
// otherwise, the size is the maximum size of the children
width = children
.iter()
.map(|(item,)| item.0)
.reduce(|accum, item| if accum >= item { accum } else { item })
.unwrap_or(0.0);
height = children
.iter()
.map(|(item,)| item.1)
.reduce(|accum, item| if accum >= item { accum } else { item })
.unwrap_or(0.0);
}
// if the node contains a width or height attribute it overrides the other size
for a in node_view.attributes().into_iter().flatten() {
match &*a.attribute.name {
"width" => width = a.value.as_float().unwrap(),
"height" => height = a.value.as_float().unwrap(),
// because Size only depends on the width and height, no other attributes will be passed to the member
_ => panic!(),
}
}
// to determine what other parts of the dom need to be updated we return a boolean that marks if this member changed
let changed = (width != self.0) || (height != self.1);
*self = Self(width, height);
changed
}
}
#[derive(Debug, Clone, Copy, PartialEq, Component)]
struct FontSize {
size: f64,
}
impl Default for FontSize {
fn default() -> Self {
Self { size: 16.0 }
}
}
#[partial_derive_state]
impl State for FontSize {
// TextColor depends on the TextColor part of the parent
type ParentDependencies = (Self,);
type ChildDependencies = ();
type NodeDependencies = ();
// TextColor only cares about the color attribute of the current node
const NODE_MASK: NodeMaskBuilder<'static> =
// Get access to the color attribute
NodeMaskBuilder::new().with_attrs(AttributeMaskBuilder::Some(&["font-size"]));
fn update<'a>(
&mut self,
node_view: NodeView<()>,
_node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_context: &SendAnyMap,
) -> bool {
let mut new = None;
for attr in node_view.attributes().into_iter().flatten() {
if attr.attribute.name == "font-size" {
new = Some(FontSize {
size: attr.value.as_float().unwrap(),
});
}
}
let new = new.unwrap_or(parent.map(|(p,)| *p).unwrap_or_default());
// check if the member has changed
let changed = new != *self;
*self = new;
changed
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut rdom: RealDom = RealDom::new([FontSize::to_type_erased(), Size::to_type_erased()]);
let mut count = 0;
// intial render
let text_id = rdom.create_node(format!("Count: {count}")).id();
let mut root = rdom.get_mut(rdom.root_id()).unwrap();
// set the color to red
if let NodeTypeMut::Element(mut element) = root.node_type_mut() {
element.set_attribute(("color", "style"), "red".to_string());
element.set_attribute(("font-size", "style"), 1.);
}
root.add_child(text_id);
let ctx = SendAnyMap::new();
// update the State for nodes in the real_dom tree
let _to_rerender = rdom.update_state(ctx);
// we need to run the vdom in a async runtime
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?
.block_on(async {
loop {
// update the count and font size
count += 1;
let mut text = rdom.get_mut(text_id).unwrap();
if let NodeTypeMut::Text(mut text) = text.node_type_mut() {
*text = format!("Count: {count}");
}
if let NodeTypeMut::Element(mut element) =
rdom.get_mut(rdom.root_id()).unwrap().node_type_mut()
{
element.set_attribute(("font-size", "style"), count as f64);
}
let ctx = SendAnyMap::new();
let _to_rerender = rdom.update_state(ctx);
// render...
rdom.traverse_depth_first_advanced(true, |node| {
let indent = " ".repeat(node.height() as usize);
let font_size = *node.get::<FontSize>().unwrap();
let size = *node.get::<Size>().unwrap();
let id = node.id();
println!("{indent}{id:?} {font_size:?} {size:?}");
});
// wait 1 second
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
})
}

View file

@ -1,222 +0,0 @@
use dioxus_native_core::exports::shipyard::Component;
use dioxus_native_core::node_ref::*;
use dioxus_native_core::prelude::*;
use dioxus_native_core::real_dom::NodeTypeMut;
use dioxus_native_core_macro::partial_derive_state;
struct FontSize(f64);
// All states need to derive Component
#[derive(Default, Debug, Copy, Clone, Component)]
struct Size(f64, f64);
/// Derive some of the boilerplate for the State implementation
#[partial_derive_state]
impl State for Size {
type ParentDependencies = ();
// The size of the current node depends on the size of its children
type ChildDependencies = (Self,);
type NodeDependencies = ();
// Size only cares about the width, height, and text parts of the current node
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new()
// Get access to the width and height attributes
.with_attrs(AttributeMaskBuilder::Some(&["width", "height"]))
// Get access to the text of the node
.with_text();
fn update<'a>(
&mut self,
node_view: NodeView<()>,
_node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> bool {
let font_size = context.get::<FontSize>().unwrap().0;
let mut width;
let mut height;
if let Some(text) = node_view.text() {
// if the node has text, use the text to size our object
width = text.len() as f64 * font_size;
height = font_size;
} else {
// otherwise, the size is the maximum size of the children
width = children
.iter()
.map(|(item,)| item.0)
.reduce(|accum, item| if accum >= item { accum } else { item })
.unwrap_or(0.0);
height = children
.iter()
.map(|(item,)| item.1)
.reduce(|accum, item| if accum >= item { accum } else { item })
.unwrap_or(0.0);
}
// if the node contains a width or height attribute it overrides the other size
for a in node_view.attributes().into_iter().flatten() {
match &*a.attribute.name {
"width" => width = a.value.as_float().unwrap(),
"height" => height = a.value.as_float().unwrap(),
// because Size only depends on the width and height, no other attributes will be passed to the member
_ => panic!(),
}
}
// to determine what other parts of the dom need to be updated we return a boolean that marks if this member changed
let changed = (width != self.0) || (height != self.1);
*self = Self(width, height);
changed
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default, Component)]
struct TextColor {
r: u8,
g: u8,
b: u8,
}
#[partial_derive_state]
impl State for TextColor {
// TextColor depends on the TextColor part of the parent
type ParentDependencies = (Self,);
type ChildDependencies = ();
type NodeDependencies = ();
// TextColor only cares about the color attribute of the current node
const NODE_MASK: NodeMaskBuilder<'static> =
// Get access to the color attribute
NodeMaskBuilder::new().with_attrs(AttributeMaskBuilder::Some(&["color"]));
fn update<'a>(
&mut self,
node_view: NodeView<()>,
_node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_context: &SendAnyMap,
) -> bool {
// TextColor only depends on the color tag, so getting the first tag is equivilent to looking through all tags
let new = match node_view
.attributes()
.and_then(|mut attrs| attrs.next())
.and_then(|attr| attr.value.as_text())
{
// if there is a color tag, translate it
Some("red") => TextColor { r: 255, g: 0, b: 0 },
Some("green") => TextColor { r: 0, g: 255, b: 0 },
Some("blue") => TextColor { r: 0, g: 0, b: 255 },
Some(color) => panic!("unknown color {color}"),
// otherwise check if the node has a parent and inherit that color
None => match parent {
Some((parent,)) => *parent,
None => Self::default(),
},
};
// check if the member has changed
let changed = new != *self;
*self = new;
changed
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default, Component)]
struct Border(bool);
#[partial_derive_state]
impl State for Border {
// TextColor depends on the TextColor part of the parent
type ParentDependencies = (Self,);
type ChildDependencies = ();
type NodeDependencies = ();
// Border does not depended on any other member in the current node
const NODE_MASK: NodeMaskBuilder<'static> =
// Get access to the border attribute
NodeMaskBuilder::new().with_attrs(AttributeMaskBuilder::Some(&["border"]));
fn update<'a>(
&mut self,
node_view: NodeView<()>,
_node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_context: &SendAnyMap,
) -> bool {
// check if the node contians a border attribute
let new = Self(
node_view
.attributes()
.and_then(|mut attrs| attrs.next().map(|a| a.attribute.name == "border"))
.is_some(),
);
// check if the member has changed
let changed = new != *self;
*self = new;
changed
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut rdom: RealDom = RealDom::new([
Border::to_type_erased(),
TextColor::to_type_erased(),
Size::to_type_erased(),
]);
let mut count = 0;
// intial render
let text_id = rdom.create_node(format!("Count: {count}")).id();
let mut root = rdom.get_mut(rdom.root_id()).unwrap();
// set the color to red
if let NodeTypeMut::Element(mut element) = root.node_type_mut() {
element.set_attribute(("color", "style"), "red".to_string());
}
root.add_child(text_id);
let mut ctx = SendAnyMap::new();
// set the font size to 3.3
ctx.insert(FontSize(3.3));
// update the State for nodes in the real_dom tree
let _to_rerender = rdom.update_state(ctx);
// we need to run the vdom in a async runtime
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?
.block_on(async {
loop {
// update the count
count += 1;
let mut text = rdom.get_mut(text_id).unwrap();
if let NodeTypeMut::Text(mut text) = text.node_type_mut() {
*text = format!("Count: {count}");
}
let mut ctx = SendAnyMap::new();
ctx.insert(FontSize(3.3));
let _to_rerender = rdom.update_state(ctx);
// render...
rdom.traverse_depth_first_advanced(true, |node| {
let indent = " ".repeat(node.height() as usize);
let color = *node.get::<TextColor>().unwrap();
let size = *node.get::<Size>().unwrap();
let border = *node.get::<Border>().unwrap();
let id = node.id();
println!("{indent}{id:?} {color:?} {size:?} {border:?}");
});
// wait 1 second
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
})
}

View file

@ -1,245 +0,0 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
use dioxus_native_core::exports::shipyard::Component;
use dioxus_native_core::node_ref::*;
use dioxus_native_core::prelude::*;
use dioxus_native_core_macro::partial_derive_state;
struct FontSize(f64);
// All states need to derive Component
#[derive(Default, Debug, Copy, Clone, Component)]
struct Size(f64, f64);
/// Derive some of the boilerplate for the State implementation
#[partial_derive_state]
impl State for Size {
type ParentDependencies = ();
// The size of the current node depends on the size of its children
type ChildDependencies = (Self,);
type NodeDependencies = ();
// Size only cares about the width, height, and text parts of the current node
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new()
// Get access to the width and height attributes
.with_attrs(AttributeMaskBuilder::Some(&["width", "height"]))
// Get access to the text of the node
.with_text();
fn update<'a>(
&mut self,
node_view: NodeView<()>,
_node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> bool {
let font_size = context.get::<FontSize>().unwrap().0;
let mut width;
let mut height;
if let Some(text) = node_view.text() {
// if the node has text, use the text to size our object
width = text.len() as f64 * font_size;
height = font_size;
} else {
// otherwise, the size is the maximum size of the children
width = children
.iter()
.map(|(item,)| item.0)
.reduce(|accum, item| if accum >= item { accum } else { item })
.unwrap_or(0.0);
height = children
.iter()
.map(|(item,)| item.1)
.reduce(|accum, item| if accum >= item { accum } else { item })
.unwrap_or(0.0);
}
// if the node contains a width or height attribute it overrides the other size
for a in node_view.attributes().into_iter().flatten() {
match &*a.attribute.name {
"width" => width = a.value.as_float().unwrap(),
"height" => height = a.value.as_float().unwrap(),
// because Size only depends on the width and height, no other attributes will be passed to the member
_ => panic!(),
}
}
// to determine what other parts of the dom need to be updated we return a boolean that marks if this member changed
let changed = (width != self.0) || (height != self.1);
*self = Self(width, height);
changed
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default, Component)]
struct TextColor {
r: u8,
g: u8,
b: u8,
}
#[partial_derive_state]
impl State for TextColor {
// TextColor depends on the TextColor part of the parent
type ParentDependencies = (Self,);
type ChildDependencies = ();
type NodeDependencies = ();
// TextColor only cares about the color attribute of the current node
const NODE_MASK: NodeMaskBuilder<'static> =
// Get access to the color attribute
NodeMaskBuilder::new().with_attrs(AttributeMaskBuilder::Some(&["color"]));
fn update<'a>(
&mut self,
node_view: NodeView<()>,
_node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_context: &SendAnyMap,
) -> bool {
// TextColor only depends on the color tag, so getting the first tag is equivilent to looking through all tags
let new = match node_view
.attributes()
.and_then(|mut attrs| attrs.next())
.and_then(|attr| attr.value.as_text())
{
// if there is a color tag, translate it
Some("red") => TextColor { r: 255, g: 0, b: 0 },
Some("green") => TextColor { r: 0, g: 255, b: 0 },
Some("blue") => TextColor { r: 0, g: 0, b: 255 },
Some(color) => panic!("unknown color {color}"),
// otherwise check if the node has a parent and inherit that color
None => match parent {
Some((parent,)) => *parent,
None => Self::default(),
},
};
// check if the member has changed
let changed = new != *self;
*self = new;
changed
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default, Component)]
struct Border(bool);
#[partial_derive_state]
impl State for Border {
// TextColor depends on the TextColor part of the parent
type ParentDependencies = (Self,);
type ChildDependencies = ();
type NodeDependencies = ();
// Border does not depended on any other member in the current node
const NODE_MASK: NodeMaskBuilder<'static> =
// Get access to the border attribute
NodeMaskBuilder::new().with_attrs(AttributeMaskBuilder::Some(&["border"]));
fn update<'a>(
&mut self,
node_view: NodeView<()>,
_node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
_parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
_children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
_context: &SendAnyMap,
) -> bool {
// check if the node contians a border attribute
let new = Self(
node_view
.attributes()
.and_then(|mut attrs| attrs.next().map(|a| a.attribute.name == "border"))
.is_some(),
);
// check if the member has changed
let changed = new != *self;
*self = new;
changed
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
fn app() -> Element {
let mut count = use_signal(|| 0);
use_future(move || async move {
loop {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
count += 1;
}
});
rsx! {
div { color: "red",
"{count}",
Comp {}
}
}
}
fn Comp() -> Element {
rsx! {
div { border: "", "hello world" }
}
}
// create the vdom, the real_dom, and the binding layer between them
let mut vdom = VirtualDom::new(app);
let mut rdom: RealDom = RealDom::new([
Border::to_type_erased(),
TextColor::to_type_erased(),
Size::to_type_erased(),
]);
let mut dioxus_intigration_state = DioxusState::create(&mut rdom);
// update the structure of the real_dom tree
let mut writer = dioxus_intigration_state.create_mutation_writer(&mut rdom);
vdom.rebuild(&mut writer);
let mut ctx = SendAnyMap::new();
// set the font size to 3.3
ctx.insert(FontSize(3.3));
// update the State for nodes in the real_dom tree
let _to_rerender = rdom.update_state(ctx);
// we need to run the vdom in a async runtime
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?
.block_on(async {
loop {
// wait for the vdom to update
vdom.wait_for_work().await;
// get the mutations from the vdom
// update the structure of the real_dom tree
let mut writer = dioxus_intigration_state.create_mutation_writer(&mut rdom);
vdom.rebuild(&mut writer);
// update the state of the real_dom tree
let mut ctx = SendAnyMap::new();
// set the font size to 3.3
ctx.insert(FontSize(3.3));
let _to_rerender = rdom.update_state(ctx);
// render...
rdom.traverse_depth_first_advanced(true, |node| {
let indent = " ".repeat(node.height() as usize);
let color = *node.get::<TextColor>().unwrap();
let size = *node.get::<Size>().unwrap();
let border = *node.get::<Border>().unwrap();
let id = node.id();
let node = node.node_type();
let node_type = &*node;
println!("{indent}{id:?} {color:?} {size:?} {border:?} {node_type:?}");
});
}
})
}

View file

@ -1,180 +0,0 @@
//! A custom element is a controlled element that renders to a shadow DOM. This allows you to create elements that act like widgets without relying on a specific framework.
//!
//! Each custom element is registered with a element name and namespace with [`RealDom::register_custom_element`] or [`RealDom::register_custom_element_with_factory`]. Once registered, they will be created automatically when the element is added to the DOM.
// Used in doc links
#[allow(unused)]
use crate::real_dom::RealDom;
use std::sync::{Arc, RwLock};
use rustc_hash::FxHashMap;
use shipyard::Component;
use crate::{
node::{FromAnyValue, NodeType},
node_ref::AttributeMask,
prelude::{NodeImmutable, NodeMut},
tree::TreeMut,
NodeId,
};
pub(crate) struct CustomElementRegistry<V: FromAnyValue + Send + Sync> {
builders: FxHashMap<(&'static str, Option<&'static str>), CustomElementBuilder<V>>,
}
impl<V: FromAnyValue + Send + Sync> Default for CustomElementRegistry<V> {
fn default() -> Self {
Self {
builders: FxHashMap::default(),
}
}
}
impl<V: FromAnyValue + Send + Sync> CustomElementRegistry<V> {
pub fn register<F, U>(&mut self)
where
F: CustomElementFactory<U, V>,
U: CustomElementUpdater<V>,
{
self.builders.insert(
(F::NAME, F::NAMESPACE),
CustomElementBuilder {
create: |node| Box::new(F::create(node)),
},
);
}
pub fn add_shadow_dom(&self, mut node: NodeMut<V>) {
let element_tag = if let NodeType::Element(el) = &*node.node_type() {
Some((el.tag.clone(), el.namespace.clone()))
} else {
None
};
if let Some((tag, ns)) = element_tag {
if let Some(builder) = self.builders.get(&(tag.as_str(), ns.as_deref())) {
let boxed_custom_element = { (builder.create)(node.reborrow()) };
let shadow_roots = boxed_custom_element.roots();
let light_id = node.id();
node.real_dom_mut().tree_mut().create_subtree(
light_id,
shadow_roots,
boxed_custom_element.slot(),
);
let boxed_custom_element = CustomElementManager {
inner: Arc::new(RwLock::new(boxed_custom_element)),
};
node.insert(boxed_custom_element);
}
}
}
}
struct CustomElementBuilder<V: FromAnyValue + Send + Sync> {
create: fn(NodeMut<V>) -> Box<dyn CustomElementUpdater<V>>,
}
/// A controlled element that renders to a shadow DOM.
///
/// Register with [`RealDom::register_custom_element`]
///
/// This is a simplified custom element trait for elements that can create themselves. For more granular control, implement [`CustomElementFactory`] and [`CustomElementUpdater`] instead.
pub trait CustomElement<V: FromAnyValue + Send + Sync = ()>: Send + Sync + 'static {
/// The tag of the element
const NAME: &'static str;
/// The namespace of the element
const NAMESPACE: Option<&'static str> = None;
/// Create a new element *without mounting* it.
/// The node passed in is the light DOM node. The element should not modify the light DOM node, but it can get the [`NodeMut::real_dom_mut`] from the node to create new nodes.
fn create(light_root: NodeMut<V>) -> Self;
/// The root node of the custom element. These roots must be not change once the element is created.
fn roots(&self) -> Vec<NodeId>;
/// The slot to render children of the element into. The slot must be not change once the element is created.
fn slot(&self) -> Option<NodeId> {
None
}
/// Update the custom element's shadow tree with the new attributes.
/// Called when the attributes of the custom element are changed.
fn attributes_changed(&mut self, light_node: NodeMut<V>, attributes: &AttributeMask);
}
/// A factory for creating custom elements
///
/// Register with [`RealDom::register_custom_element_with_factory`]
pub trait CustomElementFactory<W: CustomElementUpdater<V>, V: FromAnyValue + Send + Sync = ()>:
Send + Sync + 'static
{
/// The tag of the element
const NAME: &'static str;
/// The namespace of the element
const NAMESPACE: Option<&'static str> = None;
/// Create a new element *without mounting* it.
/// The node passed in is the light DOM node. The element should not modify the light DOM node, but it can get the [`NodeMut::real_dom_mut`] from the node to create new nodes.
fn create(dom: NodeMut<V>) -> W;
}
impl<W: CustomElement<V>, V: FromAnyValue + Send + Sync> CustomElementFactory<W, V> for W {
const NAME: &'static str = W::NAME;
const NAMESPACE: Option<&'static str> = W::NAMESPACE;
fn create(node: NodeMut<V>) -> Self {
Self::create(node)
}
}
/// A trait for updating custom elements
pub trait CustomElementUpdater<V: FromAnyValue + Send + Sync = ()>: Send + Sync + 'static {
/// Update the custom element's shadow tree with the new attributes.
/// Called when the attributes of the custom element are changed.
fn attributes_changed(&mut self, light_root: NodeMut<V>, attributes: &AttributeMask);
/// The root node of the custom element. These roots must be not change once the element is created.
fn roots(&self) -> Vec<NodeId>;
/// The slot to render children of the element into. The slot must be not change once the element is created.
fn slot(&self) -> Option<NodeId> {
None
}
}
impl<W: CustomElement<V>, V: FromAnyValue + Send + Sync> CustomElementUpdater<V> for W {
fn attributes_changed(&mut self, light_root: NodeMut<V>, attributes: &AttributeMask) {
self.attributes_changed(light_root, attributes);
}
fn roots(&self) -> Vec<NodeId> {
self.roots()
}
fn slot(&self) -> Option<NodeId> {
self.slot()
}
}
/// A dynamic trait object wrapper for [`CustomElementUpdater`]
#[derive(Component, Clone)]
pub(crate) struct CustomElementManager<V: FromAnyValue = ()> {
inner: Arc<RwLock<Box<dyn CustomElementUpdater<V>>>>,
}
impl<V: FromAnyValue + Send + Sync> CustomElementManager<V> {
/// Update the custom element based on attributes changed.
pub fn on_attributes_changed(&self, light_root: NodeMut<V>, attributes: &AttributeMask) {
self.inner
.write()
.unwrap()
.attributes_changed(light_root, attributes);
}
}

View file

@ -1,326 +0,0 @@
//! Integration between Dioxus and the RealDom
use crate::tree::TreeMut;
use dioxus_core::{AttributeValue, ElementId, TemplateNode, WriteMutations};
use rustc_hash::{FxHashMap, FxHashSet};
use shipyard::Component;
use crate::{
node::{
ElementNode, FromAnyValue, NodeType, OwnedAttributeDiscription, OwnedAttributeValue,
TextNode,
},
prelude::*,
real_dom::NodeTypeMut,
NodeId,
};
#[derive(Component)]
struct ElementIdComponent(ElementId);
/// The state of the Dioxus integration with the RealDom
pub struct DioxusState {
templates: FxHashMap<String, Vec<NodeId>>,
stack: Vec<NodeId>,
node_id_mapping: Vec<Option<NodeId>>,
}
impl DioxusState {
/// Initialize the DioxusState in the RealDom
pub fn create<V: FromAnyValue + Send + Sync>(rdom: &mut RealDom<V>) -> Self {
let root_id = rdom.root_id();
let mut root = rdom.get_mut(root_id).unwrap();
root.insert(ElementIdComponent(ElementId(0)));
Self {
templates: FxHashMap::default(),
stack: vec![root_id],
node_id_mapping: vec![Some(root_id)],
}
}
/// Convert an ElementId to a NodeId
pub fn element_to_node_id(&self, element_id: ElementId) -> NodeId {
self.try_element_to_node_id(element_id).unwrap()
}
/// Attempt to convert an ElementId to a NodeId. This will return None if the ElementId is not in the RealDom.
pub fn try_element_to_node_id(&self, element_id: ElementId) -> Option<NodeId> {
self.node_id_mapping.get(element_id.0).copied().flatten()
}
/// Create a mutation writer for the RealDom
pub fn create_mutation_writer<'a, V: FromAnyValue + Send + Sync>(
&'a mut self,
rdom: &'a mut RealDom<V>,
) -> DioxusNativeCoreMutationWriter<'a, V> {
DioxusNativeCoreMutationWriter { rdom, state: self }
}
fn set_element_id<V: FromAnyValue + Send + Sync>(
&mut self,
mut node: NodeMut<V>,
element_id: ElementId,
) {
let node_id = node.id();
node.insert(ElementIdComponent(element_id));
if self.node_id_mapping.len() <= element_id.0 {
self.node_id_mapping.resize(element_id.0 + 1, None);
} else if let Some(mut node) =
self.node_id_mapping[element_id.0].and_then(|id| node.real_dom_mut().get_mut(id))
{
node.remove();
}
self.node_id_mapping[element_id.0] = Some(node_id);
}
fn load_child<V: FromAnyValue + Send + Sync>(&self, rdom: &RealDom<V>, path: &[u8]) -> NodeId {
let mut current = rdom.get(*self.stack.last().unwrap()).unwrap();
for i in path {
let new_id = current.child_ids()[*i as usize];
current = rdom.get(new_id).unwrap();
}
current.id()
}
}
/// A writer for mutations that can be used with the RealDom.
pub struct DioxusNativeCoreMutationWriter<'a, V: FromAnyValue + Send + Sync = ()> {
/// The realdom associated with this writer
pub rdom: &'a mut RealDom<V>,
/// The state associated with this writer
pub state: &'a mut DioxusState,
}
impl<V: FromAnyValue + Send + Sync> WriteMutations for DioxusNativeCoreMutationWriter<'_, V> {
fn register_template(&mut self, template: dioxus_core::prelude::Template) {
let mut template_root_ids = Vec::new();
for root in template.roots {
let id = create_template_node(self.rdom, root);
template_root_ids.push(id);
}
self.state
.templates
.insert(template.name.to_string(), template_root_ids);
}
fn append_children(&mut self, id: ElementId, m: usize) {
let children = self.state.stack.split_off(self.state.stack.len() - m);
let parent = self.state.element_to_node_id(id);
for child in children {
self.rdom.get_mut(parent).unwrap().add_child(child);
}
}
fn assign_node_id(&mut self, path: &'static [u8], id: ElementId) {
let node_id = self.state.load_child(self.rdom, path);
self.state
.set_element_id(self.rdom.get_mut(node_id).unwrap(), id);
}
fn create_placeholder(&mut self, id: ElementId) {
let node = NodeType::Placeholder;
let node = self.rdom.create_node(node);
let node_id = node.id();
self.state.set_element_id(node, id);
self.state.stack.push(node_id);
}
fn create_text_node(&mut self, value: &str, id: ElementId) {
let node_data = NodeType::Text(TextNode {
listeners: FxHashSet::default(),
text: value.to_string(),
});
let node = self.rdom.create_node(node_data);
let node_id = node.id();
self.state.set_element_id(node, id);
self.state.stack.push(node_id);
}
fn hydrate_text_node(&mut self, path: &'static [u8], value: &str, id: ElementId) {
let node_id = self.state.load_child(self.rdom, path);
let node = self.rdom.get_mut(node_id).unwrap();
self.state.set_element_id(node, id);
let mut node = self.rdom.get_mut(node_id).unwrap();
let node_type_mut = node.node_type_mut();
if let NodeTypeMut::Text(mut text) = node_type_mut {
*text.text_mut() = value.to_string();
} else {
drop(node_type_mut);
node.set_type(NodeType::Text(TextNode {
text: value.to_string(),
listeners: FxHashSet::default(),
}));
}
}
fn load_template(&mut self, name: &'static str, index: usize, id: ElementId) {
let template_id = self.state.templates[name][index];
let clone_id = self.rdom.get_mut(template_id).unwrap().clone_node();
let clone = self.rdom.get_mut(clone_id).unwrap();
self.state.set_element_id(clone, id);
self.state.stack.push(clone_id);
}
fn replace_node_with(&mut self, id: ElementId, m: usize) {
let new_nodes = self.state.stack.split_off(self.state.stack.len() - m);
let old_node_id = self.state.element_to_node_id(id);
for new in new_nodes {
let mut node = self.rdom.get_mut(new).unwrap();
node.insert_before(old_node_id);
}
self.rdom.get_mut(old_node_id).unwrap().remove();
}
fn replace_placeholder_with_nodes(&mut self, path: &'static [u8], m: usize) {
let new_nodes = self.state.stack.split_off(self.state.stack.len() - m);
let old_node_id = self.state.load_child(self.rdom, path);
for new in new_nodes {
let mut node = self.rdom.get_mut(new).unwrap();
node.insert_before(old_node_id);
}
self.rdom.get_mut(old_node_id).unwrap().remove();
}
fn insert_nodes_after(&mut self, id: ElementId, m: usize) {
let new_nodes = self.state.stack.split_off(self.state.stack.len() - m);
let old_node_id = self.state.element_to_node_id(id);
for new in new_nodes.into_iter().rev() {
let mut node = self.rdom.get_mut(new).unwrap();
node.insert_after(old_node_id);
}
}
fn insert_nodes_before(&mut self, id: ElementId, m: usize) {
let new_nodes = self.state.stack.split_off(self.state.stack.len() - m);
let old_node_id = self.state.element_to_node_id(id);
for new in new_nodes {
self.rdom.tree_mut().insert_before(old_node_id, new);
}
}
fn set_attribute(
&mut self,
name: &'static str,
ns: Option<&'static str>,
value: &AttributeValue,
id: ElementId,
) {
let node_id = self.state.element_to_node_id(id);
let mut node = self.rdom.get_mut(node_id).unwrap();
let mut node_type_mut = node.node_type_mut();
if let NodeTypeMut::Element(element) = &mut node_type_mut {
if let AttributeValue::None = &value {
element.remove_attribute(&OwnedAttributeDiscription {
name: name.to_string(),
namespace: ns.map(|s| s.to_string()),
});
} else {
element.set_attribute(
OwnedAttributeDiscription {
name: name.to_string(),
namespace: ns.map(|s| s.to_string()),
},
OwnedAttributeValue::from(value),
);
}
}
}
fn set_node_text(&mut self, value: &str, id: ElementId) {
let node_id = self.state.element_to_node_id(id);
let mut node = self.rdom.get_mut(node_id).unwrap();
let node_type_mut = node.node_type_mut();
if let NodeTypeMut::Text(mut text) = node_type_mut {
*text.text_mut() = value.to_string();
}
}
fn create_event_listener(&mut self, name: &'static str, id: ElementId) {
let node_id = self.state.element_to_node_id(id);
let mut node = self.rdom.get_mut(node_id).unwrap();
node.add_event_listener(name);
}
fn remove_event_listener(&mut self, name: &'static str, id: ElementId) {
let node_id = self.state.element_to_node_id(id);
let mut node = self.rdom.get_mut(node_id).unwrap();
node.remove_event_listener(name);
}
fn remove_node(&mut self, id: ElementId) {
let node_id = self.state.element_to_node_id(id);
self.rdom.get_mut(node_id).unwrap().remove();
}
fn push_root(&mut self, id: ElementId) {
let node_id = self.state.element_to_node_id(id);
self.state.stack.push(node_id);
}
}
fn create_template_node<V: FromAnyValue + Send + Sync>(
rdom: &mut RealDom<V>,
node: &TemplateNode,
) -> NodeId {
match node {
TemplateNode::Element {
tag,
namespace,
attrs,
children,
} => {
let node = NodeType::Element(ElementNode {
tag: tag.to_string(),
namespace: namespace.map(|s| s.to_string()),
attributes: attrs
.iter()
.filter_map(|attr| match attr {
dioxus_core::TemplateAttribute::Static {
name,
value,
namespace,
} => Some((
OwnedAttributeDiscription {
namespace: namespace.map(|s| s.to_string()),
name: name.to_string(),
},
OwnedAttributeValue::Text(value.to_string()),
)),
dioxus_core::TemplateAttribute::Dynamic { .. } => None,
})
.collect(),
listeners: FxHashSet::default(),
});
let node_id = rdom.create_node(node).id();
for child in *children {
let child_id = create_template_node(rdom, child);
rdom.get_mut(node_id).unwrap().add_child(child_id);
}
node_id
}
TemplateNode::Text { text } => rdom
.create_node(NodeType::Text(TextNode {
text: text.to_string(),
..Default::default()
}))
.id(),
TemplateNode::Dynamic { .. } => rdom.create_node(NodeType::Placeholder).id(),
TemplateNode::DynamicText { .. } => {
rdom.create_node(NodeType::Text(TextNode::default())).id()
}
}
}
/// A trait that extends the `NodeImmutable` trait with methods that are useful for dioxus.
pub trait NodeImmutableDioxusExt<V: FromAnyValue + Send + Sync>: NodeImmutable<V> {
/// Returns the id of the element that this node is mounted to.
/// Not all nodes are mounted to an element, only nodes with dynamic content that have been renderered will have an id.
fn mounted_id(&self) -> Option<ElementId> {
let id = self.get::<ElementIdComponent>();
id.map(|id| id.0)
}
}
impl<T: NodeImmutable<V>, V: FromAnyValue + Send + Sync> NodeImmutableDioxusExt<V> for T {}

View file

@ -1,641 +0,0 @@
//! Utility functions for applying layout attributes to taffy layout
/*
- [ ] pub display: Display, ----> taffy doesnt support all display types
- [x] pub position: Position, --> taffy doesnt support everything
- [x] pub direction: Direction,
- [x] pub flex_direction: FlexDirection,
- [x] pub flex_wrap: FlexWrap,
- [x] pub flex_grow: f32,
- [x] pub flex_shrink: f32,
- [x] pub flex_basis: Dimension,
- [x]pub grid_auto_flow: GridAutoFlow,
- [x]pub grid_template_rows: GridTrackVec<TrackSizingFunction>,
- [x]pub grid_template_columns: GridTrackVec<TrackSizingFunction>,
- [x]pub grid_auto_rows: GridTrackVec<NonRepeatedTrackSizingFunction>,
- [x]pub grid_auto_columns: GridTrackVec<NonRepeatedTrackSizingFunction>,
- [x]pub grid_row: Line<GridPlacement>,
- [x]pub grid_column: Line<GridPlacement>,
- [x] pub overflow: Overflow, ---> taffy doesnt have support for directional overflow
- [x] pub align_items: AlignItems,
- [x] pub align_self: AlignSelf,
- [x] pub align_content: AlignContent,
- [x] pub margin: Rect<Dimension>,
- [x] pub padding: Rect<Dimension>,
- [x] pub justify_content: JustifyContent,
- [x] pub inset: Rect<Dimension>,
- [x] pub border: Rect<Dimension>,
- [ ] pub size: Size<Dimension>, ----> seems to only be relevant for input?
- [ ] pub min_size: Size<Dimension>,
- [ ] pub max_size: Size<Dimension>,
- [x] pub aspect_ratio: Number,
*/
use lightningcss::properties::border::LineStyle;
use lightningcss::properties::grid::{TrackBreadth, TrackSizing};
use lightningcss::properties::{align, border, display, flex, grid, position, size};
use lightningcss::values::percentage::Percentage;
use lightningcss::{
properties::{Property, PropertyId},
stylesheet::ParserOptions,
traits::Parse,
values::{
length::{Length, LengthPercentageOrAuto, LengthValue},
percentage::DimensionPercentage,
ratio::Ratio,
},
};
use taffy::{
prelude::*,
style::{FlexDirection, Position},
};
/// Default values for layout attributes
#[derive(Default)]
pub struct LayoutConfigeration {
/// the default border widths to use
pub border_widths: BorderWidths,
}
/// Default border widths
pub struct BorderWidths {
/// the default border width to use for thin borders
pub thin: f32,
/// the default border width to use for medium borders
pub medium: f32,
/// the default border width to use for thick borders
pub thick: f32,
}
impl Default for BorderWidths {
fn default() -> Self {
Self {
thin: 1.0,
medium: 3.0,
thick: 5.0,
}
}
}
/// applies the entire html namespace defined in dioxus-html
pub fn apply_layout_attributes(name: &str, value: &str, style: &mut Style) {
apply_layout_attributes_cfg(name, value, style, &LayoutConfigeration::default())
}
/// applies the entire html namespace defined in dioxus-html with the specified configeration
pub fn apply_layout_attributes_cfg(
name: &str,
value: &str,
style: &mut Style,
config: &LayoutConfigeration,
) {
if let Ok(property) =
Property::parse_string(PropertyId::from(name), value, ParserOptions::default())
{
match property {
Property::Display(display) => match display {
display::Display::Keyword(display::DisplayKeyword::None) => {
style.display = Display::None
}
display::Display::Pair(pair) => match pair.inside {
display::DisplayInside::Flex(_) => {
style.display = Display::Flex;
}
display::DisplayInside::Grid => {
style.display = Display::Grid;
}
_ => {}
},
_ => {}
},
Property::Position(position) => {
style.position = match position {
position::Position::Relative => Position::Relative,
position::Position::Absolute => Position::Absolute,
_ => return,
}
}
Property::Top(top) => style.inset.top = convert_length_percentage_or_auto(top),
Property::Bottom(bottom) => {
style.inset.bottom = convert_length_percentage_or_auto(bottom)
}
Property::Left(left) => style.inset.left = convert_length_percentage_or_auto(left),
Property::Right(right) => style.inset.right = convert_length_percentage_or_auto(right),
Property::Inset(inset) => {
style.inset.top = convert_length_percentage_or_auto(inset.top);
style.inset.bottom = convert_length_percentage_or_auto(inset.bottom);
style.inset.left = convert_length_percentage_or_auto(inset.left);
style.inset.right = convert_length_percentage_or_auto(inset.right);
}
Property::BorderTopWidth(width) => {
style.border.top = convert_border_side_width(width, &config.border_widths);
}
Property::BorderBottomWidth(width) => {
style.border.bottom = convert_border_side_width(width, &config.border_widths);
}
Property::BorderLeftWidth(width) => {
style.border.left = convert_border_side_width(width, &config.border_widths);
}
Property::BorderRightWidth(width) => {
style.border.right = convert_border_side_width(width, &config.border_widths);
}
Property::BorderWidth(width) => {
style.border.top = convert_border_side_width(width.top, &config.border_widths);
style.border.bottom =
convert_border_side_width(width.bottom, &config.border_widths);
style.border.left = convert_border_side_width(width.left, &config.border_widths);
style.border.right = convert_border_side_width(width.right, &config.border_widths);
}
Property::Border(border) => {
let width = convert_border_side_width(border.width, &config.border_widths);
style.border.top = width;
style.border.bottom = width;
style.border.left = width;
style.border.right = width;
}
Property::BorderTop(top) => {
style.border.top = convert_border_side_width(top.width, &config.border_widths);
}
Property::BorderBottom(bottom) => {
style.border.bottom =
convert_border_side_width(bottom.width, &config.border_widths);
}
Property::BorderLeft(left) => {
style.border.left = convert_border_side_width(left.width, &config.border_widths);
}
Property::BorderRight(right) => {
style.border.right = convert_border_side_width(right.width, &config.border_widths);
}
Property::BorderTopStyle(line_style) => {
if line_style != LineStyle::None {
style.border.top = convert_border_side_width(
border::BorderSideWidth::Medium,
&config.border_widths,
);
}
}
Property::BorderBottomStyle(line_style) => {
if line_style != LineStyle::None {
style.border.bottom = convert_border_side_width(
border::BorderSideWidth::Medium,
&config.border_widths,
);
}
}
Property::BorderLeftStyle(line_style) => {
if line_style != LineStyle::None {
style.border.left = convert_border_side_width(
border::BorderSideWidth::Medium,
&config.border_widths,
);
}
}
Property::BorderRightStyle(line_style) => {
if line_style != LineStyle::None {
style.border.right = convert_border_side_width(
border::BorderSideWidth::Medium,
&config.border_widths,
);
}
}
Property::BorderStyle(styles) => {
if styles.top != LineStyle::None {
style.border.top = convert_border_side_width(
border::BorderSideWidth::Medium,
&config.border_widths,
);
}
if styles.bottom != LineStyle::None {
style.border.bottom = convert_border_side_width(
border::BorderSideWidth::Medium,
&config.border_widths,
);
}
if styles.left != LineStyle::None {
style.border.left = convert_border_side_width(
border::BorderSideWidth::Medium,
&config.border_widths,
);
}
if styles.right != LineStyle::None {
style.border.right = convert_border_side_width(
border::BorderSideWidth::Medium,
&config.border_widths,
);
}
}
// Flexbox properties
Property::FlexDirection(flex_direction, _) => {
use FlexDirection::*;
style.flex_direction = match flex_direction {
flex::FlexDirection::Row => Row,
flex::FlexDirection::RowReverse => RowReverse,
flex::FlexDirection::Column => Column,
flex::FlexDirection::ColumnReverse => ColumnReverse,
}
}
Property::FlexWrap(wrap, _) => {
use FlexWrap::*;
style.flex_wrap = match wrap {
flex::FlexWrap::NoWrap => NoWrap,
flex::FlexWrap::Wrap => Wrap,
flex::FlexWrap::WrapReverse => WrapReverse,
}
}
Property::FlexGrow(grow, _) => {
style.flex_grow = grow;
}
Property::FlexShrink(shrink, _) => {
style.flex_shrink = shrink;
}
Property::FlexBasis(basis, _) => {
style.flex_basis = convert_length_percentage_or_auto(basis).into();
}
Property::Flex(flex, _) => {
style.flex_grow = flex.grow;
style.flex_shrink = flex.shrink;
style.flex_basis = convert_length_percentage_or_auto(flex.basis).into();
}
// Grid properties
Property::GridAutoFlow(grid_auto_flow) => {
let is_row = grid_auto_flow.contains(grid::GridAutoFlow::Row);
let is_dense = grid_auto_flow.contains(grid::GridAutoFlow::Dense);
style.grid_auto_flow = match (is_row, is_dense) {
(true, false) => GridAutoFlow::Row,
(false, false) => GridAutoFlow::Column,
(true, true) => GridAutoFlow::RowDense,
(false, true) => GridAutoFlow::ColumnDense,
};
}
Property::GridTemplateColumns(TrackSizing::TrackList(track_list)) => {
style.grid_template_columns = track_list
.items
.into_iter()
.map(convert_grid_track_item)
.collect();
}
Property::GridTemplateRows(TrackSizing::TrackList(track_list)) => {
style.grid_template_rows = track_list
.items
.into_iter()
.map(convert_grid_track_item)
.collect();
}
Property::GridAutoColumns(grid::TrackSizeList(track_size_list)) => {
style.grid_auto_columns = track_size_list
.into_iter()
.map(convert_grid_track_size)
.collect();
}
Property::GridAutoRows(grid::TrackSizeList(track_size_list)) => {
style.grid_auto_rows = track_size_list
.into_iter()
.map(convert_grid_track_size)
.collect();
}
Property::GridRow(grid_row) => {
style.grid_row = Line {
start: convert_grid_placement(grid_row.start),
end: convert_grid_placement(grid_row.end),
};
}
Property::GridColumn(grid_column) => {
style.grid_column = Line {
start: convert_grid_placement(grid_column.start),
end: convert_grid_placement(grid_column.end),
};
}
// Alignment properties
Property::AlignContent(align, _) => {
use AlignContent::*;
style.align_content = match align {
align::AlignContent::ContentDistribution(distribution) => match distribution {
align::ContentDistribution::SpaceBetween => Some(SpaceBetween),
align::ContentDistribution::SpaceAround => Some(SpaceAround),
align::ContentDistribution::SpaceEvenly => Some(SpaceEvenly),
align::ContentDistribution::Stretch => Some(Stretch),
},
align::AlignContent::ContentPosition {
value: position, ..
} => match position {
align::ContentPosition::Center => Some(Center),
align::ContentPosition::Start => Some(Start),
align::ContentPosition::FlexStart => Some(FlexStart),
align::ContentPosition::End => Some(End),
align::ContentPosition::FlexEnd => Some(FlexEnd),
},
_ => return,
};
}
Property::JustifyContent(justify, _) => {
use AlignContent::*;
style.justify_content = match justify {
align::JustifyContent::ContentDistribution(distribution) => {
match distribution {
align::ContentDistribution::SpaceBetween => Some(SpaceBetween),
align::ContentDistribution::SpaceAround => Some(SpaceAround),
align::ContentDistribution::SpaceEvenly => Some(SpaceEvenly),
_ => return,
}
}
align::JustifyContent::ContentPosition {
value: position, ..
} => match position {
align::ContentPosition::Center => Some(Center),
align::ContentPosition::Start => Some(Start),
align::ContentPosition::FlexStart => Some(FlexStart),
align::ContentPosition::End => Some(End),
align::ContentPosition::FlexEnd => Some(FlexEnd),
},
_ => return,
};
}
Property::AlignSelf(align, _) => {
use AlignItems::*;
style.align_self = match align {
align::AlignSelf::Auto => None,
align::AlignSelf::Stretch => Some(Stretch),
align::AlignSelf::BaselinePosition(_) => Some(Baseline),
align::AlignSelf::SelfPosition {
value: position, ..
} => match position {
align::SelfPosition::Center => Some(Center),
align::SelfPosition::Start | align::SelfPosition::SelfStart => Some(Start),
align::SelfPosition::FlexStart => Some(FlexStart),
align::SelfPosition::End | align::SelfPosition::SelfEnd => Some(End),
align::SelfPosition::FlexEnd => Some(FlexEnd),
},
_ => return,
};
}
Property::AlignItems(align, _) => {
use AlignItems::*;
style.align_items = match align {
align::AlignItems::BaselinePosition(_) => Some(Baseline),
align::AlignItems::Stretch => Some(Stretch),
align::AlignItems::SelfPosition {
value: position, ..
} => match position {
align::SelfPosition::Center => Some(Center),
align::SelfPosition::FlexStart => Some(FlexStart),
align::SelfPosition::FlexEnd => Some(FlexEnd),
align::SelfPosition::Start | align::SelfPosition::SelfStart => {
Some(FlexEnd)
}
align::SelfPosition::End | align::SelfPosition::SelfEnd => Some(FlexEnd),
},
_ => return,
};
}
Property::RowGap(row_gap) => {
style.gap.width = convert_gap_value(row_gap);
}
Property::ColumnGap(column_gap) => {
style.gap.height = convert_gap_value(column_gap);
}
Property::Gap(gap) => {
style.gap = Size {
width: convert_gap_value(gap.row),
height: convert_gap_value(gap.column),
};
}
Property::MarginTop(margin) => {
style.margin.top = convert_length_percentage_or_auto(margin);
}
Property::MarginBottom(margin) => {
style.margin.bottom = convert_length_percentage_or_auto(margin);
}
Property::MarginLeft(margin) => {
style.margin.left = convert_length_percentage_or_auto(margin);
}
Property::MarginRight(margin) => {
style.margin.right = convert_length_percentage_or_auto(margin);
}
Property::Margin(margin) => {
style.margin = Rect {
top: convert_length_percentage_or_auto(margin.top),
bottom: convert_length_percentage_or_auto(margin.bottom),
left: convert_length_percentage_or_auto(margin.left),
right: convert_length_percentage_or_auto(margin.right),
};
}
Property::PaddingTop(padding) => {
style.padding.top = convert_padding(padding);
}
Property::PaddingBottom(padding) => {
style.padding.bottom = convert_padding(padding);
}
Property::PaddingLeft(padding) => {
style.padding.left = convert_padding(padding);
}
Property::PaddingRight(padding) => {
style.padding.right = convert_padding(padding);
}
Property::Padding(padding) => {
style.padding = Rect {
top: convert_padding(padding.top),
bottom: convert_padding(padding.bottom),
left: convert_padding(padding.left),
right: convert_padding(padding.right),
};
}
Property::Width(width) => {
style.size.width = convert_size(width);
}
Property::Height(height) => {
style.size.height = convert_size(height);
}
_ => (),
}
// currently not implemented in lightningcss
if name == "aspect-ratio" {
if let Ok(ratio) = Ratio::parse_string(value) {
style.aspect_ratio = Some(ratio.0 / ratio.1);
}
}
}
}
fn extract_px_value(length_value: LengthValue) -> f32 {
match length_value {
LengthValue::Px(value) => value,
_ => todo!("Only px values are supported"),
}
}
fn convert_length_percentage(
dimension_percentage: DimensionPercentage<LengthValue>,
) -> LengthPercentage {
match dimension_percentage {
DimensionPercentage::Dimension(value) => LengthPercentage::Points(extract_px_value(value)),
DimensionPercentage::Percentage(percentage) => LengthPercentage::Percent(percentage.0),
DimensionPercentage::Calc(_) => todo!("Calc is not supported yet"),
}
}
fn convert_padding(dimension_percentage: LengthPercentageOrAuto) -> LengthPercentage {
match dimension_percentage {
LengthPercentageOrAuto::Auto => unimplemented!(),
LengthPercentageOrAuto::LengthPercentage(lp) => match lp {
DimensionPercentage::Dimension(value) => {
LengthPercentage::Points(extract_px_value(value))
}
DimensionPercentage::Percentage(percentage) => LengthPercentage::Percent(percentage.0),
DimensionPercentage::Calc(_) => unimplemented!("Calc is not supported yet"),
},
}
}
fn convert_length_percentage_or_auto(
dimension_percentage: LengthPercentageOrAuto,
) -> LengthPercentageAuto {
match dimension_percentage {
LengthPercentageOrAuto::Auto => LengthPercentageAuto::Auto,
LengthPercentageOrAuto::LengthPercentage(lp) => match lp {
DimensionPercentage::Dimension(value) => {
LengthPercentageAuto::Points(extract_px_value(value))
}
DimensionPercentage::Percentage(percentage) => {
LengthPercentageAuto::Percent(percentage.0)
}
DimensionPercentage::Calc(_) => todo!("Calc is not supported yet"),
},
}
}
fn convert_dimension(dimension_percentage: DimensionPercentage<LengthValue>) -> Dimension {
match dimension_percentage {
DimensionPercentage::Dimension(value) => Dimension::Points(extract_px_value(value)),
DimensionPercentage::Percentage(percentage) => Dimension::Percent(percentage.0),
DimensionPercentage::Calc(_) => todo!("Calc is not supported yet"),
}
}
fn convert_border_side_width(
border_side_width: border::BorderSideWidth,
border_width_config: &BorderWidths,
) -> LengthPercentage {
match border_side_width {
border::BorderSideWidth::Length(Length::Value(value)) => {
LengthPercentage::Points(extract_px_value(value))
}
border::BorderSideWidth::Thick => LengthPercentage::Points(border_width_config.thick),
border::BorderSideWidth::Medium => LengthPercentage::Points(border_width_config.medium),
border::BorderSideWidth::Thin => LengthPercentage::Points(border_width_config.thin),
border::BorderSideWidth::Length(_) => todo!("Only Length::Value is supported"),
}
}
fn convert_gap_value(gap_value: align::GapValue) -> LengthPercentage {
match gap_value {
align::GapValue::LengthPercentage(dim) => convert_length_percentage(dim),
align::GapValue::Normal => LengthPercentage::Points(0.0),
}
}
fn convert_size(size: size::Size) -> Dimension {
match size {
size::Size::Auto => Dimension::Auto,
size::Size::LengthPercentage(length) => convert_dimension(length),
size::Size::MinContent(_) => Dimension::Auto, // Unimplemented, so default auto
size::Size::MaxContent(_) => Dimension::Auto, // Unimplemented, so default auto
size::Size::FitContent(_) => Dimension::Auto, // Unimplemented, so default auto
size::Size::FitContentFunction(_) => Dimension::Auto, // Unimplemented, so default auto
size::Size::Stretch(_) => Dimension::Auto, // Unimplemented, so default auto
size::Size::Contain => Dimension::Auto, // Unimplemented, so default auto
}
}
fn convert_grid_placement(input: grid::GridLine) -> GridPlacement {
match input {
grid::GridLine::Auto => GridPlacement::Auto,
grid::GridLine::Line { index, .. } => line(index as i16),
grid::GridLine::Span { index, .. } => span(index as u16),
grid::GridLine::Area { .. } => unimplemented!(),
}
}
fn convert_grid_track_item(input: grid::TrackListItem) -> TrackSizingFunction {
match input {
grid::TrackListItem::TrackSize(size) => {
TrackSizingFunction::Single(convert_grid_track_size(size))
}
grid::TrackListItem::TrackRepeat(_) => todo!("requires TrackRepeat fields to be public!"),
}
}
fn convert_grid_track_size(input: grid::TrackSize) -> NonRepeatedTrackSizingFunction {
match input {
grid::TrackSize::TrackBreadth(breadth) => minmax(
convert_track_breadth_min(&breadth),
convert_track_breadth_max(&breadth),
),
grid::TrackSize::MinMax { min, max } => minmax(
convert_track_breadth_min(&min),
convert_track_breadth_max(&max),
),
grid::TrackSize::FitContent(limit) => match limit {
DimensionPercentage::Dimension(LengthValue::Px(len)) => minmax(auto(), points(len)),
DimensionPercentage::Percentage(Percentage(pct)) => minmax(auto(), percent(pct)),
_ => unimplemented!(),
},
}
}
fn convert_track_breadth_max(breadth: &TrackBreadth) -> MaxTrackSizingFunction {
match breadth {
grid::TrackBreadth::Length(length_percentage) => match length_percentage {
DimensionPercentage::Dimension(LengthValue::Px(len)) => points(*len),
DimensionPercentage::Percentage(Percentage(pct)) => percent(*pct),
_ => unimplemented!(),
},
grid::TrackBreadth::Flex(fraction) => fr(*fraction),
grid::TrackBreadth::MinContent => MaxTrackSizingFunction::MinContent,
grid::TrackBreadth::MaxContent => MaxTrackSizingFunction::MaxContent,
grid::TrackBreadth::Auto => MaxTrackSizingFunction::Auto,
}
}
fn convert_track_breadth_min(breadth: &TrackBreadth) -> MinTrackSizingFunction {
match breadth {
grid::TrackBreadth::Length(length_percentage) => match length_percentage {
DimensionPercentage::Dimension(LengthValue::Px(len)) => points(*len),
DimensionPercentage::Percentage(Percentage(pct)) => percent(*pct),
_ => unimplemented!(),
},
grid::TrackBreadth::MinContent => MinTrackSizingFunction::MinContent,
grid::TrackBreadth::MaxContent => MinTrackSizingFunction::MaxContent,
grid::TrackBreadth::Auto => MinTrackSizingFunction::Auto,
grid::TrackBreadth::Flex(_) => MinTrackSizingFunction::Auto,
}
}
/// parse relative or absolute value
pub fn parse_value(value: &str) -> Option<Dimension> {
if value.ends_with("px") {
if let Ok(px) = value.trim_end_matches("px").parse::<f32>() {
Some(Dimension::Points(px))
} else {
None
}
} else if value.ends_with('%') {
if let Ok(pct) = value.trim_end_matches('%').parse::<f32>() {
Some(Dimension::Percent(pct / 100.0))
} else {
None
}
} else {
None
}
}

View file

@ -1,54 +0,0 @@
#![doc = include_str!("../README.md")]
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
#![warn(missing_docs)]
use std::any::Any;
use std::hash::BuildHasherDefault;
use node_ref::NodeMask;
use rustc_hash::FxHasher;
pub mod custom_element;
#[cfg(feature = "dioxus")]
pub mod dioxus;
#[cfg(feature = "layout-attributes")]
pub mod layout_attributes;
pub mod node;
pub mod node_ref;
pub mod node_watcher;
mod passes;
pub mod real_dom;
pub mod tree;
pub mod utils;
pub use shipyard::EntityId as NodeId;
pub mod exports {
//! Important dependencies that are used by the rest of the library
//! Feel free to just add the dependencies in your own Crates.toml
// exported for the macro
#[doc(hidden)]
pub use rustc_hash::FxHashSet;
pub use shipyard;
}
/// A prelude of commonly used items
pub mod prelude {
#[cfg(feature = "dioxus")]
pub use crate::dioxus::*;
pub use crate::node::{ElementNode, FromAnyValue, NodeType, OwnedAttributeView, TextNode};
pub use crate::node_ref::{AttributeMaskBuilder, NodeMaskBuilder, NodeView};
pub use crate::passes::{run_pass, PassDirection, RunPassView, TypeErasedState};
pub use crate::passes::{Dependancy, DependancyView, Dependants, State};
pub use crate::real_dom::{NodeImmutable, NodeMut, NodeRef, RealDom};
pub use crate::NodeId;
pub use crate::SendAnyMap;
}
/// A map that can be sent between threads
pub type FxDashMap<K, V> = dashmap::DashMap<K, V, BuildHasherDefault<FxHasher>>;
/// A set that can be sent between threads
pub type FxDashSet<K> = dashmap::DashSet<K, BuildHasherDefault<FxHasher>>;
/// A map of types that can be sent between threads
pub type SendAnyMap = anymap::Map<dyn Any + Send + Sync + 'static>;

View file

@ -1,260 +0,0 @@
//! Items related to Nodes in the RealDom
use rustc_hash::{FxHashMap, FxHashSet};
use shipyard::Component;
use std::{
any::Any,
fmt::{Debug, Display},
};
/// A element node in the RealDom
#[derive(Debug, Clone, Default)]
pub struct ElementNode<V: FromAnyValue = ()> {
/// The [tag](https://developer.mozilla.org/en-US/docs/Web/API/Element/tagName) of the element
pub tag: String,
/// The [namespace](https://developer.mozilla.org/en-US/docs/Web/API/Element/namespaceURI) of the element
pub namespace: Option<String>,
/// The attributes of the element
pub attributes: FxHashMap<OwnedAttributeDiscription, OwnedAttributeValue<V>>,
/// The events the element is listening for
pub listeners: FxHashSet<String>,
}
impl ElementNode {
/// Create a new element node
pub fn new(tag: impl Into<String>, namespace: impl Into<Option<String>>) -> Self {
Self {
tag: tag.into(),
namespace: namespace.into(),
attributes: Default::default(),
listeners: Default::default(),
}
}
}
/// A text node in the RealDom
#[derive(Debug, Clone, Default)]
pub struct TextNode {
/// The text of the node
pub text: String,
/// The events the node is listening for
pub listeners: FxHashSet<String>,
}
impl TextNode {
/// Create a new text node
pub fn new(text: String) -> Self {
Self {
text,
listeners: Default::default(),
}
}
}
/// A type of node with data specific to the node type.
#[derive(Debug, Clone, Component)]
pub enum NodeType<V: FromAnyValue = ()> {
/// A text node
Text(TextNode),
/// An element node
Element(ElementNode<V>),
/// A placeholder node. This can be used as a cheaper placeholder for a node that will be created later
Placeholder,
}
impl<V: FromAnyValue, S: Into<String>> From<S> for NodeType<V> {
fn from(text: S) -> Self {
Self::Text(TextNode::new(text.into()))
}
}
impl<V: FromAnyValue> From<TextNode> for NodeType<V> {
fn from(text: TextNode) -> Self {
Self::Text(text)
}
}
impl<V: FromAnyValue> From<ElementNode<V>> for NodeType<V> {
fn from(element: ElementNode<V>) -> Self {
Self::Element(element)
}
}
/// A discription of an attribute on a DOM node, such as `id` or `href`.
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct OwnedAttributeDiscription {
/// The name of the attribute.
pub name: String,
/// The namespace of the attribute used to identify what kind of attribute it is.
///
/// For renderers that use HTML, this can be used to identify if the attribute is a style attribute.
/// Instead of parsing the style attribute every time a style is changed, you can set an attribute with the `style` namespace.
pub namespace: Option<String>,
}
impl From<String> for OwnedAttributeDiscription {
fn from(name: String) -> Self {
Self {
name,
namespace: None,
}
}
}
impl<S: Into<String>, N: Into<String>> From<(S, N)> for OwnedAttributeDiscription {
fn from(name: (S, N)) -> Self {
Self {
name: name.0.into(),
namespace: Some(name.1.into()),
}
}
}
/// An attribute on a DOM node, such as `id="my-thing"` or
/// `href="https://example.com"`.
#[derive(Clone, Copy, Debug)]
pub struct OwnedAttributeView<'a, V: FromAnyValue = ()> {
/// The discription of the attribute.
pub attribute: &'a OwnedAttributeDiscription,
/// The value of the attribute.
pub value: &'a OwnedAttributeValue<V>,
}
/// The value of an attribute on a DOM node. This contains non-text values to allow users to skip parsing attribute values in some cases.
#[derive(Clone)]
pub enum OwnedAttributeValue<V: FromAnyValue = ()> {
/// A string value. This is the most common type of attribute.
Text(String),
/// A floating point value.
Float(f64),
/// An integer value.
Int(i64),
/// A boolean value.
Bool(bool),
/// A custom value specific to the renderer
Custom(V),
}
impl<V: FromAnyValue> From<String> for OwnedAttributeValue<V> {
fn from(value: String) -> Self {
Self::Text(value)
}
}
impl<V: FromAnyValue> From<f64> for OwnedAttributeValue<V> {
fn from(value: f64) -> Self {
Self::Float(value)
}
}
impl<V: FromAnyValue> From<i64> for OwnedAttributeValue<V> {
fn from(value: i64) -> Self {
Self::Int(value)
}
}
impl<V: FromAnyValue> From<bool> for OwnedAttributeValue<V> {
fn from(value: bool) -> Self {
Self::Bool(value)
}
}
impl<V: FromAnyValue> From<V> for OwnedAttributeValue<V> {
fn from(value: V) -> Self {
Self::Custom(value)
}
}
/// Something that can be converted from a borrowed [Any] value.
pub trait FromAnyValue: Clone + 'static {
/// Convert from an [Any] value.
fn from_any_value(value: &dyn Any) -> Self;
}
impl FromAnyValue for () {
fn from_any_value(_: &dyn Any) -> Self {}
}
impl<V: FromAnyValue> Debug for OwnedAttributeValue<V> {
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::Custom(_) => f.debug_tuple("Any").finish(),
}
}
}
impl<V: FromAnyValue> Display for OwnedAttributeValue<V> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Text(arg0) => f.write_str(arg0),
Self::Float(arg0) => f.write_str(&arg0.to_string()),
Self::Int(arg0) => f.write_str(&arg0.to_string()),
Self::Bool(arg0) => f.write_str(&arg0.to_string()),
Self::Custom(_) => f.write_str("custom"),
}
}
}
#[cfg(feature = "dioxus")]
impl<V: FromAnyValue> From<&dioxus_core::AttributeValue> for OwnedAttributeValue<V> {
fn from(value: &dioxus_core::AttributeValue) -> Self {
match value {
dioxus_core::AttributeValue::Text(text) => Self::Text(text.clone()),
dioxus_core::AttributeValue::Float(float) => Self::Float(*float),
dioxus_core::AttributeValue::Int(int) => Self::Int(*int),
dioxus_core::AttributeValue::Bool(bool) => Self::Bool(*bool),
dioxus_core::AttributeValue::Any(any) => Self::Custom(V::from_any_value(any.as_any())),
dioxus_core::AttributeValue::None => panic!("None attribute values result in removing the attribute, not converting it to a None value."),
_ => panic!("Unsupported attribute value type"),
}
}
}
impl<V: FromAnyValue> OwnedAttributeValue<V> {
/// Attempt to convert the attribute value to a string.
pub fn as_text(&self) -> Option<&str> {
match self {
OwnedAttributeValue::Text(text) => Some(text),
_ => None,
}
}
/// Attempt to convert the attribute value to a float.
pub fn as_float(&self) -> Option<f64> {
match self {
OwnedAttributeValue::Float(float) => Some(*float),
OwnedAttributeValue::Int(int) => Some(*int as f64),
_ => None,
}
}
/// Attempt to convert the attribute value to an integer.
pub fn as_int(&self) -> Option<i64> {
match self {
OwnedAttributeValue::Float(float) => Some(*float as i64),
OwnedAttributeValue::Int(int) => Some(*int),
_ => None,
}
}
/// Attempt to convert the attribute value to a boolean.
pub fn as_bool(&self) -> Option<bool> {
match self {
OwnedAttributeValue::Bool(bool) => Some(*bool),
_ => None,
}
}
/// Attempt to convert the attribute value to a custom value.
pub fn as_custom(&self) -> Option<&V> {
match self {
OwnedAttributeValue::Custom(custom) => Some(custom),
_ => None,
}
}
}

View file

@ -1,332 +0,0 @@
//! Utilities that provide limited access to nodes
use rustc_hash::FxHashSet;
use crate::{
node::{ElementNode, FromAnyValue, NodeType, OwnedAttributeView},
NodeId,
};
/// A view into a [NodeType] with a mask that determines what is visible.
#[derive(Debug)]
pub struct NodeView<'a, V: FromAnyValue = ()> {
id: NodeId,
inner: &'a NodeType<V>,
mask: &'a NodeMask,
}
impl<'a, V: FromAnyValue> NodeView<'a, V> {
/// Create a new NodeView from a VNode, and mask.
pub fn new(id: NodeId, node: &'a NodeType<V>, view: &'a NodeMask) -> Self {
Self {
inner: node,
mask: view,
id,
}
}
/// Get the node id of the node
pub fn node_id(&self) -> NodeId {
self.id
}
/// Get the tag of the node if the tag is enabled in the mask
pub fn tag(&self) -> Option<&'a str> {
self.mask
.tag
.then_some(match &self.inner {
NodeType::Element(ElementNode { tag, .. }) => Some(&**tag),
_ => None,
})
.flatten()
}
/// Get the tag of the node if the namespace is enabled in the mask
pub fn namespace(&self) -> Option<&'a str> {
self.mask
.namespace
.then_some(match &self.inner {
NodeType::Element(ElementNode { namespace, .. }) => namespace.as_deref(),
_ => None,
})
.flatten()
}
/// Get any attributes that are enabled in the mask
pub fn attributes<'b>(
&'b self,
) -> Option<impl Iterator<Item = OwnedAttributeView<'a, V>> + 'b> {
match &self.inner {
NodeType::Element(ElementNode { attributes, .. }) => Some(
attributes
.iter()
.filter(move |(attr, _)| self.mask.attritutes.contains(&attr.name))
.map(|(attr, val)| OwnedAttributeView {
attribute: attr,
value: val,
}),
),
_ => None,
}
}
/// Get the text if it is enabled in the mask
pub fn text(&self) -> Option<&str> {
self.mask
.text
.then_some(match &self.inner {
NodeType::Text(text) => Some(&text.text),
_ => None,
})
.flatten()
.map(|x| &**x)
}
/// Get the listeners if it is enabled in the mask
pub fn listeners(&self) -> Option<impl Iterator<Item = &'a str> + '_> {
if self.mask.listeners {
match &self.inner {
NodeType::Element(ElementNode { listeners, .. }) => {
Some(listeners.iter().map(|l| &**l))
}
_ => None,
}
} else {
None
}
}
}
/// A mask that contains a list of attributes that are visible.
#[derive(PartialEq, Eq, Clone, Debug)]
pub enum AttributeMask {
/// All attributes are visible
All,
/// Only the given attributes are visible
Some(FxHashSet<Box<str>>),
}
impl AttributeMask {
/// Check if the mask contains the given attribute
pub fn contains(&self, attr: &str) -> bool {
match self {
AttributeMask::All => true,
AttributeMask::Some(attrs) => attrs.contains(attr),
}
}
/// Create a new dynamic attribute mask with a single attribute
pub fn single(new: &str) -> Self {
let mut set = FxHashSet::default();
set.insert(new.into());
Self::Some(set)
}
/// Combine two attribute masks
pub fn union(&self, other: &Self) -> Self {
match (self, other) {
(AttributeMask::Some(s), AttributeMask::Some(o)) => {
AttributeMask::Some(s.union(o).cloned().collect())
}
_ => AttributeMask::All,
}
}
/// Check if two attribute masks overlap
fn overlaps(&self, other: &Self) -> bool {
match (self, other) {
(AttributeMask::All, AttributeMask::Some(attrs)) => !attrs.is_empty(),
(AttributeMask::Some(attrs), AttributeMask::All) => !attrs.is_empty(),
(AttributeMask::Some(attrs1), AttributeMask::Some(attrs2)) => {
!attrs1.is_disjoint(attrs2)
}
_ => true,
}
}
}
impl Default for AttributeMask {
fn default() -> Self {
AttributeMask::Some(FxHashSet::default())
}
}
/// A mask that limits what parts of a node a dependency can see.
#[derive(Default, PartialEq, Eq, Clone, Debug)]
pub struct NodeMask {
attritutes: AttributeMask,
tag: bool,
namespace: bool,
text: bool,
listeners: bool,
}
impl NodeMask {
/// Check if two masks overlap
pub fn overlaps(&self, other: &Self) -> bool {
(self.tag && other.tag)
|| (self.namespace && other.namespace)
|| self.attritutes.overlaps(&other.attritutes)
|| (self.text && other.text)
|| (self.listeners && other.listeners)
}
/// Combine two node masks
pub fn union(&self, other: &Self) -> Self {
Self {
attritutes: self.attritutes.union(&other.attritutes),
tag: self.tag | other.tag,
namespace: self.namespace | other.namespace,
text: self.text | other.text,
listeners: self.listeners | other.listeners,
}
}
/// Allow the mask to view the given attributes
pub fn add_attributes(&mut self, attributes: AttributeMask) {
self.attritutes = self.attritutes.union(&attributes);
}
/// Get the mask for the attributes
pub fn attributes(&self) -> &AttributeMask {
&self.attritutes
}
/// Set the mask to view the tag
pub fn set_tag(&mut self) {
self.tag = true;
}
/// Get the mask for the tag
pub fn tag(&self) -> bool {
self.tag
}
/// Set the mask to view the namespace
pub fn set_namespace(&mut self) {
self.namespace = true;
}
/// Get the mask for the namespace
pub fn namespace(&self) -> bool {
self.namespace
}
/// Set the mask to view the text
pub fn set_text(&mut self) {
self.text = true;
}
/// Get the mask for the text
pub fn text(&self) -> bool {
self.text
}
/// Set the mask to view the listeners
pub fn set_listeners(&mut self) {
self.listeners = true;
}
/// Get the mask for the listeners
pub fn listeners(&self) -> bool {
self.listeners
}
}
/// A builder for a mask that controls what attributes are visible.
#[derive(PartialEq, Eq, Clone, Debug)]
pub enum AttributeMaskBuilder<'a> {
/// All attributes are visible
All,
/// Only the given attributes are visible
Some(&'a [&'a str]),
}
impl Default for AttributeMaskBuilder<'_> {
fn default() -> Self {
AttributeMaskBuilder::Some(&[])
}
}
/// A mask that limits what parts of a node a dependency can see.
#[derive(Default, PartialEq, Eq, Clone, Debug)]
pub struct NodeMaskBuilder<'a> {
attritutes: AttributeMaskBuilder<'a>,
tag: bool,
namespace: bool,
text: bool,
listeners: bool,
}
impl<'a> NodeMaskBuilder<'a> {
/// A node mask with no parts visible.
pub const NONE: Self = Self::new();
/// A node mask with every part visible.
pub const ALL: Self = Self::new()
.with_attrs(AttributeMaskBuilder::All)
.with_text()
.with_element()
.with_listeners();
/// Create a empty node mask
pub const fn new() -> Self {
Self {
attritutes: AttributeMaskBuilder::Some(&[]),
tag: false,
namespace: false,
text: false,
listeners: false,
}
}
/// Allow the mask to view the given attributes
pub const fn with_attrs(mut self, attritutes: AttributeMaskBuilder<'a>) -> Self {
self.attritutes = attritutes;
self
}
/// Allow the mask to view the tag
pub const fn with_tag(mut self) -> Self {
self.tag = true;
self
}
/// Allow the mask to view the namespace
pub const fn with_namespace(mut self) -> Self {
self.namespace = true;
self
}
/// Allow the mask to view the namespace and tag
pub const fn with_element(self) -> Self {
self.with_namespace().with_tag()
}
/// Allow the mask to view the text
pub const fn with_text(mut self) -> Self {
self.text = true;
self
}
/// Allow the mask to view the listeners
pub const fn with_listeners(mut self) -> Self {
self.listeners = true;
self
}
/// Build the mask
pub fn build(self) -> NodeMask {
NodeMask {
attritutes: match self.attritutes {
AttributeMaskBuilder::All => AttributeMask::All,
AttributeMaskBuilder::Some(attrs) => {
AttributeMask::Some(attrs.iter().map(|s| (*s).into()).collect())
}
},
tag: self.tag,
namespace: self.namespace,
text: self.text,
listeners: self.listeners,
}
}
}

View file

@ -1,19 +0,0 @@
//! Helpers for watching for changes in the DOM tree.
use crate::{node::FromAnyValue, node_ref::AttributeMask, prelude::*};
/// A trait for watching for changes in the DOM tree.
pub trait NodeWatcher<V: FromAnyValue + Send + Sync> {
/// Called after a node is added to the tree.
fn on_node_added(&mut self, _node: NodeMut<V>) {}
/// Called before a node is removed from the tree.
fn on_node_removed(&mut self, _node: NodeMut<V>) {}
/// Called after a node is moved to a new parent.
fn on_node_moved(&mut self, _node: NodeMut<V>) {}
}
/// A trait for watching for changes to attributes of an element.
pub trait AttributeWatcher<V: FromAnyValue + Send + Sync> {
/// Called before update_state is called on the RealDom
fn on_attributes_changed(&self, _node: NodeMut<V>, _attributes: &AttributeMask) {}
}

View file

@ -1,379 +0,0 @@
use parking_lot::RwLock;
use rustc_hash::{FxHashMap, FxHashSet};
use shipyard::{Borrow, BorrowInfo, Component, Unique, UniqueView, View, WorkloadSystem};
use std::any::{Any, TypeId};
use std::collections::BTreeMap;
use std::marker::PhantomData;
use std::ops::Deref;
use std::sync::Arc;
use crate::node::{FromAnyValue, NodeType};
use crate::node_ref::{NodeMaskBuilder, NodeView};
use crate::real_dom::{DirtyNodesResult, SendAnyMapWrapper};
use crate::tree::{TreeRef, TreeRefView};
use crate::SendAnyMap;
use crate::{NodeId, NodeMask};
#[derive(Default)]
struct DirtyNodes {
nodes_dirty: FxHashSet<NodeId>,
}
impl DirtyNodes {
pub fn add_node(&mut self, node_id: NodeId) {
self.nodes_dirty.insert(node_id);
}
pub fn is_empty(&self) -> bool {
self.nodes_dirty.is_empty()
}
pub fn pop(&mut self) -> Option<NodeId> {
self.nodes_dirty.iter().next().copied().map(|id| {
self.nodes_dirty.remove(&id);
id
})
}
}
/// Tracks the dirty nodes sorted by height for each pass. We resolve passes based on the height of the node in order to avoid resolving any node twice in a pass.
#[derive(Clone, Unique)]
pub struct DirtyNodeStates {
dirty: Arc<FxHashMap<TypeId, RwLock<BTreeMap<u16, DirtyNodes>>>>,
}
impl DirtyNodeStates {
pub fn with_passes(passes: impl Iterator<Item = TypeId>) -> Self {
Self {
dirty: Arc::new(
passes
.map(|pass| (pass, RwLock::new(BTreeMap::new())))
.collect(),
),
}
}
pub fn insert(&self, pass_id: TypeId, node_id: NodeId, height: u16) {
if let Some(btree) = self.dirty.get(&pass_id) {
let mut write = btree.write();
if let Some(entry) = write.get_mut(&height) {
entry.add_node(node_id);
} else {
let mut entry = DirtyNodes::default();
entry.add_node(node_id);
write.insert(height, entry);
}
}
}
fn pop_front(&self, pass_id: TypeId) -> Option<(u16, NodeId)> {
let mut values = self.dirty.get(&pass_id)?.write();
let mut value = values.first_entry()?;
let height = *value.key();
let ids = value.get_mut();
let id = ids.pop()?;
if ids.is_empty() {
value.remove_entry();
}
Some((height, id))
}
fn pop_back(&self, pass_id: TypeId) -> Option<(u16, NodeId)> {
let mut values = self.dirty.get(&pass_id)?.write();
let mut value = values.last_entry()?;
let height = *value.key();
let ids = value.get_mut();
let id = ids.pop()?;
if ids.is_empty() {
value.remove_entry();
}
Some((height, id))
}
}
/// A state that is automatically inserted in a node with dependencies.
pub trait State<V: FromAnyValue + Send + Sync = ()>: Any + Send + Sync {
/// This is a tuple of (T: State, ..) of states read from the parent required to update this state
type ParentDependencies: Dependancy;
/// This is a tuple of (T: State, ..) of states read from the children required to update this state
type ChildDependencies: Dependancy;
/// This is a tuple of (T: State, ..) of states read from the node required to update this state
type NodeDependencies: Dependancy;
/// This is a mask of what aspects of the node are required to update this state
const NODE_MASK: NodeMaskBuilder<'static>;
/// Does the state traverse into the shadow dom or pass over it. This should be true for layout and false for styles
const TRAVERSE_SHADOW_DOM: bool = false;
/// Update this state in a node, returns if the state was updated
fn update<'a>(
&mut self,
node_view: NodeView<V>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> bool;
/// Create a new instance of this state
fn create<'a>(
node_view: NodeView<V>,
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
context: &SendAnyMap,
) -> Self;
/// Create a workload system for this state
fn workload_system(
type_id: TypeId,
dependants: Arc<Dependants>,
pass_direction: PassDirection,
) -> WorkloadSystem;
/// Converts to a type erased version of the trait
fn to_type_erased() -> TypeErasedState<V>
where
Self: Sized,
{
let node_mask = Self::NODE_MASK.build();
TypeErasedState {
this_type_id: TypeId::of::<Self>(),
parent_dependancies_ids: Self::ParentDependencies::type_ids()
.iter()
.copied()
.collect(),
child_dependancies_ids: Self::ChildDependencies::type_ids()
.iter()
.copied()
.collect(),
node_dependancies_ids: Self::NodeDependencies::type_ids().iter().copied().collect(),
dependants: Default::default(),
mask: node_mask,
pass_direction: pass_direction::<V, Self>(),
enter_shadow_dom: Self::TRAVERSE_SHADOW_DOM,
workload: Self::workload_system,
phantom: PhantomData,
}
}
}
fn pass_direction<V: FromAnyValue + Send + Sync, S: State<V>>() -> PassDirection {
if S::ChildDependencies::type_ids()
.iter()
.any(|type_id| *type_id == TypeId::of::<S>())
{
PassDirection::ChildToParent
} else if S::ParentDependencies::type_ids()
.iter()
.any(|type_id| *type_id == TypeId::of::<S>())
{
PassDirection::ParentToChild
} else {
PassDirection::AnyOrder
}
}
#[doc(hidden)]
#[derive(Borrow, BorrowInfo)]
pub struct RunPassView<'a, V: FromAnyValue + Send + Sync = ()> {
pub tree: TreeRefView<'a>,
pub node_type: View<'a, NodeType<V>>,
dirty_nodes_result: UniqueView<'a, DirtyNodesResult>,
node_states: UniqueView<'a, DirtyNodeStates>,
any_map: UniqueView<'a, SendAnyMapWrapper>,
}
// This is used by the macro
/// Updates the given pass, marking any nodes that were changed
#[doc(hidden)]
pub fn run_pass<V: FromAnyValue + Send + Sync>(
type_id: TypeId,
dependants: Arc<Dependants>,
pass_direction: PassDirection,
view: RunPassView<V>,
mut update_node: impl FnMut(NodeId, &SendAnyMap) -> bool,
) {
let RunPassView {
tree,
dirty_nodes_result: nodes_updated,
node_states: dirty,
any_map: ctx,
..
} = view;
let ctx = ctx.as_ref();
match pass_direction {
PassDirection::ParentToChild => {
while let Some((height, id)) = dirty.pop_front(type_id) {
if (update_node)(id, ctx) {
nodes_updated.insert(id);
dependants.mark_dirty(&dirty, id, &tree, height);
}
}
}
PassDirection::ChildToParent => {
while let Some((height, id)) = dirty.pop_back(type_id) {
if (update_node)(id, ctx) {
nodes_updated.insert(id);
dependants.mark_dirty(&dirty, id, &tree, height);
}
}
}
PassDirection::AnyOrder => {
while let Some((height, id)) = dirty.pop_back(type_id) {
if (update_node)(id, ctx) {
nodes_updated.insert(id);
dependants.mark_dirty(&dirty, id, &tree, height);
}
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Dependant {
pub(crate) type_id: TypeId,
pub(crate) enter_shadow_dom: bool,
}
/// The states that depend on this state
#[derive(Default, Debug, Clone, PartialEq, Eq)]
pub struct Dependants {
/// The states in the parent direction that should be invalidated when this state is invalidated
pub(crate) parent: Vec<Dependant>,
/// The states in the child direction that should be invalidated when this state is invalidated
pub(crate) child: Vec<Dependant>,
/// The states in the node direction that should be invalidated when this state is invalidated
pub(crate) node: Vec<TypeId>,
}
impl Dependants {
fn mark_dirty(&self, dirty: &DirtyNodeStates, id: NodeId, tree: &impl TreeRef, height: u16) {
for &Dependant {
type_id,
enter_shadow_dom,
} in &self.child
{
for id in tree.children_ids_advanced(id, enter_shadow_dom) {
dirty.insert(type_id, id, height + 1);
}
}
for &Dependant {
type_id,
enter_shadow_dom,
} in &self.parent
{
if let Some(id) = tree.parent_id_advanced(id, enter_shadow_dom) {
dirty.insert(type_id, id, height - 1);
}
}
for dependant in &self.node {
dirty.insert(*dependant, id, height);
}
}
}
/// A type erased version of [`State`] that can be added to the [`crate::prelude::RealDom`] with [`crate::prelude::RealDom::new`]
pub struct TypeErasedState<V: FromAnyValue + Send = ()> {
pub(crate) this_type_id: TypeId,
pub(crate) parent_dependancies_ids: FxHashSet<TypeId>,
pub(crate) child_dependancies_ids: FxHashSet<TypeId>,
pub(crate) node_dependancies_ids: FxHashSet<TypeId>,
pub(crate) dependants: Arc<Dependants>,
pub(crate) mask: NodeMask,
pub(crate) workload: fn(TypeId, Arc<Dependants>, PassDirection) -> WorkloadSystem,
pub(crate) pass_direction: PassDirection,
pub(crate) enter_shadow_dom: bool,
phantom: PhantomData<V>,
}
impl<V: FromAnyValue + Send> TypeErasedState<V> {
pub(crate) fn create_workload(&self) -> WorkloadSystem {
(self.workload)(
self.this_type_id,
self.dependants.clone(),
self.pass_direction,
)
}
pub(crate) fn combined_dependancy_type_ids(&self) -> impl Iterator<Item = TypeId> + '_ {
self.parent_dependancies_ids
.iter()
.chain(self.child_dependancies_ids.iter())
.chain(self.node_dependancies_ids.iter())
.copied()
}
}
/// The direction that a pass should be run in
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub enum PassDirection {
/// The pass should be run from the root to the leaves
ParentToChild,
/// The pass should be run from the leaves to the root
ChildToParent,
/// The pass can be run in any order
AnyOrder,
}
/// A trait that is implemented for all the dependancies of a [`State`]
pub trait Dependancy {
/// A tuple with all the elements of the dependancy as [`DependancyView`]
type ElementBorrowed<'a>;
/// Returns a list of all the [`TypeId`]s of the elements in the dependancy
fn type_ids() -> Box<[TypeId]> {
Box::new([])
}
}
macro_rules! impl_dependancy {
($($t:ident),*) => {
impl< $($t: Send + Sync + Component),* > Dependancy for ($($t,)*) {
type ElementBorrowed<'a> = ($(DependancyView<'a, $t>,)*);
fn type_ids() -> Box<[TypeId]> {
Box::new([$(TypeId::of::<$t>()),*])
}
}
};
}
// TODO: track what components are actually read to update subscriptions
// making this a wrapper makes it possible to implement that optimization without a breaking change
/// A immutable view of a [`State`]
pub struct DependancyView<'a, T> {
inner: &'a T,
}
impl<'a, T> DependancyView<'a, T> {
// This should only be used in the macro. This is not a public API or stable
#[doc(hidden)]
pub fn new(inner: &'a T) -> Self {
Self { inner }
}
}
impl<'a, T> Deref for DependancyView<'a, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.inner
}
}
impl_dependancy!();
impl_dependancy!(A);
impl_dependancy!(A, B);
impl_dependancy!(A, B, C);
impl_dependancy!(A, B, C, D);
impl_dependancy!(A, B, C, D, E);
impl_dependancy!(A, B, C, D, E, F);
impl_dependancy!(A, B, C, D, E, F, G);
impl_dependancy!(A, B, C, D, E, F, G, H);
impl_dependancy!(A, B, C, D, E, F, G, H, I);
impl_dependancy!(A, B, C, D, E, F, G, H, I, J);

File diff suppressed because it is too large Load diff

View file

@ -1,599 +0,0 @@
//! A tree of nodes intigated with shipyard
use crate::NodeId;
use shipyard::{Component, EntitiesViewMut, Get, View, ViewMut};
use std::fmt::Debug;
/// A shadow tree reference inside of a tree. This tree is isolated from the main tree.
#[derive(PartialEq, Eq, Clone, Debug, Component)]
pub struct ShadowTree {
/// The root of the shadow tree
pub shadow_roots: Vec<NodeId>,
/// The node that children of the super tree should be inserted under.
pub slot: Option<NodeId>,
}
/// A node in a tree.
#[derive(PartialEq, Eq, Clone, Debug, Component)]
pub struct Node {
parent: Option<NodeId>,
children: Vec<NodeId>,
child_subtree: Option<ShadowTree>,
/// If this node is a slot in a shadow_tree, this is node whose child_subtree is that shadow_tree.
slot_for_light_tree: Option<NodeId>,
/// If this node is a root of a shadow_tree, this is the node whose child_subtree is that shadow_tree.
root_for_light_tree: Option<NodeId>,
height: u16,
}
/// A view of a tree.
pub type TreeRefView<'a> = View<'a, Node>;
/// A mutable view of a tree.
pub type TreeMutView<'a> = (EntitiesViewMut<'a>, ViewMut<'a, Node>);
/// A immutable view of a tree.
pub trait TreeRef {
/// Get the id of the parent of the current node, if enter_shadow_dom is true and the current node is a shadow root, the node the shadow root is attached to will be returned
#[inline]
fn parent_id_advanced(&self, id: NodeId, enter_shadow_dom: bool) -> Option<NodeId> {
// If this node is the root of a shadow_tree, return the node the shadow_tree is attached
let root_for_light_tree = self.root_for_light_tree(id);
match (root_for_light_tree, enter_shadow_dom) {
(Some(id), true) => Some(id),
_ => {
let parent_id = self.parent_id(id);
if enter_shadow_dom {
// If this node is attached via a slot, return the slot as the parent instead of the light tree parent
parent_id.map(|id| {
self.shadow_tree(id)
.and_then(|tree| tree.slot)
.unwrap_or(id)
})
} else {
parent_id
}
}
}
}
/// The parent id of the node.
fn parent_id(&self, id: NodeId) -> Option<NodeId>;
/// Get the ids of the children of the current node, if enter_shadow_dom is true and the current node is a shadow slot, the ids of the nodes under the node the shadow slot is attached to will be returned
#[inline]
fn children_ids_advanced(&self, id: NodeId, enter_shadow_dom: bool) -> Vec<NodeId> {
let shadow_tree = self.shadow_tree(id);
let slot_of_light_tree = self.slot_for_light_tree(id);
match (shadow_tree, slot_of_light_tree, enter_shadow_dom) {
// If this node is a shadow root, return the shadow roots
(Some(tree), _, true) => tree.shadow_roots.clone(),
// If this node is a slot, return the children of the node the slot is attached to
(None, Some(id), true) => self.children_ids(id),
_ => self.children_ids(id),
}
}
/// The children ids of the node.
fn children_ids(&self, id: NodeId) -> Vec<NodeId>;
/// The shadow tree tree under the node.
fn shadow_tree(&self, id: NodeId) -> Option<&ShadowTree>;
/// The node that contains the shadow tree this node is a slot for
fn slot_for_light_tree(&self, id: NodeId) -> Option<NodeId>;
/// The node that contains the shadow tree this node is a root of
fn root_for_light_tree(&self, id: NodeId) -> Option<NodeId>;
/// The height of the node.
fn height(&self, id: NodeId) -> Option<u16>;
/// Returns true if the node exists.
fn contains(&self, id: NodeId) -> bool;
}
/// A mutable view of a tree.
pub trait TreeMut: TreeRef {
/// Removes the node and its children from the tree but do not delete the entities.
fn remove(&mut self, id: NodeId);
/// Adds a new node to the tree.
fn create_node(&mut self, id: NodeId);
/// Adds a child to the node.
fn add_child(&mut self, parent: NodeId, new: NodeId);
/// Replaces the node with a new node.
fn replace(&mut self, old_id: NodeId, new_id: NodeId);
/// Inserts a node before another node.
fn insert_before(&mut self, old_id: NodeId, new_id: NodeId);
/// Inserts a node after another node.
fn insert_after(&mut self, old_id: NodeId, new_id: NodeId);
/// Creates a new shadow tree.
fn create_subtree(&mut self, id: NodeId, shadow_roots: Vec<NodeId>, slot: Option<NodeId>);
/// Remove any shadow tree.
fn remove_subtree(&mut self, id: NodeId);
}
impl<'a> TreeRef for TreeRefView<'a> {
fn parent_id(&self, id: NodeId) -> Option<NodeId> {
self.get(id).ok()?.parent
}
fn children_ids(&self, id: NodeId) -> Vec<NodeId> {
self.get(id)
.map(|node| node.children.clone())
.unwrap_or_default()
}
fn height(&self, id: NodeId) -> Option<u16> {
Some(self.get(id).ok()?.height)
}
fn contains(&self, id: NodeId) -> bool {
self.get(id).is_ok()
}
fn shadow_tree(&self, id: NodeId) -> Option<&ShadowTree> {
self.get(id).ok()?.child_subtree.as_ref()
}
fn slot_for_light_tree(&self, id: NodeId) -> Option<NodeId> {
self.get(id).ok()?.slot_for_light_tree
}
fn root_for_light_tree(&self, id: NodeId) -> Option<NodeId> {
self.get(id).ok()?.root_for_light_tree
}
}
impl<'a> TreeMut for TreeMutView<'a> {
fn remove(&mut self, id: NodeId) {
fn recurse(tree: &mut TreeMutView<'_>, id: NodeId) {
let (light_tree, children) = {
let node = (&mut tree.1).get(id).unwrap();
(node.slot_for_light_tree, std::mem::take(&mut node.children))
};
for child in children {
recurse(tree, child);
}
// If this node is a slot in a shadow_tree, remove it from the shadow_tree.
if let Some(light_tree) = light_tree {
let root_for_light_tree = (&mut tree.1).get(light_tree).unwrap();
if let Some(shadow_tree) = &mut root_for_light_tree.child_subtree {
shadow_tree.slot = None;
}
debug_assert!(
root_for_light_tree.children.is_empty(),
"ShadowTree root should have no children when slot is removed."
);
}
}
{
let mut node_data_mut = &mut self.1;
if let Some(parent) = node_data_mut.get(id).unwrap().parent {
let parent = (&mut node_data_mut).get(parent).unwrap();
parent.children.retain(|&child| child != id);
}
}
recurse(self, id);
}
fn create_node(&mut self, id: NodeId) {
let (entities, node_data_mut) = self;
entities.add_component(
id,
node_data_mut,
Node {
parent: None,
children: Vec::new(),
height: 0,
child_subtree: None,
slot_for_light_tree: None,
root_for_light_tree: None,
},
);
}
fn add_child(&mut self, parent: NodeId, new: NodeId) {
{
let mut node_state = &mut self.1;
(&mut node_state).get(new).unwrap().parent = Some(parent);
let parent = (&mut node_state).get(parent).unwrap();
parent.children.push(new);
}
let height = child_height((&self.1).get(parent).unwrap(), self);
set_height(self, new, height);
}
fn replace(&mut self, old_id: NodeId, new_id: NodeId) {
{
let mut node_state = &mut self.1;
// update the parent's link to the child
if let Some(parent_id) = node_state.get(old_id).unwrap().parent {
let parent = (&mut node_state).get(parent_id).unwrap();
for id in &mut parent.children {
if *id == old_id {
*id = new_id;
break;
}
}
let height = child_height((&self.1).get(parent_id).unwrap(), self);
set_height(self, new_id, height);
}
}
self.remove(old_id);
}
fn insert_before(&mut self, old_id: NodeId, new_id: NodeId) {
let parent_id = {
let old_node = self.1.get(old_id).unwrap();
old_node.parent.expect("tried to insert before root")
};
{
(&mut self.1).get(new_id).unwrap().parent = Some(parent_id);
}
let parent = (&mut self.1).get(parent_id).unwrap();
let index = parent
.children
.iter()
.position(|child| *child == old_id)
.unwrap();
parent.children.insert(index, new_id);
let height = child_height((&self.1).get(parent_id).unwrap(), self);
set_height(self, new_id, height);
}
fn insert_after(&mut self, old_id: NodeId, new_id: NodeId) {
let mut node_state = &mut self.1;
let old_node = node_state.get(old_id).unwrap();
let parent_id = old_node.parent.expect("tried to insert before root");
(&mut node_state).get(new_id).unwrap().parent = Some(parent_id);
let parent = (&mut node_state).get(parent_id).unwrap();
let index = parent
.children
.iter()
.position(|child| *child == old_id)
.unwrap();
parent.children.insert(index + 1, new_id);
let height = child_height((&self.1).get(parent_id).unwrap(), self);
set_height(self, new_id, height);
}
fn create_subtree(&mut self, id: NodeId, shadow_roots: Vec<NodeId>, slot: Option<NodeId>) {
let (_, node_data_mut) = self;
let light_root_height;
{
let shadow_tree = ShadowTree {
shadow_roots: shadow_roots.clone(),
slot,
};
let light_root = node_data_mut
.get(id)
.expect("tried to create shadow_tree with non-existent id");
light_root.child_subtree = Some(shadow_tree);
light_root_height = light_root.height;
if let Some(slot) = slot {
let slot = node_data_mut
.get(slot)
.expect("tried to create shadow_tree with non-existent slot");
slot.slot_for_light_tree = Some(id);
}
}
// Now that we have created the shadow_tree, we need to update the height of the shadow_tree roots
for root in shadow_roots {
(&mut self.1).get(root).unwrap().root_for_light_tree = Some(id);
set_height(self, root, light_root_height + 1);
}
}
fn remove_subtree(&mut self, id: NodeId) {
let (_, node_data_mut) = self;
if let Ok(node) = node_data_mut.get(id) {
if let Some(shadow_tree) = node.child_subtree.take() {
// Remove the slot's link to the shadow_tree
if let Some(slot) = shadow_tree.slot {
let slot = node_data_mut
.get(slot)
.expect("tried to remove shadow_tree with non-existent slot");
slot.slot_for_light_tree = None;
}
let node = node_data_mut.get(id).unwrap();
// Reset the height of the light root's children
let height = node.height;
for child in node.children.clone() {
println!("child: {:?}", child);
set_height(self, child, height + 1);
}
// Reset the height of the shadow roots
for root in &shadow_tree.shadow_roots {
set_height(self, *root, 0);
}
}
}
}
}
fn child_height(parent: &Node, tree: &impl TreeRef) -> u16 {
match &parent.child_subtree {
Some(shadow_tree) => {
if let Some(slot) = shadow_tree.slot {
tree.height(slot)
.expect("Attempted to read a slot that does not exist")
+ 1
} else {
panic!("Attempted to read the height of a child of a node with a shadow tree, but the shadow tree does not have a slot. Every shadow tree attached to a node with children must have a slot.")
}
}
None => parent.height + 1,
}
}
/// Sets the height of a node and updates the height of all its children
fn set_height(tree: &mut TreeMutView<'_>, node: NodeId, height: u16) {
let (shadow_tree, light_tree, children) = {
let mut node_data_mut = &mut tree.1;
let node = (&mut node_data_mut).get(node).unwrap();
node.height = height;
(
node.child_subtree.clone(),
node.slot_for_light_tree,
node.children.clone(),
)
};
// If the children are actually part of a shadow_tree, there height is determined by the height of the shadow_tree
if let Some(shadow_tree) = shadow_tree {
// Set the height of the shadow_tree roots
for &shadow_root in &shadow_tree.shadow_roots {
set_height(tree, shadow_root, height + 1);
}
} else {
// Otherwise, we just set the height of the children to be one more than the height of the parent
for child in children {
set_height(tree, child, height + 1);
}
}
// If this nodes is a slot for a shadow_tree, we need to go to the super tree and update the height of its children
if let Some(light_tree) = light_tree {
let children = (&tree.1).get(light_tree).unwrap().children.clone();
for child in children {
set_height(tree, child, height + 1);
}
}
}
impl<'a> TreeRef for TreeMutView<'a> {
fn parent_id(&self, id: NodeId) -> Option<NodeId> {
let node_data = &self.1;
node_data.get(id).unwrap().parent
}
fn children_ids(&self, id: NodeId) -> Vec<NodeId> {
let node_data = &self.1;
node_data
.get(id)
.map(|node| node.children.clone())
.unwrap_or_default()
}
fn height(&self, id: NodeId) -> Option<u16> {
let node_data = &self.1;
node_data.get(id).map(|node| node.height).ok()
}
fn contains(&self, id: NodeId) -> bool {
self.1.get(id).is_ok()
}
fn shadow_tree(&self, id: NodeId) -> Option<&ShadowTree> {
let node_data = &self.1;
node_data.get(id).ok()?.child_subtree.as_ref()
}
fn slot_for_light_tree(&self, id: NodeId) -> Option<NodeId> {
let node_data = &self.1;
node_data.get(id).ok()?.slot_for_light_tree
}
fn root_for_light_tree(&self, id: NodeId) -> Option<NodeId> {
let node_data = &self.1;
node_data.get(id).ok()?.root_for_light_tree
}
}
#[test]
fn creation() {
use shipyard::World;
#[derive(Component)]
struct Num(i32);
let mut world = World::new();
let parent_id = world.add_entity(Num(1i32));
let child_id = world.add_entity(Num(0i32));
let mut tree = world.borrow::<TreeMutView>().unwrap();
tree.create_node(parent_id);
tree.create_node(child_id);
tree.add_child(parent_id, child_id);
assert_eq!(tree.height(parent_id), Some(0));
assert_eq!(tree.height(child_id), Some(1));
assert_eq!(tree.parent_id(parent_id), None);
assert_eq!(tree.parent_id(child_id).unwrap(), parent_id);
assert_eq!(tree.children_ids(parent_id), &[child_id]);
}
#[test]
fn shadow_tree() {
use shipyard::World;
#[derive(Component)]
struct Num(i32);
let mut world = World::new();
// Create main tree
let parent_id = world.add_entity(Num(1i32));
let child_id = world.add_entity(Num(0i32));
// Create shadow tree
let shadow_parent_id = world.add_entity(Num(2i32));
let shadow_child_id = world.add_entity(Num(3i32));
let mut tree = world.borrow::<TreeMutView>().unwrap();
tree.create_node(parent_id);
tree.create_node(child_id);
tree.add_child(parent_id, child_id);
tree.create_node(shadow_parent_id);
tree.create_node(shadow_child_id);
tree.add_child(shadow_parent_id, shadow_child_id);
// Check that both trees are correct individually
assert_eq!(tree.height(parent_id), Some(0));
assert_eq!(tree.height(child_id), Some(1));
assert_eq!(tree.parent_id(parent_id), None);
assert_eq!(tree.parent_id(child_id).unwrap(), parent_id);
assert_eq!(tree.children_ids(parent_id), &[child_id]);
assert_eq!(tree.height(shadow_parent_id), Some(0));
assert_eq!(tree.height(shadow_child_id), Some(1));
assert_eq!(tree.parent_id(shadow_parent_id), None);
assert_eq!(tree.parent_id(shadow_child_id).unwrap(), shadow_parent_id);
assert_eq!(tree.children_ids(shadow_parent_id), &[shadow_child_id]);
// Add shadow tree to main tree
tree.create_subtree(parent_id, vec![shadow_parent_id], Some(shadow_child_id));
assert_eq!(tree.height(parent_id), Some(0));
assert_eq!(tree.height(shadow_parent_id), Some(1));
assert_eq!(tree.height(shadow_child_id), Some(2));
assert_eq!(tree.height(child_id), Some(3));
assert_eq!(
tree.1
.get(parent_id)
.unwrap()
.child_subtree
.as_ref()
.unwrap()
.shadow_roots,
&[shadow_parent_id]
);
assert_eq!(
tree.1.get(shadow_child_id).unwrap().slot_for_light_tree,
Some(parent_id)
);
// Remove shadow tree from main tree
tree.remove_subtree(parent_id);
// Check that both trees are correct individually
assert_eq!(tree.height(parent_id), Some(0));
assert_eq!(tree.height(child_id), Some(1));
assert_eq!(tree.parent_id(parent_id), None);
assert_eq!(tree.parent_id(child_id).unwrap(), parent_id);
assert_eq!(tree.children_ids(parent_id), &[child_id]);
assert_eq!(tree.height(shadow_parent_id), Some(0));
assert_eq!(tree.height(shadow_child_id), Some(1));
assert_eq!(tree.parent_id(shadow_parent_id), None);
assert_eq!(tree.parent_id(shadow_child_id).unwrap(), shadow_parent_id);
assert_eq!(tree.children_ids(shadow_parent_id), &[shadow_child_id]);
}
#[test]
fn insertion() {
use shipyard::World;
#[derive(Component)]
struct Num(i32);
let mut world = World::new();
let parent = world.add_entity(Num(0));
let child = world.add_entity(Num(2));
let before = world.add_entity(Num(1));
let after = world.add_entity(Num(3));
let mut tree = world.borrow::<TreeMutView>().unwrap();
tree.create_node(parent);
tree.create_node(child);
tree.create_node(before);
tree.create_node(after);
tree.add_child(parent, child);
tree.insert_before(child, before);
tree.insert_after(child, after);
assert_eq!(tree.height(parent), Some(0));
assert_eq!(tree.height(child), Some(1));
assert_eq!(tree.height(before), Some(1));
assert_eq!(tree.height(after), Some(1));
assert_eq!(tree.parent_id(before).unwrap(), parent);
assert_eq!(tree.parent_id(child).unwrap(), parent);
assert_eq!(tree.parent_id(after).unwrap(), parent);
assert_eq!(tree.children_ids(parent), &[before, child, after]);
}
#[test]
fn deletion() {
use shipyard::World;
#[derive(Component)]
struct Num(i32);
let mut world = World::new();
let parent = world.add_entity(Num(0));
let child = world.add_entity(Num(2));
let before = world.add_entity(Num(1));
let after = world.add_entity(Num(3));
let mut tree = world.borrow::<TreeMutView>().unwrap();
tree.create_node(parent);
tree.create_node(child);
tree.create_node(before);
tree.create_node(after);
tree.add_child(parent, child);
tree.insert_before(child, before);
tree.insert_after(child, after);
assert_eq!(tree.height(parent), Some(0));
assert_eq!(tree.height(child), Some(1));
assert_eq!(tree.height(before), Some(1));
assert_eq!(tree.height(after), Some(1));
assert_eq!(tree.parent_id(before).unwrap(), parent);
assert_eq!(tree.parent_id(child).unwrap(), parent);
assert_eq!(tree.parent_id(after).unwrap(), parent);
assert_eq!(tree.children_ids(parent), &[before, child, after]);
tree.remove(child);
assert_eq!(tree.height(parent), Some(0));
assert_eq!(tree.height(before), Some(1));
assert_eq!(tree.height(after), Some(1));
assert_eq!(tree.parent_id(before).unwrap(), parent);
assert_eq!(tree.parent_id(after).unwrap(), parent);
assert_eq!(tree.children_ids(parent), &[before, after]);
tree.remove(before);
assert_eq!(tree.height(parent), Some(0));
assert_eq!(tree.height(after), Some(1));
assert_eq!(tree.parent_id(after).unwrap(), parent);
assert_eq!(tree.children_ids(parent), &[after]);
tree.remove(after);
assert_eq!(tree.height(parent), Some(0));
assert_eq!(tree.children_ids(parent), &[]);
}

Some files were not shown because too many files have changed in this diff Show more