1use crate::{CompiledUi, RuntimeEngine, RuntimeError, ScriptGeneration, UiContext, UiNode};
2
3#[derive(Clone)]
4pub struct LiveScript {
5 active: CompiledUi,
6 root: UiNode,
7}
8
9impl LiveScript {
10 pub fn load(runtime: &mut RuntimeEngine, source: &str) -> Result<Self, RuntimeError> {
16 let compiled = runtime.compile(source)?;
17 let root = runtime.render(&compiled)?;
18 Ok(Self {
19 active: compiled,
20 root,
21 })
22 }
23
24 pub fn load_with_context(
30 runtime: &mut RuntimeEngine,
31 source: &str,
32 context: UiContext,
33 ) -> Result<Self, RuntimeError> {
34 let compiled = runtime.compile(source)?;
35 let root = runtime.render_with_context(&compiled, context)?;
36 Ok(Self {
37 active: compiled,
38 root,
39 })
40 }
41
42 #[must_use]
43 pub fn root(&self) -> &UiNode {
44 &self.root
45 }
46
47 #[must_use]
48 pub fn generation(&self) -> ScriptGeneration {
49 self.active.generation()
50 }
51
52 pub fn reload(&mut self, runtime: &mut RuntimeEngine, source: &str) -> ReloadOutcome {
53 let candidate = match runtime.compile(source) {
54 Ok(candidate) => candidate,
55 Err(error) => return ReloadOutcome::Rejected { error },
56 };
57 match runtime.render(&candidate) {
58 Ok(root) => {
59 self.active = candidate;
60 self.root = root;
61 ReloadOutcome::Applied {
62 generation: self.active.generation(),
63 }
64 }
65 Err(error) => ReloadOutcome::Rejected { error },
66 }
67 }
68
69 pub fn reload_with_context(
70 &mut self,
71 runtime: &mut RuntimeEngine,
72 source: &str,
73 context: UiContext,
74 ) -> ReloadOutcome {
75 let candidate = match runtime.compile(source) {
76 Ok(candidate) => candidate,
77 Err(error) => return ReloadOutcome::Rejected { error },
78 };
79 match runtime.render_with_context(&candidate, context) {
80 Ok(root) => {
81 self.active = candidate;
82 self.root = root;
83 ReloadOutcome::Applied {
84 generation: self.active.generation(),
85 }
86 }
87 Err(error) => ReloadOutcome::Rejected { error },
88 }
89 }
90}
91
92#[derive(Debug)]
93pub enum ReloadOutcome {
94 Applied { generation: ScriptGeneration },
95 Rejected { error: RuntimeError },
96}
97
98impl ReloadOutcome {
99 #[must_use]
100 pub const fn is_applied(&self) -> bool {
101 matches!(self, Self::Applied { .. })
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 #[test]
110 fn failed_reload_preserves_last_good_tree_and_generation() {
111 let mut runtime = RuntimeEngine::new();
112 let mut live = LiveScript::load(&mut runtime, "fn view() { text(\"good\") }").unwrap();
113 let generation = live.generation();
114 let root = live.root().clone();
115
116 assert!(matches!(
117 live.reload(&mut runtime, "fn view() { text("),
118 ReloadOutcome::Rejected { .. }
119 ));
120 assert_eq!(live.generation(), generation);
121 assert_eq!(live.root(), &root);
122
123 assert!(matches!(
124 live.reload(&mut runtime, "fn view() { throw \"bad render\"; }"),
125 ReloadOutcome::Rejected { .. }
126 ));
127 assert_eq!(live.generation(), generation);
128 assert_eq!(live.root(), &root);
129 assert!(runtime.is_current(generation));
130 }
131
132 #[test]
133 fn successful_reload_atomically_replaces_tree() {
134 let mut runtime = RuntimeEngine::new();
135 let mut live = LiveScript::load(&mut runtime, "fn view() { text(\"one\") }").unwrap();
136 let first = live.generation();
137 assert!(
138 live.reload(&mut runtime, "fn view() { text(\"two\") }")
139 .is_applied()
140 );
141 assert!(live.generation() > first);
142 assert!(matches!(
143 live.root().kind(),
144 crate::UiNodeKind::Text { text } if text == "two"
145 ));
146 assert!(live.root().source().is_some());
147 }
148}