diff --git a/docs/guide/src/en/getting_started/web.md b/docs/guide/src/en/getting_started/web.md index b29b8d691..7f3d04848 100644 --- a/docs/guide/src/en/getting_started/web.md +++ b/docs/guide/src/en/getting_started/web.md @@ -22,7 +22,7 @@ The Web is the best-supported target platform for Dioxus. ## Tooling -To develop your Dioxus app for the web, you'll need a tool to build and serve your assets. We recommend using [dioxus-cli](https://github.com/DioxusLabs/cli) which includes a build system, Wasm optimization, a dev server, and support hot reloading: +To develop your Dioxus app for the web, you'll need a tool to build and serve your assets. We recommend using [dioxus-cli](https://github.com/DioxusLabs/dioxus/tree/master/packages/cli) which includes a build system, Wasm optimization, a dev server, and support hot reloading: ```shell cargo install dioxus-cli diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index 7e5e60eed..c947baa63 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -39,6 +39,7 @@ indicatif = "0.17.0-rc.11" subprocess = "0.2.9" axum = { version = "0.5.1", features = ["ws", "headers"] } +axum-server = { version = "0.5.1", features = ["tls-rustls"] } tower-http = { version = "0.2.2", features = ["full"] } headers = "0.3.7" diff --git a/packages/cli/src/config.rs b/packages/cli/src/config.rs index 6324f725f..f9117f6e5 100644 --- a/packages/cli/src/config.rs +++ b/packages/cli/src/config.rs @@ -77,6 +77,12 @@ impl Default for DioxusConfig { style: Some(vec![]), script: Some(vec![]), }, + https: WebHttpsConfig { + enabled: None, + mkcert: None, + key_path: None, + cert_path: None, + }, }, plugin: toml::Value::Table(toml::map::Map::new()), } @@ -101,6 +107,8 @@ pub struct WebConfig { pub proxy: Option>, pub watcher: WebWatcherConfig, pub resource: WebResourceConfig, + #[serde(default)] + pub https: WebHttpsConfig, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -134,6 +142,14 @@ pub struct WebDevResourceConfig { pub script: Option>, } +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct WebHttpsConfig { + pub enabled: Option, + pub mkcert: Option, + pub key_path: Option, + pub cert_path: Option, +} + #[derive(Debug, Clone)] pub struct CrateConfig { pub out_dir: PathBuf, diff --git a/packages/cli/src/server/hot_reload.rs b/packages/cli/src/server/hot_reload.rs new file mode 100644 index 000000000..167e712af --- /dev/null +++ b/packages/cli/src/server/hot_reload.rs @@ -0,0 +1,70 @@ +use std::sync::{Arc, Mutex}; + +use axum::{ + extract::{ws::Message, WebSocketUpgrade}, + response::IntoResponse, + Extension, TypedHeader, +}; +use dioxus_core::Template; +use dioxus_html::HtmlCtx; +use dioxus_rsx::hot_reload::FileMap; +use tokio::sync::broadcast; + +use super::BuildManager; +use crate::CrateConfig; + +pub struct HotReloadState { + pub messages: broadcast::Sender>, + pub build_manager: Arc, + pub file_map: Arc>>, + pub watcher_config: CrateConfig, +} + +pub async fn hot_reload_handler( + ws: WebSocketUpgrade, + _: Option>, + Extension(state): Extension>, +) -> 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"); + } + + 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; + }; + } + } + }) +} diff --git a/packages/cli/src/server/mod.rs b/packages/cli/src/server/mod.rs index 6575875f7..3bd434005 100644 --- a/packages/cli/src/server/mod.rs +++ b/packages/cli/src/server/mod.rs @@ -10,8 +10,8 @@ use axum::{ routing::{get, get_service}, Router, }; +use axum_server::tls_rustls::RustlsConfig; use cargo_metadata::diagnostic::Diagnostic; -use colored::Colorize; use dioxus_core::Template; use dioxus_html::HtmlCtx; use dioxus_rsx::hot_reload::*; @@ -22,7 +22,7 @@ use std::{ process::Command, sync::{Arc, Mutex}, }; -use tokio::sync::broadcast; +use tokio::sync::broadcast::{self, Sender}; use tower::ServiceBuilder; use tower_http::services::fs::{ServeDir, ServeFileSystemResponseBody}; use tower_http::{ @@ -35,6 +35,12 @@ use plugin::PluginManager; mod proxy; +mod hot_reload; +use hot_reload::*; + +mod output; +use output::*; + pub struct BuildManager { config: CrateConfig, reload_tx: broadcast::Sender<()>, @@ -76,72 +82,16 @@ pub async fn startup(port: u16, config: CrateConfig, start_browser: bool) -> Res let ip = get_ip().unwrap_or(String::from("0.0.0.0")); - if config.hot_reload { - startup_hot_reload(ip, port, config, start_browser).await? - } else { - startup_default(ip, port, config, start_browser).await? + match config.hot_reload { + true => serve_hot_reload(ip, port, config, start_browser).await?, + false => serve_default(ip, port, config, start_browser).await?, } + Ok(()) } -pub struct HotReloadState { - pub messages: broadcast::Sender>, - pub build_manager: Arc, - pub file_map: Arc>>, - pub watcher_config: CrateConfig, -} - -pub async fn hot_reload_handler( - ws: WebSocketUpgrade, - _: Option>, - Extension(state): Extension>, -) -> 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"); - } - - 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; - }; - } - } - }) -} - -#[allow(unused_assignments)] -pub async fn startup_hot_reload( +/// Start the server without hot reload +pub async fn serve_default( ip: String, port: u16, config: CrateConfig, @@ -151,22 +101,71 @@ pub async fn startup_hot_reload( log::info!("🚀 Starting development server..."); - #[cfg(feature = "plugin")] - PluginManager::on_serve_start(&config)?; + // WS Reload Watching + let (reload_tx, _) = broadcast::channel(100); - let dist_path = config.out_dir.clone(); + // We got to own watcher so that it exists for the duration of serve + // Otherwise full reload won't work. + let _watcher = setup_file_watcher(&config, port, ip.clone(), reload_tx.clone()).await?; + + let ws_reload_state = Arc::new(WsReloadState { + update: reload_tx.clone(), + }); + + // HTTPS + // Before console info so it can stop if mkcert isn't installed or fails + let rustls_config = get_rustls(&config).await?; + + // Print serve info + print_console_info( + &ip, + port, + &config, + PrettierOptions { + changed: vec![], + warnings: first_build_result.warnings, + elapsed_time: first_build_result.elapsed_time, + }, + ); + + // Router + let router = setup_router(config, ws_reload_state, None).await?; + + // Start server + start_server(port, router, start_browser, rustls_config).await?; + + Ok(()) +} + +/// Start dx serve with hot reload +pub async fn serve_hot_reload( + ip: String, + port: u16, + config: CrateConfig, + start_browser: bool, +) -> Result<()> { + let first_build_result = crate::builder::build(&config, false)?; + + log::info!("🚀 Starting development server..."); + + // Setup hot reload let (reload_tx, _) = broadcast::channel(100); let FileMapBuildResult { map, errors } = FileMap::::create(config.crate_dir.clone()).unwrap(); + for err in errors { log::error!("{}", err); } + let file_map = Arc::new(Mutex::new(map)); let build_manager = Arc::new(BuildManager { config: config.clone(), reload_tx: reload_tx.clone(), }); + let hot_reload_tx = broadcast::channel(100).0; + + // States let hot_reload_state = Arc::new(HotReloadState { messages: hot_reload_tx.clone(), build_manager: build_manager.clone(), @@ -174,11 +173,343 @@ pub async fn startup_hot_reload( watcher_config: config.clone(), }); - let crate_dir = config.crate_dir.clone(); let ws_reload_state = Arc::new(WsReloadState { update: reload_tx.clone(), }); + // Setup file watcher + // We got to own watcher so that it exists for the duration of serve + // Otherwise hot reload won't work. + let _watcher = setup_file_watcher_hot_reload( + &config, + port, + ip.clone(), + hot_reload_tx, + file_map, + build_manager, + ) + .await?; + + // HTTPS + // Before console info so it can stop if mkcert isn't installed or fails + let rustls_config = get_rustls(&config).await?; + + // Print serve info + print_console_info( + &ip, + port, + &config, + PrettierOptions { + changed: vec![], + warnings: first_build_result.warnings, + elapsed_time: first_build_result.elapsed_time, + }, + ); + + // Router + let router = setup_router(config, ws_reload_state, Some(hot_reload_state)).await?; + + // Start server + start_server(port, router, start_browser, rustls_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> { + let web_config = &config.dioxus_config.web.https; + if web_config.enabled != Some(true) { + return Ok(None); + } + + let (cert_path, key_path) = match web_config.mkcert { + // mkcert, use it + Some(true) => { + // 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()?; + } + } + + (cert_path, key_path) + } + // not mkcert + Some(false) => { + // get paths to cert & key + if let (Some(key), Some(cert)) = + (web_config.key_path.clone(), web_config.cert_path.clone()) + { + (cert, key) + } else { + // missing cert or key + return Err("https is enabled but cert or key path is missing".into()); + } + } + // other + _ => return Ok(None), + }; + + Ok(Some( + RustlsConfig::from_pem_file(cert_path, key_path).await?, + )) +} + +/// Sets up and returns a router +async fn setup_router( + config: CrateConfig, + ws_reload: Arc, + hot_reload: Option>, +) -> Result { + // 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| async move { + let response = if file_service_config + .dioxus_config + .web + .watcher + .index_on_404 + .unwrap_or(false) + && response.status() == StatusCode::NOT_FOUND + { + let body = Full::from( + // TODO: Cache/memoize this. + std::fs::read_to_string( + file_service_config + .crate_dir + .join(file_service_config.out_dir) + .join("index.html"), + ) + .ok() + .unwrap(), + ) + .map_err(|err| match err {}) + .boxed(); + Response::builder() + .status(StatusCode::OK) + .body(body) + .unwrap() + } else { + response.map(|body| body.boxed()) + }; + Ok(response) + }, + ) + .service(ServeDir::new(config.crate_dir.join(&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.unwrap_or_default() { + router = proxy::add_proxy(router, &proxy_config)?; + } + + // Route file service + router = router.fallback(get_service(file_service).handle_error( + |error: std::io::Error| async move { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Unhandled internal error: {}", error), + ) + }, + )); + + // 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, + start_browser: bool, + rustls: Option, +) -> Result<()> { + // If plugins, call on_serve_start event + #[cfg(feature = "plugin")] + PluginManager::on_serve_start(&config)?; + + // Parse address + let addr = 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)), + } + } + + // Start the server with or without rustls + match rustls { + Some(rustls) => { + axum_server::bind_rustls(addr, rustls) + .serve(router.into_make_service()) + .await? + } + None => { + axum::Server::bind(&addr) + .serve(router.into_make_service()) + .await? + } + } + + Ok(()) +} + +/// Sets up a file watcher +async fn setup_file_watcher( + config: &CrateConfig, + port: u16, + watcher_ip: String, + reload_tx: Sender<()>, +) -> Result { + let build_manager = BuildManager { + config: config.clone(), + reload_tx, + }; + + 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() + .unwrap_or_else(|| vec![PathBuf::from("src")]); + + let watcher_config = config.clone(); + let mut watcher = notify::recommended_watcher(move |info: notify::Result| { + let config = watcher_config.clone(); + if let Ok(e) = info { + if chrono::Local::now().timestamp() > last_update_time { + match build_manager.rebuild() { + Ok(res) => { + last_update_time = chrono::Local::now().timestamp(); + + #[allow(clippy::redundant_clone)] + print_console_info( + &watcher_ip, + port, + &config, + PrettierOptions { + changed: e.paths.clone(), + warnings: res.warnings, + elapsed_time: res.elapsed_time, + }, + ); + + #[cfg(feature = "plugin")] + let _ = PluginManager::on_serve_rebuild( + chrono::Local::now().timestamp(), + e.paths, + ); + } + Err(e) => log::error!("{}", e), + } + } + } + }) + .unwrap(); + + for sub_path in allow_watch_path { + watcher + .watch( + &config.crate_dir.join(sub_path), + notify::RecursiveMode::Recursive, + ) + .unwrap(); + } + Ok(watcher) +} + +// Todo: reduce duplication and merge with setup_file_watcher() +/// Sets up a file watcher with hot reload +async fn setup_file_watcher_hot_reload( + config: &CrateConfig, + port: u16, + watcher_ip: String, + hot_reload_tx: Sender>, + file_map: Arc>>, + build_manager: Arc, +) -> Result { // file watcher: check file change let allow_watch_path = config .dioxus_config @@ -189,7 +520,6 @@ pub async fn startup_hot_reload( .unwrap_or_else(|| vec![PathBuf::from("src")]); let watcher_config = config.clone(); - let watcher_ip = ip.clone(); let mut last_update_time = chrono::Local::now().timestamp(); let mut watcher = RecommendedWatcher::new( @@ -225,7 +555,7 @@ pub async fn startup_hot_reload( // find changes to the rsx in the file let mut map = file_map.lock().unwrap(); - match map.update_rsx(&path, &crate_dir) { + match map.update_rsx(&path, &config.crate_dir) { Ok(UpdateResult::UpdatedRsx(msgs)) => { messages.extend(msgs); } @@ -274,449 +604,10 @@ pub async fn startup_hot_reload( } } - // start serve dev-server at 0.0.0.0:8080 - print_console_info( - &ip, - port, - &config, - PrettierOptions { - changed: vec![], - warnings: first_build_result.warnings, - elapsed_time: first_build_result.elapsed_time, - }, - ); - - 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"), - ) - }; - - 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| async move { - let response = if file_service_config - .dioxus_config - .web - .watcher - .index_on_404 - .unwrap_or(false) - && response.status() == StatusCode::NOT_FOUND - { - let body = Full::from( - // TODO: Cache/memoize this. - std::fs::read_to_string( - file_service_config - .crate_dir - .join(file_service_config.out_dir) - .join("index.html"), - ) - .ok() - .unwrap(), - ) - .map_err(|err| match err {}) - .boxed(); - Response::builder() - .status(StatusCode::OK) - .body(body) - .unwrap() - } else { - response.map(|body| body.boxed()) - }; - Ok(response) - }, - ) - .service(ServeDir::new(config.crate_dir.join(&dist_path))); - - let mut router = Router::new().route("/_dioxus/ws", get(ws_handler)); - for proxy_config in config.dioxus_config.web.proxy.unwrap_or_default() { - router = proxy::add_proxy(router, &proxy_config)?; - } - router = router.fallback(get_service(file_service).handle_error( - |error: std::io::Error| async move { - ( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Unhandled internal error: {}", error), - ) - }, - )); - - let router = router - .route("/_dioxus/hot_reload", get(hot_reload_handler)) - .layer(cors) - .layer(Extension(ws_reload_state)) - .layer(Extension(hot_reload_state)); - - let addr = format!("0.0.0.0:{}", port).parse().unwrap(); - - let server = axum::Server::bind(&addr).serve(router.into_make_service()); - - if start_browser { - let _ = open::that(format!("http://{}", addr)); - } - - server.await?; - - Ok(()) -} - -pub async fn startup_default( - ip: String, - port: u16, - config: CrateConfig, - start_browser: bool, -) -> Result<()> { - let first_build_result = crate::builder::build(&config, false)?; - - log::info!("🚀 Starting development server..."); - - let dist_path = config.out_dir.clone(); - - let (reload_tx, _) = broadcast::channel(100); - - let build_manager = BuildManager { - config: config.clone(), - reload_tx: reload_tx.clone(), - }; - - let ws_reload_state = Arc::new(WsReloadState { - update: reload_tx.clone(), - }); - - 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() - .unwrap_or_else(|| vec![PathBuf::from("src")]); - - let watcher_config = config.clone(); - let watcher_ip = ip.clone(); - let mut watcher = notify::recommended_watcher(move |info: notify::Result| { - let config = watcher_config.clone(); - if let Ok(e) = info { - if chrono::Local::now().timestamp() > last_update_time { - match build_manager.rebuild() { - Ok(res) => { - last_update_time = chrono::Local::now().timestamp(); - - #[allow(clippy::redundant_clone)] - print_console_info( - &watcher_ip, - port, - &config, - PrettierOptions { - changed: e.paths.clone(), - warnings: res.warnings, - elapsed_time: res.elapsed_time, - }, - ); - - #[cfg(feature = "plugin")] - let _ = PluginManager::on_serve_rebuild( - chrono::Local::now().timestamp(), - e.paths, - ); - } - Err(e) => log::error!("{}", e), - } - } - } - }) - .unwrap(); - - for sub_path in allow_watch_path { - watcher - .watch( - &config.crate_dir.join(sub_path), - notify::RecursiveMode::Recursive, - ) - .unwrap(); - } - - // start serve dev-server at 0.0.0.0 - print_console_info( - &ip, - port, - &config, - PrettierOptions { - changed: vec![], - warnings: first_build_result.warnings, - elapsed_time: first_build_result.elapsed_time, - }, - ); - - #[cfg(feature = "plugin")] - PluginManager::on_serve_start(&config)?; - - 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"), - ) - }; - - 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| async move { - let response = if file_service_config - .dioxus_config - .web - .watcher - .index_on_404 - .unwrap_or(false) - && response.status() == StatusCode::NOT_FOUND - { - let body = Full::from( - // TODO: Cache/memoize this. - std::fs::read_to_string( - file_service_config - .crate_dir - .join(file_service_config.out_dir) - .join("index.html"), - ) - .ok() - .unwrap(), - ) - .map_err(|err| match err {}) - .boxed(); - Response::builder() - .status(StatusCode::OK) - .body(body) - .unwrap() - } else { - response.map(|body| body.boxed()) - }; - Ok(response) - }, - ) - .service(ServeDir::new(config.crate_dir.join(&dist_path))); - - let mut router = Router::new().route("/_dioxus/ws", get(ws_handler)); - for proxy_config in config.dioxus_config.web.proxy.unwrap_or_default() { - router = proxy::add_proxy(router, &proxy_config)?; - } - router = router - .fallback( - get_service(file_service).handle_error(|error: std::io::Error| async move { - ( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Unhandled internal error: {}", error), - ) - }), - ) - .layer(cors) - .layer(Extension(ws_reload_state)); - - let addr = format!("0.0.0.0:{}", port).parse().unwrap(); - let server = axum::Server::bind(&addr).serve(router.into_make_service()); - - if start_browser { - let _ = open::that(format!("http://{}", addr)); - } - - server.await?; - - Ok(()) -} - -#[derive(Debug, Default)] -pub struct PrettierOptions { - changed: Vec, - warnings: Vec, - elapsed_time: u128, -} - -fn print_console_info(ip: &String, port: u16, config: &CrateConfig, options: PrettierOptions) { - if let Ok(native_clearseq) = Command::new(if cfg!(target_os = "windows") { - "cls" - } else { - "clear" - }) - .output() - { - print!("{}", String::from_utf8_lossy(&native_clearseq.stdout)); - } else { - // Try ANSI-Escape characters - print!("\x1b[2J\x1b[H"); - } - - // for path in &changed { - // let path = path - // .strip_prefix(crate::crate_root().unwrap()) - // .unwrap() - // .to_path_buf(); - // log::info!("Updated {}", format!("{}", path.to_str().unwrap()).green()); - // } - - let mut profile = if config.release { "Release" } else { "Debug" }.to_string(); - if config.custom_profile.is_some() { - profile = config.custom_profile.as_ref().unwrap().to_string(); - } - let hot_reload = if config.hot_reload { "RSX" } else { "Normal" }; - let crate_root = crate::cargo::crate_root().unwrap(); - let custom_html_file = if crate_root.join("index.html").is_file() { - "Custom [index.html]" - } else { - "Default" - }; - let url_rewrite = if config - .dioxus_config - .web - .watcher - .index_on_404 - .unwrap_or(false) - { - "True" - } else { - "False" - }; - - let proxies = config.dioxus_config.web.proxy.as_ref(); - - if options.changed.is_empty() { - println!( - "{} @ v{} [{}] \n", - "Dioxus".bold().green(), - crate::DIOXUS_CLI_VERSION, - chrono::Local::now().format("%H:%M:%S").to_string().dimmed() - ); - } else { - println!( - "Project Reloaded: {}\n", - format!( - "Changed {} files. [{}]", - options.changed.len(), - chrono::Local::now().format("%H:%M:%S").to_string().dimmed() - ) - .purple() - .bold() - ); - } - println!( - "\t> Local : {}", - format!("http://localhost:{}/", port).blue() - ); - println!( - "\t> Network : {}", - format!("http://{}:{}/", ip, port).blue() - ); - println!(); - println!("\t> Profile : {}", profile.green()); - println!("\t> Hot Reload : {}", hot_reload.cyan()); - if let Some(proxies) = proxies { - if !proxies.is_empty() { - println!("\t> Proxies :"); - for proxy in proxies { - println!("\t\t- {}", proxy.backend.blue()); - } - } - } - println!("\t> Index Template : {}", custom_html_file.green()); - println!("\t> URL Rewrite [index_on_404] : {}", url_rewrite.purple()); - println!(); - println!( - "\t> Build Time Use : {} millis", - options.elapsed_time.to_string().green().bold() - ); - println!(); - - if options.warnings.is_empty() { - log::info!("{}\n", "A perfect compilation!".green().bold()); - } else { - log::warn!( - "{}", - format!( - "There were {} warning messages during the build.", - options.warnings.len() - 1 - ) - .yellow() - .bold() - ); - // for info in &options.warnings { - // let message = info.message.clone(); - // if message == format!("{} warnings emitted", options.warnings.len() - 1) { - // continue; - // } - // let mut console = String::new(); - // for span in &info.spans { - // let file = &span.file_name; - // let line = (span.line_start, span.line_end); - // let line_str = if line.0 == line.1 { - // line.0.to_string() - // } else { - // format!("{}~{}", line.0, line.1) - // }; - // let code = span.text.clone(); - // let span_info = if code.len() == 1 { - // let code = code.get(0).unwrap().text.trim().blue().bold().to_string(); - // format!( - // "[{}: {}]: '{}' --> {}", - // file, - // line_str, - // code, - // message.yellow().bold() - // ) - // } else { - // let code = code - // .iter() - // .enumerate() - // .map(|(_i, s)| format!("\t{}\n", s.text).blue().bold().to_string()) - // .collect::(); - // format!("[{}: {}]:\n{}\n#:{}", file, line_str, code, message) - // }; - // console = format!("{console}\n\t{span_info}"); - // } - // println!("{console}"); - // } - // println!( - // "\n{}\n", - // "Resolving all warnings will help your code run better!".yellow() - // ); - } + Ok(watcher) } +/// Get the network ip fn get_ip() -> Option { let socket = match UdpSocket::bind("0.0.0.0:0") { Ok(s) => s, @@ -734,6 +625,7 @@ fn get_ip() -> Option { } } +/// Handle websockets async fn ws_handler( ws: WebSocketUpgrade, _: Option>, diff --git a/packages/cli/src/server/output.rs b/packages/cli/src/server/output.rs new file mode 100644 index 000000000..4c6e1f3b2 --- /dev/null +++ b/packages/cli/src/server/output.rs @@ -0,0 +1,127 @@ +use crate::server::Diagnostic; +use crate::CrateConfig; +use colored::Colorize; +use std::path::PathBuf; +use std::process::Command; + +#[derive(Debug, Default)] +pub struct PrettierOptions { + pub changed: Vec, + pub warnings: Vec, + pub elapsed_time: u128, +} + +pub fn print_console_info(ip: &String, port: u16, config: &CrateConfig, options: PrettierOptions) { + if let Ok(native_clearseq) = Command::new(if cfg!(target_os = "windows") { + "cls" + } else { + "clear" + }) + .output() + { + print!("{}", String::from_utf8_lossy(&native_clearseq.stdout)); + } else { + // Try ANSI-Escape characters + print!("\x1b[2J\x1b[H"); + } + + let mut profile = if config.release { "Release" } else { "Debug" }.to_string(); + if config.custom_profile.is_some() { + profile = config.custom_profile.as_ref().unwrap().to_string(); + } + let hot_reload = if config.hot_reload { "RSX" } else { "Normal" }; + let crate_root = crate::cargo::crate_root().unwrap(); + let custom_html_file = if crate_root.join("index.html").is_file() { + "Custom [index.html]" + } else { + "Default" + }; + let url_rewrite = if config + .dioxus_config + .web + .watcher + .index_on_404 + .unwrap_or(false) + { + "True" + } else { + "False" + }; + + let proxies = config.dioxus_config.web.proxy.as_ref(); + + if options.changed.is_empty() { + println!( + "{} @ v{} [{}] \n", + "Dioxus".bold().green(), + crate::DIOXUS_CLI_VERSION, + chrono::Local::now().format("%H:%M:%S").to_string().dimmed() + ); + } else { + println!( + "Project Reloaded: {}\n", + format!( + "Changed {} files. [{}]", + options.changed.len(), + chrono::Local::now().format("%H:%M:%S").to_string().dimmed() + ) + .purple() + .bold() + ); + } + + if config.dioxus_config.web.https.enabled == Some(true) { + println!( + "\t> Local : {}", + format!("https://localhost:{}/", port).blue() + ); + println!( + "\t> Network : {}", + format!("https://{}:{}/", ip, port).blue() + ); + println!("\t> HTTPS : {}", "Enabled".to_string().green()); + } else { + println!( + "\t> Local : {}", + format!("http://localhost:{}/", port).blue() + ); + println!( + "\t> Network : {}", + format!("http://{}:{}/", ip, port).blue() + ); + println!("\t> HTTPS : {}", "Disabled".to_string().red()); + } + println!(); + println!("\t> Profile : {}", profile.green()); + println!("\t> Hot Reload : {}", hot_reload.cyan()); + if let Some(proxies) = proxies { + if !proxies.is_empty() { + println!("\t> Proxies :"); + for proxy in proxies { + println!("\t\t- {}", proxy.backend.blue()); + } + } + } + println!("\t> Index Template : {}", custom_html_file.green()); + println!("\t> URL Rewrite [index_on_404] : {}", url_rewrite.purple()); + println!(); + println!( + "\t> Build Time Use : {} millis", + options.elapsed_time.to_string().green().bold() + ); + println!(); + + if options.warnings.is_empty() { + log::info!("{}\n", "A perfect compilation!".green().bold()); + } else { + log::warn!( + "{}", + format!( + "There were {} warning messages during the build.", + options.warnings.len() - 1 + ) + .yellow() + .bold() + ); + } +} diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 48391456d..0b668f43d 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -27,8 +27,6 @@ slab = { workspace = true } futures-channel = { workspace = true } -indexmap = "1.7" - smallbox = "0.8.1" log = { workspace = true } diff --git a/packages/dioxus-tui/examples/widgets.rs b/packages/dioxus-tui/examples/widgets.rs index 04690522c..b5df35388 100644 --- a/packages/dioxus-tui/examples/widgets.rs +++ b/packages/dioxus-tui/examples/widgets.rs @@ -1,6 +1,5 @@ use dioxus::prelude::*; -use dioxus_html::FormData; -use dioxus_tui::prelude::*; +use dioxus_tui::Config; fn main() { dioxus_tui::launch_cfg(app, Config::new()); @@ -18,8 +17,8 @@ fn app(cx: Scope) -> Element { align_items: "center", justify_content: "center", - Input{ - oninput: |data: FormData| if &data.value == "good"{ + input { + oninput: |data| if &data.value == "good"{ bg_green.set(true); } else{ bg_green.set(false); @@ -30,8 +29,8 @@ fn app(cx: Scope) -> Element { height: "10%", checked: "true", } - Input{ - oninput: |data: FormData| if &data.value == "hello world"{ + input { + oninput: |data| if &data.value == "hello world"{ bg_green.set(true); } else{ bg_green.set(false); @@ -40,8 +39,8 @@ fn app(cx: Scope) -> Element { height: "10%", maxlength: "11", } - Input{ - oninput: |data: FormData| { + input { + oninput: |data| { if (data.value.parse::().unwrap() - 40.0).abs() < 5.0 { bg_green.set(true); } else{ @@ -54,8 +53,8 @@ fn app(cx: Scope) -> Element { min: "20", max: "80", } - Input{ - oninput: |data: FormData| { + input { + oninput: |data| { if data.value == "10"{ bg_green.set(true); } else{ @@ -67,8 +66,8 @@ fn app(cx: Scope) -> Element { height: "10%", maxlength: "4", } - Input{ - oninput: |data: FormData| { + input { + oninput: |data| { if data.value == "hello world"{ bg_green.set(true); } else{ @@ -80,8 +79,10 @@ fn app(cx: Scope) -> Element { height: "10%", maxlength: "11", } - Input{ - onclick: |_: FormData| bg_green.set(true), + input { + oninput: |_| { + bg_green.set(true) + }, r#type: "button", value: "green", width: "50%", diff --git a/packages/dioxus-tui/src/lib.rs b/packages/dioxus-tui/src/lib.rs index 46ada5537..8804336be 100644 --- a/packages/dioxus-tui/src/lib.rs +++ b/packages/dioxus-tui/src/lib.rs @@ -1,6 +1,4 @@ mod element; -pub mod prelude; -pub mod widgets; use std::{ any::Any, @@ -10,7 +8,6 @@ use std::{ }; use dioxus_core::{Component, ElementId, VirtualDom}; -use dioxus_html::EventData; use dioxus_native_core::dioxus::{DioxusState, NodeImmutableDioxusExt}; use dioxus_native_core::prelude::*; @@ -48,7 +45,7 @@ pub fn launch_cfg_with_props(app: Component, props: Props let mut dioxus_state = dioxus_state.write().unwrap(); // Find any mount events - let mounted = dbg!(find_mount_events(&muts)); + let mounted = find_mount_events(&muts); dioxus_state.apply_mutations(&mut rdom, muts); @@ -119,13 +116,14 @@ impl Driver for DioxusRenderer { rdom: &Arc>, id: NodeId, event: &str, - value: Rc, + value: Rc, bubbles: bool, ) { let id = { rdom.read().unwrap().get(id).unwrap().mounted_id() }; if let Some(id) = id { + let inner_value = value.deref().clone(); self.vdom - .handle_event(event, value.deref().clone().into_any(), id, bubbles); + .handle_event(event, inner_value.into_any(), id, bubbles); } } diff --git a/packages/dioxus-tui/src/prelude/mod.rs b/packages/dioxus-tui/src/prelude/mod.rs deleted file mode 100644 index cb4fdee65..000000000 --- a/packages/dioxus-tui/src/prelude/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub use crate::widgets::*; -pub use rink::Config; diff --git a/packages/dioxus-tui/src/widgets/button.rs b/packages/dioxus-tui/src/widgets/button.rs deleted file mode 100644 index a6578905e..000000000 --- a/packages/dioxus-tui/src/widgets/button.rs +++ /dev/null @@ -1,59 +0,0 @@ -use std::collections::HashMap; - -use dioxus::prelude::*; -use dioxus_elements::input_data::keyboard_types::Key; -use dioxus_html as dioxus_elements; -use dioxus_html::FormData; - -#[derive(Props)] -pub(crate) struct ButtonProps<'a> { - #[props(!optional)] - raw_onclick: Option<&'a EventHandler<'a, FormData>>, - #[props(!optional)] - value: Option<&'a str>, - #[props(!optional)] - width: Option<&'a str>, - #[props(!optional)] - height: Option<&'a str>, -} - -#[allow(non_snake_case)] -pub(crate) fn Button<'a>(cx: Scope<'a, ButtonProps>) -> Element<'a> { - let state = use_state(cx, || false); - let width = cx.props.width.unwrap_or("1px"); - let height = cx.props.height.unwrap_or("1px"); - - let single_char = width == "1px" || height == "1px"; - let text = if let Some(v) = cx.props.value { v } else { "" }; - let border_style = if single_char { "none" } else { "solid" }; - let update = || { - let new_state = !state.get(); - if let Some(callback) = cx.props.raw_onclick { - callback.call(FormData { - value: text.to_string(), - values: HashMap::new(), - files: None, - }); - } - state.set(new_state); - }; - render! { - div{ - width: "{width}", - height: "{height}", - border_style: "{border_style}", - flex_direction: "row", - align_items: "center", - justify_content: "center", - onclick: move |_| { - update(); - }, - onkeydown: move |evt|{ - if !evt.is_auto_repeating() && match evt.key(){ Key::Character(c) if c == " " =>true, Key::Enter=>true, _=>false } { - update(); - } - }, - "{text}" - } - } -} diff --git a/packages/dioxus-tui/src/widgets/checkbox.rs b/packages/dioxus-tui/src/widgets/checkbox.rs deleted file mode 100644 index 0b5ebf671..000000000 --- a/packages/dioxus-tui/src/widgets/checkbox.rs +++ /dev/null @@ -1,82 +0,0 @@ -use std::collections::HashMap; - -use dioxus::prelude::*; -use dioxus_elements::input_data::keyboard_types::Key; -use dioxus_html as dioxus_elements; -use dioxus_html::FormData; - -#[derive(Props)] -pub(crate) struct CheckBoxProps<'a> { - #[props(!optional)] - raw_oninput: Option<&'a EventHandler<'a, FormData>>, - #[props(!optional)] - value: Option<&'a str>, - #[props(!optional)] - width: Option<&'a str>, - #[props(!optional)] - height: Option<&'a str>, - #[props(!optional)] - checked: Option<&'a str>, -} - -#[allow(non_snake_case)] -pub(crate) fn CheckBox<'a>(cx: Scope<'a, CheckBoxProps>) -> Element<'a> { - let state = use_state(cx, || cx.props.checked.filter(|&c| c == "true").is_some()); - let width = cx.props.width.unwrap_or("1px"); - let height = cx.props.height.unwrap_or("1px"); - - let single_char = width == "1px" && height == "1px"; - let text = if single_char { - if *state.get() { - "☑" - } else { - "☐" - } - } else if *state.get() { - "✓" - } else { - " " - }; - let border_style = if width == "1px" || height == "1px" { - "none" - } else { - "solid" - }; - let update = move || { - let new_state = !state.get(); - if let Some(callback) = cx.props.raw_oninput { - callback.call(FormData { - value: if let Some(value) = &cx.props.value { - if new_state { - value.to_string() - } else { - String::new() - } - } else { - "on".to_string() - }, - values: HashMap::new(), - files: None, - }); - } - state.set(new_state); - }; - render! { - div { - width: "{width}", - height: "{height}", - border_style: "{border_style}", - align_items: "center", - justify_content: "center", - onclick: move |_| { - update(); - }, - onkeydown: move |evt| { - if !evt.is_auto_repeating() && match evt.key(){ Key::Character(c) if c == " " =>true, Key::Enter=>true, _=>false } { - update(); - } - }, - "{text}" - } - } -} diff --git a/packages/dioxus-tui/src/widgets/input.rs b/packages/dioxus-tui/src/widgets/input.rs deleted file mode 100644 index 2294b0f10..000000000 --- a/packages/dioxus-tui/src/widgets/input.rs +++ /dev/null @@ -1,102 +0,0 @@ -use dioxus::prelude::*; -use dioxus_core::prelude::fc_to_builder; -use dioxus_html::FormData; - -use crate::widgets::button::Button; -use crate::widgets::checkbox::CheckBox; -use crate::widgets::number::NumbericInput; -use crate::widgets::password::Password; -use crate::widgets::slider::Slider; -use crate::widgets::textbox::TextBox; - -#[derive(Props)] -pub struct InputProps<'a> { - r#type: Option<&'static str>, - oninput: Option>, - onclick: Option>, - value: Option<&'a str>, - size: Option<&'a str>, - maxlength: Option<&'a str>, - width: Option<&'a str>, - height: Option<&'a str>, - min: Option<&'a str>, - max: Option<&'a str>, - step: Option<&'a str>, - checked: Option<&'a str>, -} - -#[allow(non_snake_case)] -pub fn Input<'a>(cx: Scope<'a, InputProps<'a>>) -> Element<'a> { - cx.render(match cx.props.r#type { - Some("checkbox") => { - rsx! { - CheckBox{ - raw_oninput: cx.props.oninput.as_ref(), - value: cx.props.value, - width: cx.props.width, - height: cx.props.height, - checked: cx.props.checked, - } - } - } - Some("range") => { - rsx! { - Slider{ - raw_oninput: cx.props.oninput.as_ref(), - value: cx.props.value, - width: cx.props.width, - height: cx.props.height, - max: cx.props.max, - min: cx.props.min, - step: cx.props.step, - } - } - } - Some("button") => { - rsx! { - Button{ - raw_onclick: cx.props.onclick.as_ref(), - value: cx.props.value, - width: cx.props.width, - height: cx.props.height, - } - } - } - Some("number") => { - rsx! { - NumbericInput{ - raw_oninput: cx.props.oninput.as_ref(), - value: cx.props.value, - size: cx.props.size, - max_length: cx.props.maxlength, - width: cx.props.width, - height: cx.props.height, - } - } - } - Some("password") => { - rsx! { - Password{ - raw_oninput: cx.props.oninput.as_ref(), - value: cx.props.value, - size: cx.props.size, - max_length: cx.props.maxlength, - width: cx.props.width, - height: cx.props.height, - } - } - } - _ => { - rsx! { - TextBox{ - raw_oninput: cx.props.oninput.as_ref(), - value: cx.props.value, - size: cx.props.size, - max_length: cx.props.maxlength, - width: cx.props.width, - height: cx.props.height, - } - } - } - }) -} diff --git a/packages/dioxus-tui/src/widgets/mod.rs b/packages/dioxus-tui/src/widgets/mod.rs deleted file mode 100644 index 18e3b12b4..000000000 --- a/packages/dioxus-tui/src/widgets/mod.rs +++ /dev/null @@ -1,22 +0,0 @@ -mod button; -mod checkbox; -mod input; -mod number; -mod password; -mod slider; -mod textbox; - -use dioxus_core::{RenderReturn, Scope}; -use dioxus_native_core::NodeId; -pub use input::*; - -use crate::DioxusElementToNodeId; - -pub(crate) fn get_root_id(cx: Scope) -> Option { - if let RenderReturn::Ready(sync) = cx.root_node() { - let mapping: DioxusElementToNodeId = cx.consume_context()?; - mapping.get_node_id(sync.root_ids.get(0)?) - } else { - None - } -} diff --git a/packages/dioxus-tui/src/widgets/number.rs b/packages/dioxus-tui/src/widgets/number.rs deleted file mode 100644 index f82732981..000000000 --- a/packages/dioxus-tui/src/widgets/number.rs +++ /dev/null @@ -1,209 +0,0 @@ -use crate::widgets::get_root_id; -use crossterm::{cursor::MoveTo, execute}; -use dioxus::prelude::*; -use dioxus_elements::input_data::keyboard_types::Key; -use dioxus_html as dioxus_elements; -use dioxus_html::FormData; -use dioxus_native_core::utils::cursor::{Cursor, Pos}; -use rink::Query; -use std::{collections::HashMap, io::stdout}; -use taffy::geometry::Point; - -#[derive(Props)] -pub(crate) struct NumbericInputProps<'a> { - #[props(!optional)] - raw_oninput: Option<&'a EventHandler<'a, FormData>>, - #[props(!optional)] - value: Option<&'a str>, - #[props(!optional)] - size: Option<&'a str>, - #[props(!optional)] - max_length: Option<&'a str>, - #[props(!optional)] - width: Option<&'a str>, - #[props(!optional)] - height: Option<&'a str>, -} -#[allow(non_snake_case)] -pub(crate) fn NumbericInput<'a>(cx: Scope<'a, NumbericInputProps>) -> Element<'a> { - let tui_query: Query = cx.consume_context().unwrap(); - let tui_query_clone = tui_query.clone(); - - let text_ref = use_ref(cx, || { - if let Some(intial_text) = cx.props.value { - intial_text.to_string() - } else { - String::new() - } - }); - let cursor = use_ref(cx, Cursor::default); - let dragging = use_state(cx, || false); - - let text = text_ref.read().clone(); - let start_highlight = cursor.read().first().idx(&*text); - let end_highlight = cursor.read().last().idx(&*text); - let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight); - let (text_highlighted, text_after_second_cursor) = - text_after_first_cursor.split_at(end_highlight - start_highlight); - - let max_len = cx - .props - .max_length - .as_ref() - .and_then(|s| s.parse().ok()) - .unwrap_or(usize::MAX); - - let width = cx - .props - .width - .map(|s| s.to_string()) - // px is the same as em in tui - .or_else(|| cx.props.size.map(|s| s.to_string() + "px")) - .unwrap_or_else(|| "10px".to_string()); - let height = cx.props.height.unwrap_or("3px"); - - // don't draw a border unless there is enough space - let border = if width - .strip_suffix("px") - .and_then(|w| w.parse::().ok()) - .filter(|w| *w < 3) - .is_some() - || height - .strip_suffix("px") - .and_then(|h| h.parse::().ok()) - .filter(|h| *h < 3) - .is_some() - { - "none" - } else { - "solid" - }; - - let update = |text: String| { - if let Some(input_handler) = &cx.props.raw_oninput { - input_handler.call(FormData { - value: text, - values: HashMap::new(), - files: None, - }); - } - }; - let increase = move || { - let mut text = text_ref.write(); - *text = (text.parse::().unwrap_or(0.0) + 1.0).to_string(); - update(text.clone()); - }; - let decrease = move || { - let mut text = text_ref.write(); - *text = (text.parse::().unwrap_or(0.0) - 1.0).to_string(); - update(text.clone()); - }; - - render! { - div{ - width: "{width}", - height: "{height}", - border_style: "{border}", - - onkeydown: move |k| { - let is_text = match k.key(){ - Key::ArrowLeft | Key::ArrowRight | Key::Backspace => true, - Key::Character(c) if c=="." || c== "-" || c.chars().all(|c|c.is_numeric())=> true, - _ => false, - }; - if is_text{ - let mut text = text_ref.write(); - cursor.write().handle_input(&k.code(), &k.key(), &k.modifiers(), &mut *text, max_len); - update(text.clone()); - - let node = tui_query.get(get_root_id(cx).unwrap()); - let Point{ x, y } = node.pos().unwrap(); - - let Pos { col, row } = cursor.read().start; - let (x, y) = (col as u16 + x as u16 + u16::from(border != "none"), row as u16 + y as u16 + u16::from(border != "none")); - if let Ok(pos) = crossterm::cursor::position() { - if pos != (x, y){ - execute!(stdout(), MoveTo(x, y)).unwrap(); - } - } - else{ - execute!(stdout(), MoveTo(x, y)).unwrap(); - } - } - else{ - match k.key() { - Key::ArrowUp =>{ - increase(); - } - Key::ArrowDown =>{ - decrease(); - } - _ => () - } - } - }, - onmousemove: move |evt| { - if *dragging.get() { - let offset = evt.data.element_coordinates(); - let mut new = Pos::new(offset.x as usize, offset.y as usize); - if border != "none" { - new.col = new.col.saturating_sub(1); - } - // textboxs are only one line tall - new.row = 0; - - if new != cursor.read().start { - cursor.write().end = Some(new); - } - } - }, - onmousedown: move |evt| { - let offset = evt.data.element_coordinates(); - let mut new = Pos::new(offset.x as usize, offset.y as usize); - if border != "none" { - new.col = new.col.saturating_sub(1); - } - new.row = 0; - - new.realize_col(text_ref.read().as_str()); - cursor.set(Cursor::from_start(new)); - dragging.set(true); - let node = tui_query_clone.get(get_root_id(cx).unwrap()); - let Point{ x, y } = node.pos().unwrap(); - - let Pos { col, row } = cursor.read().start; - let (x, y) = (col as u16 + x as u16 + u16::from(border != "none"), row as u16 + y as u16 + u16::from(border != "none")); - if let Ok(pos) = crossterm::cursor::position() { - if pos != (x, y){ - execute!(stdout(), MoveTo(x, y)).unwrap(); - } - } - else{ - execute!(stdout(), MoveTo(x, y)).unwrap(); - } - }, - onmouseup: move |_| { - dragging.set(false); - }, - onmouseleave: move |_| { - dragging.set(false); - }, - onmouseenter: move |_| { - dragging.set(false); - }, - onfocusout: |_| { - execute!(stdout(), MoveTo(0, 1000)).unwrap(); - }, - - "{text_before_first_cursor}" - - span{ - background_color: "rgba(255, 255, 255, 50%)", - - "{text_highlighted}" - } - - "{text_after_second_cursor}" - } - } -} diff --git a/packages/dioxus-tui/src/widgets/password.rs b/packages/dioxus-tui/src/widgets/password.rs deleted file mode 100644 index 07491c140..000000000 --- a/packages/dioxus-tui/src/widgets/password.rs +++ /dev/null @@ -1,193 +0,0 @@ -use crate::widgets::get_root_id; -use crossterm::{cursor::*, execute}; -use dioxus::prelude::*; -use dioxus_elements::input_data::keyboard_types::Key; -use dioxus_html as dioxus_elements; -use dioxus_html::FormData; -use dioxus_native_core::utils::cursor::{Cursor, Pos}; -use rink::Query; -use std::{collections::HashMap, io::stdout}; -use taffy::geometry::Point; - -#[derive(Props)] -pub(crate) struct PasswordProps<'a> { - #[props(!optional)] - raw_oninput: Option<&'a EventHandler<'a, FormData>>, - #[props(!optional)] - value: Option<&'a str>, - #[props(!optional)] - size: Option<&'a str>, - #[props(!optional)] - max_length: Option<&'a str>, - #[props(!optional)] - width: Option<&'a str>, - #[props(!optional)] - height: Option<&'a str>, -} -#[allow(non_snake_case)] -pub(crate) fn Password<'a>(cx: Scope<'a, PasswordProps>) -> Element<'a> { - let tui_query: Query = cx.consume_context().unwrap(); - let tui_query_clone = tui_query.clone(); - - let text_ref = use_ref(cx, || { - if let Some(intial_text) = cx.props.value { - intial_text.to_string() - } else { - String::new() - } - }); - let cursor = use_ref(cx, Cursor::default); - let dragging = use_state(cx, || false); - - let text = text_ref.read().clone(); - let start_highlight = cursor.read().first().idx(&*text); - let end_highlight = cursor.read().last().idx(&*text); - let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight); - let (text_highlighted, text_after_second_cursor) = - text_after_first_cursor.split_at(end_highlight - start_highlight); - - let text_before_first_cursor = ".".repeat(text_before_first_cursor.len()); - let text_highlighted = ".".repeat(text_highlighted.len()); - let text_after_second_cursor = ".".repeat(text_after_second_cursor.len()); - - let max_len = cx - .props - .max_length - .as_ref() - .and_then(|s| s.parse().ok()) - .unwrap_or(usize::MAX); - - let width = cx - .props - .width - .map(|s| s.to_string()) - // px is the same as em in tui - .or_else(|| cx.props.size.map(|s| s.to_string() + "px")) - .unwrap_or_else(|| "10px".to_string()); - let height = cx.props.height.unwrap_or("3px"); - - // don't draw a border unless there is enough space - let border = if width - .strip_suffix("px") - .and_then(|w| w.parse::().ok()) - .filter(|w| *w < 3) - .is_some() - || height - .strip_suffix("px") - .and_then(|h| h.parse::().ok()) - .filter(|h| *h < 3) - .is_some() - { - "none" - } else { - "solid" - }; - - let onkeydown = move |k: KeyboardEvent| { - if k.key() == Key::Enter { - return; - } - let mut text = text_ref.write(); - cursor - .write() - .handle_input(&k.code(), &k.key(), &k.modifiers(), &mut *text, max_len); - if let Some(input_handler) = &cx.props.raw_oninput { - input_handler.call(FormData { - value: text.clone(), - values: HashMap::new(), - files: None, - }); - } - - let node = tui_query.get(get_root_id(cx).unwrap()); - let Point { x, y } = node.pos().unwrap(); - - let Pos { col, row } = cursor.read().start; - let (x, y) = ( - col as u16 + x as u16 + u16::from(border != "none"), - row as u16 + y as u16 + u16::from(border != "none"), - ); - if let Ok(pos) = crossterm::cursor::position() { - if pos != (x, y) { - execute!(stdout(), MoveTo(x, y)).unwrap(); - } - } else { - execute!(stdout(), MoveTo(x, y)).unwrap(); - } - }; - - render! { - div { - width: "{width}", - height: "{height}", - border_style: "{border}", - - onkeydown: onkeydown, - - onmousemove: move |evt| { - if *dragging.get() { - let offset = evt.data.element_coordinates(); - let mut new = Pos::new(offset.x as usize, offset.y as usize); - if border != "none" { - new.col = new.col.saturating_sub(1); - } - // textboxs are only one line tall - new.row = 0; - - if new != cursor.read().start { - cursor.write().end = Some(new); - } - } - }, - - onmousedown: move |evt| { - let offset = evt.data.element_coordinates(); - let mut new = Pos::new(offset.x as usize, offset.y as usize); - if border != "none" { - new.col = new.col.saturating_sub(1); - } - // textboxs are only one line tall - new.row = 0; - - new.realize_col(text_ref.read().as_str()); - cursor.set(Cursor::from_start(new)); - dragging.set(true); - let node = tui_query_clone.get(get_root_id(cx).unwrap()); - let Point{ x, y } = node.pos().unwrap(); - - let Pos { col, row } = cursor.read().start; - let (x, y) = (col as u16 + x as u16 + u16::from(border != "none"), row as u16 + y as u16 + u16::from(border != "none")); - if let Ok(pos) = crossterm::cursor::position() { - if pos != (x, y){ - execute!(stdout(), MoveTo(x, y)).unwrap(); - } - } - else{ - execute!(stdout(), MoveTo(x, y)).unwrap(); - } - }, - onmouseup: move |_| { - dragging.set(false); - }, - onmouseleave: move |_| { - dragging.set(false); - }, - onmouseenter: move |_| { - dragging.set(false); - }, - onfocusout: |_| { - execute!(stdout(), MoveTo(0, 1000)).unwrap(); - }, - - "{text_before_first_cursor}" - - span{ - background_color: "rgba(255, 255, 255, 50%)", - - "{text_highlighted}" - } - - "{text_after_second_cursor}" - } - } -} diff --git a/packages/dioxus-tui/src/widgets/slider.rs b/packages/dioxus-tui/src/widgets/slider.rs deleted file mode 100644 index 9e8d63cb9..000000000 --- a/packages/dioxus-tui/src/widgets/slider.rs +++ /dev/null @@ -1,107 +0,0 @@ -use std::collections::HashMap; - -use crate::widgets::get_root_id; -use dioxus::prelude::*; -use dioxus_elements::input_data::keyboard_types::Key; -use dioxus_html as dioxus_elements; -use dioxus_html::FormData; -use rink::Query; - -#[derive(Props)] -pub(crate) struct SliderProps<'a> { - #[props(!optional)] - raw_oninput: Option<&'a EventHandler<'a, FormData>>, - #[props(!optional)] - value: Option<&'a str>, - #[props(!optional)] - width: Option<&'a str>, - #[props(!optional)] - height: Option<&'a str>, - #[props(!optional)] - min: Option<&'a str>, - #[props(!optional)] - max: Option<&'a str>, - #[props(!optional)] - step: Option<&'a str>, -} - -#[allow(non_snake_case)] -pub(crate) fn Slider<'a>(cx: Scope<'a, SliderProps>) -> Element<'a> { - let tui_query: Query = cx.consume_context().unwrap(); - - let value_state = use_state(cx, || 0.0); - let value: Option = cx.props.value.and_then(|v| v.parse().ok()); - let width = cx.props.width.unwrap_or("20px"); - let height = cx.props.height.unwrap_or("1px"); - let min = cx.props.min.and_then(|v| v.parse().ok()).unwrap_or(0.0); - let max = cx.props.max.and_then(|v| v.parse().ok()).unwrap_or(100.0); - let size = max - min; - let step = cx - .props - .step - .and_then(|v| v.parse().ok()) - .unwrap_or(size / 10.0); - - let current_value = match value { - Some(value) => value, - None => *value_state.get(), - } - .clamp(min, max); - - let fst_width = 100.0 * (current_value - min) / size; - let snd_width = 100.0 * (max - current_value) / size; - assert!(fst_width + snd_width > 99.0 && fst_width + snd_width < 101.0); - - let update = |value: String| { - if let Some(oninput) = cx.props.raw_oninput { - oninput.call(FormData { - value, - values: HashMap::new(), - files: None, - }); - } - }; - - render! { - div{ - width: "{width}", - height: "{height}", - display: "flex", - flex_direction: "row", - onkeydown: move |event| { - match event.key() { - Key::ArrowLeft => { - value_state.set((current_value - step).clamp(min, max)); - update(value_state.current().to_string()); - } - Key::ArrowRight => { - value_state.set((current_value + step).clamp(min, max)); - update(value_state.current().to_string()); - } - _ => () - } - }, - onmousemove: move |evt| { - let mouse = evt.data; - if !mouse.held_buttons().is_empty(){ - let node = tui_query.get(get_root_id(cx).unwrap()); - let width = node.size().unwrap().width; - let offset = mouse.element_coordinates(); - value_state.set(min + size*(offset.x as f32) / width as f32); - update(value_state.current().to_string()); - } - }, - div{ - width: "{fst_width}%", - background_color: "rgba(10,10,10,0.5)", - } - div{ - "|" - } - div{ - width: "{snd_width}%", - background_color: "rgba(10,10,10,0.5)", - } - } - } -} diff --git a/packages/dioxus-tui/src/widgets/textbox.rs b/packages/dioxus-tui/src/widgets/textbox.rs deleted file mode 100644 index fcfaf0a90..000000000 --- a/packages/dioxus-tui/src/widgets/textbox.rs +++ /dev/null @@ -1,182 +0,0 @@ -use crate::widgets::get_root_id; -use crossterm::{cursor::*, execute}; -use dioxus::prelude::*; -use dioxus_elements::input_data::keyboard_types::Key; -use dioxus_html as dioxus_elements; -use dioxus_html::FormData; -use dioxus_native_core::utils::cursor::{Cursor, Pos}; -use rink::Query; -use std::{collections::HashMap, io::stdout}; -use taffy::geometry::Point; - -#[derive(Props)] -pub(crate) struct TextBoxProps<'a> { - #[props(!optional)] - raw_oninput: Option<&'a EventHandler<'a, FormData>>, - #[props(!optional)] - value: Option<&'a str>, - #[props(!optional)] - size: Option<&'a str>, - #[props(!optional)] - max_length: Option<&'a str>, - #[props(!optional)] - width: Option<&'a str>, - #[props(!optional)] - height: Option<&'a str>, -} -#[allow(non_snake_case)] -pub(crate) fn TextBox<'a>(cx: Scope<'a, TextBoxProps>) -> Element<'a> { - let tui_query: Query = cx.consume_context().unwrap(); - let tui_query_clone = tui_query.clone(); - - let text_ref = use_ref(cx, || { - if let Some(intial_text) = cx.props.value { - intial_text.to_string() - } else { - String::new() - } - }); - let cursor = use_ref(cx, Cursor::default); - let dragging = use_state(cx, || false); - - let text = text_ref.read().clone(); - let start_highlight = cursor.read().first().idx(&*text); - let end_highlight = cursor.read().last().idx(&*text); - let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight); - let (text_highlighted, text_after_second_cursor) = - text_after_first_cursor.split_at(end_highlight - start_highlight); - - let max_len = cx - .props - .max_length - .as_ref() - .and_then(|s| s.parse().ok()) - .unwrap_or(usize::MAX); - - let width = cx - .props - .width - .map(|s| s.to_string()) - // px is the same as em in tui - .or_else(|| cx.props.size.map(|s| s.to_string() + "px")) - .unwrap_or_else(|| "10px".to_string()); - let height = cx.props.height.unwrap_or("3px"); - - // don't draw a border unless there is enough space - let border = if width - .strip_suffix("px") - .and_then(|w| w.parse::().ok()) - .filter(|w| *w < 3) - .is_some() - || height - .strip_suffix("px") - .and_then(|h| h.parse::().ok()) - .filter(|h| *h < 3) - .is_some() - { - "none" - } else { - "solid" - }; - - render! { - div{ - width: "{width}", - height: "{height}", - border_style: "{border}", - - onkeydown: move |k| { - if k.key() == Key::Enter { - return; - } - let mut text = text_ref.write(); - cursor.write().handle_input(&k.code(), &k.key(), &k.modifiers(), &mut *text, max_len); - if let Some(input_handler) = &cx.props.raw_oninput{ - input_handler.call(FormData{ - value: text.clone(), - values: HashMap::new(), - files: None - }); - } - - let node = tui_query.get(get_root_id(cx).unwrap()); - let Point{ x, y } = node.pos().unwrap(); - - let Pos { col, row } = cursor.read().start; - let (x, y) = (col as u16 + x as u16 + u16::from(border != "none"), row as u16 + y as u16 + u16::from(border != "none")); - if let Ok(pos) = crossterm::cursor::position() { - if pos != (x, y){ - execute!(stdout(), MoveTo(x, y)).unwrap(); - } - } - else{ - execute!(stdout(), MoveTo(x, y)).unwrap(); - } - }, - - onmousemove: move |evt| { - if *dragging.get() { - let offset = evt.data.element_coordinates(); - let mut new = Pos::new(offset.x as usize, offset.y as usize); - if border != "none" { - new.col = new.col.saturating_sub(1); - } - // textboxs are only one line tall - new.row = 0; - - if new != cursor.read().start { - cursor.write().end = Some(new); - } - } - }, - onmousedown: move |evt| { - let offset = evt.data.element_coordinates(); - let mut new = Pos::new(offset.x as usize, offset.y as usize); - if border != "none" { - new.col = new.col.saturating_sub(1); - } - // textboxs are only one line tall - new.row = 0; - - new.realize_col(text_ref.read().as_str()); - cursor.set(Cursor::from_start(new)); - dragging.set(true); - let node = tui_query_clone.get(get_root_id(cx).unwrap()); - let Point{ x, y } = node.pos().unwrap(); - - let Pos { col, row } = cursor.read().start; - let (x, y) = (col as u16 + x as u16 + u16::from(border != "none"), row as u16 + y as u16 + u16::from(border != "none")); - if let Ok(pos) = crossterm::cursor::position() { - if pos != (x, y){ - execute!(stdout(), MoveTo(x, y)).unwrap(); - } - } - else{ - execute!(stdout(), MoveTo(x, y)).unwrap(); - } - }, - onmouseup: move |_| { - dragging.set(false); - }, - onmouseleave: move |_| { - dragging.set(false); - }, - onmouseenter: move |_| { - dragging.set(false); - }, - onfocusout: |_| { - execute!(stdout(), MoveTo(0, 1000)).unwrap(); - }, - - "{text_before_first_cursor}" - - span{ - background_color: "rgba(255, 255, 255, 50%)", - - "{text_highlighted}" - } - - "{text_after_second_cursor}" - } - } -} diff --git a/packages/hooks/Cargo.toml b/packages/hooks/Cargo.toml index 6a3205c68..598ffedab 100644 --- a/packages/hooks/Cargo.toml +++ b/packages/hooks/Cargo.toml @@ -20,3 +20,4 @@ log = { workspace = true } futures-util = { workspace = true, default-features = false } dioxus-core = { workspace = true } dioxus = { workspace = true } +web-sys = { version = "0.3.64", features = ["Document", "Window", "Element"] } diff --git a/packages/hooks/src/lib.rs b/packages/hooks/src/lib.rs index 21795a9d6..58b6c82f5 100644 --- a/packages/hooks/src/lib.rs +++ b/packages/hooks/src/lib.rs @@ -50,6 +50,9 @@ macro_rules! to_owned { }; } +mod use_on_unmount; +pub use use_on_unmount::*; + mod usecontext; pub use usecontext::*; diff --git a/packages/hooks/src/use_on_unmount.rs b/packages/hooks/src/use_on_unmount.rs new file mode 100644 index 000000000..1fa4449c1 --- /dev/null +++ b/packages/hooks/src/use_on_unmount.rs @@ -0,0 +1,74 @@ +/// Creats a callback that will be run before the component is removed. This can be used to clean up side effects from the component (created with use_effect) +/// +/// Example: +/// ```rust +/// use dioxus::prelude::*; + +/// fn app(cx: Scope) -> Element { +/// let state = use_state(cx, || true); +/// render! { +/// for _ in 0..100 { +/// h1 { +/// "spacer" +/// } +/// } +/// if **state { +/// render! { +/// child_component {} +/// } +/// } +/// button { +/// onclick: move |_| { +/// state.set(!*state.get()); +/// }, +/// "Unmount element" +/// } +/// } +/// } + +/// fn child_component(cx: Scope) -> Element { +/// let original_scroll_position = use_state(cx, || 0.0); +/// use_effect(cx, (), move |_| { +/// to_owned![original_scroll_position]; +/// async move { +/// let window = web_sys::window().unwrap(); +/// let document = window.document().unwrap(); +/// let element = document.get_element_by_id("my_element").unwrap(); +/// element.scroll_into_view(); +/// original_scroll_position.set(window.scroll_y().unwrap()); +/// } +/// }); + +/// use_on_unmount(cx, { +/// to_owned![original_scroll_position]; +/// /// restore scroll to the top of the page +/// move || { +/// let window = web_sys::window().unwrap(); +/// window.scroll_with_x_and_y(*original_scroll_position.current(), 0.0); +/// } +/// }); + +/// render!{ +/// div { +/// id: "my_element", +/// "hello" +/// } +/// } +/// } +/// ``` +pub fn use_on_unmount(cx: &dioxus_core::ScopeState, destroy: D) { + cx.use_hook(|| LifeCycle { + ondestroy: Some(destroy), + }); +} + +struct LifeCycle { + ondestroy: Option, +} + +impl Drop for LifeCycle { + fn drop(&mut self) { + let f = self.ondestroy.take().unwrap(); + f(); + } +} diff --git a/packages/native-core-macro/src/lib.rs b/packages/native-core-macro/src/lib.rs index 2c8cef835..ff0e4629d 100644 --- a/packages/native-core-macro/src/lib.rs +++ b/packages/native-core-macro/src/lib.rs @@ -224,14 +224,14 @@ pub fn partial_derive_state(_: TokenStream, input: TokenStream) -> TokenStream { let get_parent_view = { if parent_dependencies.is_empty() { quote! { - let raw_parent = tree.parent_id(id).map(|_| ()); + 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::>(); quote! { - let raw_parent = tree.parent_id(id).and_then(|parent_id| { + 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 _,)*) @@ -261,14 +261,14 @@ pub fn partial_derive_state(_: TokenStream, input: TokenStream) -> TokenStream { let get_child_view = { if child_dependencies.is_empty() { quote! { - let raw_children: Vec<_> = tree.children_ids(id).into_iter().map(|_| ()).collect(); + 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::>(); quote! { - let raw_children: Vec<_> = tree.children_ids(id).into_iter().filter_map(|id| { + 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 _,)*) diff --git a/packages/native-core/examples/custom_attr.rs b/packages/native-core/examples/custom_attr.rs index 5a1fcfb8c..e7bbb36ea 100644 --- a/packages/native-core/examples/custom_attr.rs +++ b/packages/native-core/examples/custom_attr.rs @@ -215,7 +215,7 @@ fn main() -> Result<(), Box> { let _to_rerender = rdom.update_state(ctx); // render... - rdom.traverse_depth_first(|node| { + rdom.traverse_depth_first_advanced(true, |node| { let indent = " ".repeat(node.height() as usize); let color = *node.get::().unwrap(); let size = *node.get::().unwrap(); diff --git a/packages/native-core/examples/font_size.rs b/packages/native-core/examples/font_size.rs index 937db0c17..40261baa2 100644 --- a/packages/native-core/examples/font_size.rs +++ b/packages/native-core/examples/font_size.rs @@ -160,7 +160,7 @@ fn main() -> Result<(), Box> { let _to_rerender = rdom.update_state(ctx); // render... - rdom.traverse_depth_first(|node| { + rdom.traverse_depth_first_advanced(true, |node| { let indent = " ".repeat(node.height() as usize); let font_size = *node.get::().unwrap(); let size = *node.get::().unwrap(); diff --git a/packages/native-core/examples/simple.rs b/packages/native-core/examples/simple.rs index 469147c35..276d06d08 100644 --- a/packages/native-core/examples/simple.rs +++ b/packages/native-core/examples/simple.rs @@ -206,7 +206,7 @@ fn main() -> Result<(), Box> { let _to_rerender = rdom.update_state(ctx); // render... - rdom.traverse_depth_first(|node| { + rdom.traverse_depth_first_advanced(true, |node| { let indent = " ".repeat(node.height() as usize); let color = *node.get::().unwrap(); let size = *node.get::().unwrap(); diff --git a/packages/native-core/examples/simple_dioxus.rs b/packages/native-core/examples/simple_dioxus.rs index fa1b202dd..af0e4ca00 100644 --- a/packages/native-core/examples/simple_dioxus.rs +++ b/packages/native-core/examples/simple_dioxus.rs @@ -234,7 +234,7 @@ fn main() -> Result<(), Box> { let _to_rerender = rdom.update_state(ctx); // render... - rdom.traverse_depth_first(|node| { + rdom.traverse_depth_first_advanced(true, |node| { let indent = " ".repeat(node.height() as usize); let color = *node.get::().unwrap(); let size = *node.get::().unwrap(); diff --git a/packages/native-core/src/custom_element.rs b/packages/native-core/src/custom_element.rs new file mode 100644 index 000000000..3675089f6 --- /dev/null +++ b/packages/native-core/src/custom_element.rs @@ -0,0 +1,180 @@ +//! 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 { + builders: FxHashMap<(&'static str, Option<&'static str>), CustomElementBuilder>, +} + +impl Default for CustomElementRegistry { + fn default() -> Self { + Self { + builders: FxHashMap::default(), + } + } +} + +impl CustomElementRegistry { + pub fn register(&mut self) + where + F: CustomElementFactory, + U: CustomElementUpdater, + { + self.builders.insert( + (F::NAME, F::NAMESPACE), + CustomElementBuilder { + create: |node| Box::new(F::create(node)), + }, + ); + } + + pub fn add_shadow_dom(&self, mut node: NodeMut) { + 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 { + create: fn(NodeMut) -> Box>, +} + +/// 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: 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) -> Self; + + /// The root node of the custom element. These roots must be not change once the element is created. + fn roots(&self) -> Vec; + + /// The slot to render children of the element into. The slot must be not change once the element is created. + fn slot(&self) -> Option { + 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, attributes: &AttributeMask); +} + +/// A factory for creating custom elements +/// +/// Register with [`RealDom::register_custom_element_with_factory`] +pub trait CustomElementFactory, 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) -> W; +} + +impl, V: FromAnyValue + Send + Sync> CustomElementFactory for W { + const NAME: &'static str = W::NAME; + + const NAMESPACE: Option<&'static str> = W::NAMESPACE; + + fn create(node: NodeMut) -> Self { + Self::create(node) + } +} + +/// A trait for updating custom elements +pub trait CustomElementUpdater: 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, attributes: &AttributeMask); + + /// The root node of the custom element. These roots must be not change once the element is created. + fn roots(&self) -> Vec; + + /// The slot to render children of the element into. The slot must be not change once the element is created. + fn slot(&self) -> Option { + None + } +} + +impl, V: FromAnyValue + Send + Sync> CustomElementUpdater for W { + fn attributes_changed(&mut self, light_root: NodeMut, attributes: &AttributeMask) { + self.attributes_changed(light_root, attributes); + } + + fn roots(&self) -> Vec { + self.roots() + } + + fn slot(&self) -> Option { + self.slot() + } +} + +/// A dynamic trait object wrapper for [`CustomElementUpdater`] +#[derive(Component, Clone)] +pub(crate) struct CustomElementManager { + inner: Arc>>>, +} + +impl CustomElementManager { + /// Update the custom element based on attributes changed. + pub fn on_attributes_changed(&self, light_root: NodeMut, attributes: &AttributeMask) { + self.inner + .write() + .unwrap() + .attributes_changed(light_root, attributes); + } +} diff --git a/packages/native-core/src/lib.rs b/packages/native-core/src/lib.rs index 5a88ff205..c5ad38341 100644 --- a/packages/native-core/src/lib.rs +++ b/packages/native-core/src/lib.rs @@ -7,6 +7,7 @@ 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")] @@ -18,6 +19,7 @@ mod passes; pub mod real_dom; pub mod tree; pub mod utils; + pub use shipyard::EntityId as NodeId; pub mod exports { diff --git a/packages/native-core/src/node_ref.rs b/packages/native-core/src/node_ref.rs index 218348c90..156ecdbd6 100644 --- a/packages/native-core/src/node_ref.rs +++ b/packages/native-core/src/node_ref.rs @@ -60,7 +60,7 @@ impl<'a, V: FromAnyValue> NodeView<'a, V> { NodeType::Element(ElementNode { attributes, .. }) => Some( attributes .iter() - .filter(move |(attr, _)| self.mask.attritutes.contains_attribute(&attr.name)) + .filter(move |(attr, _)| self.mask.attritutes.contains(&attr.name)) .map(|(attr, val)| OwnedAttributeView { attribute: attr, value: val, @@ -107,8 +107,8 @@ pub enum AttributeMask { } impl AttributeMask { - /// Check if the given attribute is visible - fn contains_attribute(&self, attr: &str) -> bool { + /// 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), @@ -187,25 +187,50 @@ impl NodeMask { 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. diff --git a/packages/native-core/src/node_watcher.rs b/packages/native-core/src/node_watcher.rs index 27a449ef9..d437b158c 100644 --- a/packages/native-core/src/node_watcher.rs +++ b/packages/native-core/src/node_watcher.rs @@ -1,17 +1,19 @@ //! Helpers for watching for changes in the DOM tree. -use crate::{node::FromAnyValue, prelude::*}; +use crate::{node::FromAnyValue, node_ref::AttributeMask, prelude::*}; /// A trait for watching for changes in the DOM tree. pub trait NodeWatcher { /// Called after a node is added to the tree. - fn on_node_added(&self, _node: NodeMut) {} + fn on_node_added(&mut self, _node: NodeMut) {} /// Called before a node is removed from the tree. - fn on_node_removed(&self, _node: NodeMut) {} + fn on_node_removed(&mut self, _node: NodeMut) {} /// Called after a node is moved to a new parent. - fn on_node_moved(&self, _node: NodeMut) {} - // /// Called after the text content of a node is changed. - // fn on_text_changed(&self, _node: NodeMut) {} - // /// Called after an attribute of an element is changed. - // fn on_attribute_changed(&self, _node: NodeMut, attribute: &str) {} + fn on_node_moved(&mut self, _node: NodeMut) {} +} + +/// A trait for watching for changes to attributes of an element. +pub trait AttributeWatcher { + /// Called before update_state is called on the RealDom + fn on_attributes_changed(&self, _node: NodeMut, _attributes: &AttributeMask) {} } diff --git a/packages/native-core/src/passes.rs b/packages/native-core/src/passes.rs index b453e32dd..a76dbc61c 100644 --- a/packages/native-core/src/passes.rs +++ b/packages/native-core/src/passes.rs @@ -104,6 +104,9 @@ pub trait State: Any + Send + Sync { /// 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, @@ -150,6 +153,7 @@ pub trait State: Any + Send + Sync { dependants: Default::default(), mask: node_mask, pass_direction: pass_direction::(), + enter_shadow_dom: Self::TRAVERSE_SHADOW_DOM, workload: Self::workload_system, phantom: PhantomData, } @@ -228,28 +232,42 @@ pub fn run_pass( } } +#[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 parent: Vec, + pub(crate) parent: Vec, /// The states in the child direction that should be invalidated when this state is invalidated - pub child: Vec, + pub(crate) child: Vec, /// The states in the node direction that should be invalidated when this state is invalidated - pub node: Vec, + pub(crate) node: Vec, } impl Dependants { fn mark_dirty(&self, dirty: &DirtyNodeStates, id: NodeId, tree: &impl TreeRef, height: u16) { - for dependant in &self.child { - for id in tree.children_ids(id) { - dirty.insert(*dependant, id, height + 1); + 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 in &self.parent { - if let Some(id) = tree.parent_id(id) { - dirty.insert(*dependant, 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); } } @@ -269,6 +287,7 @@ pub struct TypeErasedState { pub(crate) mask: NodeMask, pub(crate) workload: fn(TypeId, Arc, PassDirection) -> WorkloadSystem, pub(crate) pass_direction: PassDirection, + pub(crate) enter_shadow_dom: bool, phantom: PhantomData, } diff --git a/packages/native-core/src/real_dom.rs b/packages/native-core/src/real_dom.rs index c344b9041..a0eff4b2c 100644 --- a/packages/native-core/src/real_dom.rs +++ b/packages/native-core/src/real_dom.rs @@ -10,12 +10,16 @@ use std::collections::VecDeque; use std::ops::{Deref, DerefMut}; use std::sync::{Arc, RwLock}; +use crate::custom_element::{ + CustomElement, CustomElementFactory, CustomElementManager, CustomElementRegistry, + CustomElementUpdater, +}; use crate::node::{ ElementNode, FromAnyValue, NodeType, OwnedAttributeDiscription, OwnedAttributeValue, TextNode, }; use crate::node_ref::{NodeMask, NodeMaskBuilder}; -use crate::node_watcher::NodeWatcher; -use crate::passes::{DirtyNodeStates, PassDirection, TypeErasedState}; +use crate::node_watcher::{AttributeWatcher, NodeWatcher}; +use crate::passes::{Dependant, DirtyNodeStates, PassDirection, TypeErasedState}; use crate::prelude::AttributeMaskBuilder; use crate::tree::{TreeMut, TreeMutView, TreeRef, TreeRefView}; use crate::NodeId; @@ -49,6 +53,7 @@ impl Deref for DirtyNodesResult { pub(crate) struct NodesDirty { passes_updated: FxHashMap>, nodes_updated: FxHashMap, + nodes_created: FxHashSet, pub(crate) passes: Box<[TypeErasedState]>, } @@ -92,6 +97,7 @@ impl NodesDirty { } type NodeWatchers = Arc + Send + Sync>>>>; +type AttributeWatchers = Arc + Send + Sync>>>>; /// A Dom that can sync with the VirtualDom mutations intended for use in lazy renderers. /// The render state passes from parent to children and or accumulates state from children to parents. @@ -108,8 +114,10 @@ pub struct RealDom { nodes_listening: FxHashMap>, pub(crate) dirty_nodes: NodesDirty, node_watchers: NodeWatchers, + attribute_watchers: AttributeWatchers, workload: ScheduledWorkload, root_id: NodeId, + custom_elements: Arc>>, phantom: std::marker::PhantomData, } @@ -123,19 +131,25 @@ impl RealDom { let (current, before) = before.split_last_mut().unwrap(); for state in before.iter_mut().chain(after.iter_mut()) { let dependants = Arc::get_mut(&mut state.dependants).unwrap(); + + let current_dependant = Dependant { + type_id: current.this_type_id, + enter_shadow_dom: current.enter_shadow_dom, + }; + // If this node depends on the other state as a parent, then the other state should update its children of the current type when it is invalidated if current .parent_dependancies_ids .contains(&state.this_type_id) - && !dependants.child.contains(¤t.this_type_id) + && !dependants.child.contains(¤t_dependant) { - dependants.child.push(current.this_type_id); + dependants.child.push(current_dependant); } // If this node depends on the other state as a child, then the other state should update its parent of the current type when it is invalidated if current.child_dependancies_ids.contains(&state.this_type_id) - && !dependants.parent.contains(¤t.this_type_id) + && !dependants.parent.contains(¤t_dependant) { - dependants.parent.push(current.this_type_id); + dependants.parent.push(current_dependant); } // If this node depends on the other state as a sibling, then the other state should update its siblings of the current type when it is invalidated if current.node_dependancies_ids.contains(&state.this_type_id) @@ -146,15 +160,19 @@ impl RealDom { } // If the current state depends on itself, then it should update itself when it is invalidated let dependants = Arc::get_mut(&mut current.dependants).unwrap(); + let current_dependant = Dependant { + type_id: current.this_type_id, + enter_shadow_dom: current.enter_shadow_dom, + }; match current.pass_direction { PassDirection::ChildToParent => { - if !dependants.parent.contains(¤t.this_type_id) { - dependants.parent.push(current.this_type_id); + if !dependants.parent.contains(¤t_dependant) { + dependants.parent.push(current_dependant); } } PassDirection::ParentToChild => { - if !dependants.child.contains(¤t.this_type_id) { - dependants.child.push(current.this_type_id); + if !dependants.child.contains(¤t_dependant) { + dependants.child.push(current_dependant); } } _ => {} @@ -191,10 +209,13 @@ impl RealDom { passes_updated, nodes_updated, passes: tracked_states, + nodes_created: [root_id].into_iter().collect(), }, node_watchers: Default::default(), + attribute_watchers: Default::default(), workload, root_id, + custom_elements: Default::default(), phantom: std::marker::PhantomData, } } @@ -211,8 +232,12 @@ impl RealDom { /// Create a new node of the given type in the dom and return a mutable reference to it. pub fn create_node(&mut self, node: impl Into>) -> NodeMut<'_, V> { - let id = self.world.add_entity(node.into()); + let node = node.into(); + let is_element = matches!(node, NodeType::Element(_)); + + let id = self.world.add_entity(node); self.tree_mut().create_node(id); + self.dirty_nodes .passes_updated .entry(id) @@ -220,10 +245,17 @@ impl RealDom { .extend(self.dirty_nodes.passes.iter().map(|x| x.this_type_id)); self.dirty_nodes .mark_dirty(id, NodeMaskBuilder::ALL.build()); - let watchers = self.node_watchers.clone(); - for watcher in &*watchers.read().unwrap() { - watcher.on_node_added(NodeMut::new(id, self)); + self.dirty_nodes.nodes_created.insert(id); + + // Create a custom element if needed + if is_element { + let custom_elements = self.custom_elements.clone(); + custom_elements + .read() + .unwrap() + .add_shadow_dom(NodeMut::new(id, self)); } + NodeMut::new(id, self) } @@ -284,8 +316,48 @@ impl RealDom { &mut self, ctx: SendAnyMap, ) -> (FxDashSet, FxHashMap) { + let nodes_created = std::mem::take(&mut self.dirty_nodes.nodes_created); + + // call node watchers + { + let watchers = self.node_watchers.clone(); + + // ignore watchers if they are already being modified + if let Ok(mut watchers) = watchers.try_write() { + for id in &nodes_created { + for watcher in &mut *watchers { + watcher.on_node_added(NodeMut::new(*id, self)); + } + } + }; + } + let passes = std::mem::take(&mut self.dirty_nodes.passes_updated); let nodes_updated = std::mem::take(&mut self.dirty_nodes.nodes_updated); + + for (node_id, mask) in &nodes_updated { + if self.contains(*node_id) { + // call attribute watchers but ignore watchers if they are already being modified + let watchers = self.attribute_watchers.clone(); + if let Ok(mut watchers) = watchers.try_write() { + for watcher in &mut *watchers { + watcher.on_attributes_changed( + self.get_mut(*node_id).unwrap(), + mask.attributes(), + ); + } + }; + + // call custom element watchers + let node = self.get_mut(*node_id).unwrap(); + let custom_element_manager = + node.get::>().map(|x| x.clone()); + if let Some(custom_element_manager) = custom_element_manager { + custom_element_manager.on_attributes_changed(node, mask.attributes()); + } + } + } + let dirty_nodes = DirtyNodeStates::with_passes(self.dirty_nodes.passes.iter().map(|p| p.this_type_id)); let tree = self.tree_ref(); @@ -312,27 +384,42 @@ impl RealDom { } /// Traverses the dom in a depth first manner, calling the provided function on each node. - pub fn traverse_depth_first(&self, mut f: impl FnMut(NodeRef)) { + /// If `enter_shadow_dom` is true, then the traversal will enter shadow doms in the tree. + pub fn traverse_depth_first_advanced( + &self, + enter_shadow_dom: bool, + mut f: impl FnMut(NodeRef), + ) { let mut stack = vec![self.root_id()]; let tree = self.tree_ref(); while let Some(id) = stack.pop() { if let Some(node) = self.get(id) { f(node); - let children = tree.children_ids(id); + let children = tree.children_ids_advanced(id, enter_shadow_dom); stack.extend(children.iter().copied().rev()); } } } + /// Traverses the dom in a depth first manner, calling the provided function on each node. + pub fn traverse_depth_first(&self, f: impl FnMut(NodeRef)) { + self.traverse_depth_first_advanced(true, f) + } + /// Traverses the dom in a breadth first manner, calling the provided function on each node. - pub fn traverse_breadth_first(&self, mut f: impl FnMut(NodeRef)) { + /// If `enter_shadow_dom` is true, then the traversal will enter shadow doms in the tree. + pub fn traverse_breadth_first_advanced( + &self, + enter_shadow_doms: bool, + mut f: impl FnMut(NodeRef), + ) { let mut queue = VecDeque::new(); queue.push_back(self.root_id()); let tree = self.tree_ref(); while let Some(id) = queue.pop_front() { if let Some(node) = self.get(id) { f(node); - let children = tree.children_ids(id); + let children = tree.children_ids_advanced(id, enter_shadow_doms); for id in children { queue.push_back(id); } @@ -340,12 +427,22 @@ impl RealDom { } } + /// Traverses the dom in a breadth first manner, calling the provided function on each node. + pub fn traverse_breadth_first(&self, f: impl FnMut(NodeRef)) { + self.traverse_breadth_first_advanced(true, f); + } + /// Traverses the dom in a depth first manner mutably, calling the provided function on each node. - pub fn traverse_depth_first_mut(&mut self, mut f: impl FnMut(NodeMut)) { + /// If `enter_shadow_dom` is true, then the traversal will enter shadow doms in the tree. + pub fn traverse_depth_first_mut_advanced( + &mut self, + enter_shadow_doms: bool, + mut f: impl FnMut(NodeMut), + ) { let mut stack = vec![self.root_id()]; while let Some(id) = stack.pop() { let tree = self.tree_ref(); - let mut children = tree.children_ids(id); + let mut children = tree.children_ids_advanced(id, enter_shadow_doms); drop(tree); children.reverse(); if let Some(node) = self.get_mut(id) { @@ -356,13 +453,23 @@ impl RealDom { } } + /// Traverses the dom in a depth first manner mutably, calling the provided function on each node. + pub fn traverse_depth_first_mut(&mut self, f: impl FnMut(NodeMut)) { + self.traverse_depth_first_mut_advanced(true, f) + } + /// Traverses the dom in a breadth first manner mutably, calling the provided function on each node. - pub fn traverse_breadth_first_mut(&mut self, mut f: impl FnMut(NodeMut)) { + /// If `enter_shadow_dom` is true, then the traversal will enter shadow doms in the tree. + pub fn traverse_breadth_first_mut_advanced( + &mut self, + enter_shadow_doms: bool, + mut f: impl FnMut(NodeMut), + ) { let mut queue = VecDeque::new(); queue.push_back(self.root_id()); while let Some(id) = queue.pop_front() { let tree = self.tree_ref(); - let children = tree.children_ids(id); + let children = tree.children_ids_advanced(id, enter_shadow_doms); drop(tree); if let Some(node) = self.get_mut(id) { f(node); @@ -373,11 +480,27 @@ impl RealDom { } } + /// Traverses the dom in a breadth first manner mutably, calling the provided function on each node. + pub fn traverse_breadth_first_mut(&mut self, f: impl FnMut(NodeMut)) { + self.traverse_breadth_first_mut_advanced(true, f); + } + /// Adds a [`NodeWatcher`] to the dom. Node watchers are called whenever a node is created or removed. pub fn add_node_watcher(&mut self, watcher: impl NodeWatcher + 'static + Send + Sync) { self.node_watchers.write().unwrap().push(Box::new(watcher)); } + /// Adds an [`AttributeWatcher`] to the dom. Attribute watchers are called whenever an attribute is changed. + pub fn add_attribute_watcher( + &mut self, + watcher: impl AttributeWatcher + 'static + Send + Sync, + ) { + self.attribute_watchers + .write() + .unwrap() + .push(Box::new(watcher)); + } + /// Returns a reference to the underlying world. Any changes made to the world will not update the reactive system. pub fn raw_world(&self) -> &World { &self.world @@ -387,6 +510,20 @@ impl RealDom { pub fn raw_world_mut(&mut self) -> &mut World { &mut self.world } + + /// Registers a new custom element. + pub fn register_custom_element>(&mut self) { + self.register_custom_element_with_factory::() + } + + /// Registers a new custom element with a custom factory. + pub fn register_custom_element_with_factory(&mut self) + where + F: CustomElementFactory, + U: CustomElementUpdater, + { + self.custom_elements.write().unwrap().register::() + } } /// A reference to a tracked component in a node. @@ -458,6 +595,14 @@ pub trait NodeImmutable: Sized { .then(|| ViewEntry::new(view, self.id())) } + /// 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 { + self.real_dom() + .tree_ref() + .children_ids_advanced(id, enter_shadow_dom) + } + /// Get the ids of the children of the current node #[inline] fn child_ids(&self) -> Vec { @@ -476,6 +621,14 @@ pub trait NodeImmutable: Sized { .collect() } + /// 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 { + self.real_dom() + .tree_ref() + .parent_id_advanced(id, enter_shadow_dom) + } + /// Get the id of the parent of the current node #[inline] fn parent_id(&self) -> Option { @@ -585,6 +738,14 @@ impl<'a, V: FromAnyValue + Send + Sync> NodeImmutable for NodeMut<'a, V> { } impl<'a, V: FromAnyValue + Send + Sync> NodeMut<'a, V> { + /// Reborrow the node mutably + pub fn reborrow(&mut self) -> NodeMut<'_, V> { + NodeMut { + id: self.id, + dom: self.dom, + } + } + /// Get the real dom this node was created in mutably #[inline(always)] pub fn real_dom_mut(&mut self) -> &mut RealDom { @@ -741,6 +902,7 @@ impl<'a, V: FromAnyValue + Send + Sync> NodeMut<'a, V> { } let id = self.id(); self.dom.tree_mut().replace(id, new); + self.remove(); } /// Add an event listener @@ -798,7 +960,7 @@ impl<'a, V: FromAnyValue + Send + Sync> NodeMut<'a, V> { /// mark that this node was removed for the incremental system fn mark_removed(&mut self) { let watchers = self.dom.node_watchers.clone(); - for watcher in &*watchers.read().unwrap() { + for watcher in &mut *watchers.write().unwrap() { watcher.on_node_removed(NodeMut::new(self.id(), self.dom)); } } @@ -806,9 +968,12 @@ impl<'a, V: FromAnyValue + Send + Sync> NodeMut<'a, V> { /// mark that this node was moved for the incremental system fn mark_moved(&mut self) { let watchers = self.dom.node_watchers.clone(); - for watcher in &*watchers.read().unwrap() { - watcher.on_node_moved(NodeMut::new(self.id(), self.dom)); - } + // ignore watchers if the we are inside of a watcher + if let Ok(mut watchers) = watchers.try_write() { + for watcher in &mut *watchers { + watcher.on_node_moved(NodeMut::new(self.id(), self.dom)); + } + }; } /// Get a mutable reference to the type of the current node @@ -925,6 +1090,15 @@ pub struct ElementNodeMut<'a, V: FromAnyValue + Send + Sync = ()> { dirty_nodes: &'a mut NodesDirty, } +impl std::fmt::Debug for ElementNodeMut<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ElementNodeMut") + .field("id", &self.id) + .field("element", &*self.element) + .finish() + } +} + impl ElementNodeMut<'_, V> { /// Get the current element fn element(&self) -> &ElementNode { @@ -1016,6 +1190,14 @@ impl ElementNodeMut<'_, V> { self.element_mut().attributes.get_mut(name) } + /// Get an attribute of the element + pub fn get_attribute( + &self, + name: &OwnedAttributeDiscription, + ) -> Option<&OwnedAttributeValue> { + self.element().attributes.get(name) + } + /// Get the set of all events the element is listening to pub fn listeners(&self) -> &FxHashSet { &self.element().listeners diff --git a/packages/native-core/src/tree.rs b/packages/native-core/src/tree.rs index e0c55eece..e7239d307 100644 --- a/packages/native-core/src/tree.rs +++ b/packages/native-core/src/tree.rs @@ -4,11 +4,25 @@ 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, + /// The node that children of the super tree should be inserted under. + pub slot: Option, +} + /// A node in a tree. #[derive(PartialEq, Eq, Clone, Debug, Component)] pub struct Node { parent: Option, children: Vec, + child_subtree: Option, + /// 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, + /// 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, height: u16, } @@ -19,10 +33,51 @@ 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 { + // 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; + /// 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 { + 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; + /// 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; + /// The node that contains the shadow tree this node is a root of + fn root_for_light_tree(&self, id: NodeId) -> Option; /// The height of the node. fn height(&self, id: NodeId) -> Option; /// Returns true if the node exists. @@ -31,10 +86,8 @@ pub trait TreeRef { /// A mutable view of a tree. pub trait TreeMut: TreeRef { - /// Removes the node and all of its children. + /// Removes the node and its children from the tree but do not delete the entities. fn remove(&mut self, id: NodeId); - /// Removes the node and all of its children. - fn remove_single(&mut self, id: NodeId); /// Adds a new node to the tree. fn create_node(&mut self, id: NodeId); /// Adds a child to the node. @@ -45,6 +98,10 @@ pub trait TreeMut: TreeRef { 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, slot: Option); + /// Remove any shadow tree. + fn remove_subtree(&mut self, id: NodeId); } impl<'a> TreeRef for TreeRefView<'a> { @@ -65,16 +122,47 @@ impl<'a> TreeRef for TreeRefView<'a> { 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 { + self.get(id).ok()?.slot_for_light_tree + } + + fn root_for_light_tree(&self, id: NodeId) -> Option { + 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 children = tree.children_ids(id); + 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 { @@ -86,16 +174,6 @@ impl<'a> TreeMut for TreeMutView<'a> { recurse(self, id); } - fn remove_single(&mut self, id: NodeId) { - { - 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); - } - } - } - fn create_node(&mut self, id: NodeId) { let (entities, node_data_mut) = self; entities.add_component( @@ -105,19 +183,21 @@ impl<'a> TreeMut for TreeMutView<'a> { 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 height; { 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); - height = parent.height + 1; } + let height = child_height((&self.1).get(parent).unwrap(), self); set_height(self, new, height); } @@ -133,27 +213,29 @@ impl<'a> TreeMut for TreeMutView<'a> { break; } } - let height = parent.height + 1; + let height = child_height((&self.1).get(parent_id).unwrap(), self); set_height(self, new_id, height); } } - // remove the old node self.remove(old_id); } fn insert_before(&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 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 = parent.height + 1; + let height = child_height((&self.1).get(parent_id).unwrap(), self); set_height(self, new_id, height); } @@ -169,21 +251,121 @@ impl<'a> TreeMut for TreeMutView<'a> { .position(|child| *child == old_id) .unwrap(); parent.children.insert(index + 1, new_id); - let height = parent.height + 1; + 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, slot: Option) { + 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 children = { + 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.children.clone() + + ( + node.child_subtree.clone(), + node.slot_for_light_tree, + node.children.clone(), + ) }; - for child in children { - set_height(tree, child, height + 1); + + // 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); + } } } @@ -209,6 +391,21 @@ impl<'a> TreeRef for TreeMutView<'a> { 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 { + let node_data = &self.1; + node_data.get(id).ok()?.slot_for_light_tree + } + + fn root_for_light_tree(&self, id: NodeId) -> Option { + let node_data = &self.1; + node_data.get(id).ok()?.root_for_light_tree + } } #[test] @@ -235,6 +432,85 @@ fn creation() { 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::().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; diff --git a/packages/native-core/src/utils/persistant_iterator.rs b/packages/native-core/src/utils/persistant_iterator.rs index 96c2cffb7..5d4902b87 100644 --- a/packages/native-core/src/utils/persistant_iterator.rs +++ b/packages/native-core/src/utils/persistant_iterator.rs @@ -60,7 +60,7 @@ struct PersistantElementIterUpdater { } impl NodeWatcher for PersistantElementIterUpdater { - fn on_node_moved(&self, node: NodeMut) { + fn on_node_moved(&mut self, node: NodeMut) { // if any element is moved, update its parents in the stack let mut stack = self.stack.lock().unwrap(); let moved = node.id(); @@ -78,7 +78,7 @@ impl NodeWatcher for PersistantElementIterUpda } } - fn on_node_removed(&self, node: NodeMut) { + fn on_node_removed(&mut self, node: NodeMut) { // if any element is removed in the chain, remove it and its children from the stack let mut stack = self.stack.lock().unwrap(); let removed = node.id(); diff --git a/packages/native-core/tests/called_minimally_on_build.rs b/packages/native-core/tests/called_minimally_on_build.rs index 394cf464a..3fa789a6c 100644 --- a/packages/native-core/tests/called_minimally_on_build.rs +++ b/packages/native-core/tests/called_minimally_on_build.rs @@ -146,7 +146,7 @@ macro_rules! test_state{ dioxus_state.apply_mutations(&mut dom, mutations); dom.update_state(SendAnyMap::new()); - dom.traverse_depth_first(|n| { + dom.traverse_depth_first_advanced(false, |n| { $( assert_eq!(n.get::<$state>().unwrap().0, 1); )* diff --git a/packages/native-core/tests/custom_element.rs b/packages/native-core/tests/custom_element.rs new file mode 100644 index 000000000..e875342d9 --- /dev/null +++ b/packages/native-core/tests/custom_element.rs @@ -0,0 +1,389 @@ +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, + _: ::ElementBorrowed<'a>, + parent: Option<::ElementBorrowed<'a>>, + _: Vec<::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: ::ElementBorrowed<'a>, + parent: Option<::ElementBorrowed<'a>>, + children: Vec<::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, + _: ::ElementBorrowed<'a>, + parent: Option<::ElementBorrowed<'a>>, + _: Vec<::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: ::ElementBorrowed<'a>, + parent: Option<::ElementBorrowed<'a>>, + children: Vec<::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 { + Some(self.slot) + } + + fn roots(&self) -> Vec { + 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 { + 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(cx: Scope) -> Element { + let count = use_state(cx, || 0); + + use_future!(cx, |count| async move { + count.with_mut(|count| *count += 1); + }); + + cx.render(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::(); + 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::().unwrap(); + let size = *node.get::().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(cx: Scope) -> Element { + cx.render(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::(); + 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(cx: Scope) -> Element { + cx.render(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::(); + 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); + }); +} diff --git a/packages/rink/examples/counter_button.rs b/packages/rink/examples/counter_button.rs index 4d2428d8a..7bf52a53e 100644 --- a/packages/rink/examples/counter_button.rs +++ b/packages/rink/examples/counter_button.rs @@ -1,11 +1,10 @@ -use dioxus_html::EventData; use dioxus_native_core::{ node::TextNode, prelude::*, real_dom::{NodeImmutable, NodeTypeMut}, NodeId, }; -use rink::{render, Config, Driver}; +use rink::{render, Config, Driver, EventData}; use std::rc::Rc; use std::sync::{Arc, RwLock}; diff --git a/packages/rink/examples/grid.rs b/packages/rink/examples/grid.rs index 7abc4669f..ab4449066 100644 --- a/packages/rink/examples/grid.rs +++ b/packages/rink/examples/grid.rs @@ -1,11 +1,10 @@ -use dioxus_html::EventData; use dioxus_native_core::{ node::TextNode, prelude::*, real_dom::{NodeImmutable, NodeTypeMut}, NodeId, }; -use rink::{render, Config, Driver}; +use rink::{render, Config, Driver, EventData}; use rustc_hash::FxHashSet; use std::rc::Rc; use std::sync::{Arc, RwLock}; diff --git a/packages/rink/examples/widgets.rs b/packages/rink/examples/widgets.rs new file mode 100644 index 000000000..b39a3b4c2 --- /dev/null +++ b/packages/rink/examples/widgets.rs @@ -0,0 +1,101 @@ +use dioxus_native_core::{ + prelude::*, + real_dom::{NodeImmutable, NodeTypeMut}, + NodeId, +}; +use rink::{render, Config, Driver, EventData}; +use std::rc::Rc; +use std::sync::{Arc, RwLock}; + +#[derive(Default)] +struct Counter { + count: f64, + button_id: NodeId, +} + +impl Counter { + fn create(mut root: NodeMut) -> Self { + let mut myself = Self::default(); + + let root_id = root.id(); + let rdom = root.real_dom_mut(); + + // create the counter + let count = myself.count; + let mut button = rdom.create_node(NodeType::Element(ElementNode { + tag: "input".to_string(), + attributes: [ + // supported types: button, checkbox, textbox, password, number, range + ("type".to_string().into(), "range".to_string().into()), + ("display".to_string().into(), "flex".to_string().into()), + (("flex-direction", "style").into(), "row".to_string().into()), + ( + ("justify-content", "style").into(), + "center".to_string().into(), + ), + (("align-items", "style").into(), "center".to_string().into()), + ( + "value".to_string().into(), + format!("click me {count}").into(), + ), + (("width", "style").into(), "50%".to_string().into()), + (("height", "style").into(), "10%".to_string().into()), + ("min".to_string().into(), "20".to_string().into()), + ("max".to_string().into(), "80".to_string().into()), + ] + .into_iter() + .collect(), + ..Default::default() + })); + button.add_event_listener("input"); + myself.button_id = button.id(); + rdom.get_mut(root_id).unwrap().add_child(myself.button_id); + + myself + } +} + +impl Driver for Counter { + fn update(&mut self, rdom: &Arc>) { + // update the counter + let mut rdom = rdom.write().unwrap(); + let mut node = rdom.get_mut(self.button_id).unwrap(); + if let NodeTypeMut::Element(mut el) = node.node_type_mut() { + el.set_attribute( + ("background-color", "style"), + format!("rgb({}, {}, {})", 255.0 - self.count * 2.0, 0, 0,), + ); + }; + } + + fn handle_event( + &mut self, + _: &Arc>, + _: NodeId, + event_type: &str, + event: Rc, + _: bool, + ) { + if event_type == "input" { + // when the button is clicked, increment the counter + if let EventData::Form(input_event) = &*event { + if let Ok(value) = input_event.value.parse::() { + self.count = value; + } + } + } + } + + fn poll_async(&mut self) -> std::pin::Pin + '_>> { + Box::pin(async move { tokio::time::sleep(std::time::Duration::from_millis(1000)).await }) + } +} + +fn main() { + render(Config::new(), |rdom, _, _| { + let mut rdom = rdom.write().unwrap(); + let root = rdom.root_id(); + Counter::create(rdom.get_mut(root).unwrap()) + }) + .unwrap(); +} diff --git a/packages/rink/src/hooks.rs b/packages/rink/src/hooks.rs index 00bae0747..f8c4500d2 100644 --- a/packages/rink/src/hooks.rs +++ b/packages/rink/src/hooks.rs @@ -13,7 +13,9 @@ use dioxus_html::geometry::{ use dioxus_html::input_data::keyboard_types::{Code, Key, Location, Modifiers}; use dioxus_html::input_data::MouseButtonSet as DioxusMouseButtons; use dioxus_html::input_data::{MouseButton as DioxusMouseButton, MouseButtonSet}; -use dioxus_html::{event_bubbles, EventData, FocusData, KeyboardData, MouseData, WheelData}; +use dioxus_html::{event_bubbles, FocusData, KeyboardData, MouseData, WheelData}; +use std::any::Any; +use std::collections::HashMap; use std::{ cell::{RefCell, RefMut}, rc::Rc, @@ -24,15 +26,64 @@ use taffy::{prelude::Layout, Taffy}; use crate::focus::{Focus, Focused}; use crate::layout::TaffyLayout; -use crate::{layout_to_screen_space, FocusState}; +use crate::{get_abs_layout, layout_to_screen_space, FocusState}; -pub(crate) struct Event { +#[derive(Debug, Clone, PartialEq)] +pub struct Event { pub id: NodeId, pub name: &'static str, - pub data: Rc, + pub data: EventData, pub bubbles: bool, } +#[derive(Debug, Clone, PartialEq)] +pub enum EventData { + Mouse(MouseData), + Keyboard(KeyboardData), + Focus(FocusData), + Wheel(WheelData), + Form(FormData), +} + +impl EventData { + pub fn into_any(self) -> Rc { + match self { + EventData::Mouse(m) => Rc::new(m), + EventData::Keyboard(k) => Rc::new(k), + EventData::Focus(f) => Rc::new(f), + EventData::Wheel(w) => Rc::new(w), + EventData::Form(f) => Rc::new(f.into_html()), + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct FormData { + pub value: String, + + pub values: HashMap>, + + pub files: Option, +} + +impl FormData { + fn into_html(self) -> dioxus_html::FormData { + dioxus_html::FormData { + value: self.value, + values: self.values, + files: None, + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Files { + files: FxHashMap, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct File {} + type EventCore = (&'static str, EventData); const MAX_REPEAT_TIME: Duration = Duration::from_millis(100); @@ -148,13 +199,13 @@ impl InnerInputState { resolved_events.push(Event { name: "focus", id, - data: Rc::new(EventData::Focus(FocusData {})), + data: EventData::Focus(FocusData {}), bubbles: event_bubbles("focus"), }); resolved_events.push(Event { name: "focusin", id, - data: Rc::new(EventData::Focus(FocusData {})), + data: EventData::Focus(FocusData {}), bubbles: event_bubbles("focusin"), }); } @@ -162,7 +213,7 @@ impl InnerInputState { resolved_events.push(Event { name: "focusout", id, - data: Rc::new(EventData::Focus(FocusData {})), + data: EventData::Focus(FocusData {}), bubbles: event_bubbles("focusout"), }); } @@ -198,7 +249,7 @@ impl InnerInputState { fn try_create_event( name: &'static str, - data: Rc, + data: EventData, will_bubble: &mut FxHashSet, resolved_events: &mut Vec, node: NodeRef, @@ -273,7 +324,7 @@ impl InnerInputState { if old_pos != Some(new_pos) { let mut will_bubble = FxHashSet::default(); for node in dom.get_listening_sorted("mousemove") { - let node_layout = get_abs_layout(node, dom, layout); + let node_layout = get_abs_layout(node, layout); let previously_contained = old_pos .filter(|pos| layout_contains_point(&node_layout, *pos)) .is_some(); @@ -282,10 +333,7 @@ impl InnerInputState { if currently_contains && previously_contained { try_create_event( "mousemove", - Rc::new(EventData::Mouse(prepare_mouse_data( - mouse_data, - &node_layout, - ))), + EventData::Mouse(prepare_mouse_data(mouse_data, &node_layout)), &mut will_bubble, resolved_events, node, @@ -300,7 +348,7 @@ impl InnerInputState { // mouseenter let mut will_bubble = FxHashSet::default(); for node in dom.get_listening_sorted("mouseenter") { - let node_layout = get_abs_layout(node, dom, layout); + let node_layout = get_abs_layout(node, layout); let previously_contained = old_pos .filter(|pos| layout_contains_point(&node_layout, *pos)) .is_some(); @@ -309,7 +357,7 @@ impl InnerInputState { if currently_contains && !previously_contained { try_create_event( "mouseenter", - Rc::new(dioxus_html::EventData::Mouse(mouse_data.clone())), + EventData::Mouse(mouse_data.clone()), &mut will_bubble, resolved_events, node, @@ -323,7 +371,7 @@ impl InnerInputState { // mouseover let mut will_bubble = FxHashSet::default(); for node in dom.get_listening_sorted("mouseover") { - let node_layout = get_abs_layout(node, dom, layout); + let node_layout = get_abs_layout(node, layout); let previously_contained = old_pos .filter(|pos| layout_contains_point(&node_layout, *pos)) .is_some(); @@ -332,10 +380,7 @@ impl InnerInputState { if currently_contains && !previously_contained { try_create_event( "mouseover", - Rc::new(EventData::Mouse(prepare_mouse_data( - mouse_data, - &node_layout, - ))), + EventData::Mouse(prepare_mouse_data(mouse_data, &node_layout)), &mut will_bubble, resolved_events, node, @@ -349,16 +394,13 @@ impl InnerInputState { if was_pressed { let mut will_bubble = FxHashSet::default(); for node in dom.get_listening_sorted("mousedown") { - let node_layout = get_abs_layout(node, dom, layout); + let node_layout = get_abs_layout(node, layout); let currently_contains = layout_contains_point(&node_layout, new_pos); if currently_contains { try_create_event( "mousedown", - Rc::new(EventData::Mouse(prepare_mouse_data( - mouse_data, - &node_layout, - ))), + EventData::Mouse(prepare_mouse_data(mouse_data, &node_layout)), &mut will_bubble, resolved_events, node, @@ -373,16 +415,13 @@ impl InnerInputState { if was_released { let mut will_bubble = FxHashSet::default(); for node in dom.get_listening_sorted("mouseup") { - let node_layout = get_abs_layout(node, dom, layout); + let node_layout = get_abs_layout(node, layout); let currently_contains = layout_contains_point(&node_layout, new_pos); if currently_contains { try_create_event( "mouseup", - Rc::new(EventData::Mouse(prepare_mouse_data( - mouse_data, - &node_layout, - ))), + EventData::Mouse(prepare_mouse_data(mouse_data, &node_layout)), &mut will_bubble, resolved_events, node, @@ -398,16 +437,13 @@ impl InnerInputState { if mouse_data.trigger_button() == Some(DioxusMouseButton::Primary) && was_released { let mut will_bubble = FxHashSet::default(); for node in dom.get_listening_sorted("click") { - let node_layout = get_abs_layout(node, dom, layout); + let node_layout = get_abs_layout(node, layout); let currently_contains = layout_contains_point(&node_layout, new_pos); if currently_contains { try_create_event( "click", - Rc::new(EventData::Mouse(prepare_mouse_data( - mouse_data, - &node_layout, - ))), + EventData::Mouse(prepare_mouse_data(mouse_data, &node_layout)), &mut will_bubble, resolved_events, node, @@ -424,16 +460,13 @@ impl InnerInputState { { let mut will_bubble = FxHashSet::default(); for node in dom.get_listening_sorted("contextmenu") { - let node_layout = get_abs_layout(node, dom, layout); + let node_layout = get_abs_layout(node, layout); let currently_contains = layout_contains_point(&node_layout, new_pos); if currently_contains { try_create_event( "contextmenu", - Rc::new(EventData::Mouse(prepare_mouse_data( - mouse_data, - &node_layout, - ))), + EventData::Mouse(prepare_mouse_data(mouse_data, &node_layout)), &mut will_bubble, resolved_events, node, @@ -450,14 +483,14 @@ impl InnerInputState { if was_scrolled { let mut will_bubble = FxHashSet::default(); for node in dom.get_listening_sorted("wheel") { - let node_layout = get_abs_layout(node, dom, layout); + let node_layout = get_abs_layout(node, layout); let currently_contains = layout_contains_point(&node_layout, new_pos); if currently_contains { try_create_event( "wheel", - Rc::new(EventData::Wheel(w.clone())), + EventData::Wheel(w.clone()), &mut will_bubble, resolved_events, node, @@ -473,7 +506,7 @@ impl InnerInputState { // mouseleave let mut will_bubble = FxHashSet::default(); for node in dom.get_listening_sorted("mouseleave") { - let node_layout = get_abs_layout(node, dom, layout); + let node_layout = get_abs_layout(node, layout); let previously_contained = old_pos .filter(|pos| layout_contains_point(&node_layout, *pos)) .is_some(); @@ -482,10 +515,7 @@ impl InnerInputState { if !currently_contains && previously_contained { try_create_event( "mouseleave", - Rc::new(EventData::Mouse(prepare_mouse_data( - mouse_data, - &node_layout, - ))), + EventData::Mouse(prepare_mouse_data(mouse_data, &node_layout)), &mut will_bubble, resolved_events, node, @@ -499,7 +529,7 @@ impl InnerInputState { // mouseout let mut will_bubble = FxHashSet::default(); for node in dom.get_listening_sorted("mouseout") { - let node_layout = get_abs_layout(node, dom, layout); + let node_layout = get_abs_layout(node, layout); let previously_contained = old_pos .filter(|pos| layout_contains_point(&node_layout, *pos)) .is_some(); @@ -508,10 +538,7 @@ impl InnerInputState { if !currently_contains && previously_contained { try_create_event( "mouseout", - Rc::new(EventData::Mouse(prepare_mouse_data( - mouse_data, - &node_layout, - ))), + EventData::Mouse(prepare_mouse_data(mouse_data, &node_layout)), &mut will_bubble, resolved_events, node, @@ -546,24 +573,6 @@ impl InnerInputState { // } } -fn get_abs_layout(node: NodeRef, dom: &RealDom, taffy: &Taffy) -> Layout { - let mut node_layout = *taffy - .layout(node.get::().unwrap().node.unwrap()) - .unwrap(); - let mut current = node; - - while let Some(parent) = current.parent_id() { - let parent = dom.get(parent).unwrap(); - current = parent; - let parent_layout = taffy - .layout(parent.get::().unwrap().node.unwrap()) - .unwrap(); - node_layout.location.x += parent_layout.location.x; - node_layout.location.y += parent_layout.location.y; - } - node_layout -} - pub struct RinkInputHandler { state: Rc>, queued_events: Rc>>, @@ -628,12 +637,12 @@ impl RinkInputHandler { }) .map(|evt| (evt.0, evt.1)); - let mut hm: FxHashMap<&'static str, Vec>> = FxHashMap::default(); + let mut hm: FxHashMap<&'static str, Vec> = FxHashMap::default(); for (event, data) in events { if let Some(v) = hm.get_mut(event) { - v.push(Rc::new(data)); + v.push(data); } else { - hm.insert(event, vec![Rc::new(data)]); + hm.insert(event, vec![data]); } } for (event, datas) in hm { diff --git a/packages/rink/src/layout.rs b/packages/rink/src/layout.rs index 944d39400..dbdadac3f 100644 --- a/packages/rink/src/layout.rs +++ b/packages/rink/src/layout.rs @@ -25,12 +25,6 @@ impl PossiblyUninitalized { _ => panic!("uninitalized"), } } - pub fn ok(self) -> Option { - match self { - Self::Initialized(i) => Some(i), - _ => None, - } - } } impl Default for PossiblyUninitalized { fn default() -> Self { @@ -54,6 +48,9 @@ impl State for TaffyLayout { .with_attrs(AttributeMaskBuilder::Some(SORTED_LAYOUT_ATTRS)) .with_text(); + // The layout state should be effected by the shadow dom + const TRAVERSE_SHADOW_DOM: bool = true; + fn update<'a>( &mut self, node_view: NodeView, diff --git a/packages/rink/src/lib.rs b/packages/rink/src/lib.rs index 6f843ab3e..8c8cbad9d 100644 --- a/packages/rink/src/lib.rs +++ b/packages/rink/src/lib.rs @@ -6,8 +6,7 @@ use crossterm::{ execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; -use dioxus_html::EventData; -use dioxus_native_core::prelude::*; +use dioxus_native_core::{prelude::*, tree::TreeRef}; use dioxus_native_core::{real_dom::RealDom, FxDashSet, NodeId, SendAnyMap}; use focus::FocusState; use futures::{channel::mpsc::UnboundedSender, pin_mut, Future, StreamExt}; @@ -24,18 +23,19 @@ use style_attributes::StyleModifier; pub use taffy::{geometry::Point, prelude::*}; use tokio::select; use tui::{backend::CrosstermBackend, Terminal}; +use widgets::{register_widgets, RinkWidgetResponder, RinkWidgetTraitObject}; mod config; mod focus; mod hooks; mod layout; -pub mod prelude; mod prevent_default; pub mod query; mod render; mod style; mod style_attributes; mod widget; +mod widgets; pub use config::*; pub use hooks::*; @@ -90,11 +90,14 @@ pub fn render( PreventDefault::to_type_erased(), ]); - let (handler, mut register_event) = RinkInputHandler::create(&mut rdom); - // Setup input handling + + // The event channel for fully resolved events let (event_tx, mut event_reciever) = unbounded(); - let event_tx_clone = event_tx.clone(); + + // The event channel for raw terminal events + let (raw_event_tx, mut raw_event_reciever) = unbounded(); + let event_tx_clone = raw_event_tx.clone(); if !cfg.headless { std::thread::spawn(move || { // Timeout after 10ms when waiting for events @@ -102,7 +105,10 @@ pub fn render( loop { if crossterm::event::poll(tick_rate).unwrap() { let evt = crossterm::event::read().unwrap(); - if event_tx.unbounded_send(InputEvent::UserInput(evt)).is_err() { + if raw_event_tx + .unbounded_send(InputEvent::UserInput(evt)) + .is_err() + { break; } } @@ -110,10 +116,21 @@ pub fn render( }); } + register_widgets(&mut rdom, event_tx); + + let (handler, mut register_event) = RinkInputHandler::create(&mut rdom); + let rdom = Arc::new(RwLock::new(rdom)); let taffy = Arc::new(Mutex::new(Taffy::new())); let mut renderer = create_renderer(&rdom, &taffy, event_tx_clone); + // insert the query engine into the rdom + let query_engine = Query::new(rdom.clone(), taffy.clone()); + { + let mut rdom = rdom.write().unwrap(); + rdom.raw_world_mut().add_unique(query_engine); + } + { renderer.update(&rdom); let mut any_map = SendAnyMap::new(); @@ -160,7 +177,8 @@ pub fn render( if !to_rerender.is_empty() || updated { updated = false; fn resize(dims: tui::layout::Rect, taffy: &mut Taffy, rdom: &RealDom) { - let width: f32 = screen_to_layout_space(dims.width); + + let width = screen_to_layout_space(dims.width); let height = screen_to_layout_space(dims.height); let root_node = rdom .get(rdom.root_id()) @@ -213,6 +231,7 @@ pub fn render( } } + let mut event_recieved = None; { let wait = renderer.poll_async(); @@ -222,7 +241,7 @@ pub fn render( _ = wait => { }, - evt = event_reciever.next() => { + evt = raw_event_reciever.next() => { match evt.as_ref().unwrap() { InputEvent::UserInput(event) => match event { TermEvent::Key(key) => { @@ -243,21 +262,32 @@ pub fn render( register_event(evt); } }, + Some(evt) = event_reciever.next() => { + event_recieved = Some(evt); + } } } { + if let Some(evt) = event_recieved { + renderer.handle_event( + &rdom, + evt.id, + evt.name, + Rc::new(evt.data), + evt.bubbles, + ); + } { - let evts = { - handler.get_events( - &taffy.lock().expect("taffy lock poisoned"), - &mut rdom.write().unwrap(), - ) - }; + let evts = handler.get_events( + &taffy.lock().expect("taffy lock poisoned"), + &mut rdom.write().unwrap(), + ); updated |= handler.state().focus_state.clean(); for e in evts { - renderer.handle_event(&rdom, e.id, e.name, e.data, e.bubbles); + bubble_event_to_widgets(&mut rdom.write().unwrap(), &e); + renderer.handle_event(&rdom, e.id, e.name, Rc::new(e.data), e.bubbles); } } // updates the dom's nodes @@ -309,3 +339,54 @@ pub trait Driver { ); fn poll_async(&mut self) -> Pin + '_>>; } + +/// Before sending the event to drivers, we need to bubble it up the tree to any widgets that are listening +fn bubble_event_to_widgets(rdom: &mut RealDom, event: &Event) { + let id = event.id; + let mut node = Some(id); + + while let Some(node_id) = node { + let parent_id = { + let tree = rdom.tree_ref(); + tree.parent_id_advanced(node_id, true) + }; + + { + // println!("@ bubbling event to node {:?}", node_id); + let mut node_mut = rdom.get_mut(node_id).unwrap(); + if let Some(mut widget) = node_mut + .get_mut::() + .map(|w| w.clone()) + { + widget.handle_event(event, node_mut) + } + } + + if !event.bubbles { + // println!("event does not bubble"); + break; + } + node = parent_id; + } +} + +pub(crate) fn get_abs_layout(node: NodeRef, taffy: &Taffy) -> Layout { + let mut node_layout = *taffy + .layout(node.get::().unwrap().node.unwrap()) + .unwrap(); + let mut current = node; + + let dom = node.real_dom(); + let tree = dom.tree_ref(); + + while let Some(parent) = tree.parent_id_advanced(current.id(), true) { + let parent = dom.get(parent).unwrap(); + current = parent; + let parent_layout = taffy + .layout(parent.get::().unwrap().node.unwrap()) + .unwrap(); + node_layout.location.x += parent_layout.location.x; + node_layout.location.y += parent_layout.location.y; + } + node_layout +} diff --git a/packages/rink/src/prelude/mod.rs b/packages/rink/src/prelude/mod.rs deleted file mode 100644 index f7bbb3696..000000000 --- a/packages/rink/src/prelude/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -#[cfg(feature = "dioxus-bindings")] -pub use crate::widgets::*; diff --git a/packages/rink/src/query.rs b/packages/rink/src/query.rs index 2f4fe02e0..3b8f319cf 100644 --- a/packages/rink/src/query.rs +++ b/packages/rink/src/query.rs @@ -1,13 +1,14 @@ use std::sync::{Arc, Mutex, MutexGuard, RwLock, RwLockReadGuard}; use dioxus_native_core::prelude::*; +use shipyard::Unique; use taffy::{ geometry::Point, prelude::{Layout, Size}, Taffy, }; -use crate::{layout::TaffyLayout, layout_to_screen_space}; +use crate::{get_abs_layout, layout_to_screen_space}; /// Allows querying the layout of nodes after rendering. It will only provide a correct value after a node is rendered. /// Provided as a root context for all tui applictions. @@ -40,7 +41,7 @@ use crate::{layout::TaffyLayout, layout_to_screen_space}; /// }) /// } /// ``` -#[derive(Clone)] +#[derive(Clone, Unique)] pub struct Query { pub(crate) rdom: Arc>, pub(crate) stretch: Arc>, @@ -69,7 +70,7 @@ pub struct ElementRef<'a> { } impl<'a> ElementRef<'a> { - fn new( + pub(crate) fn new( inner: RwLockReadGuard<'a, RealDom>, stretch: MutexGuard<'a, Taffy>, id: NodeId, @@ -89,17 +90,20 @@ impl<'a> ElementRef<'a> { } pub fn layout(&self) -> Option { - let layout = self - .stretch - .layout(self.inner.get(self.id)?.get::()?.node.ok()?) - .ok(); - layout.map(|layout| Layout { - order: layout.order, - size: layout.size.map(layout_to_screen_space), - location: Point { - x: layout_to_screen_space(layout.location.x), - y: layout_to_screen_space(layout.location.y), - }, - }) + get_layout(self.inner.get(self.id).unwrap(), &self.stretch) } } + +pub(crate) fn get_layout(node: NodeRef, stretch: &Taffy) -> Option { + let layout = get_abs_layout(node, stretch); + let pos = layout.location; + + Some(Layout { + order: layout.order, + size: layout.size.map(layout_to_screen_space), + location: Point { + x: layout_to_screen_space(pos.x).round(), + y: layout_to_screen_space(pos.y).round(), + }, + }) +} diff --git a/packages/rink/src/render.rs b/packages/rink/src/render.rs index ef433976d..19d642720 100644 --- a/packages/rink/src/render.rs +++ b/packages/rink/src/render.rs @@ -1,4 +1,4 @@ -use dioxus_native_core::prelude::*; +use dioxus_native_core::{prelude::*, tree::TreeRef}; use std::io::Stdout; use taffy::{ geometry::Point, @@ -83,7 +83,10 @@ pub(crate) fn render_vnode( frame.render_widget(WidgetWithContext::new(node, cfg), area); } - for c in node.children() { + let node_id = node.id(); + let rdom = node.real_dom(); + for child_id in rdom.tree_ref().children_ids_advanced(node_id, true) { + let c = rdom.get(child_id).unwrap(); render_vnode(frame, layout, c, cfg, location); } } diff --git a/packages/rink/src/widgets/button.rs b/packages/rink/src/widgets/button.rs new file mode 100644 index 000000000..e4d3754f1 --- /dev/null +++ b/packages/rink/src/widgets/button.rs @@ -0,0 +1,206 @@ +use std::collections::HashMap; + +use dioxus_html::input_data::keyboard_types::Key; +use dioxus_native_core::{ + custom_element::CustomElement, + node::OwnedAttributeDiscription, + node_ref::AttributeMask, + prelude::NodeType, + real_dom::{ElementNodeMut, NodeImmutable, NodeMut, NodeTypeMut, RealDom}, + NodeId, +}; +use shipyard::UniqueView; + +use crate::FormData; + +use super::{RinkWidget, WidgetContext}; + +#[derive(Debug, Default)] +pub(crate) struct Button { + text_id: NodeId, + value: String, +} + +impl Button { + fn width(el: &ElementNodeMut) -> String { + if let Some(value) = el + .get_attribute(&OwnedAttributeDiscription { + name: "width".to_string(), + namespace: None, + }) + .and_then(|value| value.as_text()) + .map(|value| value.to_string()) + { + value + } else { + "1px".to_string() + } + } + + fn height(el: &ElementNodeMut) -> String { + if let Some(value) = el + .get_attribute(&OwnedAttributeDiscription { + name: "height".to_string(), + namespace: None, + }) + .and_then(|value| value.as_text()) + .map(|value| value.to_string()) + { + value + } else { + "1px".to_string() + } + } + + fn update_size_attr(&mut self, el: &mut ElementNodeMut) { + let width = Self::width(el); + let height = Self::height(el); + let single_char = width == "1px" || height == "1px"; + let border_style = if single_char { "none" } else { "solid" }; + el.set_attribute( + OwnedAttributeDiscription { + name: "border-style".to_string(), + namespace: Some("style".to_string()), + }, + border_style.to_string(), + ); + } + + fn update_value_attr(&mut self, el: &ElementNodeMut) { + if let Some(value) = el + .get_attribute(&OwnedAttributeDiscription { + name: "value".to_string(), + namespace: None, + }) + .and_then(|value| value.as_text()) + .map(|value| value.to_string()) + { + self.value = value; + } + } + + fn write_value(&self, rdom: &mut RealDom) { + if let Some(mut text) = rdom.get_mut(self.text_id) { + let node_type = text.node_type_mut(); + let NodeTypeMut::Text(mut text) = node_type else { panic!("input must be an element") }; + *text.text_mut() = self.value.clone(); + } + } + + fn switch(&mut self, ctx: &mut WidgetContext, node: NodeMut) { + let data = FormData { + value: self.value.to_string(), + values: HashMap::new(), + files: None, + }; + ctx.send(crate::Event { + id: node.id(), + name: "input", + data: crate::EventData::Form(data), + bubbles: true, + }); + } +} + +impl CustomElement for Button { + const NAME: &'static str = "input"; + + fn roots(&self) -> Vec { + vec![self.text_id] + } + + fn create(mut root: dioxus_native_core::real_dom::NodeMut) -> Self { + let node_type = root.node_type(); + let NodeType::Element(el) = &*node_type else { panic!("input must be an element") }; + + let value = el + .attributes + .get(&OwnedAttributeDiscription { + name: "value".to_string(), + namespace: None, + }) + .and_then(|value| value.as_text()) + .map(|value| value.to_string()); + + drop(node_type); + + let rdom = root.real_dom_mut(); + let text = rdom.create_node(value.clone().unwrap_or_default()); + let text_id = text.id(); + + root.add_event_listener("keydown"); + root.add_event_listener("click"); + + Self { + text_id, + value: value.unwrap_or_default(), + } + } + + fn attributes_changed( + &mut self, + mut root: dioxus_native_core::real_dom::NodeMut, + attributes: &dioxus_native_core::node_ref::AttributeMask, + ) { + match attributes { + AttributeMask::All => { + { + let node_type = root.node_type_mut(); + let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") }; + self.update_value_attr(&el); + self.update_size_attr(&mut el); + } + self.write_value(root.real_dom_mut()); + } + AttributeMask::Some(attrs) => { + { + let node_type = root.node_type_mut(); + let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") }; + if attrs.contains("width") || attrs.contains("height") { + self.update_size_attr(&mut el); + } + if attrs.contains("value") { + self.update_value_attr(&el); + } + } + if attrs.contains("value") { + self.write_value(root.real_dom_mut()); + } + } + } + } +} + +impl RinkWidget for Button { + fn handle_event( + &mut self, + event: &crate::Event, + mut node: dioxus_native_core::real_dom::NodeMut, + ) { + let mut ctx: WidgetContext = { + node.real_dom_mut() + .raw_world_mut() + .borrow::>() + .expect("expected widget context") + .clone() + }; + + match event.name { + "click" => self.switch(&mut ctx, node), + "keydown" => { + if let crate::EventData::Keyboard(data) = &event.data { + if !data.is_auto_repeating() + && match data.key() { + Key::Character(c) if c == " " => true, + Key::Enter => true, + _ => false, + } + { + self.switch(&mut ctx, node); + } + } + } + _ => {} + } + } +} diff --git a/packages/rink/src/widgets/checkbox.rs b/packages/rink/src/widgets/checkbox.rs new file mode 100644 index 000000000..24b3f6244 --- /dev/null +++ b/packages/rink/src/widgets/checkbox.rs @@ -0,0 +1,249 @@ +use std::collections::HashMap; + +use dioxus_html::input_data::keyboard_types::Key; +use dioxus_native_core::{ + custom_element::CustomElement, + node::OwnedAttributeDiscription, + node_ref::AttributeMask, + prelude::NodeType, + real_dom::{ElementNodeMut, NodeImmutable, NodeMut, NodeTypeMut}, + NodeId, +}; +use shipyard::UniqueView; + +use crate::FormData; + +use super::{RinkWidget, WidgetContext}; + +#[derive(Debug, Default)] +pub(crate) struct CheckBox { + div_id: NodeId, + text_id: NodeId, + value: String, + checked: bool, +} + +impl CheckBox { + fn width(el: &ElementNodeMut) -> String { + if let Some(value) = el + .get_attribute(&OwnedAttributeDiscription { + name: "width".to_string(), + namespace: None, + }) + .and_then(|value| value.as_text()) + .map(|value| value.to_string()) + { + value + } else { + "1px".to_string() + } + } + + fn height(el: &ElementNodeMut) -> String { + if let Some(value) = el + .get_attribute(&OwnedAttributeDiscription { + name: "height".to_string(), + namespace: None, + }) + .and_then(|value| value.as_text()) + .map(|value| value.to_string()) + { + value + } else { + "1px".to_string() + } + } + + fn update_size_attr(&mut self, el: &mut ElementNodeMut) { + let width = Self::width(el); + let height = Self::height(el); + let single_char = width == "1px" || height == "1px"; + let border_style = if single_char { "none" } else { "solid" }; + el.set_attribute( + OwnedAttributeDiscription { + name: "border-style".to_string(), + namespace: Some("style".to_string()), + }, + border_style.to_string(), + ); + } + + fn update_value_attr(&mut self, el: &ElementNodeMut) { + self.value = el + .get_attribute(&OwnedAttributeDiscription { + name: "value".to_string(), + namespace: None, + }) + .and_then(|value| value.as_text()) + .map(|value| value.to_string()) + .unwrap_or_else(|| "on".to_string()); + } + + fn update_checked_attr(&mut self, el: &ElementNodeMut) { + self.checked = el + .get_attribute(&OwnedAttributeDiscription { + name: "checked".to_string(), + namespace: None, + }) + .and_then(|value| value.as_text()) + .map(|value| value.to_string()) + .unwrap_or_else(|| "false".to_string()) + == "true"; + } + + fn write_value(&self, mut root: NodeMut) { + let single_char = { + let node_type = root.node_type_mut(); + let NodeTypeMut::Element( el) = node_type else { panic!("input must be an element") }; + Self::width(&el) == "1px" || Self::height(&el) == "1px" + }; + let rdom = root.real_dom_mut(); + + if let Some(mut text) = rdom.get_mut(self.text_id) { + let node_type = text.node_type_mut(); + let NodeTypeMut::Text(mut text) = node_type else { panic!("input must be an element") }; + let value = if single_char { + if self.checked { + "☑" + } else { + "☐" + } + } else if self.checked { + "✓" + } else { + " " + }; + *text.text_mut() = value.to_string(); + } + } + + fn switch(&mut self, mut node: NodeMut) { + let new_state = !self.checked; + + let data = FormData { + value: new_state + .then(|| self.value.to_string()) + .unwrap_or_default(), + values: HashMap::new(), + files: None, + }; + { + let ctx: UniqueView = node + .real_dom_mut() + .raw_world_mut() + .borrow() + .expect("expected widget context"); + ctx.send(crate::Event { + id: self.div_id, + name: "input", + data: crate::EventData::Form(data), + bubbles: true, + }); + } + + self.checked = new_state; + + self.write_value(node); + } +} + +impl CustomElement for CheckBox { + const NAME: &'static str = "input"; + + fn roots(&self) -> Vec { + vec![self.text_id] + } + + fn create(mut root: dioxus_native_core::real_dom::NodeMut) -> Self { + let node_type = root.node_type(); + let NodeType::Element(el) = &*node_type else { panic!("input must be an element") }; + + let value = el + .attributes + .get(&OwnedAttributeDiscription { + name: "value".to_string(), + namespace: None, + }) + .and_then(|value| value.as_text()) + .map(|value| value.to_string()); + + drop(node_type); + + let rdom = root.real_dom_mut(); + let text = rdom.create_node(String::new()); + let text_id = text.id(); + + root.add_event_listener("click"); + root.add_event_listener("keydown"); + let div_id = root.id(); + + let myself = Self { + div_id, + text_id, + value: value.unwrap_or_default(), + checked: false, + }; + myself.write_value(root); + + myself + } + + fn attributes_changed( + &mut self, + mut root: dioxus_native_core::real_dom::NodeMut, + attributes: &dioxus_native_core::node_ref::AttributeMask, + ) { + match attributes { + AttributeMask::All => { + { + let node_type = root.node_type_mut(); + let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") }; + self.update_value_attr(&el); + self.update_size_attr(&mut el); + self.update_checked_attr(&el); + } + self.write_value(root); + } + AttributeMask::Some(attrs) => { + { + let node_type = root.node_type_mut(); + let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") }; + if attrs.contains("width") || attrs.contains("height") { + self.update_size_attr(&mut el); + } + if attrs.contains("value") { + self.update_value_attr(&el); + } + if attrs.contains("checked") { + self.update_checked_attr(&el); + } + } + if attrs.contains("checked") { + self.write_value(root); + } + } + } + } +} + +impl RinkWidget for CheckBox { + fn handle_event(&mut self, event: &crate::Event, node: dioxus_native_core::real_dom::NodeMut) { + match event.name { + "click" => self.switch(node), + "keydown" => { + if let crate::EventData::Keyboard(data) = &event.data { + if !data.is_auto_repeating() + && match data.key() { + Key::Character(c) if c == " " => true, + Key::Enter => true, + _ => false, + } + { + self.switch(node); + } + } + } + _ => {} + } + } +} diff --git a/packages/rink/src/widgets/input.rs b/packages/rink/src/widgets/input.rs new file mode 100644 index 000000000..c6a3e56b9 --- /dev/null +++ b/packages/rink/src/widgets/input.rs @@ -0,0 +1,153 @@ +use dioxus_native_core::{ + custom_element::CustomElement, node::OwnedAttributeDiscription, prelude::NodeType, + real_dom::NodeImmutable, +}; + +use super::{ + checkbox::CheckBox, number::Number, password::Password, slider::Slider, textbox::TextBox, + RinkWidget, +}; +use crate::widgets::button::Button; + +pub(crate) enum Input { + Button(Button), + CheckBox(CheckBox), + TextBox(TextBox), + Password(Password), + Number(Number), + Slider(Slider), +} + +impl CustomElement for Input { + const NAME: &'static str = "input"; + + fn roots(&self) -> Vec { + match self { + Input::Button(button) => button.roots(), + Input::CheckBox(checkbox) => checkbox.roots(), + Input::TextBox(textbox) => textbox.roots(), + Input::Password(password) => password.roots(), + Input::Number(number) => number.roots(), + Input::Slider(slider) => slider.roots(), + } + } + + fn slot(&self) -> Option { + match self { + Input::Button(button) => button.slot(), + Input::CheckBox(checkbox) => checkbox.slot(), + Input::TextBox(textbox) => textbox.slot(), + Input::Password(password) => password.slot(), + Input::Number(number) => number.slot(), + Input::Slider(slider) => slider.slot(), + } + } + + fn create(mut root: dioxus_native_core::real_dom::NodeMut) -> Self { + { + // currently widgets are not allowed to have children + let children = root.child_ids(); + let rdom = root.real_dom_mut(); + for child in children { + if let Some(mut child) = rdom.get_mut(child) { + child.remove(); + } + } + } + + let node_type = root.node_type(); + let NodeType::Element(el) = &*node_type else { panic!("input must be an element") }; + let input_type = el + .attributes + .get(&OwnedAttributeDiscription { + name: "type".to_string(), + namespace: None, + }) + .and_then(|value| value.as_text()); + match input_type + .map(|type_| type_.trim().to_lowercase()) + .as_deref() + { + Some("button") => { + drop(node_type); + Input::Button(Button::create(root)) + } + Some("checkbox") => { + drop(node_type); + Input::CheckBox(CheckBox::create(root)) + } + Some("textbox") => { + drop(node_type); + Input::TextBox(TextBox::create(root)) + } + Some("password") => { + drop(node_type); + Input::Password(Password::create(root)) + } + Some("number") => { + drop(node_type); + Input::Number(Number::create(root)) + } + Some("range") => { + drop(node_type); + Input::Slider(Slider::create(root)) + } + _ => { + drop(node_type); + Input::TextBox(TextBox::create(root)) + } + } + } + + fn attributes_changed( + &mut self, + root: dioxus_native_core::real_dom::NodeMut, + attributes: &dioxus_native_core::node_ref::AttributeMask, + ) { + match self { + Input::Button(button) => { + button.attributes_changed(root, attributes); + } + Input::CheckBox(checkbox) => { + checkbox.attributes_changed(root, attributes); + } + Input::TextBox(textbox) => { + textbox.attributes_changed(root, attributes); + } + Input::Password(password) => { + password.attributes_changed(root, attributes); + } + Input::Number(number) => { + number.attributes_changed(root, attributes); + } + Input::Slider(slider) => { + slider.attributes_changed(root, attributes); + } + } + } +} + +impl RinkWidget for Input { + fn handle_event(&mut self, event: &crate::Event, node: dioxus_native_core::real_dom::NodeMut) { + match self { + Input::Button(button) => { + button.handle_event(event, node); + } + Input::CheckBox(checkbox) => { + checkbox.handle_event(event, node); + } + Input::TextBox(textbox) => { + textbox.handle_event(event, node); + } + Input::Password(password) => { + password.handle_event(event, node); + } + Input::Number(number) => { + number.handle_event(event, node); + } + Input::Slider(slider) => { + slider.handle_event(event, node); + } + } + } +} diff --git a/packages/rink/src/widgets/mod.rs b/packages/rink/src/widgets/mod.rs new file mode 100644 index 000000000..c3ecaad78 --- /dev/null +++ b/packages/rink/src/widgets/mod.rs @@ -0,0 +1,128 @@ +mod button; +mod checkbox; +mod input; +mod number; +mod password; +mod slider; +mod text_like; +mod textbox; + +use std::sync::{Arc, RwLock}; + +use dioxus_native_core::{ + custom_element::{CustomElement, CustomElementUpdater}, + real_dom::{NodeMut, RealDom}, +}; +use futures_channel::mpsc::UnboundedSender; +use shipyard::{Component, Unique}; + +use crate::Event; + +pub(crate) fn register_widgets(rdom: &mut RealDom, sender: UnboundedSender) { + // inject the widget context + rdom.raw_world().add_unique(WidgetContext { sender }); + + rdom.register_custom_element::>(); +} + +trait RinkWidget: Sync + Send + CustomElement + 'static { + fn handle_event(&mut self, event: &Event, node: dioxus_native_core::real_dom::NodeMut); +} + +pub trait RinkWidgetResponder: CustomElementUpdater { + fn handle_event(&mut self, event: &Event, node: dioxus_native_core::real_dom::NodeMut); +} + +impl RinkWidgetResponder for W { + fn handle_event(&mut self, event: &Event, node: dioxus_native_core::real_dom::NodeMut) { + RinkWidget::handle_event(self, event, node) + } +} + +struct RinkWidgetWrapper { + inner: RinkWidgetTraitObject, + _marker: std::marker::PhantomData, +} + +impl CustomElement for RinkWidgetWrapper { + const NAME: &'static str = W::NAME; + + const NAMESPACE: Option<&'static str> = W::NAMESPACE; + + fn create(mut node: NodeMut) -> Self { + let myself = RinkWidgetTraitObject { + widget: Arc::new(RwLock::new(W::create(node.reborrow()))), + }; + + // Insert the widget as an arbitrary data node so that it can be recognized when bubbling events + node.insert(myself.clone()); + + RinkWidgetWrapper { + inner: myself, + _marker: std::marker::PhantomData, + } + } + + fn attributes_changed( + &mut self, + root: dioxus_native_core::real_dom::NodeMut, + attributes: &dioxus_native_core::node_ref::AttributeMask, + ) { + let mut widget = self.inner.widget.write().unwrap(); + widget.attributes_changed(root, attributes); + } + + fn roots(&self) -> Vec { + let widget = self.inner.widget.read().unwrap(); + widget.roots() + } + + fn slot(&self) -> Option { + let widget = self.inner.widget.read().unwrap(); + widget.slot() + } +} + +#[derive(Clone, Component)] +pub(crate) struct RinkWidgetTraitObject { + widget: Arc>, +} + +impl CustomElementUpdater for RinkWidgetTraitObject { + fn attributes_changed( + &mut self, + light_root: dioxus_native_core::real_dom::NodeMut, + attributes: &dioxus_native_core::node_ref::AttributeMask, + ) { + let mut widget = self.widget.write().unwrap(); + widget.attributes_changed(light_root, attributes); + } + + fn roots(&self) -> Vec { + let widget = self.widget.read().unwrap(); + widget.roots() + } + + fn slot(&self) -> Option { + let widget = self.widget.read().unwrap(); + widget.slot() + } +} + +impl RinkWidgetResponder for RinkWidgetTraitObject { + fn handle_event(&mut self, event: &Event, node: dioxus_native_core::real_dom::NodeMut) { + let mut widget = self.widget.write().unwrap(); + widget.handle_event(event, node); + } +} + +#[derive(Unique, Clone)] +pub(crate) struct WidgetContext { + sender: UnboundedSender, +} + +impl WidgetContext { + pub(crate) fn send(&self, event: Event) { + self.sender.unbounded_send(event).unwrap(); + } +} diff --git a/packages/rink/src/widgets/number.rs b/packages/rink/src/widgets/number.rs new file mode 100644 index 000000000..371c8b41d --- /dev/null +++ b/packages/rink/src/widgets/number.rs @@ -0,0 +1,91 @@ +use dioxus_html::input_data::keyboard_types::Key; +use dioxus_native_core::{ + custom_element::CustomElement, + real_dom::{NodeImmutable, RealDom}, + NodeId, +}; + +use crate::EventData; + +use super::{text_like::TextLike, RinkWidget}; + +#[derive(Debug, Default)] +pub(crate) struct Number { + text: TextLike, +} + +impl Number { + fn increase(&mut self, rdom: &mut RealDom, id: NodeId) { + let num = self.text.text().parse::().unwrap_or(0.0); + self.text.set_text((num + 1.0).to_string(), rdom, id); + } + + fn decrease(&mut self, rdom: &mut RealDom, id: NodeId) { + let num = self.text.text().parse::().unwrap_or(0.0); + self.text.set_text((num - 1.0).to_string(), rdom, id); + } +} + +impl CustomElement for Number { + const NAME: &'static str = "input"; + + fn roots(&self) -> Vec { + self.text.roots() + } + + fn create(mut root: dioxus_native_core::real_dom::NodeMut) -> Self { + Number { + text: TextLike::create(root.reborrow()), + } + } + + fn attributes_changed( + &mut self, + root: dioxus_native_core::real_dom::NodeMut, + attributes: &dioxus_native_core::node_ref::AttributeMask, + ) { + self.text.attributes_changed(root, attributes) + } +} + +impl RinkWidget for Number { + fn handle_event( + &mut self, + event: &crate::Event, + mut node: dioxus_native_core::real_dom::NodeMut, + ) { + if event.name == "keydown" { + if let EventData::Keyboard(data) = &event.data { + let key = data.key(); + let is_num_like = match key.clone() { + Key::ArrowLeft | Key::ArrowRight | Key::Backspace => true, + Key::Character(c) + if c == "." || c == "-" || c.chars().all(|c| c.is_numeric()) => + { + true + } + _ => false, + }; + + if is_num_like { + self.text.handle_event(event, node) + } else { + let id = node.id(); + let rdom = node.real_dom_mut(); + match key { + Key::ArrowUp => { + self.increase(rdom, id); + } + Key::ArrowDown => { + self.decrease(rdom, id); + } + _ => (), + } + } + return; + } + } + + self.text.handle_event(event, node) + } +} diff --git a/packages/rink/src/widgets/password.rs b/packages/rink/src/widgets/password.rs new file mode 100644 index 000000000..9a8228ad1 --- /dev/null +++ b/packages/rink/src/widgets/password.rs @@ -0,0 +1,12 @@ +use super::text_like::{TextLike, TextLikeController}; + +pub(crate) type Password = TextLike; + +#[derive(Debug, Default)] +pub(crate) struct PasswordController; + +impl TextLikeController for PasswordController { + fn display_text(&self, text: &str) -> String { + text.chars().map(|_| '.').collect() + } +} diff --git a/packages/rink/src/widgets/slider.rs b/packages/rink/src/widgets/slider.rs new file mode 100644 index 000000000..f97c12bf6 --- /dev/null +++ b/packages/rink/src/widgets/slider.rs @@ -0,0 +1,456 @@ +use std::collections::HashMap; + +use dioxus_html::{input_data::keyboard_types::Key, KeyboardData, MouseData}; +use dioxus_native_core::{ + custom_element::CustomElement, + node::{OwnedAttributeDiscription, OwnedAttributeValue}, + node_ref::AttributeMask, + prelude::{ElementNode, NodeType}, + real_dom::{ElementNodeMut, NodeImmutable, NodeMut, NodeTypeMut, RealDom}, + NodeId, +}; +use shipyard::UniqueView; + +use super::{RinkWidget, WidgetContext}; +use crate::{query::get_layout, Event, EventData, FormData, Query}; + +#[derive(Debug)] +pub(crate) struct Slider { + div_wrapper: NodeId, + pre_cursor_div: NodeId, + post_cursor_div: NodeId, + min: f64, + max: f64, + step: Option, + value: f64, + border: bool, +} + +impl Default for Slider { + fn default() -> Self { + Self { + div_wrapper: Default::default(), + pre_cursor_div: Default::default(), + post_cursor_div: Default::default(), + min: 0.0, + max: 100.0, + step: None, + value: 0.0, + border: false, + } + } +} + +impl Slider { + fn size(&self) -> f64 { + self.max - self.min + } + + fn step(&self) -> f64 { + self.step.unwrap_or(self.size() / 10.0) + } + + fn width(el: &ElementNodeMut) -> String { + if let Some(value) = el + .get_attribute(&OwnedAttributeDiscription { + name: "width".to_string(), + namespace: Some("style".to_string()), + }) + .and_then(|value| value.as_text()) + .map(|value| value.to_string()) + { + value + } else { + "1px".to_string() + } + } + + fn height(el: &ElementNodeMut) -> String { + if let Some(value) = el + .get_attribute(&OwnedAttributeDiscription { + name: "height".to_string(), + namespace: Some("style".to_string()), + }) + .and_then(|value| value.as_text()) + .map(|value| value.to_string()) + { + value + } else { + "1px".to_string() + } + } + + fn update_min_attr(&mut self, el: &ElementNodeMut) { + if let Some(value) = el + .get_attribute(&OwnedAttributeDiscription { + name: "min".to_string(), + namespace: None, + }) + .and_then(|value| value.as_text()) + .map(|value| value.to_string()) + { + self.min = value.parse().ok().unwrap_or(0.0); + } + } + + fn update_max_attr(&mut self, el: &ElementNodeMut) { + if let Some(value) = el + .get_attribute(&OwnedAttributeDiscription { + name: "max".to_string(), + namespace: None, + }) + .and_then(|value| value.as_text()) + .map(|value| value.to_string()) + { + self.max = value.parse().ok().unwrap_or(100.0); + } + } + + fn update_step_attr(&mut self, el: &ElementNodeMut) { + if let Some(value) = el + .get_attribute(&OwnedAttributeDiscription { + name: "step".to_string(), + namespace: None, + }) + .and_then(|value| value.as_text()) + .map(|value| value.to_string()) + { + self.step = value.parse().ok(); + } + } + + fn update_size_attr(&mut self, el: &mut ElementNodeMut) { + let width = Self::width(el); + let height = Self::height(el); + let single_char = width + .strip_prefix("px") + .and_then(|n| n.parse::().ok().filter(|num| *num > 3)) + .is_some() + || height + .strip_prefix("px") + .and_then(|n| n.parse::().ok().filter(|num| *num > 3)) + .is_some(); + self.border = !single_char; + let border_style = if self.border { "solid" } else { "none" }; + el.set_attribute( + OwnedAttributeDiscription { + name: "border-style".to_string(), + namespace: Some("style".to_string()), + }, + border_style.to_string(), + ); + } + + fn update_value(&mut self, new: f64) { + self.value = new.clamp(self.min, self.max); + } + + fn update_value_attr(&mut self, el: &ElementNodeMut) { + if let Some(value) = el + .get_attribute(&OwnedAttributeDiscription { + name: "value".to_string(), + namespace: None, + }) + .and_then(|value| value.as_text()) + .map(|value| value.to_string()) + { + self.update_value(value.parse().ok().unwrap_or(0.0)); + } + } + + fn write_value(&self, rdom: &mut RealDom, id: NodeId) { + let value_percent = (self.value - self.min) / self.size() * 100.0; + + if let Some(mut div) = rdom.get_mut(self.pre_cursor_div) { + let node_type = div.node_type_mut(); + let NodeTypeMut::Element(mut element) = node_type else { panic!("input must be an element") }; + element.set_attribute( + OwnedAttributeDiscription { + name: "width".to_string(), + namespace: Some("style".to_string()), + }, + format!("{}%", value_percent), + ); + } + + if let Some(mut div) = rdom.get_mut(self.post_cursor_div) { + let node_type = div.node_type_mut(); + let NodeTypeMut::Element(mut element) = node_type else { panic!("input must be an element") }; + element.set_attribute( + OwnedAttributeDiscription { + name: "width".to_string(), + namespace: Some("style".to_string()), + }, + format!("{}%", 100.0 - value_percent), + ); + } + + // send the event + let world = rdom.raw_world_mut(); + + { + let ctx: UniqueView = world.borrow().expect("expected widget context"); + + let data = FormData { + value: self.value.to_string(), + values: HashMap::new(), + files: None, + }; + ctx.send(Event { + id, + name: "input", + data: EventData::Form(data), + bubbles: true, + }); + } + } + + fn handle_keydown(&mut self, mut root: NodeMut, data: &KeyboardData) { + let key = data.key(); + + let step = self.step(); + match key { + Key::ArrowDown | Key::ArrowLeft => { + self.update_value(self.value - step); + } + Key::ArrowUp | Key::ArrowRight => { + self.update_value(self.value + step); + } + _ => { + return; + } + } + + let id = root.id(); + + let rdom = root.real_dom_mut(); + self.write_value(rdom, id); + } + + fn handle_mousemove(&mut self, mut root: NodeMut, data: &MouseData) { + if !data.held_buttons().is_empty() { + let id = root.id(); + let rdom = root.real_dom_mut(); + let world = rdom.raw_world_mut(); + let taffy = { + let query: UniqueView = world.borrow().unwrap(); + query.stretch.clone() + }; + + let taffy = taffy.lock().unwrap(); + + let layout = get_layout(rdom.get(self.div_wrapper).unwrap(), &taffy).unwrap(); + + let width = layout.size.width as f64; + let offset = data.element_coordinates(); + self.update_value(self.min + self.size() * offset.x / width); + + self.write_value(rdom, id); + } + } +} + +impl CustomElement for Slider { + const NAME: &'static str = "input"; + + fn roots(&self) -> Vec { + vec![self.div_wrapper] + } + + fn create(mut root: dioxus_native_core::real_dom::NodeMut) -> Self { + let node_type = root.node_type(); + let NodeType::Element(el) = &*node_type else { panic!("input must be an element") }; + + let value = el.attributes.get(&OwnedAttributeDiscription { + name: "value".to_string(), + namespace: None, + }); + let value = value + .and_then(|value| match value { + OwnedAttributeValue::Text(text) => text.as_str().parse().ok(), + OwnedAttributeValue::Float(float) => Some(*float), + OwnedAttributeValue::Int(int) => Some(*int as f64), + _ => None, + }) + .unwrap_or(0.0); + + drop(node_type); + + let rdom = root.real_dom_mut(); + + let pre_cursor_div = rdom.create_node(NodeType::Element(ElementNode { + tag: "div".to_string(), + attributes: [( + OwnedAttributeDiscription { + name: "background-color".to_string(), + namespace: Some("style".to_string()), + }, + "rgba(10,10,10,0.5)".to_string().into(), + )] + .into_iter() + .collect(), + ..Default::default() + })); + let pre_cursor_div_id = pre_cursor_div.id(); + + let cursor_text = rdom.create_node("|".to_string()); + let cursor_text_id = cursor_text.id(); + let mut cursor_span = rdom.create_node(NodeType::Element(ElementNode { + tag: "div".to_string(), + attributes: [].into_iter().collect(), + ..Default::default() + })); + cursor_span.add_child(cursor_text_id); + let cursor_span_id = cursor_span.id(); + + let post_cursor_div = rdom.create_node(NodeType::Element(ElementNode { + tag: "span".to_string(), + attributes: [ + ( + OwnedAttributeDiscription { + name: "width".to_string(), + namespace: Some("style".to_string()), + }, + "100%".to_string().into(), + ), + ( + OwnedAttributeDiscription { + name: "background-color".to_string(), + namespace: Some("style".to_string()), + }, + "rgba(10,10,10,0.5)".to_string().into(), + ), + ] + .into_iter() + .collect(), + ..Default::default() + })); + let post_cursor_div_id = post_cursor_div.id(); + + let mut div_wrapper = rdom.create_node(NodeType::Element(ElementNode { + tag: "div".to_string(), + attributes: [ + ( + OwnedAttributeDiscription { + name: "display".to_string(), + namespace: Some("style".to_string()), + }, + "flex".to_string().into(), + ), + ( + OwnedAttributeDiscription { + name: "flex-direction".to_string(), + namespace: Some("style".to_string()), + }, + "row".to_string().into(), + ), + ( + OwnedAttributeDiscription { + name: "width".to_string(), + namespace: Some("style".to_string()), + }, + "100%".to_string().into(), + ), + ( + OwnedAttributeDiscription { + name: "height".to_string(), + namespace: Some("style".to_string()), + }, + "100%".to_string().into(), + ), + ] + .into_iter() + .collect(), + ..Default::default() + })); + let div_wrapper_id = div_wrapper.id(); + div_wrapper.add_child(pre_cursor_div_id); + div_wrapper.add_child(cursor_span_id); + div_wrapper.add_child(post_cursor_div_id); + + root.add_event_listener("mousemove"); + root.add_event_listener("mousedown"); + root.add_event_listener("keydown"); + + Self { + pre_cursor_div: pre_cursor_div_id, + post_cursor_div: post_cursor_div_id, + div_wrapper: div_wrapper_id, + value, + ..Default::default() + } + } + + fn attributes_changed( + &mut self, + mut root: dioxus_native_core::real_dom::NodeMut, + attributes: &dioxus_native_core::node_ref::AttributeMask, + ) { + match attributes { + AttributeMask::All => { + { + let node_type = root.node_type_mut(); + let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") }; + self.update_value_attr(&el); + self.update_size_attr(&mut el); + self.update_max_attr(&el); + self.update_min_attr(&el); + self.update_step_attr(&el); + } + let id = root.id(); + self.write_value(root.real_dom_mut(), id); + } + AttributeMask::Some(attrs) => { + { + let node_type = root.node_type_mut(); + let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") }; + if attrs.contains("width") || attrs.contains("height") { + self.update_size_attr(&mut el); + } + if attrs.contains("max") { + self.update_max_attr(&el); + } + if attrs.contains("min") { + self.update_min_attr(&el); + } + if attrs.contains("step") { + self.update_step_attr(&el); + } + if attrs.contains("value") { + self.update_value_attr(&el); + } + } + if attrs.contains("value") { + let id = root.id(); + self.write_value(root.real_dom_mut(), id); + } + } + } + } +} + +impl RinkWidget for Slider { + fn handle_event(&mut self, event: &crate::Event, node: dioxus_native_core::real_dom::NodeMut) { + match event.name { + "keydown" => { + if let EventData::Keyboard(data) = &event.data { + self.handle_keydown(node, data); + } + } + + "mousemove" => { + if let EventData::Mouse(data) = &event.data { + self.handle_mousemove(node, data); + } + } + + "mousedown" => { + if let EventData::Mouse(data) = &event.data { + self.handle_mousemove(node, data); + } + } + + _ => {} + } + } +} diff --git a/packages/rink/src/widgets/text_like.rs b/packages/rink/src/widgets/text_like.rs new file mode 100644 index 000000000..b5874c776 --- /dev/null +++ b/packages/rink/src/widgets/text_like.rs @@ -0,0 +1,444 @@ +use std::{collections::HashMap, io::stdout}; + +use crossterm::{cursor::MoveTo, execute}; +use dioxus_html::{input_data::keyboard_types::Key, KeyboardData, MouseData}; +use dioxus_native_core::{ + custom_element::CustomElement, + node::OwnedAttributeDiscription, + node_ref::AttributeMask, + prelude::{ElementNode, NodeType}, + real_dom::{ElementNodeMut, NodeImmutable, NodeMut, NodeTypeMut, RealDom}, + utils::cursor::{Cursor, Pos}, + NodeId, +}; +use shipyard::UniqueView; +use taffy::geometry::Point; + +use crate::{query::get_layout, Event, EventData, FormData, Query}; + +use super::{RinkWidget, WidgetContext}; + +pub(crate) trait TextLikeController { + fn display_text(&self, text: &str) -> String { + text.to_string() + } +} + +#[derive(Debug, Default)] +pub(crate) struct EmptyController; + +impl TextLikeController for EmptyController {} + +#[derive(Debug, Default)] +pub(crate) struct TextLike { + text: String, + div_wrapper: NodeId, + pre_cursor_text: NodeId, + highlighted_text: NodeId, + post_cursor_text: NodeId, + cursor: Cursor, + dragging: bool, + border: bool, + max_len: Option, + controller: C, +} + +impl TextLike { + fn width(el: &ElementNodeMut) -> String { + if let Some(value) = el + .get_attribute(&OwnedAttributeDiscription { + name: "width".to_string(), + namespace: Some("style".to_string()), + }) + .and_then(|value| value.as_text()) + .map(|value| value.to_string()) + { + value + } else { + "1px".to_string() + } + } + + fn height(el: &ElementNodeMut) -> String { + if let Some(value) = el + .get_attribute(&OwnedAttributeDiscription { + name: "height".to_string(), + namespace: Some("style".to_string()), + }) + .and_then(|value| value.as_text()) + .map(|value| value.to_string()) + { + value + } else { + "1px".to_string() + } + } + + fn update_max_width_attr(&mut self, el: &ElementNodeMut) { + if let Some(value) = el + .get_attribute(&OwnedAttributeDiscription { + name: "maxlength".to_string(), + namespace: None, + }) + .and_then(|value| value.as_text()) + .map(|value| value.to_string()) + { + if let Ok(max_len) = value.parse::() { + self.max_len = Some(max_len); + } + } + } + + fn update_size_attr(&mut self, el: &mut ElementNodeMut) { + let width = Self::width(el); + let height = Self::height(el); + let single_char = width + .strip_prefix("px") + .and_then(|n| n.parse::().ok().filter(|num| *num > 3)) + .is_some() + || height + .strip_prefix("px") + .and_then(|n| n.parse::().ok().filter(|num| *num > 3)) + .is_some(); + self.border = !single_char; + let border_style = if self.border { "solid" } else { "none" }; + el.set_attribute( + OwnedAttributeDiscription { + name: "border-style".to_string(), + namespace: Some("style".to_string()), + }, + border_style.to_string(), + ); + } + + fn update_value_attr(&mut self, el: &ElementNodeMut) { + if let Some(value) = el + .get_attribute(&OwnedAttributeDiscription { + name: "value".to_string(), + namespace: None, + }) + .and_then(|value| value.as_text()) + .map(|value| value.to_string()) + { + self.text = value; + } + } + + pub(crate) fn set_text(&mut self, text: String, rdom: &mut RealDom, id: NodeId) { + self.text = text; + self.write_value(rdom, id); + } + + pub(crate) fn text(&self) -> &str { + self.text.as_str() + } + + fn write_value(&self, rdom: &mut RealDom, id: NodeId) { + let start_highlight = self.cursor.first().idx(self.text.as_str()); + let end_highlight = self.cursor.last().idx(self.text.as_str()); + let (text_before_first_cursor, text_after_first_cursor) = + self.text.split_at(start_highlight); + let (text_highlighted, text_after_second_cursor) = + text_after_first_cursor.split_at(end_highlight - start_highlight); + + if let Some(mut text) = rdom.get_mut(self.pre_cursor_text) { + let node_type = text.node_type_mut(); + let NodeTypeMut::Text(mut text) = node_type else { panic!("input must be an element") }; + *text.text_mut() = self.controller.display_text(text_before_first_cursor); + } + + if let Some(mut text) = rdom.get_mut(self.highlighted_text) { + let node_type = text.node_type_mut(); + let NodeTypeMut::Text(mut text) = node_type else { panic!("input must be an element") }; + *text.text_mut() = self.controller.display_text(text_highlighted); + } + + if let Some(mut text) = rdom.get_mut(self.post_cursor_text) { + let node_type = text.node_type_mut(); + let NodeTypeMut::Text(mut text) = node_type else { panic!("input must be an element") }; + *text.text_mut() = self.controller.display_text(text_after_second_cursor); + } + + // send the event + { + let world = rdom.raw_world_mut(); + let data: FormData = FormData { + value: self.text.clone(), + values: HashMap::new(), + files: None, + }; + let ctx: UniqueView = world.borrow().expect("expected widget context"); + + ctx.send(Event { + id, + name: "input", + data: EventData::Form(data), + bubbles: true, + }); + } + } + + fn handle_keydown(&mut self, mut root: NodeMut, data: &KeyboardData) { + let key = data.key(); + let modifiers = data.modifiers(); + let code = data.code(); + + if key == Key::Enter { + return; + } + self.cursor.handle_input( + &code, + &key, + &modifiers, + &mut self.text, + self.max_len.unwrap_or(1000), + ); + + let id = root.id(); + + let rdom = root.real_dom_mut(); + self.write_value(rdom, id); + let world = rdom.raw_world_mut(); + + // move cursor to new position + let taffy = { + let query: UniqueView = world.borrow().unwrap(); + query.stretch.clone() + }; + + let taffy = taffy.lock().unwrap(); + + let layout = get_layout(rdom.get(self.div_wrapper).unwrap(), &taffy).unwrap(); + let Point { x, y } = layout.location; + + let Pos { col, row } = self.cursor.start; + let (x, y) = (col as u16 + x as u16, row as u16 + y as u16); + if let Ok(pos) = crossterm::cursor::position() { + if pos != (x, y) { + execute!(stdout(), MoveTo(x, y)).unwrap(); + } + } else { + execute!(stdout(), MoveTo(x, y)).unwrap(); + } + } + + fn handle_mousemove(&mut self, mut root: NodeMut, data: &MouseData) { + if self.dragging { + let id = root.id(); + let offset = data.element_coordinates(); + let mut new = Pos::new(offset.x as usize, offset.y as usize); + + // textboxs are only one line tall + new.row = 0; + + if new != self.cursor.start { + self.cursor.end = Some(new); + } + let rdom = root.real_dom_mut(); + self.write_value(rdom, id); + } + } + + fn handle_mousedown(&mut self, mut root: NodeMut, data: &MouseData) { + let offset = data.element_coordinates(); + let mut new = Pos::new(offset.x as usize, offset.y as usize); + + // textboxs are only one line tall + new.row = 0; + + new.realize_col(self.text.as_str()); + self.cursor = Cursor::from_start(new); + self.dragging = true; + + let id = root.id(); + + // move cursor to new position + let rdom = root.real_dom_mut(); + let world = rdom.raw_world_mut(); + let taffy = { + let query: UniqueView = world.borrow().unwrap(); + query.stretch.clone() + }; + + let taffy = taffy.lock().unwrap(); + + let layout = get_layout(rdom.get(self.div_wrapper).unwrap(), &taffy).unwrap(); + let Point { x, y } = layout.location; + + let Pos { col, row } = self.cursor.start; + let (x, y) = (col as u16 + x as u16, row as u16 + y as u16); + if let Ok(pos) = crossterm::cursor::position() { + if pos != (x, y) { + execute!(stdout(), MoveTo(x, y)).unwrap(); + } + } else { + execute!(stdout(), MoveTo(x, y)).unwrap(); + } + + self.write_value(rdom, id) + } +} + +impl CustomElement for TextLike { + const NAME: &'static str = "input"; + + fn roots(&self) -> Vec { + vec![self.div_wrapper] + } + + fn create(mut root: dioxus_native_core::real_dom::NodeMut) -> Self { + let node_type = root.node_type(); + let NodeType::Element(el) = &*node_type else { panic!("input must be an element") }; + + let value = el + .attributes + .get(&OwnedAttributeDiscription { + name: "value".to_string(), + namespace: None, + }) + .and_then(|value| value.as_text()) + .map(|value| value.to_string()); + + drop(node_type); + + let rdom = root.real_dom_mut(); + + let pre_text = rdom.create_node(String::new()); + let pre_text_id = pre_text.id(); + let highlighted_text = rdom.create_node(String::new()); + let highlighted_text_id = highlighted_text.id(); + let mut highlighted_text_span = rdom.create_node(NodeType::Element(ElementNode { + tag: "span".to_string(), + attributes: [( + OwnedAttributeDiscription { + name: "background-color".to_string(), + namespace: Some("style".to_string()), + }, + "rgba(255, 255, 255, 50%)".to_string().into(), + )] + .into_iter() + .collect(), + ..Default::default() + })); + highlighted_text_span.add_child(highlighted_text_id); + let highlighted_text_span_id = highlighted_text_span.id(); + let post_text = rdom.create_node(value.clone().unwrap_or_default()); + let post_text_id = post_text.id(); + let mut div_wrapper = rdom.create_node(NodeType::Element(ElementNode { + tag: "div".to_string(), + attributes: [( + OwnedAttributeDiscription { + name: "display".to_string(), + namespace: Some("style".to_string()), + }, + "flex".to_string().into(), + )] + .into_iter() + .collect(), + ..Default::default() + })); + let div_wrapper_id = div_wrapper.id(); + div_wrapper.add_child(pre_text_id); + div_wrapper.add_child(highlighted_text_span_id); + div_wrapper.add_child(post_text_id); + + div_wrapper.add_event_listener("mousemove"); + div_wrapper.add_event_listener("mousedown"); + div_wrapper.add_event_listener("mouseup"); + div_wrapper.add_event_listener("mouseleave"); + div_wrapper.add_event_listener("mouseenter"); + root.add_event_listener("keydown"); + root.add_event_listener("focusout"); + + Self { + pre_cursor_text: pre_text_id, + highlighted_text: highlighted_text_id, + post_cursor_text: post_text_id, + div_wrapper: div_wrapper_id, + cursor: Cursor::default(), + text: value.unwrap_or_default(), + ..Default::default() + } + } + + fn attributes_changed( + &mut self, + mut root: dioxus_native_core::real_dom::NodeMut, + attributes: &dioxus_native_core::node_ref::AttributeMask, + ) { + match attributes { + AttributeMask::All => { + { + let node_type = root.node_type_mut(); + let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") }; + self.update_value_attr(&el); + self.update_size_attr(&mut el); + self.update_max_width_attr(&el); + } + let id = root.id(); + self.write_value(root.real_dom_mut(), id); + } + AttributeMask::Some(attrs) => { + { + let node_type = root.node_type_mut(); + let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") }; + if attrs.contains("width") || attrs.contains("height") { + self.update_size_attr(&mut el); + } + if attrs.contains("maxlength") { + self.update_max_width_attr(&el); + } + if attrs.contains("value") { + self.update_value_attr(&el); + } + } + if attrs.contains("value") { + let id = root.id(); + self.write_value(root.real_dom_mut(), id); + } + } + } + } +} + +impl RinkWidget for TextLike { + fn handle_event(&mut self, event: &crate::Event, node: NodeMut) { + match event.name { + "keydown" => { + if let EventData::Keyboard(data) = &event.data { + self.handle_keydown(node, data); + } + } + + "mousemove" => { + if let EventData::Mouse(data) = &event.data { + self.handle_mousemove(node, data); + } + } + + "mousedown" => { + if let EventData::Mouse(data) = &event.data { + self.handle_mousedown(node, data); + } + } + + "mouseup" => { + self.dragging = false; + } + + "mouseleave" => { + self.dragging = false; + } + + "mouseenter" => { + self.dragging = false; + } + + "focusout" => { + execute!(stdout(), MoveTo(0, 1000)).unwrap(); + } + + _ => {} + } + } +} diff --git a/packages/rink/src/widgets/textbox.rs b/packages/rink/src/widgets/textbox.rs new file mode 100644 index 000000000..df9bd7758 --- /dev/null +++ b/packages/rink/src/widgets/textbox.rs @@ -0,0 +1,3 @@ +use super::text_like::TextLike; + +pub(crate) type TextBox = TextLike; diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 09645ccc6..098ddfe65 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -92,4 +92,6 @@ dioxus = { workspace = true } wasm-bindgen-test = "0.3.29" dioxus-ssr = { workspace = true} wasm-logger = "0.2.0" +gloo-timers = "0.2.3" +gloo-dialogs = "0.1.1" dioxus-web = { workspace = true, features = ["hydrate"] } diff --git a/packages/web/examples/README.md b/packages/web/examples/README.md new file mode 100644 index 000000000..faa322df3 --- /dev/null +++ b/packages/web/examples/README.md @@ -0,0 +1,10 @@ +Examples +======== + +# Hydrate + +- `hydrate` show hydrate + +# Async + +- `timeout_count` button to add count and show count in the future diff --git a/packages/web/examples/timeout_count.rs b/packages/web/examples/timeout_count.rs new file mode 100644 index 000000000..742e6a49e --- /dev/null +++ b/packages/web/examples/timeout_count.rs @@ -0,0 +1,31 @@ +// https://jakelazaroff.com/words/were-react-hooks-a-mistake/ +use dioxus::prelude::*; + +fn main() { + dioxus_web::launch(app); +} + +fn app(cx: Scope) -> Element { + let count = use_ref(cx, || 0); + let started = use_state(cx, || false); + + let start = move || { + if !*started.get() { + let count = count.clone(); // clone reference rather than value + let alert = move || gloo_dialogs::alert(&format!("Your score was {}!", count.read())); + gloo_timers::callback::Timeout::new(5_000, alert).forget(); + } + started.set(true); // this cannot be done inside condition or infinite loop + }; + + cx.render(rsx! { + button { + onclick: move |_event| { + start(); + *count.write() += 1; + }, + // format is needed as {count} does not seemed to work in `if` within content + if **started { format!("Current score: {}", count.write()) } else { "Start".to_string() } + } + }) +}