فهرست منبع

CLI: Toasts & Tweaks (#2702)

* progress: cli toasts

* forgot the html

* progress: toasts

* revision: don't open splash on desktop

* fix: fmt, spellcheck
Miles Murgaw 11 ماه پیش
والد
کامیت
bd58a92441

+ 0 - 2
packages/cli/assets/loading.html

@@ -325,7 +325,6 @@
     ws.onmessage = (event) => {
     ws.onmessage = (event) => {
       // Parse the message as json
       // Parse the message as json
       let data = JSON.parse(event.data);
       let data = JSON.parse(event.data);
-      console.log(data);
 
 
       // If the message is "Ready", reload the page
       // If the message is "Ready", reload the page
       if (data.type === "Ready") {
       if (data.type === "Ready") {
@@ -364,7 +363,6 @@
         let errorBlock = document.getElementById("error-block");
         let errorBlock = document.getElementById("error-block");
         errorBlock.innerHTML = formatting2;
         errorBlock.innerHTML = formatting2;
 
 
-        console.log(data.data.error);
       } else if (data.type === "Building") {
       } else if (data.type === "Building") {
         // Show correct view for message.
         // Show correct view for message.
         let errorContainer = document.getElementById("error");
         let errorContainer = document.getElementById("error");

+ 206 - 0
packages/cli/assets/toast.html

@@ -0,0 +1,206 @@
+<style>
+  /* Inter Font */
+  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap');
+
+  #dx-toast-template {
+    display: none;
+    visibility: hidden;
+  }
+
+  .dx-toast {
+    position: absolute;
+    top: 10px;
+    right: 0;
+    padding-right: 10px;
+    user-select: none;
+    transition: transform 0.2s ease;
+    overflow: hidden;
+  }
+
+  .dx-toast .dx-toast-inner {
+    transition: right 0.2s ease-out;
+    position: relative;
+
+    background-color: #181B20;
+    color: #ffffff;
+    font-family: "Inter", sans-serif;
+
+    display: grid;
+    grid-template-columns: auto auto;
+    min-width: 280px;
+    min-height: 92px;
+    width: min-content;
+    border-radius: 5px;
+
+  }
+
+  .dx-toast:hover {
+    cursor: pointer;
+    transform: translateX(-5px);
+  }
+
+  .dx-toast .dx-toast-level-bar-container {
+    height: 100%;
+    width: 6px;
+  }
+
+  .dx-toast .dx-toast-level-bar-container .dx-toast-level-bar {
+    width: 100%;
+    height: 100%;
+    border-radius: 5px 0px 0px 5px;
+  }
+
+  .dx-toast .dx-toast-content {
+    padding: 13px;
+  }
+
+  .dx-toast .dx-toast-header {
+    display: flex;
+    flex-direction: row;
+    justify-content: start;
+    align-items: end;
+    margin-bottom: 13px;
+  }
+
+  .dx-toast .dx-toast-header>svg {
+    height: 22px;
+    margin-right: 5px;
+  }
+
+  .dx-toast .dx-toast-header .dx-toast-header-text {
+    font-size: 16px;
+    font-weight: 700;
+    padding: 0;
+    margin: 0;
+  }
+
+  .dx-toast .dx-toast-msg {
+    font-size: 14px;
+    font-weight: 400;
+    padding: 0;
+    margin: 0;
+  }
+
+  .dx-toast-level-bar.info {
+    background-color: #428EFF;
+  }
+
+  .dx-toast-level-bar.success {
+    background-color: #42FF65;
+  }
+
+  .dx-toast-level-bar.error {
+    background-color: #FF4242;
+  }
+</style>
+
+<div id="dx-toast-template" class="dx-toast">
+  <div class="dx-toast-inner" style="right:-300px;">
+    <!-- Level/Color decor -->
+    <div class="dx-toast-level-bar-container">
+      <div class="dx-toast-level-bar info"></div>
+    </div>
+
+    <!-- Content -->
+    <div class="dx-toast-content">
+      <!-- Header -->
+      <div class="dx-toast-header">
+        <!-- Dioxus Logo -->
+        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" preserveAspectRatio="none">
+          <path
+            d="M22.158 1.783c0 3.077-.851 5.482-2.215 7.377s-3.32 3.557-5.447 5.33-4.425 3.657-6.252 6.195-3.102 5.515-3.102 9.532h4.699c0-3.077.853-5.377 2.217-7.272s3.32-3.557 5.447-5.33 4.425-3.657 6.252-6.195 3.102-5.62 3.102-9.637z"
+            fill="#e96020" />
+          <path
+            d="M9.531 25.927c-.635 0-1.021.515-1.02 1.15s.385 1.151 1.02 1.15H22.47a1.151 1.151 0 1 0 0-2.301zm1.361-4.076c-.608 0-.954.558-.953 1.166s.346 1.035.953 1.035h10.217a1.101 1.101 0 1 0 0-2.201zm0-13.594a1.101 1.101 0 1 0 0 2.201h10.217c.607 0 .953-.598.953-1.205s-.345-.996-.953-.996zM9.531 4.021A1.15 1.15 0 0 0 8.38 5.17a1.15 1.15 0 0 0 1.15 1.15h12.94c.635 0 1.021-.498 1.02-1.133s-.386-1.166-1.02-1.166z"
+            fill="#2d323b" />
+          <path
+            d="M5.142 1.783c0 4.016 1.275 7.099 3.102 9.637s4.125 4.422 6.252 6.195 4.083 3.656 5.447 5.551 2.215 3.974 2.215 7.051h4.701c0-4.016-1.275-7.038-3.102-9.576s-4.125-4.422-6.252-6.195-4.083-3.435-5.447-5.33S9.841 4.86 9.841 1.783z"
+            fill="#00a8d6" />
+        </svg>
+        <!-- Toast Title Text -->
+        <h3 class="dx-toast-header-text">Your app is being rebuilt.</h3>
+      </div>
+
+      <!-- Message -->
+      <p class="dx-toast-msg">A non-hot-reloadable change occurred and we must rebuild.</p>
+    </div>
+  </div>
+</div>
+
+<script>
+  const STORAGE_KEY = "SCHEDULED-DX-TOAST";
+  let currentToast = null;
+  let currentTimeout = null;
+
+  // Show a toast, removing the previous one.
+  function showDXToast(headerText, message, progressLevel, durationMs) {
+    // Close current toast if exists.
+    closeDXToast();
+
+    // Clone template and add unique id.
+    let toastTemplate = document.getElementById("dx-toast-template");
+    let cloned = toastTemplate.cloneNode(true);
+    let toastId = `dx-toast`;
+    cloned.id = toastId;
+    currentToast = cloned;
+
+    let innerElem = currentToast.querySelector(`#${toastId} .dx-toast-inner`);
+
+    // Set the progress level
+    let progressBarElem = innerElem.querySelector(".dx-toast-inner .dx-toast-level-bar-container .dx-toast-level-bar");
+    progressBarElem.className = `dx-toast-level-bar ${progressLevel}`;
+
+    // Set header text
+    let headerTextElem = innerElem.querySelector(".dx-toast-inner .dx-toast-header .dx-toast-header-text");
+    headerTextElem.innerText = headerText;
+
+    // Set message
+    let messageElem = innerElem.querySelector(".dx-toast-inner .dx-toast-msg");
+    messageElem.innerText = message;
+
+    document.body.appendChild(currentToast);
+
+    // Add listener to close toasts when clicked.
+    // Safety: Calling `closeToast` removes the element and all event listeners with it.
+    currentToast.addEventListener("click", closeDXToast);
+
+    // Wait a bit of time so animation plays correctly.
+    setTimeout(() => {
+      innerElem.style.right = "0";
+
+      currentTimeout = setTimeout(() => {
+        closeDXToast();
+      }, durationMs);
+    }, 100);
+  }
+
+  // Schedule a toast to be displayed after reload.
+  function scheduleDXToast(headerText, message, level, durationMs) {
+    let data = {
+      headerText,
+      message,
+      level,
+      durationMs,
+    };
+
+    let jsonData = JSON.stringify(data);
+    sessionStorage.setItem(STORAGE_KEY, jsonData);
+  }
+
+  // Close the current toast.
+  function closeDXToast() {
+    if (currentToast) {
+      currentToast.remove();
+    }
+    clearTimeout(currentTimeout);
+  }
+
+  // Handle any scheduled toasts after reload.
+  let potentialData = sessionStorage.getItem(STORAGE_KEY);
+  if (potentialData) {
+    sessionStorage.removeItem(STORAGE_KEY);
+    let data = JSON.parse(potentialData);
+    showDXToast(data.headerText, data.message, data.level, data.durationMs);
+  }
+
+</script>

+ 9 - 1
packages/cli/src/builder/prepare_html.rs

@@ -10,6 +10,7 @@ use std::path::{Path, PathBuf};
 use tracing::Level;
 use tracing::Level;
 
 
 const DEFAULT_HTML: &str = include_str!("../../assets/index.html");
 const DEFAULT_HTML: &str = include_str!("../../assets/index.html");
+const TOAST_HTML: &str = include_str!("../../assets/toast.html");
 
 
 impl BuildRequest {
 impl BuildRequest {
     pub(crate) fn prepare_html(
     pub(crate) fn prepare_html(
@@ -110,9 +111,16 @@ impl BuildRequest {
                 });
                 });
                 }
                 }
             );
             );
-            </script></body"#,
+            </script>
+            {DX_TOAST_UTILITIES}
+            </body"#,
         );
         );
 
 
