mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-26 22:20:19 +00:00
Merge branch 'master' into feature/use-shared-state-better-diagnostics
This commit is contained in:
commit
9d2b44aa0f
63 changed files with 4018 additions and 1715 deletions
4
.github/workflows/playwright.yml
vendored
4
.github/workflows/playwright.yml
vendored
|
@ -8,7 +8,7 @@ jobs:
|
|||
test:
|
||||
if: github.event.pull_request.draft == false
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
|
@ -33,7 +33,7 @@ jobs:
|
|||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: install
|
||||
args: --git https://github.com/DioxusLabs/cli
|
||||
args: --path packages/cli
|
||||
- name: Run Playwright tests
|
||||
run: npx playwright test
|
||||
- uses: actions/upload-artifact@v3
|
||||
|
|
|
@ -116,8 +116,3 @@ fern = { version = "0.6.0", features = ["colored"] }
|
|||
env_logger = "0.10.0"
|
||||
simple_logger = "4.0.0"
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = true
|
||||
debug = true
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
use dioxus::prelude::*;
|
||||
use dioxus_signals::{use_init_signal_rt, use_signal};
|
||||
use std::time::Duration;
|
||||
|
||||
fn main() {
|
||||
|
@ -7,9 +6,7 @@ fn main() {
|
|||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
use_init_signal_rt(cx);
|
||||
|
||||
let mut count = use_signal(cx, || 0);
|
||||
let mut count = dioxus_signals::use_signal(cx, || 0);
|
||||
|
||||
use_future!(cx, || async move {
|
||||
loop {
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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<Vec<WebProxyConfig>>,
|
||||
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<Vec<PathBuf>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||
pub struct WebHttpsConfig {
|
||||
pub enabled: Option<bool>,
|
||||
pub mkcert: Option<bool>,
|
||||
pub key_path: Option<String>,
|
||||
pub cert_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CrateConfig {
|
||||
pub out_dir: PathBuf,
|
||||
|
|
70
packages/cli/src/server/hot_reload.rs
Normal file
70
packages/cli/src/server/hot_reload.rs
Normal file
|
@ -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<Template<'static>>,
|
||||
pub build_manager: Arc<BuildManager>,
|
||||
pub file_map: Arc<Mutex<FileMap<HtmlCtx>>>,
|
||||
pub watcher_config: CrateConfig,
|
||||
}
|
||||
|
||||
pub async fn hot_reload_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
_: Option<TypedHeader<headers::UserAgent>>,
|
||||
Extension(state): Extension<Arc<HotReloadState>>,
|
||||
) -> impl IntoResponse {
|
||||
ws.on_upgrade(|mut socket| async move {
|
||||
log::info!("🔥 Hot Reload WebSocket connected");
|
||||
{
|
||||
// update any rsx calls that changed before the websocket connected.
|
||||
{
|
||||
log::info!("🔮 Finding updates since last compile...");
|
||||
let templates: Vec<_> = {
|
||||
state
|
||||
.file_map
|
||||
.lock()
|
||||
.unwrap()
|
||||
.map
|
||||
.values()
|
||||
.filter_map(|(_, template_slot)| *template_slot)
|
||||
.collect()
|
||||
};
|
||||
for template in templates {
|
||||
if socket
|
||||
.send(Message::Text(serde_json::to_string(&template).unwrap()))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
log::info!("finished");
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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<Template<'static>>,
|
||||
pub build_manager: Arc<BuildManager>,
|
||||
pub file_map: Arc<Mutex<FileMap<HtmlCtx>>>,
|
||||
pub watcher_config: CrateConfig,
|
||||
}
|
||||
|
||||
pub async fn hot_reload_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
_: Option<TypedHeader<headers::UserAgent>>,
|
||||
Extension(state): Extension<Arc<HotReloadState>>,
|
||||
) -> impl IntoResponse {
|
||||
ws.on_upgrade(|mut socket| async move {
|
||||
log::info!("🔥 Hot Reload WebSocket connected");
|
||||
{
|
||||
// update any rsx calls that changed before the websocket connected.
|
||||
{
|
||||
log::info!("🔮 Finding updates since last compile...");
|
||||
let templates: Vec<_> = {
|
||||
state
|
||||
.file_map
|
||||
.lock()
|
||||
.unwrap()
|
||||
.map
|
||||
.values()
|
||||
.filter_map(|(_, template_slot)| *template_slot)
|
||||
.collect()
|
||||
};
|
||||
for template in templates {
|
||||
if socket
|
||||
.send(Message::Text(serde_json::to_string(&template).unwrap()))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
log::info!("finished");
|
||||
}
|
||||
|
||||
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::<HtmlCtx>::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<Option<RustlsConfig>> {
|
||||
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<WsReloadState>,
|
||||
hot_reload: Option<Arc<HotReloadState>>,
|
||||
) -> Result<Router> {
|
||||
// Setup cors
|
||||
let cors = CorsLayer::new()
|
||||
// allow `GET` and `POST` when accessing the resource
|
||||
.allow_methods([Method::GET, Method::POST])
|
||||
// allow requests from any origin
|
||||
.allow_origin(Any)
|
||||
.allow_headers(Any);
|
||||
|
||||
let (coep, coop) = if config.cross_origin_policy {
|
||||
(
|
||||
HeaderValue::from_static("require-corp"),
|
||||
HeaderValue::from_static("same-origin"),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
HeaderValue::from_static("unsafe-none"),
|
||||
HeaderValue::from_static("unsafe-none"),
|
||||
)
|
||||
};
|
||||
|
||||
// Create file service
|
||||
let file_service_config = config.clone();
|
||||
let file_service = ServiceBuilder::new()
|
||||
.override_response_header(
|
||||
HeaderName::from_static("cross-origin-embedder-policy"),
|
||||
coep,
|
||||
)
|
||||
.override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop)
|
||||
.and_then(
|
||||
move |response: Response<ServeFileSystemResponseBody>| async move {
|
||||
let 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<RustlsConfig>,
|
||||
) -> 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<RecommendedWatcher> {
|
||||
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<notify::Event>| {
|
||||
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<Template<'static>>,
|
||||
file_map: Arc<Mutex<FileMap<HtmlCtx>>>,
|
||||
build_manager: Arc<BuildManager>,
|
||||
) -> Result<RecommendedWatcher> {
|
||||
// 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<ServeFileSystemResponseBody>| 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<notify::Event>| {
|
||||
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<ServeFileSystemResponseBody>| 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<PathBuf>,
|
||||
warnings: Vec<Diagnostic>,
|
||||
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::<String>();
|
||||
// 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<String> {
|
||||
let socket = match UdpSocket::bind("0.0.0.0:0") {
|
||||
Ok(s) => s,
|
||||
|
@ -734,6 +625,7 @@ fn get_ip() -> Option<String> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Handle websockets
|
||||
async fn ws_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
_: Option<TypedHeader<headers::UserAgent>>,
|
||||
|
|
127
packages/cli/src/server/output.rs
Normal file
127
packages/cli/src/server/output.rs
Normal file
|
@ -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<PathBuf>,
|
||||
pub warnings: Vec<Diagnostic>,
|
||||
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()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,3 @@
|
|||
use std::borrow::Borrow;
|
||||
|
||||
use dioxus_core_macro::*;
|
||||
|
||||
#[test]
|
||||
|
@ -20,8 +18,8 @@ fn formatting_compiles() {
|
|||
|
||||
// function calls in formatings work
|
||||
assert_eq!(
|
||||
format_args_f!("{x.borrow():?}").to_string(),
|
||||
format!("{:?}", x.borrow())
|
||||
format_args_f!("{blah(&x):?}").to_string(),
|
||||
format!("{:?}", blah(&x))
|
||||
);
|
||||
|
||||
// allows duplicate format args
|
||||
|
@ -30,3 +28,7 @@ fn formatting_compiles() {
|
|||
format!("{x:?} {x:?}")
|
||||
);
|
||||
}
|
||||
|
||||
fn blah(hi: &(i32, i32)) -> String {
|
||||
format_args_f!("{hi.0} {hi.1}").to_string()
|
||||
}
|
||||
|
|
|
@ -27,8 +27,6 @@ slab = { workspace = true }
|
|||
|
||||
futures-channel = { workspace = true }
|
||||
|
||||
indexmap = "1.7"
|
||||
|
||||
smallbox = "0.8.1"
|
||||
log = { workspace = true }
|
||||
|
||||
|
|
|
@ -418,6 +418,25 @@ impl<'src> ScopeState {
|
|||
value
|
||||
}
|
||||
|
||||
/// Provide a context to the root and then consume it
|
||||
///
|
||||
/// This is intended for "global" state management solutions that would rather be implicit for the entire app.
|
||||
/// Things like signal runtimes and routers are examples of "singletons" that would benefit from lazy initialization.
|
||||
///
|
||||
/// Note that you should be checking if the context existed before trying to provide a new one. Providing a context
|
||||
/// when a context already exists will swap the context out for the new one, which may not be what you want.
|
||||
pub fn provide_root_context<T: 'static + Clone>(&self, context: T) -> T {
|
||||
let mut parent = self;
|
||||
|
||||
// Walk upwards until there is no more parent - and tada we have the root
|
||||
while let Some(next_parent) = parent.parent {
|
||||
parent = unsafe { &*next_parent };
|
||||
debug_assert_eq!(parent.scope_id(), ScopeId(0));
|
||||
}
|
||||
|
||||
parent.provide_context(context)
|
||||
}
|
||||
|
||||
/// Pushes the future onto the poll queue to be polled after the component renders.
|
||||
pub fn push_future(&self, fut: impl Future<Output = ()> + 'static) -> TaskId {
|
||||
let id = self.tasks.spawn(self.id, fut);
|
||||
|
|
|
@ -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::<f32>().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%",
|
||||
|
|
|
@ -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<Props: 'static>(app: Component<Props>, 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<RwLock<RealDom>>,
|
||||
id: NodeId,
|
||||
event: &str,
|
||||
value: Rc<EventData>,
|
||||
value: Rc<rink::EventData>,
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
pub use crate::widgets::*;
|
||||
pub use rink::Config;
|
|
@ -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}"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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}"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<EventHandler<'a, FormData>>,
|
||||
onclick: Option<EventHandler<'a, FormData>>,
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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<T>(cx: Scope<T>) -> Option<NodeId> {
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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::<i32>().ok())
|
||||
.filter(|w| *w < 3)
|
||||
.is_some()
|
||||
|| height
|
||||
.strip_suffix("px")
|
||||
.and_then(|h| h.parse::<i32>().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::<f64>().unwrap_or(0.0) + 1.0).to_string();
|
||||
update(text.clone());
|
||||
};
|
||||
let decrease = move || {
|
||||
let mut text = text_ref.write();
|
||||
*text = (text.parse::<f64>().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}"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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::<i32>().ok())
|
||||
.filter(|w| *w < 3)
|
||||
.is_some()
|
||||
|| height
|
||||
.strip_suffix("px")
|
||||
.and_then(|h| h.parse::<i32>().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}"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<f32> = 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)",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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::<i32>().ok())
|
||||
.filter(|w| *w < 3)
|
||||
.is_some()
|
||||
|| height
|
||||
.strip_suffix("px")
|
||||
.and_then(|h| h.parse::<i32>().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}"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -24,3 +24,4 @@ debug-cell = { git = "https://github.com/Niedzwiedzw/debug-cell", rev = "3352a1f
|
|||
futures-util = { workspace = true, default-features = false }
|
||||
dioxus-core = { workspace = true }
|
||||
dioxus = { workspace = true }
|
||||
web-sys = { version = "0.3.64", features = ["Document", "Window", "Element"] }
|
||||
|
|
|
@ -52,6 +52,9 @@ macro_rules! to_owned {
|
|||
};
|
||||
}
|
||||
|
||||
mod use_on_unmount;
|
||||
pub use use_on_unmount::*;
|
||||
|
||||
mod usecontext;
|
||||
pub use usecontext::*;
|
||||
|
||||
|
@ -78,3 +81,6 @@ pub use usecallback::*;
|
|||
|
||||
mod usememo;
|
||||
pub use usememo::*;
|
||||
|
||||
mod userootcontext;
|
||||
pub use userootcontext::*;
|
||||
|
|
74
packages/hooks/src/use_on_unmount.rs
Normal file
74
packages/hooks/src/use_on_unmount.rs
Normal file
|
@ -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<D: FnOnce() + 'static>(cx: &dioxus_core::ScopeState, destroy: D) {
|
||||
cx.use_hook(|| LifeCycle {
|
||||
ondestroy: Some(destroy),
|
||||
});
|
||||
}
|
||||
|
||||
struct LifeCycle<D: FnOnce()> {
|
||||
ondestroy: Option<D>,
|
||||
}
|
||||
|
||||
impl<D: FnOnce()> Drop for LifeCycle<D> {
|
||||
fn drop(&mut self) {
|
||||
let f = self.ondestroy.take().unwrap();
|
||||
f();
|
||||
}
|
||||
}
|
9
packages/hooks/src/userootcontext.rs
Normal file
9
packages/hooks/src/userootcontext.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
use dioxus_core::ScopeState;
|
||||
|
||||
///
|
||||
pub fn use_root_context<T: 'static + Clone>(cx: &ScopeState, new: impl FnOnce() -> T) -> &T {
|
||||
cx.use_hook(|| {
|
||||
cx.consume_context::<T>()
|
||||
.unwrap_or_else(|| cx.provide_root_context(new()))
|
||||
})
|
||||
}
|
|
@ -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::<Vec<_>>();
|
||||
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::<Vec<_>>();
|
||||
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 _,)*)
|
||||
|
|
|
@ -215,7 +215,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
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::<TextColor>().unwrap();
|
||||
let size = *node.get::<Size>().unwrap();
|
||||
|
|
|
@ -160,7 +160,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
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::<FontSize>().unwrap();
|
||||
let size = *node.get::<Size>().unwrap();
|
||||
|
|
|
@ -206,7 +206,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
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::<TextColor>().unwrap();
|
||||
let size = *node.get::<Size>().unwrap();
|
||||
|
|
|
@ -234,7 +234,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
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::<TextColor>().unwrap();
|
||||
let size = *node.get::<Size>().unwrap();
|
||||
|
|
180
packages/native-core/src/custom_element.rs
Normal file
180
packages/native-core/src/custom_element.rs
Normal file
|
@ -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<V: FromAnyValue + Send + Sync> {
|
||||
builders: FxHashMap<(&'static str, Option<&'static str>), CustomElementBuilder<V>>,
|
||||
}
|
||||
|
||||
impl<V: FromAnyValue + Send + Sync> Default for CustomElementRegistry<V> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
builders: FxHashMap::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: FromAnyValue + Send + Sync> CustomElementRegistry<V> {
|
||||
pub fn register<F, U>(&mut self)
|
||||
where
|
||||
F: CustomElementFactory<U, V>,
|
||||
U: CustomElementUpdater<V>,
|
||||
{
|
||||
self.builders.insert(
|
||||
(F::NAME, F::NAMESPACE),
|
||||
CustomElementBuilder {
|
||||
create: |node| Box::new(F::create(node)),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn add_shadow_dom(&self, mut node: NodeMut<V>) {
|
||||
let element_tag = if let NodeType::Element(el) = &*node.node_type() {
|
||||
Some((el.tag.clone(), el.namespace.clone()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some((tag, ns)) = element_tag {
|
||||
if let Some(builder) = self.builders.get(&(tag.as_str(), ns.as_deref())) {
|
||||
let boxed_custom_element = { (builder.create)(node.reborrow()) };
|
||||
|
||||
let shadow_roots = boxed_custom_element.roots();
|
||||
|
||||
let light_id = node.id();
|
||||
node.real_dom_mut().tree_mut().create_subtree(
|
||||
light_id,
|
||||
shadow_roots,
|
||||
boxed_custom_element.slot(),
|
||||
);
|
||||
|
||||
let boxed_custom_element = CustomElementManager {
|
||||
inner: Arc::new(RwLock::new(boxed_custom_element)),
|
||||
};
|
||||
|
||||
node.insert(boxed_custom_element);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CustomElementBuilder<V: FromAnyValue + Send + Sync> {
|
||||
create: fn(NodeMut<V>) -> Box<dyn CustomElementUpdater<V>>,
|
||||
}
|
||||
|
||||
/// A controlled element that renders to a shadow DOM.
|
||||
///
|
||||
/// Register with [`RealDom::register_custom_element`]
|
||||
///
|
||||
/// This is a simplified custom element trait for elements that can create themselves. For more granular control, implement [`CustomElementFactory`] and [`CustomElementUpdater`] instead.
|
||||
pub trait CustomElement<V: FromAnyValue + Send + Sync = ()>: Send + Sync + 'static {
|
||||
/// The tag of the element
|
||||
const NAME: &'static str;
|
||||
|
||||
/// The namespace of the element
|
||||
const NAMESPACE: Option<&'static str> = None;
|
||||
|
||||
/// Create a new element *without mounting* it.
|
||||
/// The node passed in is the light DOM node. The element should not modify the light DOM node, but it can get the [`NodeMut::real_dom_mut`] from the node to create new nodes.
|
||||
fn create(light_root: NodeMut<V>) -> Self;
|
||||
|
||||
/// The root node of the custom element. These roots must be not change once the element is created.
|
||||
fn roots(&self) -> Vec<NodeId>;
|
||||
|
||||
/// The slot to render children of the element into. The slot must be not change once the element is created.
|
||||
fn slot(&self) -> Option<NodeId> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Update the custom element's shadow tree with the new attributes.
|
||||
/// Called when the attributes of the custom element are changed.
|
||||
fn attributes_changed(&mut self, light_node: NodeMut<V>, attributes: &AttributeMask);
|
||||
}
|
||||
|
||||
/// A factory for creating custom elements
|
||||
///
|
||||
/// Register with [`RealDom::register_custom_element_with_factory`]
|
||||
pub trait CustomElementFactory<W: CustomElementUpdater<V>, V: FromAnyValue + Send + Sync = ()>:
|
||||
Send + Sync + 'static
|
||||
{
|
||||
/// The tag of the element
|
||||
const NAME: &'static str;
|
||||
|
||||
/// The namespace of the element
|
||||
const NAMESPACE: Option<&'static str> = None;
|
||||
|
||||
/// Create a new element *without mounting* it.
|
||||
/// The node passed in is the light DOM node. The element should not modify the light DOM node, but it can get the [`NodeMut::real_dom_mut`] from the node to create new nodes.
|
||||
fn create(dom: NodeMut<V>) -> W;
|
||||
}
|
||||
|
||||
impl<W: CustomElement<V>, V: FromAnyValue + Send + Sync> CustomElementFactory<W, V> for W {
|
||||
const NAME: &'static str = W::NAME;
|
||||
|
||||
const NAMESPACE: Option<&'static str> = W::NAMESPACE;
|
||||
|
||||
fn create(node: NodeMut<V>) -> Self {
|
||||
Self::create(node)
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for updating custom elements
|
||||
pub trait CustomElementUpdater<V: FromAnyValue + Send + Sync = ()>: Send + Sync + 'static {
|
||||
/// Update the custom element's shadow tree with the new attributes.
|
||||
/// Called when the attributes of the custom element are changed.
|
||||
fn attributes_changed(&mut self, light_root: NodeMut<V>, attributes: &AttributeMask);
|
||||
|
||||
/// The root node of the custom element. These roots must be not change once the element is created.
|
||||
fn roots(&self) -> Vec<NodeId>;
|
||||
|
||||
/// The slot to render children of the element into. The slot must be not change once the element is created.
|
||||
fn slot(&self) -> Option<NodeId> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: CustomElement<V>, V: FromAnyValue + Send + Sync> CustomElementUpdater<V> for W {
|
||||
fn attributes_changed(&mut self, light_root: NodeMut<V>, attributes: &AttributeMask) {
|
||||
self.attributes_changed(light_root, attributes);
|
||||
}
|
||||
|
||||
fn roots(&self) -> Vec<NodeId> {
|
||||
self.roots()
|
||||
}
|
||||
|
||||
fn slot(&self) -> Option<NodeId> {
|
||||
self.slot()
|
||||
}
|
||||
}
|
||||
|
||||
/// A dynamic trait object wrapper for [`CustomElementUpdater`]
|
||||
#[derive(Component, Clone)]
|
||||
pub(crate) struct CustomElementManager<V: FromAnyValue = ()> {
|
||||
inner: Arc<RwLock<Box<dyn CustomElementUpdater<V>>>>,
|
||||
}
|
||||
|
||||
impl<V: FromAnyValue + Send + Sync> CustomElementManager<V> {
|
||||
/// Update the custom element based on attributes changed.
|
||||
pub fn on_attributes_changed(&self, light_root: NodeMut<V>, attributes: &AttributeMask) {
|
||||
self.inner
|
||||
.write()
|
||||
.unwrap()
|
||||
.attributes_changed(light_root, attributes);
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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<V: FromAnyValue + Send + Sync> {
|
||||
/// Called after a node is added to the tree.
|
||||
fn on_node_added(&self, _node: NodeMut<V>) {}
|
||||
fn on_node_added(&mut self, _node: NodeMut<V>) {}
|
||||
/// Called before a node is removed from the tree.
|
||||
fn on_node_removed(&self, _node: NodeMut<V>) {}
|
||||
fn on_node_removed(&mut self, _node: NodeMut<V>) {}
|
||||
/// Called after a node is moved to a new parent.
|
||||
fn on_node_moved(&self, _node: NodeMut<V>) {}
|
||||
// /// Called after the text content of a node is changed.
|
||||
// fn on_text_changed(&self, _node: NodeMut<V>) {}
|
||||
// /// Called after an attribute of an element is changed.
|
||||
// fn on_attribute_changed(&self, _node: NodeMut<V>, attribute: &str) {}
|
||||
fn on_node_moved(&mut self, _node: NodeMut<V>) {}
|
||||
}
|
||||
|
||||
/// A trait for watching for changes to attributes of an element.
|
||||
pub trait AttributeWatcher<V: FromAnyValue + Send + Sync> {
|
||||
/// Called before update_state is called on the RealDom
|
||||
fn on_attributes_changed(&self, _node: NodeMut<V>, _attributes: &AttributeMask) {}
|
||||
}
|
||||
|
|
|
@ -104,6 +104,9 @@ pub trait State<V: FromAnyValue + Send + Sync = ()>: 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<V: FromAnyValue + Send + Sync = ()>: Any + Send + Sync {
|
|||
dependants: Default::default(),
|
||||
mask: node_mask,
|
||||
pass_direction: pass_direction::<V, Self>(),
|
||||
enter_shadow_dom: Self::TRAVERSE_SHADOW_DOM,
|
||||
workload: Self::workload_system,
|
||||
phantom: PhantomData,
|
||||
}
|
||||
|
@ -228,28 +232,42 @@ pub fn run_pass<V: FromAnyValue + Send + Sync>(
|
|||
}
|
||||
}
|
||||
|
||||
#[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<TypeId>,
|
||||
pub(crate) parent: Vec<Dependant>,
|
||||
/// The states in the child direction that should be invalidated when this state is invalidated
|
||||
pub child: Vec<TypeId>,
|
||||
pub(crate) child: Vec<Dependant>,
|
||||
/// The states in the node direction that should be invalidated when this state is invalidated
|
||||
pub node: Vec<TypeId>,
|
||||
pub(crate) node: Vec<TypeId>,
|
||||
}
|
||||
|
||||
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<V: FromAnyValue + Send = ()> {
|
|||
pub(crate) mask: NodeMask,
|
||||
pub(crate) workload: fn(TypeId, Arc<Dependants>, PassDirection) -> WorkloadSystem,
|
||||
pub(crate) pass_direction: PassDirection,
|
||||
pub(crate) enter_shadow_dom: bool,
|
||||
phantom: PhantomData<V>,
|
||||
}
|
||||
|
||||
|
|
|
@ -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<V: FromAnyValue + Send + Sync> {
|
||||
passes_updated: FxHashMap<NodeId, FxHashSet<TypeId>>,
|
||||
nodes_updated: FxHashMap<NodeId, NodeMask>,
|
||||
nodes_created: FxHashSet<NodeId>,
|
||||
pub(crate) passes: Box<[TypeErasedState<V>]>,
|
||||
}
|
||||
|
||||
|
@ -92,6 +97,7 @@ impl<V: FromAnyValue + Send + Sync> NodesDirty<V> {
|
|||
}
|
||||
|
||||
type NodeWatchers<V> = Arc<RwLock<Vec<Box<dyn NodeWatcher<V> + Send + Sync>>>>;
|
||||
type AttributeWatchers<V> = Arc<RwLock<Vec<Box<dyn AttributeWatcher<V> + 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<V: FromAnyValue + Send + Sync = ()> {
|
|||
nodes_listening: FxHashMap<String, FxHashSet<NodeId>>,
|
||||
pub(crate) dirty_nodes: NodesDirty<V>,
|
||||
node_watchers: NodeWatchers<V>,
|
||||
attribute_watchers: AttributeWatchers<V>,
|
||||
workload: ScheduledWorkload,
|
||||
root_id: NodeId,
|
||||
custom_elements: Arc<RwLock<CustomElementRegistry<V>>>,
|
||||
phantom: std::marker::PhantomData<V>,
|
||||
}
|
||||
|
||||
|
@ -123,19 +131,25 @@ impl<V: FromAnyValue + Send + Sync> RealDom<V> {
|
|||
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<V: FromAnyValue + Send + Sync> RealDom<V> {
|
|||
}
|
||||
// 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<V: FromAnyValue + Send + Sync> RealDom<V> {
|
|||
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<V: FromAnyValue + Send + Sync> RealDom<V> {
|
|||
|
||||
/// 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<NodeType<V>>) -> 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<V: FromAnyValue + Send + Sync> RealDom<V> {
|
|||
.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<V: FromAnyValue + Send + Sync> RealDom<V> {
|
|||
&mut self,
|
||||
ctx: SendAnyMap,
|
||||
) -> (FxDashSet<NodeId>, FxHashMap<NodeId, NodeMask>) {
|
||||
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::<CustomElementManager<V>>().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<V: FromAnyValue + Send + Sync> RealDom<V> {
|
|||
}
|
||||
|
||||
/// 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<V>)) {
|
||||
/// 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<V>),
|
||||
) {
|
||||
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<V>)) {
|
||||
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<V>)) {
|
||||
/// 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<V>),
|
||||
) {
|
||||
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<V: FromAnyValue + Send + Sync> RealDom<V> {
|
|||
}
|
||||
}
|
||||
|
||||
/// 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<V>)) {
|
||||
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<V>)) {
|
||||
/// 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<V>),
|
||||
) {
|
||||
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<V: FromAnyValue + Send + Sync> RealDom<V> {
|
|||
}
|
||||
}
|
||||
|
||||
/// 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<V>)) {
|
||||
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<V>)) {
|
||||
/// 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<V>),
|
||||
) {
|
||||
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<V: FromAnyValue + Send + Sync> RealDom<V> {
|
|||
}
|
||||
}
|
||||
|
||||
/// 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<V>)) {
|
||||
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<V> + '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<V> + '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<V: FromAnyValue + Send + Sync> RealDom<V> {
|
|||
pub fn raw_world_mut(&mut self) -> &mut World {
|
||||
&mut self.world
|
||||
}
|
||||
|
||||
/// Registers a new custom element.
|
||||
pub fn register_custom_element<E: CustomElement<V>>(&mut self) {
|
||||
self.register_custom_element_with_factory::<E, E>()
|
||||
}
|
||||
|
||||
/// Registers a new custom element with a custom factory.
|
||||
pub fn register_custom_element_with_factory<F, U>(&mut self)
|
||||
where
|
||||
F: CustomElementFactory<U, V>,
|
||||
U: CustomElementUpdater<V>,
|
||||
{
|
||||
self.custom_elements.write().unwrap().register::<F, U>()
|
||||
}
|
||||
}
|
||||
|
||||
/// A reference to a tracked component in a node.
|
||||
|
@ -458,6 +595,14 @@ pub trait NodeImmutable<V: FromAnyValue + Send + Sync = ()>: 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<NodeId> {
|
||||
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<NodeId> {
|
||||
|
@ -476,6 +621,14 @@ pub trait NodeImmutable<V: FromAnyValue + Send + Sync = ()>: 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<NodeId> {
|
||||
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<NodeId> {
|
||||
|
@ -585,6 +738,14 @@ impl<'a, V: FromAnyValue + Send + Sync> NodeImmutable<V> 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<V> {
|
||||
|
@ -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<V>,
|
||||
}
|
||||
|
||||
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<V: FromAnyValue + Send + Sync> ElementNodeMut<'_, V> {
|
||||
/// Get the current element
|
||||
fn element(&self) -> &ElementNode<V> {
|
||||
|
@ -1016,6 +1190,14 @@ impl<V: FromAnyValue + Send + Sync> ElementNodeMut<'_, V> {
|
|||
self.element_mut().attributes.get_mut(name)
|
||||
}
|
||||
|
||||
/// Get an attribute of the element
|
||||
pub fn get_attribute(
|
||||
&self,
|
||||
name: &OwnedAttributeDiscription,
|
||||
) -> Option<&OwnedAttributeValue<V>> {
|
||||
self.element().attributes.get(name)
|
||||
}
|
||||
|
||||
/// Get the set of all events the element is listening to
|
||||
pub fn listeners(&self) -> &FxHashSet<String> {
|
||||
&self.element().listeners
|
||||
|
|
|
@ -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<NodeId>,
|
||||
/// The node that children of the super tree should be inserted under.
|
||||
pub slot: Option<NodeId>,
|
||||
}
|
||||
|
||||
/// A node in a tree.
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Component)]
|
||||
pub struct Node {
|
||||
parent: Option<NodeId>,
|
||||
children: Vec<NodeId>,
|
||||
child_subtree: Option<ShadowTree>,
|
||||
/// If this node is a slot in a shadow_tree, this is node whose child_subtree is that shadow_tree.
|
||||
slot_for_light_tree: Option<NodeId>,
|
||||
/// If this node is a root of a shadow_tree, this is the node whose child_subtree is that shadow_tree.
|
||||
root_for_light_tree: Option<NodeId>,
|
||||
height: u16,
|
||||
}
|
||||
|
||||
|
@ -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<NodeId> {
|
||||
// If this node is the root of a shadow_tree, return the node the shadow_tree is attached
|
||||
let root_for_light_tree = self.root_for_light_tree(id);
|
||||
match (root_for_light_tree, enter_shadow_dom) {
|
||||
(Some(id), true) => Some(id),
|
||||
_ => {
|
||||
let parent_id = self.parent_id(id);
|
||||
if enter_shadow_dom {
|
||||
// If this node is attached via a slot, return the slot as the parent instead of the light tree parent
|
||||
parent_id.map(|id| {
|
||||
self.shadow_tree(id)
|
||||
.and_then(|tree| tree.slot)
|
||||
.unwrap_or(id)
|
||||
})
|
||||
} else {
|
||||
parent_id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/// The parent id of the node.
|
||||
fn parent_id(&self, id: NodeId) -> Option<NodeId>;
|
||||
/// Get the ids of the children of the current node, if enter_shadow_dom is true and the current node is a shadow slot, the ids of the nodes under the node the shadow slot is attached to will be returned
|
||||
#[inline]
|
||||
fn children_ids_advanced(&self, id: NodeId, enter_shadow_dom: bool) -> Vec<NodeId> {
|
||||
let shadow_tree = self.shadow_tree(id);
|
||||
let slot_of_light_tree = self.slot_for_light_tree(id);
|
||||
match (shadow_tree, slot_of_light_tree, enter_shadow_dom) {
|
||||
// If this node is a shadow root, return the shadow roots
|
||||
(Some(tree), _, true) => tree.shadow_roots.clone(),
|
||||
// If this node is a slot, return the children of the node the slot is attached to
|
||||
(None, Some(id), true) => self.children_ids(id),
|
||||
_ => self.children_ids(id),
|
||||
}
|
||||
}
|
||||
/// The children ids of the node.
|
||||
fn children_ids(&self, id: NodeId) -> Vec<NodeId>;
|
||||
/// The shadow tree tree under the node.
|
||||
fn shadow_tree(&self, id: NodeId) -> Option<&ShadowTree>;
|
||||
/// The node that contains the shadow tree this node is a slot for
|
||||
fn slot_for_light_tree(&self, id: NodeId) -> Option<NodeId>;
|
||||
/// The node that contains the shadow tree this node is a root of
|
||||
fn root_for_light_tree(&self, id: NodeId) -> Option<NodeId>;
|
||||
/// The height of the node.
|
||||
fn height(&self, id: NodeId) -> Option<u16>;
|
||||
/// Returns true if the node exists.
|
||||
|
@ -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<NodeId>, slot: Option<NodeId>);
|
||||
/// 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<NodeId> {
|
||||
self.get(id).ok()?.slot_for_light_tree
|
||||
}
|
||||
|
||||
fn root_for_light_tree(&self, id: NodeId) -> Option<NodeId> {
|
||||
self.get(id).ok()?.root_for_light_tree
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TreeMut for TreeMutView<'a> {
|
||||
fn remove(&mut self, id: NodeId) {
|
||||
fn recurse(tree: &mut TreeMutView<'_>, id: NodeId) {
|
||||
let 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<NodeId>, slot: Option<NodeId>) {
|
||||
let (_, node_data_mut) = self;
|
||||
|
||||
let light_root_height;
|
||||
{
|
||||
let shadow_tree = ShadowTree {
|
||||
shadow_roots: shadow_roots.clone(),
|
||||
slot,
|
||||
};
|
||||
|
||||
let light_root = node_data_mut
|
||||
.get(id)
|
||||
.expect("tried to create shadow_tree with non-existent id");
|
||||
|
||||
light_root.child_subtree = Some(shadow_tree);
|
||||
light_root_height = light_root.height;
|
||||
|
||||
if let Some(slot) = slot {
|
||||
let slot = node_data_mut
|
||||
.get(slot)
|
||||
.expect("tried to create shadow_tree with non-existent slot");
|
||||
slot.slot_for_light_tree = Some(id);
|
||||
}
|
||||
}
|
||||
|
||||
// Now that we have created the shadow_tree, we need to update the height of the shadow_tree roots
|
||||
for root in shadow_roots {
|
||||
(&mut self.1).get(root).unwrap().root_for_light_tree = Some(id);
|
||||
set_height(self, root, light_root_height + 1);
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_subtree(&mut self, id: NodeId) {
|
||||
let (_, node_data_mut) = self;
|
||||
|
||||
if let Ok(node) = node_data_mut.get(id) {
|
||||
if let Some(shadow_tree) = node.child_subtree.take() {
|
||||
// Remove the slot's link to the shadow_tree
|
||||
if let Some(slot) = shadow_tree.slot {
|
||||
let slot = node_data_mut
|
||||
.get(slot)
|
||||
.expect("tried to remove shadow_tree with non-existent slot");
|
||||
slot.slot_for_light_tree = None;
|
||||
}
|
||||
|
||||
let node = node_data_mut.get(id).unwrap();
|
||||
|
||||
// Reset the height of the light root's children
|
||||
let height = node.height;
|
||||
for child in node.children.clone() {
|
||||
println!("child: {:?}", child);
|
||||
set_height(self, child, height + 1);
|
||||
}
|
||||
|
||||
// Reset the height of the shadow roots
|
||||
for root in &shadow_tree.shadow_roots {
|
||||
set_height(self, *root, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn child_height(parent: &Node, tree: &impl TreeRef) -> u16 {
|
||||
match &parent.child_subtree {
|
||||
Some(shadow_tree) => {
|
||||
if let Some(slot) = shadow_tree.slot {
|
||||
tree.height(slot)
|
||||
.expect("Attempted to read a slot that does not exist")
|
||||
+ 1
|
||||
} else {
|
||||
panic!("Attempted to read the height of a child of a node with a shadow tree, but the shadow tree does not have a slot. Every shadow tree attached to a node with children must have a slot.")
|
||||
}
|
||||
}
|
||||
None => parent.height + 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the height of a node and updates the height of all its children
|
||||
fn set_height(tree: &mut TreeMutView<'_>, node: NodeId, height: u16) {
|
||||
let 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<NodeId> {
|
||||
let node_data = &self.1;
|
||||
node_data.get(id).ok()?.slot_for_light_tree
|
||||
}
|
||||
|
||||
fn root_for_light_tree(&self, id: NodeId) -> Option<NodeId> {
|
||||
let node_data = &self.1;
|
||||
node_data.get(id).ok()?.root_for_light_tree
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -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::<TreeMutView>().unwrap();
|
||||
|
||||
tree.create_node(parent_id);
|
||||
tree.create_node(child_id);
|
||||
|
||||
tree.add_child(parent_id, child_id);
|
||||
|
||||
tree.create_node(shadow_parent_id);
|
||||
tree.create_node(shadow_child_id);
|
||||
|
||||
tree.add_child(shadow_parent_id, shadow_child_id);
|
||||
|
||||
// Check that both trees are correct individually
|
||||
assert_eq!(tree.height(parent_id), Some(0));
|
||||
assert_eq!(tree.height(child_id), Some(1));
|
||||
assert_eq!(tree.parent_id(parent_id), None);
|
||||
assert_eq!(tree.parent_id(child_id).unwrap(), parent_id);
|
||||
assert_eq!(tree.children_ids(parent_id), &[child_id]);
|
||||
|
||||
assert_eq!(tree.height(shadow_parent_id), Some(0));
|
||||
assert_eq!(tree.height(shadow_child_id), Some(1));
|
||||
assert_eq!(tree.parent_id(shadow_parent_id), None);
|
||||
assert_eq!(tree.parent_id(shadow_child_id).unwrap(), shadow_parent_id);
|
||||
assert_eq!(tree.children_ids(shadow_parent_id), &[shadow_child_id]);
|
||||
|
||||
// Add shadow tree to main tree
|
||||
tree.create_subtree(parent_id, vec![shadow_parent_id], Some(shadow_child_id));
|
||||
|
||||
assert_eq!(tree.height(parent_id), Some(0));
|
||||
assert_eq!(tree.height(shadow_parent_id), Some(1));
|
||||
assert_eq!(tree.height(shadow_child_id), Some(2));
|
||||
assert_eq!(tree.height(child_id), Some(3));
|
||||
assert_eq!(
|
||||
tree.1
|
||||
.get(parent_id)
|
||||
.unwrap()
|
||||
.child_subtree
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.shadow_roots,
|
||||
&[shadow_parent_id]
|
||||
);
|
||||
assert_eq!(
|
||||
tree.1.get(shadow_child_id).unwrap().slot_for_light_tree,
|
||||
Some(parent_id)
|
||||
);
|
||||
|
||||
// Remove shadow tree from main tree
|
||||
tree.remove_subtree(parent_id);
|
||||
|
||||
// Check that both trees are correct individually
|
||||
assert_eq!(tree.height(parent_id), Some(0));
|
||||
assert_eq!(tree.height(child_id), Some(1));
|
||||
assert_eq!(tree.parent_id(parent_id), None);
|
||||
assert_eq!(tree.parent_id(child_id).unwrap(), parent_id);
|
||||
assert_eq!(tree.children_ids(parent_id), &[child_id]);
|
||||
|
||||
assert_eq!(tree.height(shadow_parent_id), Some(0));
|
||||
assert_eq!(tree.height(shadow_child_id), Some(1));
|
||||
assert_eq!(tree.parent_id(shadow_parent_id), None);
|
||||
assert_eq!(tree.parent_id(shadow_child_id).unwrap(), shadow_parent_id);
|
||||
assert_eq!(tree.children_ids(shadow_parent_id), &[shadow_child_id]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insertion() {
|
||||
use shipyard::World;
|
||||
|
|
|
@ -60,7 +60,7 @@ struct PersistantElementIterUpdater<V> {
|
|||
}
|
||||
|
||||
impl<V: FromAnyValue + Sync + Send> NodeWatcher<V> for PersistantElementIterUpdater<V> {
|
||||
fn on_node_moved(&self, node: NodeMut<V>) {
|
||||
fn on_node_moved(&mut self, node: NodeMut<V>) {
|
||||
// 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<V: FromAnyValue + Sync + Send> NodeWatcher<V> for PersistantElementIterUpda
|
|||
}
|
||||
}
|
||||
|
||||
fn on_node_removed(&self, node: NodeMut<V>) {
|
||||
fn on_node_removed(&mut self, node: NodeMut<V>) {
|
||||
// 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();
|
||||
|
|
|
@ -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);
|
||||
)*
|
||||
|
|
389
packages/native-core/tests/custom_element.rs
Normal file
389
packages/native-core/tests/custom_element.rs
Normal file
|
@ -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,
|
||||
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
|
||||
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
|
||||
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
|
||||
_: &SendAnyMap,
|
||||
) -> bool {
|
||||
if let Some(size) = view
|
||||
.attributes()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.find(|attr| attr.attribute.name == "color")
|
||||
{
|
||||
self.color = size
|
||||
.value
|
||||
.as_float()
|
||||
.or_else(|| size.value.as_int().map(|i| i as f64))
|
||||
.or_else(|| size.value.as_text().and_then(|i| i.parse().ok()))
|
||||
.unwrap_or(0.0) as usize;
|
||||
} else if let Some((parent,)) = parent {
|
||||
*self = *parent;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn create<'a>(
|
||||
node_view: NodeView<()>,
|
||||
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
|
||||
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
|
||||
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
|
||||
context: &SendAnyMap,
|
||||
) -> Self {
|
||||
let mut myself = Self::default();
|
||||
myself.update(node_view, node, parent, children, context);
|
||||
myself
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Component)]
|
||||
pub struct LayoutState {
|
||||
size: usize,
|
||||
}
|
||||
|
||||
#[partial_derive_state]
|
||||
impl State for LayoutState {
|
||||
type ParentDependencies = (Self,);
|
||||
type ChildDependencies = ();
|
||||
type NodeDependencies = ();
|
||||
|
||||
// The layout state should be effected by the shadow dom
|
||||
const TRAVERSE_SHADOW_DOM: bool = true;
|
||||
|
||||
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new()
|
||||
.with_attrs(AttributeMaskBuilder::Some(&["size"]))
|
||||
.with_element();
|
||||
|
||||
fn update<'a>(
|
||||
&mut self,
|
||||
view: NodeView,
|
||||
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
|
||||
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
|
||||
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
|
||||
_: &SendAnyMap,
|
||||
) -> bool {
|
||||
if let Some(size) = view
|
||||
.attributes()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.find(|attr| attr.attribute.name == "size")
|
||||
{
|
||||
self.size = size
|
||||
.value
|
||||
.as_float()
|
||||
.or_else(|| size.value.as_int().map(|i| i as f64))
|
||||
.or_else(|| size.value.as_text().and_then(|i| i.parse().ok()))
|
||||
.unwrap_or(0.0) as usize;
|
||||
} else if let Some((parent,)) = parent {
|
||||
if parent.size > 0 {
|
||||
self.size = parent.size - 1;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn create<'a>(
|
||||
node_view: NodeView<()>,
|
||||
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
|
||||
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
|
||||
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
|
||||
context: &SendAnyMap,
|
||||
) -> Self {
|
||||
let mut myself = Self::default();
|
||||
myself.update(node_view, node, parent, children, context);
|
||||
myself
|
||||
}
|
||||
}
|
||||
|
||||
mod dioxus_elements {
|
||||
macro_rules! builder_constructors {
|
||||
(
|
||||
$(
|
||||
$(#[$attr:meta])*
|
||||
$name:ident {
|
||||
$(
|
||||
$(#[$attr_method:meta])*
|
||||
$fil:ident: $vil:ident,
|
||||
)*
|
||||
};
|
||||
)*
|
||||
) => {
|
||||
$(
|
||||
#[allow(non_camel_case_types)]
|
||||
$(#[$attr])*
|
||||
pub struct $name;
|
||||
|
||||
#[allow(non_upper_case_globals, unused)]
|
||||
impl $name {
|
||||
pub const TAG_NAME: &'static str = stringify!($name);
|
||||
pub const NAME_SPACE: Option<&'static str> = None;
|
||||
|
||||
$(
|
||||
pub const $fil: (&'static str, Option<&'static str>, bool) = (stringify!($fil), None, false);
|
||||
)*
|
||||
}
|
||||
|
||||
impl GlobalAttributes for $name {}
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
pub trait GlobalAttributes {}
|
||||
|
||||
pub trait SvgAttributes {}
|
||||
|
||||
builder_constructors! {
|
||||
customelementslot {
|
||||
size: attr,
|
||||
color: attr,
|
||||
};
|
||||
customelementnoslot {
|
||||
size: attr,
|
||||
color: attr,
|
||||
};
|
||||
testing132 {
|
||||
color: attr,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
struct CustomElementWithSlot {
|
||||
root: NodeId,
|
||||
slot: NodeId,
|
||||
}
|
||||
|
||||
impl CustomElement for CustomElementWithSlot {
|
||||
const NAME: &'static str = "customelementslot";
|
||||
|
||||
fn create(mut node: NodeMut<()>) -> Self {
|
||||
let dom = node.real_dom_mut();
|
||||
let child = dom.create_node(ElementNode {
|
||||
tag: "div".into(),
|
||||
namespace: None,
|
||||
attributes: Default::default(),
|
||||
listeners: Default::default(),
|
||||
});
|
||||
let slot_id = child.id();
|
||||
let mut root = dom.create_node(ElementNode {
|
||||
tag: "div".into(),
|
||||
namespace: None,
|
||||
attributes: Default::default(),
|
||||
listeners: Default::default(),
|
||||
});
|
||||
root.add_child(slot_id);
|
||||
|
||||
Self {
|
||||
root: root.id(),
|
||||
slot: slot_id,
|
||||
}
|
||||
}
|
||||
|
||||
fn slot(&self) -> Option<NodeId> {
|
||||
Some(self.slot)
|
||||
}
|
||||
|
||||
fn roots(&self) -> Vec<NodeId> {
|
||||
vec![self.root]
|
||||
}
|
||||
|
||||
fn attributes_changed(
|
||||
&mut self,
|
||||
node: NodeMut<()>,
|
||||
attributes: &dioxus_native_core::node_ref::AttributeMask,
|
||||
) {
|
||||
println!("attributes_changed");
|
||||
println!("{:?}", attributes);
|
||||
println!("{:?}: {:#?}", node.id(), &*node.node_type());
|
||||
}
|
||||
}
|
||||
|
||||
struct CustomElementWithNoSlot {
|
||||
root: NodeId,
|
||||
}
|
||||
|
||||
impl CustomElement for CustomElementWithNoSlot {
|
||||
const NAME: &'static str = "customelementnoslot";
|
||||
|
||||
fn create(mut node: NodeMut<()>) -> Self {
|
||||
let dom = node.real_dom_mut();
|
||||
let root = dom.create_node(ElementNode {
|
||||
tag: "div".into(),
|
||||
namespace: None,
|
||||
attributes: Default::default(),
|
||||
listeners: Default::default(),
|
||||
});
|
||||
Self { root: root.id() }
|
||||
}
|
||||
|
||||
fn roots(&self) -> Vec<NodeId> {
|
||||
vec![self.root]
|
||||
}
|
||||
|
||||
fn attributes_changed(
|
||||
&mut self,
|
||||
node: NodeMut<()>,
|
||||
attributes: &dioxus_native_core::node_ref::AttributeMask,
|
||||
) {
|
||||
println!("attributes_changed");
|
||||
println!("{:?}", attributes);
|
||||
println!("{:?}: {:#?}", node.id(), &*node.node_type());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_elements_work() {
|
||||
fn app(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::<CustomElementWithSlot>();
|
||||
let mut dioxus_state = DioxusState::create(&mut rdom);
|
||||
let mut dom = VirtualDom::new(app);
|
||||
|
||||
let mutations = dom.rebuild();
|
||||
dioxus_state.apply_mutations(&mut rdom, mutations);
|
||||
|
||||
let ctx = SendAnyMap::new();
|
||||
rdom.update_state(ctx);
|
||||
|
||||
for i in 0..10usize {
|
||||
dom.wait_for_work().await;
|
||||
|
||||
let mutations = dom.render_immediate();
|
||||
dioxus_state.apply_mutations(&mut rdom, mutations);
|
||||
|
||||
let ctx = SendAnyMap::new();
|
||||
rdom.update_state(ctx);
|
||||
|
||||
// render...
|
||||
rdom.traverse_depth_first_advanced(true, |node| {
|
||||
let node_type = &*node.node_type();
|
||||
let height = node.height() as usize;
|
||||
let indent = " ".repeat(height);
|
||||
let color = *node.get::<ColorState>().unwrap();
|
||||
let size = *node.get::<LayoutState>().unwrap();
|
||||
let id = node.id();
|
||||
println!("{indent}{id:?} {color:?} {size:?} {node_type:?}");
|
||||
if let NodeType::Element(el) = node_type {
|
||||
match el.tag.as_str() {
|
||||
// the color should bubble up from customelementslot
|
||||
"testing132" | "customelementslot" => {
|
||||
assert_eq!(color.color, 1);
|
||||
}
|
||||
// the color of the light dom should not effect the color of the shadow dom, so the color of divs in the shadow dom should be 0
|
||||
"div" => {
|
||||
assert_eq!(color.color, 0);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if el.tag != "Root" {
|
||||
assert_eq!(size.size, (i + 2).saturating_sub(height));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn slotless_custom_element_cant_have_children() {
|
||||
fn app(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::<CustomElementWithNoSlot>();
|
||||
let mut dioxus_state = DioxusState::create(&mut rdom);
|
||||
let mut dom = VirtualDom::new(app);
|
||||
|
||||
let mutations = dom.rebuild();
|
||||
dioxus_state.apply_mutations(&mut rdom, mutations);
|
||||
|
||||
let ctx = SendAnyMap::new();
|
||||
rdom.update_state(ctx);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slotless_custom_element() {
|
||||
fn app(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::<CustomElementWithNoSlot>();
|
||||
let mut dioxus_state = DioxusState::create(&mut rdom);
|
||||
let mut dom = VirtualDom::new(app);
|
||||
|
||||
let mutations = dom.rebuild();
|
||||
dioxus_state.apply_mutations(&mut rdom, mutations);
|
||||
|
||||
let ctx = SendAnyMap::new();
|
||||
rdom.update_state(ctx);
|
||||
});
|
||||
}
|
|
@ -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};
|
||||
|
||||
|
|
|
@ -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};
|
||||
|
|
101
packages/rink/examples/widgets.rs
Normal file
101
packages/rink/examples/widgets.rs
Normal file
|
@ -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<RwLock<RealDom>>) {
|
||||
// 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<RwLock<RealDom>>,
|
||||
_: NodeId,
|
||||
event_type: &str,
|
||||
event: Rc<EventData>,
|
||||
_: 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::<f64>() {
|
||||
self.count = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_async(&mut self) -> std::pin::Pin<Box<dyn futures::Future<Output = ()> + '_>> {
|
||||
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();
|
||||
}
|
|
@ -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<EventData>,
|
||||
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<dyn Any> {
|
||||
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<String, Vec<String>>,
|
||||
|
||||
pub files: Option<Files>,
|
||||
}
|
||||
|
||||
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<String, File>,
|
||||
}
|
||||
|
||||
#[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<EventData>,
|
||||
data: EventData,
|
||||
will_bubble: &mut FxHashSet<NodeId>,
|
||||
resolved_events: &mut Vec<Event>,
|
||||
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::<TaffyLayout>().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::<TaffyLayout>().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<RefCell<InnerInputState>>,
|
||||
queued_events: Rc<RefCell<Vec<EventCore>>>,
|
||||
|
@ -628,12 +637,12 @@ impl RinkInputHandler {
|
|||
})
|
||||
.map(|evt| (evt.0, evt.1));
|
||||
|
||||
let mut hm: FxHashMap<&'static str, Vec<Rc<EventData>>> = FxHashMap::default();
|
||||
let mut hm: FxHashMap<&'static str, Vec<EventData>> = 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 {
|
||||
|
|
|
@ -25,12 +25,6 @@ impl<T> PossiblyUninitalized<T> {
|
|||
_ => panic!("uninitalized"),
|
||||
}
|
||||
}
|
||||
pub fn ok(self) -> Option<T> {
|
||||
match self {
|
||||
Self::Initialized(i) => Some(i),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T> Default for PossiblyUninitalized<T> {
|
||||
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,
|
||||
|
|
|
@ -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};
|
||||
|
@ -21,22 +20,22 @@ use std::{
|
|||
};
|
||||
use std::{rc::Rc, sync::RwLock};
|
||||
use style_attributes::StyleModifier;
|
||||
use taffy::Taffy;
|
||||
pub use taffy::{geometry::Point, prelude::*};
|
||||
use tokio::select;
|
||||
use tui::{backend::CrosstermBackend, layout::Rect, Terminal};
|
||||
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::*;
|
||||
|
@ -91,11 +90,14 @@ pub fn render<R: Driver>(
|
|||
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
|
||||
|
@ -103,7 +105,10 @@ pub fn render<R: Driver>(
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
@ -111,10 +116,21 @@ pub fn render<R: Driver>(
|
|||
});
|
||||
}
|
||||
|
||||
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 +176,7 @@ pub fn render<R: Driver>(
|
|||
|
||||
if !to_rerender.is_empty() || updated {
|
||||
updated = false;
|
||||
fn resize(dims: Rect, taffy: &mut Taffy, rdom: &RealDom) {
|
||||
fn resize(dims: tui::layout::Rect, taffy: &mut Taffy, rdom: &RealDom) {
|
||||
let width = screen_to_layout_space(dims.width);
|
||||
let height = screen_to_layout_space(dims.height);
|
||||
let root_node = rdom
|
||||
|
@ -202,7 +218,7 @@ pub fn render<R: Driver>(
|
|||
} else {
|
||||
let rdom = rdom.read().unwrap();
|
||||
resize(
|
||||
Rect {
|
||||
tui::layout::Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1000,
|
||||
|
@ -214,6 +230,7 @@ pub fn render<R: Driver>(
|
|||
}
|
||||
}
|
||||
|
||||
let mut event_recieved = None;
|
||||
{
|
||||
let wait = renderer.poll_async();
|
||||
|
||||
|
@ -223,7 +240,7 @@ pub fn render<R: Driver>(
|
|||
_ = wait => {
|
||||
|
||||
},
|
||||
evt = event_reciever.next() => {
|
||||
evt = raw_event_reciever.next() => {
|
||||
match evt.as_ref().unwrap() {
|
||||
InputEvent::UserInput(event) => match event {
|
||||
TermEvent::Key(key) => {
|
||||
|
@ -244,21 +261,32 @@ pub fn render<R: Driver>(
|
|||
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
|
||||
|
@ -310,3 +338,54 @@ pub trait Driver {
|
|||
);
|
||||
fn poll_async(&mut self) -> Pin<Box<dyn Future<Output = ()> + '_>>;
|
||||
}
|
||||
|
||||
/// 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::<RinkWidgetTraitObject>()
|
||||
.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::<TaffyLayout>().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::<TaffyLayout>().unwrap().node.unwrap())
|
||||
.unwrap();
|
||||
node_layout.location.x += parent_layout.location.x;
|
||||
node_layout.location.y += parent_layout.location.y;
|
||||
}
|
||||
node_layout
|
||||
}
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
#[cfg(feature = "dioxus-bindings")]
|
||||
pub use crate::widgets::*;
|
|
@ -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<RwLock<RealDom>>,
|
||||
pub(crate) stretch: Arc<Mutex<Taffy>>,
|
||||
|
@ -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<Layout> {
|
||||
let layout = self
|
||||
.stretch
|
||||
.layout(self.inner.get(self.id)?.get::<TaffyLayout>()?.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<Layout> {
|
||||
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(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
206
packages/rink/src/widgets/button.rs
Normal file
206
packages/rink/src/widgets/button.rs
Normal file
|
@ -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<NodeId> {
|
||||
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::<UniqueView<WidgetContext>>()
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
249
packages/rink/src/widgets/checkbox.rs
Normal file
249
packages/rink/src/widgets/checkbox.rs
Normal file
|
@ -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<WidgetContext> = 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<NodeId> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
153
packages/rink/src/widgets/input.rs
Normal file
153
packages/rink/src/widgets/input.rs
Normal file
|
@ -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<dioxus_native_core::NodeId> {
|
||||
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<dioxus_native_core::NodeId> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
128
packages/rink/src/widgets/mod.rs
Normal file
128
packages/rink/src/widgets/mod.rs
Normal file
|
@ -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<Event>) {
|
||||
// inject the widget context
|
||||
rdom.raw_world().add_unique(WidgetContext { sender });
|
||||
|
||||
rdom.register_custom_element::<RinkWidgetWrapper<input::Input>>();
|
||||
}
|
||||
|
||||
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<W: RinkWidget> 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<W: RinkWidget> {
|
||||
inner: RinkWidgetTraitObject,
|
||||
_marker: std::marker::PhantomData<W>,
|
||||
}
|
||||
|
||||
impl<W: RinkWidget> CustomElement for RinkWidgetWrapper<W> {
|
||||
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<dioxus_native_core::NodeId> {
|
||||
let widget = self.inner.widget.read().unwrap();
|
||||
widget.roots()
|
||||
}
|
||||
|
||||
fn slot(&self) -> Option<dioxus_native_core::NodeId> {
|
||||
let widget = self.inner.widget.read().unwrap();
|
||||
widget.slot()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Component)]
|
||||
pub(crate) struct RinkWidgetTraitObject {
|
||||
widget: Arc<RwLock<dyn RinkWidgetResponder + Send + Sync>>,
|
||||
}
|
||||
|
||||
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<dioxus_native_core::NodeId> {
|
||||
let widget = self.widget.read().unwrap();
|
||||
widget.roots()
|
||||
}
|
||||
|
||||
fn slot(&self) -> Option<dioxus_native_core::NodeId> {
|
||||
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<Event>,
|
||||
}
|
||||
|
||||
impl WidgetContext {
|
||||
pub(crate) fn send(&self, event: Event) {
|
||||
self.sender.unbounded_send(event).unwrap();
|
||||
}
|
||||
}
|
91
packages/rink/src/widgets/number.rs
Normal file
91
packages/rink/src/widgets/number.rs
Normal file
|
@ -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::<f64>().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::<f64>().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<NodeId> {
|
||||
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)
|
||||
}
|
||||
}
|
12
packages/rink/src/widgets/password.rs
Normal file
12
packages/rink/src/widgets/password.rs
Normal file
|
@ -0,0 +1,12 @@
|
|||
use super::text_like::{TextLike, TextLikeController};
|
||||
|
||||
pub(crate) type Password = TextLike<PasswordController>;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct PasswordController;
|
||||
|
||||
impl TextLikeController for PasswordController {
|
||||
fn display_text(&self, text: &str) -> String {
|
||||
text.chars().map(|_| '.').collect()
|
||||
}
|
||||
}
|
456
packages/rink/src/widgets/slider.rs
Normal file
456
packages/rink/src/widgets/slider.rs
Normal file
|
@ -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<f64>,
|
||||
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::<u32>().ok().filter(|num| *num > 3))
|
||||
.is_some()
|
||||
|| height
|
||||
.strip_prefix("px")
|
||||
.and_then(|n| n.parse::<u32>().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<WidgetContext> = 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<Query> = 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<NodeId> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
444
packages/rink/src/widgets/text_like.rs
Normal file
444
packages/rink/src/widgets/text_like.rs
Normal file
|
@ -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<C: TextLikeController = EmptyController> {
|
||||
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<usize>,
|
||||
controller: C,
|
||||
}
|
||||
|
||||
impl<C: TextLikeController> TextLike<C> {
|
||||
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::<usize>() {
|
||||
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::<u32>().ok().filter(|num| *num > 3))
|
||||
.is_some()
|
||||
|| height
|
||||
.strip_prefix("px")
|
||||
.and_then(|n| n.parse::<u32>().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<WidgetContext> = 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<Query> = 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<Query> = 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<C: TextLikeController + Send + Sync + Default + 'static> CustomElement for TextLike<C> {
|
||||
const NAME: &'static str = "input";
|
||||
|
||||
fn roots(&self) -> Vec<NodeId> {
|
||||
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<C: TextLikeController + Send + Sync + Default + 'static> RinkWidget for TextLike<C> {
|
||||
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();
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
3
packages/rink/src/widgets/textbox.rs
Normal file
3
packages/rink/src/widgets/textbox.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
use super::text_like::TextLike;
|
||||
|
||||
pub(crate) type TextBox = TextLike;
|
|
@ -10,16 +10,22 @@ mod rt;
|
|||
use dioxus_core::ScopeState;
|
||||
pub use rt::*;
|
||||
|
||||
use crate::rt::claim_rt;
|
||||
|
||||
pub fn use_init_signal_rt(cx: &ScopeState) {
|
||||
cx.use_hook(|| {
|
||||
let rt = crate::rt::claim_rt(cx.schedule_update_any());
|
||||
let rt = claim_rt(cx.schedule_update_any());
|
||||
cx.provide_context(rt);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn use_signal<T: 'static>(cx: &ScopeState, f: impl FnOnce() -> T) -> Signal<T> {
|
||||
cx.use_hook(|| {
|
||||
let rt: &'static SignalRt = cx.consume_context().unwrap();
|
||||
let rt: &'static SignalRt = match cx.consume_context() {
|
||||
Some(rt) => rt,
|
||||
None => cx.provide_context(claim_rt(cx.schedule_update_any())),
|
||||
};
|
||||
|
||||
let id = rt.init(f());
|
||||
rt.subscribe(id, cx.scope_id());
|
||||
|
||||
|
|
|
@ -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"] }
|
||||
|
|
10
packages/web/examples/README.md
Normal file
10
packages/web/examples/README.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
Examples
|
||||
========
|
||||
|
||||
# Hydrate
|
||||
|
||||
- `hydrate` show hydrate
|
||||
|
||||
# Async
|
||||
|
||||
- `timeout_count` button to add count and show count in the future
|
31
packages/web/examples/timeout_count.rs
Normal file
31
packages/web/examples/timeout_count.rs
Normal file
|
@ -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() }
|
||||
}
|
||||
})
|
||||
}
|
Loading…
Reference in a new issue