Merge pull request #754 from Demonthos/desktop-hot-reload

Implement hot reloading for Desktop, TUI, and Liveview
This commit is contained in:
Jon Kelley 2023-01-23 09:48:54 -10:00 committed by GitHub
commit 1b597f43d1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 705 additions and 79 deletions

View file

@ -20,6 +20,7 @@ members = [
"packages/native-core-macro",
"packages/rsx-rosetta",
"packages/signals",
"packages/hot-reload",
"docs/guide",
]

View file

@ -5,11 +5,11 @@
- [Getting Started](getting_started/index.md)
- [Desktop](getting_started/desktop.md)
- [Web](getting_started/web.md)
- [Hot Reload](getting_started/hot_reload.md)
- [Server-Side Rendering](getting_started/ssr.md)
- [Liveview](getting_started/liveview.md)
- [Terminal UI](getting_started/tui.md)
- [Mobile](getting_started/mobile.md)
- [Hot Reloading](getting_started/hot_reload.md)
- [Describing the UI](describing_ui/index.md)
- [Special Attributes](describing_ui/special_attributes.md)
- [Components](describing_ui/components.md)

View file

@ -2,21 +2,48 @@
1. Hot reloading allows much faster iteration times inside of rsx calls by interpreting them and streaming the edits.
2. It is useful when changing the styling/layout of a program, but will not help with changing the logic of a program.
3. Currently the cli only implements hot reloading for the web renderer.
3. Currently the cli only implements hot reloading for the web renderer. For TUI, desktop, and LiveView you can use the hot reload macro instead.
# Setup
# Web
For the web renderer, you can use the dioxus cli to serve your application with hot reloading enabled.
## Setup
Install [dioxus-cli](https://github.com/DioxusLabs/cli).
Hot reloading is automatically enabled when using the web renderer on debug builds.
# Usage
1. run:
```
## Usage
1. Run:
```bash
dioxus serve --hot-reload
```
2. change some code within a rsx macro
3. open your localhost in a browser
4. save and watch the style change without recompiling
2. Change some code within a rsx or render macro
3. Open your localhost in a browser
4. Save and watch the style change without recompiling
# Desktop/Liveview/TUI
For desktop, LiveView, and tui, you can place the hot reload macro at the top of your main function to enable hot reloading.
Hot reloading is automatically enabled on debug builds.
For more information about hot reloading on native platforms and configuration options see the [dioxus-hot-reload](https://crates.io/crates/dioxus-hot-reload) crate.
## Setup
Add the following to your main function:
```rust
fn main() {
hot_reload_init!();
// launch your application
}
```
## Usage
1. Run:
```bash
cargo run
```
2. Change some code within a rsx or render macro
3. Save and watch the style change without recompiling
# Limitations
1. The interpreter can only use expressions that existed on the last full recompile. If you introduce a new variable or expression to the rsx call, it will trigger a full recompile to capture the expression.
2. Components and Iterators can contain arbitrary rust code and will trigger a full recompile when changed.
1. The interpreter can only use expressions that existed on the last full recompile. If you introduce a new variable or expression to the rsx call, it will require a full recompile to capture the expression.
2. Components, Iterators, and some attributes can contain arbitrary rust code and will trigger a full recompile when changed.

View file

@ -15,6 +15,7 @@ keywords = ["dom", "ui", "gui", "react"]
dioxus-core = { path = "../core", version = "^0.3.0", features = ["serialize"] }
dioxus-html = { path = "../html", features = ["serialize"], version = "^0.3.0" }
dioxus-interpreter-js = { path = "../interpreter", version = "^0.3.0" }
dioxus-hot-reload = { path = "../hot-reload", optional = true }
serde = "1.0.136"
serde_json = "1.0.79"
@ -34,7 +35,6 @@ infer = "0.11.0"
dunce = "1.0.2"
slab = "0.4"
interprocess = { version = "1.1.1", optional = true }
futures-util = "0.3.25"
[target.'cfg(target_os = "ios")'.dependencies]
@ -50,7 +50,7 @@ tokio_runtime = ["tokio"]
fullscreen = ["wry/fullscreen"]
transparent = ["wry/transparent"]
tray = ["wry/tray"]
hot-reload = ["interprocess"]
hot-reload = ["dioxus-hot-reload"]
[dev-dependencies]
dioxus-core-macro = { path = "../core-macro" }

View file

@ -9,6 +9,7 @@ use crate::Config;
use crate::WebviewHandler;
use dioxus_core::ScopeState;
use dioxus_core::VirtualDom;
use dioxus_hot_reload::HotReloadMsg;
use serde_json::Value;
use slab::Slab;
use wry::application::event::Event;
@ -285,6 +286,8 @@ pub enum EventData {
Ipc(IpcMessage),
HotReloadEvent(HotReloadMsg),
NewWindow,
CloseWindow,

View file

@ -1,54 +0,0 @@
#![allow(dead_code)]
use dioxus_core::Template;
use interprocess::local_socket::{LocalSocketListener, LocalSocketStream};
use std::io::{BufRead, BufReader};
use std::time::Duration;
use std::{sync::Arc, sync::Mutex};
fn handle_error(connection: std::io::Result<LocalSocketStream>) -> Option<LocalSocketStream> {
connection
.map_err(|error| eprintln!("Incoming connection failed: {}", error))
.ok()
}
pub(crate) fn init(proxy: futures_channel::mpsc::UnboundedSender<Template<'static>>) {
let latest_in_connection: Arc<Mutex<Option<BufReader<LocalSocketStream>>>> =
Arc::new(Mutex::new(None));
let latest_in_connection_handle = latest_in_connection.clone();
// connect to processes for incoming data
std::thread::spawn(move || {
let temp_file = std::env::temp_dir().join("@dioxusin");
if let Ok(listener) = LocalSocketListener::bind(temp_file) {
for conn in listener.incoming().filter_map(handle_error) {
*latest_in_connection_handle.lock().unwrap() = Some(BufReader::new(conn));
}
}
});
std::thread::spawn(move || {
loop {
if let Some(conn) = &mut *latest_in_connection.lock().unwrap() {
let mut buf = String::new();
match conn.read_line(&mut buf) {
Ok(_) => {
let msg: Template<'static> =
serde_json::from_str(Box::leak(buf.into_boxed_str())).unwrap();
proxy.unbounded_send(msg).unwrap();
}
Err(err) => {
if err.kind() != std::io::ErrorKind::WouldBlock {
break;
}
}
}
}
// give the error handler time to take the mutex
std::thread::sleep(Duration::from_millis(100));
}
});
}

View file

@ -12,9 +12,6 @@ mod protocol;
mod waker;
mod webview;
#[cfg(all(feature = "hot-reload", debug_assertions))]
mod hot_reload;
pub use cfg::Config;
pub use desktop_context::{
use_window, use_wry_event_handler, DesktopContext, WryEventHandler, WryEventHandlerId,
@ -111,6 +108,18 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
let proxy = event_loop.create_proxy();
// Intialize hot reloading if it is enabled
#[cfg(all(feature = "hot-reload", debug_assertions))]
{
let proxy = proxy.clone();
dioxus_hot_reload::connect(move |template| {
let _ = proxy.send_event(UserWindowEvent(
EventData::HotReloadEvent(template),
unsafe { WindowId::dummy() },
));
});
}
// We start the tokio runtime *on this thread*
// Any future we poll later will use this runtime to spawn tasks and for IO
let rt = tokio::runtime::Builder::new_multi_thread()
@ -176,6 +185,19 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
}
Event::UserEvent(event) => match event.0 {
EventData::HotReloadEvent(msg) => match msg {
dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => {
for webview in webviews.values_mut() {
webview.dom.replace_template(template);
poll_vdom(webview);
}
}
dioxus_hot_reload::HotReloadMsg::Shutdown => {
*control_flow = ControlFlow::Exit;
}
},
EventData::CloseWindow => {
webviews.remove(&event.1);

View file

@ -18,11 +18,15 @@ dioxus-core-macro = { path = "../core-macro", version = "^0.3.0", optional = tru
dioxus-hooks = { path = "../hooks", version = "^0.3.0", optional = true }
dioxus-rsx = { path = "../rsx", version = "0.0.2", optional = true }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
dioxus-hot-reload = { path = "../hot-reload", version = "0.1.0", optional = true }
[features]
default = ["macro", "hooks", "html"]
default = ["macro", "hooks", "html", "hot-reload"]
macro = ["dioxus-core-macro", "dioxus-rsx"]
html = ["dioxus-html"]
hooks = ["dioxus-hooks"]
hot-reload = ["dioxus-hot-reload"]
[dev-dependencies]

View file

@ -31,4 +31,7 @@ pub mod prelude {
#[cfg(feature = "html")]
pub use dioxus_elements::{prelude::*, GlobalAttributes, SvgAttributes};
#[cfg(all(not(target_arch = "wasm32"), feature = "hot-reload"))]
pub use dioxus_hot_reload::{self, hot_reload_init};
}

View file

@ -0,0 +1,26 @@
[package]
name = "dioxus-hot-reload"
version = "0.1.0"
edition = "2021"
license = "MIT/Apache-2.0"
repository = "https://github.com/DioxusLabs/dioxus/"
homepage = "https://dioxuslabs.com"
description = "Hot reloading utilites for Dioxus"
documentation = "https://dioxuslabs.com"
keywords = ["dom", "ui", "gui", "react", "hot-reloading", "watch"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus-rsx = { path = "../rsx" }
dioxus-core = { path = "../core", features = ["serialize"] }
dioxus-html = { path = "../html", features = ["hot-reload-context"] }
interprocess = { version = "1.2.1" }
notify = "5.0.0"
chrono = "0.4.23"
serde_json = "1.0.91"
serde = { version = "1", features = ["derive"] }
execute = "0.2.11"
once_cell = "1.17.0"
ignore = "0.4.19"

View file

@ -0,0 +1,170 @@
# `dioxus-hot-reload`: Hot Reloading Utilites for Dioxus
[![Crates.io][crates-badge]][crates-url]
[![MIT licensed][mit-badge]][mit-url]
[![Build Status][actions-badge]][actions-url]
[![Discord chat][discord-badge]][discord-url]
[crates-badge]: https://img.shields.io/crates/v/dioxus-hot-reload.svg
[crates-url]: https://crates.io/crates/dioxus-hot-reload
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
[discord-url]: https://discord.gg/XgGxMSkvUM
[Website](https://dioxuslabs.com) |
[Guides](https://dioxuslabs.com/guide/) |
[API Docs](https://docs.rs/dioxus-hot-reload/latest/dioxus_hot_reload) |
[Chat](https://discord.gg/XgGxMSkvUM)
## Overview
Dioxus supports hot reloading for static parts of rsx macros. This enables changing the styling of your application without recompiling the rust code. This is useful for rapid iteration on the styling of your application.
Hot reloading could update the following change without recompiling:
```rust
rsx! {
div {
"Count: {count}",
}
}
```
=>
```rust
rsx! {
div {
color: "red",
font_size: "2em",
"Count: {count}",
}
}
```
But it could not update the following change:
```rust
rsx! {
div {
"Count: {count}",
}
}
```
=>
```rust
rsx! {
div {
"Count: {count*2}",
onclick: |_| println!("clicked"),
}
}
```
## Usage
> This crate implements hot reloading for native compilation targets not WASM. For hot relaoding with the web renderer, see the [dioxus-cli](https://github.com/DioxusLabs/cli) project.
Add this to the top of your main function on any renderer that supports hot reloading to start the hot reloading server:
```rust
fn main(){
hot_reload_init!();
// launch your application
}
```
By default the dev server watches on the root of the crate the macro is called in and ignores changes in the `/target` directory and any directories set in the `.gitignore` file in the root directory. To watch on custom paths pass call the `with_paths` function on the config builder:
```rust
fn main(){
hot_reload_init!(Config::new().with_paths(&["src", "examples", "assets"]));
// launch your application
}
```
By default the hot reloading server will output some logs in the console, to disable these logs call the `with_logging` function on the config builder:
```rust
fn main(){
hot_reload_init!(Config::new().with_logging(false));
// launch your application
}
```
To rebuild the application when the logic changes, you can use the `with_rebuild_command` function on the config builder. This command will be called when hot reloading fails to quickly update the rsx:
```rust
fn main(){
hot_reload_init!(Config::new().with_rebuild_command("cargo run"));
// launch your application
}
```
If you are using a namespace other than html, you can implement the [HotReloadingContext](https://docs.rs/dioxus-rsx/latest/dioxus_rsx/trait.HotReloadingContext.html) trait to provide a mapping between the rust names of your elements/attributes and the resulting strings.
You can then provide the Context to the builder to make hot reloading work with your custom namespace:
```rust
fn main(){
// Note: Use default instead of new if you are using a custom namespace
hot_reload_init!(Config::<MyContext>::default());
// launch your application
}
```
## Implementing Hot Reloading for a Custom Renderer
To add hot reloading support to your custom renderer you can use the connect function. This will connect to the dev server you just need to provide a way to transfer `Template`s to the `VirtualDom`. Once you implement this your users can use the hot_reload_init function just like any other render.
```rust
async fn launch(app: Component) {
let mut vdom = VirtualDom::new(app);
// ...
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
dioxus_hot_reload::connect(move |msg| {
let _ = tx.send(msg);
});
loop {
tokio::select! {
Some(msg) = rx.recv() => {
match msg{
HotReloadMsg::Shutdown => {
// ... shutdown the application
}
HotReloadMsg::UpdateTemplate(template) => {
// update the template in the virtual dom
vdom.replace_template(template);
}
}
}
_ = vdom.wait_for_work() => {
// ...
}
}
let mutations = vdom.render_immediate();
// apply the mutations to the dom
}
}
```
## Contributing
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
- Join the discord and ask questions!
## License
This project is licensed under the [MIT license].
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in Dioxus by you shall be licensed as MIT without any additional
terms or conditions.

View file

@ -0,0 +1,360 @@
use std::{
io::{BufRead, BufReader, Write},
path::PathBuf,
str::FromStr,
sync::{Arc, Mutex},
};
use dioxus_core::Template;
use dioxus_rsx::{
hot_reload::{FileMap, UpdateResult},
HotReloadingContext,
};
use interprocess::local_socket::{LocalSocketListener, LocalSocketStream};
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
#[cfg(debug_assertions)]
pub use dioxus_html::HtmlCtx;
use serde::{Deserialize, Serialize};
/// A message the hot reloading server sends to the client
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
pub enum HotReloadMsg {
/// A template has been updated
#[serde(borrow = "'static")]
UpdateTemplate(Template<'static>),
/// The program needs to be recompiled, and the client should shut down
Shutdown,
}
pub struct Config<Ctx: HotReloadingContext = HtmlCtx> {
root_path: &'static str,
listening_paths: &'static [&'static str],
excluded_paths: &'static [&'static str],
log: bool,
rebuild_with: Option<Box<dyn FnMut() -> bool + Send + 'static>>,
phantom: std::marker::PhantomData<Ctx>,
}
impl<Ctx: HotReloadingContext> Default for Config<Ctx> {
fn default() -> Self {
Self {
root_path: "",
listening_paths: &[""],
excluded_paths: &["./target"],
log: true,
rebuild_with: None,
phantom: std::marker::PhantomData,
}
}
}
impl Config<HtmlCtx> {
pub const fn new() -> Self {
Self {
root_path: "",
listening_paths: &[""],
excluded_paths: &["./target"],
log: true,
rebuild_with: None,
phantom: std::marker::PhantomData,
}
}
}
impl<Ctx: HotReloadingContext> Config<Ctx> {
/// Set the root path of the project (where the Cargo.toml file is). This is automatically set by the [`hot_reload_init`] macro.
pub fn root(self, path: &'static str) -> Self {
Self {
root_path: path,
..self
}
}
/// Set whether to enable logs
pub fn with_logging(self, log: bool) -> Self {
Self { log, ..self }
}
/// Set the command to run to rebuild the project
///
/// For example to restart the application after a change is made, you could use `cargo run`
pub fn with_rebuild_command(self, rebuild_command: &'static str) -> Self {
self.with_rebuild_callback(move || {
execute::shell(rebuild_command)
.spawn()
.expect("Failed to spawn the rebuild command");
true
})
}
/// Set a callback to run to when the project needs to be rebuilt and returns if the server should shut down
///
/// For example a CLI application could rebuild the application when a change is made
pub fn with_rebuild_callback(
self,
rebuild_callback: impl FnMut() -> bool + Send + 'static,
) -> Self {
Self {
rebuild_with: Some(Box::new(rebuild_callback)),
..self
}
}
/// Set the paths to listen for changes in to trigger hot reloading. If this is a directory it will listen for changes in all files in that directory recursively.
pub fn with_paths(self, paths: &'static [&'static str]) -> Self {
Self {
listening_paths: paths,
..self
}
}
/// Sets paths to ignore changes on. This will override any paths set in the [`Config::with_paths`] method in the case of conflicts.
pub fn excluded_paths(self, paths: &'static [&'static str]) -> Self {
Self {
excluded_paths: paths,
..self
}
}
}
/// Initialize the hot reloading listener
pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
let Config {
root_path,
listening_paths,
log,
mut rebuild_with,
excluded_paths,
phantom: _,
} = cfg;
if let Ok(crate_dir) = PathBuf::from_str(root_path) {
let temp_file = std::env::temp_dir().join("@dioxusin");
let channels = Arc::new(Mutex::new(Vec::new()));
let file_map = Arc::new(Mutex::new(FileMap::<Ctx>::new(crate_dir.clone())));
if let Ok(local_socket_stream) = LocalSocketListener::bind(temp_file.as_path()) {
let aborted = Arc::new(Mutex::new(false));
// listen for connections
std::thread::spawn({
let file_map = file_map.clone();
let channels = channels.clone();
let aborted = aborted.clone();
let _ = local_socket_stream.set_nonblocking(true);
move || {
loop {
if let Ok(mut connection) = local_socket_stream.accept() {
// send any templates than have changed before the socket connected
let templates: Vec<_> = {
file_map
.lock()
.unwrap()
.map
.values()
.filter_map(|(_, template_slot)| *template_slot)
.collect()
};
for template in templates {
if !send_msg(
HotReloadMsg::UpdateTemplate(template),
&mut connection,
) {
continue;
}
}
channels.lock().unwrap().push(connection);
if log {
println!("Connected to hot reloading 🚀");
}
}
std::thread::sleep(std::time::Duration::from_millis(10));
if *aborted.lock().unwrap() {
break;
}
}
}
});
// watch for changes
std::thread::spawn(move || {
// try to find the gitingore file
let gitignore_file_path = crate_dir.join(".gitignore");
let (gitignore, _) = ignore::gitignore::Gitignore::new(gitignore_file_path);
let mut last_update_time = chrono::Local::now().timestamp();
let (tx, rx) = std::sync::mpsc::channel();
let mut watcher = RecommendedWatcher::new(tx, notify::Config::default()).unwrap();
for path in listening_paths {
let full_path = crate_dir.join(path);
if let Err(err) = watcher.watch(&full_path, RecursiveMode::Recursive) {
if log {
println!(
"hot reloading failed to start watching {full_path:?}:\n{err:?}",
);
}
}
}
let excluded_paths = excluded_paths
.iter()
.map(|path| crate_dir.join(PathBuf::from(path)))
.collect::<Vec<_>>();
let mut rebuild = {
let aborted = aborted.clone();
let channels = channels.clone();
move || {
if let Some(rebuild_callback) = &mut rebuild_with {
if log {
println!("Rebuilding the application...");
}
let shutdown = rebuild_callback();
if shutdown {
*aborted.lock().unwrap() = true;
}
for channel in &mut *channels.lock().unwrap() {
send_msg(HotReloadMsg::Shutdown, channel);
}
return shutdown;
} else if log {
println!(
"Rebuild needed... shutting down hot reloading.\nManually rebuild the application to view futher changes."
);
}
true
}
};
for evt in rx {
if chrono::Local::now().timestamp() > last_update_time {
if let Ok(evt) = evt {
let real_paths = evt
.paths
.iter()
.filter(|path| {
// skip non rust files
matches!(
path.extension().and_then(|p| p.to_str()),
Some("rs" | "toml" | "css" | "html" | "js")
)&&
// skip excluded paths
!excluded_paths.iter().any(|p| path.starts_with(p)) &&
// respect .gitignore
!gitignore
.matched_path_or_any_parents(path, false)
.is_ignore()
})
.collect::<Vec<_>>();
// Give time for the change to take effect before reading the file
if !real_paths.is_empty() {
std::thread::sleep(std::time::Duration::from_millis(10));
}
let mut channels = channels.lock().unwrap();
for path in real_paths {
// if this file type cannot be hot reloaded, rebuild the application
if path.extension().and_then(|p| p.to_str()) != Some("rs")
&& rebuild()
{
return;
}
// find changes to the rsx in the file
match file_map
.lock()
.unwrap()
.update_rsx(path, crate_dir.as_path())
{
UpdateResult::UpdatedRsx(msgs) => {
for msg in msgs {
let mut i = 0;
while i < channels.len() {
let channel = &mut channels[i];
if send_msg(
HotReloadMsg::UpdateTemplate(msg),
channel,
) {
i += 1;
} else {
channels.remove(i);
}
}
}
}
UpdateResult::NeedsRebuild => {
drop(channels);
if rebuild() {
return;
}
break;
}
}
}
}
last_update_time = chrono::Local::now().timestamp();
}
}
});
}
}
}
fn send_msg(msg: HotReloadMsg, channel: &mut impl Write) -> bool {
if let Ok(msg) = serde_json::to_string(&msg) {
if channel.write_all(msg.as_bytes()).is_err() {
return false;
}
if channel.write_all(&[b'\n']).is_err() {
return false;
}
true
} else {
false
}
}
/// Connect to the hot reloading listener. The callback provided will be called every time a template change is detected
pub fn connect(mut f: impl FnMut(HotReloadMsg) + Send + 'static) {
std::thread::spawn(move || {
let temp_file = std::env::temp_dir().join("@dioxusin");
if let Ok(socket) = LocalSocketStream::connect(temp_file.as_path()) {
let mut buf_reader = BufReader::new(socket);
loop {
let mut buf = String::new();
match buf_reader.read_line(&mut buf) {
Ok(_) => {
let template: HotReloadMsg =
serde_json::from_str(Box::leak(buf.into_boxed_str())).unwrap();
f(template);
}
Err(err) => {
if err.kind() != std::io::ErrorKind::WouldBlock {
break;
}
}
}
}
}
});
}
/// Start the hot reloading server with the current directory as the root
#[macro_export]
macro_rules! hot_reload_init {
() => {
#[cfg(debug_assertions)]
dioxus_hot_reload::init(dioxus_hot_reload::Config::new().root(env!("CARGO_MANIFEST_DIR")));
};
($cfg: expr) => {
#[cfg(debug_assertions)]
dioxus_hot_reload::init($cfg.root(env!("CARGO_MANIFEST_DIR")));
};
}

View file

@ -26,6 +26,7 @@ serde_json = "1.0.91"
dioxus-html = { path = "../html", features = ["serialize"], version = "^0.3.0" }
dioxus-core = { path = "../core", features = ["serialize"], version = "^0.3.0" }
dioxus-interpreter-js = { path = "../interpreter", version = "0.3.0" }
dioxus-hot-reload = { path = "../hot-reload", optional = true }
# warp
warp = { version = "0.3.3", optional = true }
@ -51,8 +52,9 @@ salvo = { version = "0.37.7", features = ["affix", "ws"] }
tower = "0.4.13"
[features]
default = []
default = ["hot-reload"]
# actix = ["actix-files", "actix-web", "actix-ws"]
hot-reload = ["dioxus-hot-reload"]
[[example]]
name = "axum"

View file

@ -103,6 +103,15 @@ pub async fn run<T>(
where
T: Send + 'static,
{
#[cfg(all(feature = "hot-reload", debug_assertions))]
let mut hot_reload_rx = {
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
dioxus_hot_reload::connect(move |template| {
let _ = tx.send(template);
});
rx
};
let mut vdom = VirtualDom::new_with_props(app, props);
// todo: use an efficient binary packed format for this
@ -122,6 +131,11 @@ where
}
loop {
#[cfg(all(feature = "hot-reload", debug_assertions))]
let hot_reload_wait = hot_reload_rx.recv();
#[cfg(not(all(feature = "hot-reload", debug_assertions)))]
let hot_reload_wait = std::future::pending();
tokio::select! {
// poll any futures or suspense
_ = vdom.wait_for_work() => {}
@ -142,6 +156,19 @@ where
None => return Ok(()),
}
}
msg = hot_reload_wait => {
if let Some(msg) = msg {
match msg{
dioxus_hot_reload::HotReloadMsg::UpdateTemplate(new_template) => {
vdom.replace_template(new_template);
}
dioxus_hot_reload::HotReloadMsg::Shutdown => {
std::process::exit(0);
},
}
}
}
}
let edits = vdom

View file

@ -14,10 +14,11 @@ license = "MIT/Apache-2.0"
[dependencies]
dioxus = { path = "../dioxus", version = "^0.3.0" }
dioxus-core = { path = "../core", version = "^0.3.0" }
dioxus-core = { path = "../core", version = "^0.3.0", features = ["serialize"] }
dioxus-html = { path = "../html", version = "^0.3.0" }
dioxus-native-core = { path = "../native-core", version = "^0.2.0" }
dioxus-native-core-macro = { path = "../native-core-macro", version = "^0.2.0" }
dioxus-hot-reload = { path = "../hot-reload", optional = true }
tui = "0.17.0"
crossterm = "0.23.0"
@ -38,3 +39,7 @@ criterion = "0.3.5"
[[bench]]
name = "update"
harness = false
[features]
default = ["hot-reload"]
hot-reload = ["dioxus-hot-reload"]

View file

@ -22,6 +22,7 @@ use std::{
use std::{io, time::Duration};
use taffy::Taffy;
pub use taffy::{geometry::Point, prelude::*};
use tokio::{select, sync::mpsc::unbounded_channel};
use tui::{backend::CrosstermBackend, layout::Rect, Terminal};
mod config;
@ -144,6 +145,15 @@ fn render_vdom(
.enable_all()
.build()?
.block_on(async {
#[cfg(all(feature = "hot-reload", debug_assertions))]
let mut hot_reload_rx = {
let (hot_reload_tx, hot_reload_rx) =
unbounded_channel::<dioxus_hot_reload::HotReloadMsg>();
dioxus_hot_reload::connect(move |msg| {
let _ = hot_reload_tx.send(msg);
});
hot_reload_rx
};
let mut terminal = (!cfg.headless).then(|| {
enable_raw_mode().unwrap();
let mut stdout = std::io::stdout();
@ -223,16 +233,21 @@ fn render_vdom(
}
}
use futures::future::{select, Either};
let mut hot_reload_msg = None;
{
let wait = vdom.wait_for_work();
#[cfg(all(feature = "hot-reload", debug_assertions))]
let hot_reload_wait = hot_reload_rx.recv();
#[cfg(not(all(feature = "hot-reload", debug_assertions)))]
let hot_reload_wait = std::future::pending();
pin_mut!(wait);
match select(wait, event_reciever.next()).await {
Either::Left((_a, _b)) => {
//
}
Either::Right((evt, _o)) => {
select! {
_ = wait => {
},
evt = event_reciever.next() => {
match evt.as_ref().unwrap() {
InputEvent::UserInput(event) => match event {
TermEvent::Key(key) => {
@ -252,6 +267,21 @@ fn render_vdom(
if let InputEvent::UserInput(evt) = evt.unwrap() {
register_event(evt);
}
},
Some(msg) = hot_reload_wait => {
hot_reload_msg = Some(msg);
}
}
}
// if we have a new template, replace the old one
if let Some(msg) = hot_reload_msg {
match msg {
dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => {
vdom.replace_template(template);
}
dioxus_hot_reload::HotReloadMsg::Shutdown => {
break;
}
}
}