Hot-reloadable applications for Iced Edit your GUI code and see changes instantly without restarting your application.
- True Hot Reloading - Update your UI code without restarting the application
- State Preservation - Application state persists across reloads
- Three Reload Modes - Choose the level of hot reloading that fits your needs
- Automatic Compilation - Built-in file watcher triggers incremental builds
- Function Status Display - Visual indicator shows which functions are hot-reloaded
- Panic Recovery - Gracefully handles panics in hot-reloaded code
- Full Iced Compatibility - Works with all Iced widgets and features
Hot Ice requires a workspace with separate crates for your binary and hot-reloadable UI:
my_app/
├── Cargo.toml # Workspace manifest
├── my_app/ # Binary crate
│ ├── Cargo.toml
│ └── src/
│ └── main.rs
└── ui/ # Hot-reloadable library crate
├── Cargo.toml
└── src/
└── lib.rs[workspace]
members = ["my_app", "ui"]
[workspace.dependencies]
hot_ice = { git = "https://github.com/anthropics/hot_ice" }
ui = { path = "ui" }[package]
name = "ui"
version = "0.1.0"
edition = "2024"
[dependencies]
hot_ice.workspace = true[package]
name = "my_app"
version = "0.1.0"
edition = "2024"
[dependencies]
hot_ice.workspace = true
ui.workspace = trueuse hot_ice::iced::widget::{button, column, text};
use hot_ice::iced::{Element, Task};
#[derive(Debug, Clone)]
pub enum Message {
Increment,
Decrement,
}
#[derive(Debug, Clone)]
pub struct State {
value: i32,
}
impl State {
#[hot_ice::hot_fn]
pub fn boot() -> (Self, Task<Message>) {
(State { value: 0 }, Task::none())
}
#[hot_ice::hot_fn]
pub fn update(&mut self, message: Message) -> Task<Message> {
match message {
Message::Increment => self.value += 1,
Message::Decrement => self.value -= 1,
}
Task::none()
}
#[hot_ice::hot_fn]
pub fn view(&self) -> Element<'_, Message> {
column![
button("+").on_press(Message::Increment),
text(self.value).size(50),
button("-").on_press(Message::Decrement),
]
.spacing(10)
.into()
}
}use ui::State;
fn main() {
hot_ice::application(State::boot, State::update, State::view)
.title(|_| String::from("My Hot App"))
.window_size((400, 300))
.centered()
.run()
.unwrap();
}cargo run --releaseNow edit your view function and save - your changes appear instantly!
Hot Ice supports three levels of hot reloading, each with different trade-offs:
The simplest mode - only the view function is hot-reloadable:
impl State {
// No macro needed
pub fn boot() -> (Self, Task<Message>) { /* ... */ }
// No macro needed
pub fn update(&mut self, message: Message) -> Task<Message> { /* ... */ }
#[hot_ice::hot_fn(cold_message)] // Only this is hot-reloadable
pub fn view(&self) -> Element<'_, Message> { /* ... */ }
}Best for: UI iteration, styling, layout tweaks
All message-returning functions are hot-reloadable:
impl State {
#[hot_ice::hot_fn]
pub fn boot() -> (Self, Task<Message>) { /* ... */ }
#[hot_ice::hot_fn]
pub fn update(&mut self, message: Message) -> Task<Message> { /* ... */ }
#[hot_ice::hot_fn]
pub fn view(&self) -> Element<'_, Message> { /* ... */ }
#[hot_ice::hot_fn]
pub fn subscription(&self) -> Subscription<Message> { /* ... */ }
// Non-message functions don't need the macro
pub fn theme(&self) -> Option<Theme> { /* ... */ }
}Best for: Iterating on application logic without state serialization overhead
Hot reload everything including state structure changes:
#[hot_ice::hot_state] // Enables state serialization
#[derive(Debug, Clone)]
pub struct State {
value: i32,
// Add new fields - they'll be initialized to default
}
impl State {
#[hot_ice::hot_fn(hot_state)]
pub fn boot() -> (Self, Task<Message>) { /* ... */ }
#[hot_ice::hot_fn(hot_state)]
pub fn update(&mut self, message: Message) -> Task<Message> { /* ... */ }
#[hot_ice::hot_fn(hot_state)]
pub fn view(&self) -> Element<'_, Message> { /* ... */ }
// All functions need the macro with hot_state
#[hot_ice::hot_fn(hot_state)]
pub fn theme(&self) -> Option<Theme> { /* ... */ }
}Best for: Rapid prototyping with evolving state structures
| Feature | View-Only | Message | Hot State |
|---|---|---|---|
| Hot-reload view | Yes | Yes | Yes |
| Hot-reload update | No | Yes | Yes |
| Hot-reload subscription | No | Yes | Yes |
| State type changes | Recompile | Recompile | Hot reload |
| Serialization required | No | No | Yes |
| Setup complexity | Minimal | Low | Medium |
The application function returns a builder for configuring your app:
hot_ice::application(State::boot, State::update, State::view)
// Callbacks
.subscription(State::subscription)
.theme(State::theme)
.style(State::style)
.scale_factor(State::scale_factor)
.title(State::title)
// Window settings
.window_size((1024, 768))
.centered()
.resizable(true)
.decorations(true)
// Rendering
.antialiasing(true)
.default_font(Font::MONOSPACE)
.font(include_bytes!("../fonts/custom.ttf").as_slice())
// Hot reloading
.reloader_settings(ReloaderSettings {
compile_in_reloader: true,
..Default::default()
})
.run()
.unwrap();Transforms functions for hot reloading. Supports these arguments:
| Argument | Description |
|---|---|
| (none) | Default hot reloading with message conversion |
hot_state |
Use with #[hot_state] for state persistence |
not_hot |
Disable hot reloading for this function |
cold_message |
Keep original Message type (view only) |
feature = "..." |
Conditional compilation |
Enables state serialization for persistence across reloads:
- Automatically derives
Serialize,Deserialize,Default - Adds
#[serde(default)]for backward compatibility - Generates serialization functions for the hot reload system
Requirements: All nested types must implement
Serialize, Deserialize, and Default.
Configure hot reloading behavior:
use hot_ice::ReloaderSettings;
use std::time::Duration;
ReloaderSettings {
// Build directory for the dynamic library
target_dir: "target/reload".to_string(),
// Location of compiled library
lib_dir: "target/reload/debug".to_string(),
// Auto-run cargo watch (set false for manual control)
compile_in_reloader: true,
// File change detection interval
file_watch_debounce: Duration::from_millis(25),
// Custom watch directory (None = auto-detect)
watch_dir: None,
}Hot Ice displays a status bar showing the state of each function:
| Color | Meaning |
|---|---|
| White | Static (not hot-reloadable) |
| Green | Hot (loaded from dynamic library) |
| Orange | Fallback (failed to load, using static) |
| Red | Error (function returned an error) |
The examples/ directory contains complete working examples:
| Example | Description |
|---|---|
hot_view |
View-only hot reloading (simplest setup) |
hot_message |
hot reload message type changes |
hot_state |
Full state persistence |
manual_reload |
Manual compilation control |
Run an example:
cd examples/hot_state
cargo run --release- Startup: Hot Ice compiles your UI crate as a dynamic library (
.so/.dll/.dylib) - File Watching:
cargo watchmonitors your source files for changes - Recompilation: On save, an incremental rebuild is triggered
- Hot Reload: The new library is loaded while your app keeps running
- State Transfer: If using
hot_state, state is serialized and restored
The status bar updates to show which functions are successfully hot-reloaded.
| Platform | Status |
|---|---|
| Linux | Fully supported |
| macOS | Supported (with automatic code signing) |
| Windows | Supported |
- Ensure files are saved
- Check console for compilation errors
- Verify
crate-type = ["rlib", "cdylib"]in your UI crate
- Make sure the correct macro is applied to all required functions
- Try a full rebuild:
cargo clean && cargo run --release
- Ensure all nested types implement
Serialize,Deserialize,Default - Add
#[serde(default)]to structs - Check for non-serializable types (use
#[serde(skip)]if needed)
Hot Ice automatically cleans up cargo watch when the application exits.
If processes remain orphaned, they can be killed manually.
Built on Iced - A cross-platform GUI library for Rust focused on simplicity and type-safety.