+        *html = match self.serve && !self.build_arguments.release {
+            true => html.replace("{DX_TOAST_UTILITIES}", TOAST_HTML),
+            false => html.replace("{DX_TOAST_UTILITIES}", ""),
+        };
+
         // And try to insert preload links for the wasm and js files
         // And try to insert preload links for the wasm and js files
         *html = html.replace(
         *html = html.replace(
             "</head",
             "</head",

+ 1 - 0
packages/cli/src/builder/progress.rs

@@ -43,6 +43,7 @@ impl std::fmt::Display for Stage {
     }
     }
 }
 }
 
 
+#[derive(Debug, Clone)]
 pub struct UpdateBuildProgress {
 pub struct UpdateBuildProgress {
     pub stage: Stage,
     pub stage: Stage,
     pub update: UpdateStage,
     pub update: UpdateStage,

+ 13 - 4
packages/cli/src/serve/mod.rs

@@ -1,3 +1,4 @@
+use crate::builder::{Stage, UpdateBuildProgress, UpdateStage};
 use crate::cli::serve::Serve;
 use crate::cli::serve::Serve;
 use crate::dioxus_crate::DioxusCrate;
 use crate::dioxus_crate::DioxusCrate;
 use crate::Result;
 use crate::Result;
@@ -106,9 +107,17 @@ pub async fn serve_all(serve: Serve, dioxus_crate: DioxusCrate) -> Result<()> {
                 // We also can check the status of the builds here in case we have multiple ongoing builds
                 // We also can check the status of the builds here in case we have multiple ongoing builds
                 match application {
                 match application {
                     Ok(BuilderUpdate::Progress { platform, update }) => {
                     Ok(BuilderUpdate::Progress { platform, update }) => {
-                        let update_stage = update.stage;
-                        screen.new_build_logs(platform, update);
-                        server.update_build_status(screen.build_progress.progress(), update_stage.to_string()).await;
+                        let update_clone = update.clone();
+                        screen.new_build_logs(platform, update_clone);
+                        server.update_build_status(screen.build_progress.progress(), update.stage.to_string()).await;
+
+                        match update {
+                            // Send rebuild start message.
+                            UpdateBuildProgress { stage: Stage::Compiling, update: UpdateStage::Start } => server.send_reload_start().await,
+                            // Send rebuild failed message.
+                            UpdateBuildProgress { stage: Stage::Finished, update: UpdateStage::Failed(_) } => server.send_reload_failed().await,
+                            _ => {},
+                        }
                     }
                     }
                     Ok(BuilderUpdate::Ready { results }) => {
                     Ok(BuilderUpdate::Ready { results }) => {
                         if !results.is_empty() {
                         if !results.is_empty() {
@@ -130,7 +139,7 @@ pub async fn serve_all(serve: Serve, dioxus_crate: DioxusCrate) -> Result<()> {
                         screen.new_ready_app(&mut builder, results);
                         screen.new_ready_app(&mut builder, results);
 
 
                         // And then finally tell the server to reload
                         // And then finally tell the server to reload
-                        server.send_reload().await;
+                        server.send_reload_command().await;
                     },
                     },
                     Err(err) => {
                     Err(err) => {
                         server.send_build_error(err).await;
                         server.send_build_error(err).await;

+ 30 - 13
packages/cli/src/serve/server.rs

@@ -131,6 +131,7 @@ impl Server {
         // Actually just start the server, cloning in a few bits of config
         // Actually just start the server, cloning in a few bits of config
         let web_config = cfg.dioxus_config.web.https.clone();
         let web_config = cfg.dioxus_config.web.https.clone();
         let base_path = cfg.dioxus_config.web.app.base_path.clone();
         let base_path = cfg.dioxus_config.web.app.base_path.clone();
+        let platform = serve.platform();
         let _server_task = tokio::spawn(async move {
         let _server_task = tokio::spawn(async move {
             let web_config = web_config.clone();
             let web_config = web_config.clone();
             // HTTPS
             // HTTPS
@@ -139,7 +140,7 @@ impl Server {
             let rustls: Option<RustlsConfig> = get_rustls(&web_config).await.unwrap();
             let rustls: Option<RustlsConfig> = get_rustls(&web_config).await.unwrap();
 
 
             // Open the browser
             // Open the browser
-            if start_browser {
+            if start_browser && platform != Platform::Desktop {
                 open_browser(base_path, addr, rustls.is_some());
                 open_browser(base_path, addr, rustls.is_some());
             }
             }
 
 
@@ -175,6 +176,7 @@ impl Server {
         }
         }
     }
     }
 
 
+    /// Sends the current build status to all clients.
     async fn send_build_status(&mut self) {
     async fn send_build_status(&mut self) {
         let mut i = 0;
         let mut i = 0;
         while i < self.build_status_sockets.len() {
         while i < self.build_status_sockets.len() {
@@ -190,6 +192,7 @@ impl Server {
         }
         }
     }
     }
 
 
+    /// Sends a start build message to all clients.
     pub async fn start_build(&mut self) {
     pub async fn start_build(&mut self) {
         self.build_status.set(Status::Building {
         self.build_status.set(Status::Building {
             progress: 0.0,
             progress: 0.0,
@@ -198,6 +201,7 @@ impl Server {
         self.send_build_status().await;
         self.send_build_status().await;
     }
     }
 
 
+    /// Sends an updated build status to all clients.
     pub async fn update_build_status(&mut self, progress: f64, build_message: String) {
     pub async fn update_build_status(&mut self, progress: f64, build_message: String) {
         if !matches!(self.build_status.get(), Status::Building { .. }) {
         if !matches!(self.build_status.get(), Status::Building { .. }) {
             return;
             return;
@@ -209,6 +213,7 @@ impl Server {
         self.send_build_status().await;
         self.send_build_status().await;
     }
     }
 
 
+    /// Sends hot reloadable changes to all clients.
     pub async fn send_hotreload(&mut self, reload: HotReloadMsg) {
     pub async fn send_hotreload(&mut self, reload: HotReloadMsg) {
         let msg = DevserverMsg::HotReload(reload);
         let msg = DevserverMsg::HotReload(reload);
         let msg = serde_json::to_string(&msg).unwrap();
         let msg = serde_json::to_string(&msg).unwrap();
@@ -275,6 +280,7 @@ impl Server {
         None
         None
     }
     }
 
 
+    /// Converts a `cargo` error to HTML and sends it to clients.
     pub async fn send_build_error(&mut self, error: Error) {
     pub async fn send_build_error(&mut self, error: Error) {
         let error = error.to_string();
         let error = error.to_string();
         self.build_status.set(Status::BuildError {
         self.build_status.set(Status::BuildError {
@@ -283,25 +289,36 @@ impl Server {
         self.send_build_status().await;
         self.send_build_status().await;
     }
     }
 
 
-    pub async fn send_reload(&mut self) {
+    /// Tells all clients that a full rebuild has started.
+    pub async fn send_reload_start(&mut self) {
+        self.send_devserver_message(DevserverMsg::FullReloadStart)
+            .await;
+    }
+
+    /// Tells all clients that a full rebuild has failed.
+    pub async fn send_reload_failed(&mut self) {
+        self.send_devserver_message(DevserverMsg::FullReloadFailed)
+            .await;
+    }
+
+    /// Tells all clients to reload if possible for new changes.
+    pub async fn send_reload_command(&mut self) {
         self.build_status.set(Status::Ready);
         self.build_status.set(Status::Ready);
         self.send_build_status().await;
         self.send_build_status().await;
-        for socket in self.hot_reload_sockets.iter_mut() {
-            _ = socket
-                .send(Message::Text(
-                    serde_json::to_string(&DevserverMsg::FullReload).unwrap(),
-                ))
-                .await;
-        }
+        self.send_devserver_message(DevserverMsg::FullReloadCommand)
+            .await;
     }
     }
 
 
-    /// Send a shutdown message to all connected clients
+    /// Send a shutdown message to all connected clients.
     pub async fn send_shutdown(&mut self) {
     pub async fn send_shutdown(&mut self) {
+        self.send_devserver_message(DevserverMsg::Shutdown).await;
+    }
+
+    /// Sends a devserver message to all connected clients.
+    async fn send_devserver_message(&mut self, msg: DevserverMsg) {
         for socket in self.hot_reload_sockets.iter_mut() {
         for socket in self.hot_reload_sockets.iter_mut() {
             _ = socket
             _ = socket
-                .send(Message::Text(
-                    serde_json::to_string(&DevserverMsg::Shutdown).unwrap(),
-                ))
+                .send(Message::Text(serde_json::to_string(&msg).unwrap()))
                 .await;
                 .await;
         }
         }
     }
     }

+ 7 - 3
packages/desktop/src/app.rs

@@ -330,8 +330,10 @@ impl App {
         not(target_os = "ios")
         not(target_os = "ios")
     ))]
     ))]
     pub fn handle_hot_reload_msg(&mut self, msg: dioxus_hot_reload::DevserverMsg) {
     pub fn handle_hot_reload_msg(&mut self, msg: dioxus_hot_reload::DevserverMsg) {
+        use dioxus_hot_reload::DevserverMsg;
+
         match msg {
         match msg {
-            dioxus_hot_reload::DevserverMsg::HotReload(hr_msg) => {
+            DevserverMsg::HotReload(hr_msg) => {
                 for webview in self.webviews.values_mut() {
                 for webview in self.webviews.values_mut() {
                     dioxus_hot_reload::apply_changes(&mut webview.dom, &hr_msg);
                     dioxus_hot_reload::apply_changes(&mut webview.dom, &hr_msg);
                     webview.poll_vdom();
                     webview.poll_vdom();
@@ -343,11 +345,13 @@ impl App {
                     }
                     }
                 }
                 }
             }
             }
-            dioxus_hot_reload::DevserverMsg::FullReload => {
+            DevserverMsg::FullReloadCommand
+            | DevserverMsg::FullReloadStart
+            | DevserverMsg::FullReloadFailed => {
                 // usually only web gets this message - what are we supposed to do?
                 // usually only web gets this message - what are we supposed to do?
                 // Maybe we could just binary patch ourselves in place without losing window state?
                 // Maybe we could just binary patch ourselves in place without losing window state?
             }
             }
-            dioxus_hot_reload::DevserverMsg::Shutdown => {
+            DevserverMsg::Shutdown => {
                 self.control_flow = ControlFlow::Exit;
                 self.control_flow = ControlFlow::Exit;
             }
             }
         }
         }

+ 7 - 1
packages/hot-reload/src/lib.rs

@@ -22,8 +22,14 @@ pub enum DevserverMsg {
     /// This includes all the templates/literals/assets/binary patches that have changed in one shot
     /// This includes all the templates/literals/assets/binary patches that have changed in one shot
     HotReload(HotReloadMsg),
     HotReload(HotReloadMsg),
 
 
+    /// The devserver is starting a full rebuild.
+    FullReloadStart,
+
+    /// The full reload failed.
+    FullReloadFailed,
+
     /// The app should reload completely if it can
     /// The app should reload completely if it can
-    FullReload,
+    FullReloadCommand,
 
 
     /// The program is shutting down completely - maybe toss up a splash screen or something?
     /// The program is shutting down completely - maybe toss up a splash screen or something?
     Shutdown,
     Shutdown,

+ 7 - 4
packages/liveview/src/pool.rs

@@ -6,6 +6,7 @@ use crate::{
     LiveViewError,
     LiveViewError,
 };
 };
 use dioxus_core::prelude::*;
 use dioxus_core::prelude::*;
+use dioxus_hot_reload::DevserverMsg;
 use dioxus_html::{EventData, HtmlEvent, PlatformEventData};
 use dioxus_html::{EventData, HtmlEvent, PlatformEventData};
 use dioxus_interpreter_js::MutationState;
 use dioxus_interpreter_js::MutationState;
 use futures_util::{pin_mut, SinkExt, StreamExt};
 use futures_util::{pin_mut, SinkExt, StreamExt};
@@ -213,16 +214,18 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
             Some(msg) = hot_reload_wait => {
             Some(msg) = hot_reload_wait => {
                 #[cfg(all(feature = "hot-reload", debug_assertions))]
                 #[cfg(all(feature = "hot-reload", debug_assertions))]
                 match msg{
                 match msg{
-                    dioxus_hot_reload::DevserverMsg::HotReload(msg)=> {
+                    DevserverMsg::HotReload(msg)=> {
                         dioxus_hot_reload::apply_changes(&mut vdom, &msg);
                         dioxus_hot_reload::apply_changes(&mut vdom, &msg);
                     }
                     }
-                    dioxus_hot_reload::DevserverMsg::Shutdown => {
+                    DevserverMsg::Shutdown => {
                         std::process::exit(0);
                         std::process::exit(0);
                     },
                     },
-                    dioxus_hot_reload::DevserverMsg::FullReload => {
+                    DevserverMsg::FullReloadCommand
+                    | DevserverMsg::FullReloadStart
+                    | DevserverMsg::FullReloadFailed => {
                         // usually only web gets this message - what are we supposed to do?
                         // usually only web gets this message - what are we supposed to do?
                         // Maybe we could just binary patch ourselves in place without losing window state?
                         // Maybe we could just binary patch ourselves in place without losing window state?
-                    }
+                    },
                 }
                 }
                 #[cfg(not(all(feature = "hot-reload", debug_assertions)))]
                 #[cfg(not(all(feature = "hot-reload", debug_assertions)))]
                 let () = msg;
                 let () = msg;

+ 81 - 1
packages/web/src/hot_reload.rs

@@ -3,7 +3,12 @@
 //! This sets up a websocket connection to the devserver and handles messages from it.
 //! This sets up a websocket connection to the devserver and handles messages from it.
 //! We also set up a little recursive timer that will attempt to reconnect if the connection is lost.
 //! We also set up a little recursive timer that will attempt to reconnect if the connection is lost.
 
 
+use std::fmt::Display;
+use std::time::Duration;
+
+use dioxus_core::ScopeId;
 use dioxus_hot_reload::{DevserverMsg, HotReloadMsg};
 use dioxus_hot_reload::{DevserverMsg, HotReloadMsg};
+use dioxus_html::prelude::eval;
 use futures_channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender};
 use futures_channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender};
 use js_sys::JsString;
 use js_sys::JsString;
 use wasm_bindgen::JsCast;
 use wasm_bindgen::JsCast;
@@ -14,6 +19,9 @@ const POLL_INTERVAL_MIN: i32 = 250;
 const POLL_INTERVAL_MAX: i32 = 4000;
 const POLL_INTERVAL_MAX: i32 = 4000;
 const POLL_INTERVAL_SCALE_FACTOR: i32 = 2;
 const POLL_INTERVAL_SCALE_FACTOR: i32 = 2;
 
 
+/// Amount of time that toats should be displayed.
+const TOAST_TIMEOUT: Duration = Duration::from_secs(5);
+
 pub(crate) fn init() -> UnboundedReceiver<HotReloadMsg> {
 pub(crate) fn init() -> UnboundedReceiver<HotReloadMsg> {
     // Create the tx/rx pair that we'll use for the top-level future in the dioxus loop
     // Create the tx/rx pair that we'll use for the top-level future in the dioxus loop
     let (tx, rx) = unbounded();
     let (tx, rx) = unbounded();
@@ -62,8 +70,34 @@ fn make_ws(tx: UnboundedSender<HotReloadMsg>, poll_interval: i32, reload: bool)
                     web_sys::console::error_1(&"Connection to the devserver was closed".into())
                     web_sys::console::error_1(&"Connection to the devserver was closed".into())
                 }
                 }
 
 
+                // The devserver is telling us that it started a full rebuild. This does not mean that it is ready.
+                Ok(DevserverMsg::FullReloadStart) => show_toast(
+                    "Your app is being rebuilt.",
+                    "A non-hot-reloadable change occurred and we must rebuild.",
+                    ToastLevel::Info,
+                    TOAST_TIMEOUT,
+                    false,
+                ),
+                // The devserver is telling us that the full rebuild failed.
+                Ok(DevserverMsg::FullReloadFailed) => show_toast(
+                    "Oops! The build failed.",
+                    "We tried to rebuild your app, but something went wrong.",
+                    ToastLevel::Error,
+                    TOAST_TIMEOUT,
+                    false,
+                ),
+
                 // The devserver is telling us to reload the whole page
                 // The devserver is telling us to reload the whole page
-                Ok(DevserverMsg::FullReload) => window().unwrap().location().reload().unwrap(),
+                Ok(DevserverMsg::FullReloadCommand) => {
+                    show_toast(
+                        "Successfully rebuilt.",
+                        "Your app was rebuilt successfully and without error.",
+                        ToastLevel::Success,
+                        TOAST_TIMEOUT,
+                        true,
+                    );
+                    window().unwrap().location().reload().unwrap()
+                }
 
 
                 Err(e) => web_sys::console::error_1(
                 Err(e) => web_sys::console::error_1(
                     &format!("Error parsing devserver message: {}", e).into(),
                     &format!("Error parsing devserver message: {}", e).into(),
@@ -133,6 +167,52 @@ fn make_ws(tx: UnboundedSender<HotReloadMsg>, poll_interval: i32, reload: bool)
     }
     }
 }
 }
 
 
+/// Represents what color the toast should have.
+enum ToastLevel {
+    /// Green
+    Success,
+    /// Blue
+    Info,
+    /// Red
+    Error,
+}
+
+impl Display for ToastLevel {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            ToastLevel::Success => write!(f, "success"),
+            ToastLevel::Info => write!(f, "info"),
+            ToastLevel::Error => write!(f, "error"),
+        }
+    }
+}
+
+/// Displays a toast to the developer.
+fn show_toast(
+    header_text: &str,
+    message: &str,
+    level: ToastLevel,
+    duration: Duration,
+    after_reload: bool,
+) {
+    let as_ms = duration.as_millis();
+
+    let js_fn_name = match after_reload {
+        true => "scheduleDXToast",
+        false => "showDXToast",
+    };
+
+    ScopeId::ROOT.in_runtime(|| {
+        eval(&format!(
+            r#"
+            if (typeof {js_fn_name} !== "undefined") {{
+                {js_fn_name}("{header_text}", "{message}", "{level}", {as_ms});
+            }}
+            "#,
+        ));
+    });
+}
+
 /// Force a hotreload of the assets on this page by walking them and changing their URLs to include
 /// Force a hotreload of the assets on this page by walking them and changing their URLs to include
 /// some extra entropy.
 /// some extra entropy.
 ///
 ///