ferroui_hot_reload/
styles.rs1use crate::{PreparedReload, ReloadError, ReloadTarget};
3use ferroui_base::{
4 styling::{style_ptr_eq, IStyle, Styles},
5 Ref, WeakRef,
6};
7use ferroui_markup_xaml::{
8 source_overlay::Sources, FromXamlObject, RuntimeXamlLoaderConfiguration,
9 RuntimeXamlLoaderDocument,
10};
11use std::{
12 cell::RefCell,
13 rc::{Rc, Weak},
14};
15
16pub struct StyleReloadTarget {
22 container: WeakRef<Styles>,
23 current: Rc<RefCell<Weak<dyn IStyle>>>,
24 uri: String,
25 dependencies: Vec<String>,
26 configuration: RuntimeXamlLoaderConfiguration,
27}
28impl StyleReloadTarget {
29 pub fn with_dependencies(mut self, dependencies: impl IntoIterator<Item = String>) -> Self {
33 self.dependencies = dependencies
34 .into_iter()
35 .map(|uri| crate::document_key(&uri))
36 .collect();
37 self
38 }
39
40 pub fn contribution(
41 container: &Ref<Styles>,
42 index: usize,
43 uri: String,
44 configuration: RuntimeXamlLoaderConfiguration,
45 ) -> Result<Self, ReloadError> {
46 let provider = container
47 .snapshot()
48 .get(index)
49 .cloned()
50 .ok_or_else(|| ReloadError::Failed("Invalid style contribution index".into()))?;
51 Ok(Self {
52 container: container.downgrade(),
53 current: Rc::new(RefCell::new(Rc::downgrade(&provider))),
54 uri: crate::document_key(&uri),
55 dependencies: Vec::new(),
56 configuration,
57 })
58 }
59 pub fn discover(
61 container: &Ref<Styles>,
62 configuration: RuntimeXamlLoaderConfiguration,
63 ) -> Result<Vec<Self>, ReloadError> {
64 container
65 .snapshot()
66 .iter()
67 .enumerate()
68 .filter_map(|(index, style)| {
69 style
70 .as_resource_provider()?
71 .development_source()
72 .map(|uri| (index, uri))
73 })
74 .map(|(index, uri)| Self::contribution(container, index, uri, configuration.clone()))
75 .collect()
76 }
77
78 pub fn is_alive(&self) -> bool {
79 self.container.upgrade().is_some()
80 }
81 pub(crate) fn tracks(&self, style: &Rc<dyn IStyle>) -> bool {
82 self.current
83 .borrow()
84 .upgrade()
85 .is_some_and(|current| style_ptr_eq(¤t, style))
86 }
87 pub(crate) fn is_attached(&self) -> bool {
88 self.container.upgrade().is_some_and(|container| {
89 container.snapshot().iter().any(|style| self.tracks(style))
90 })
91 }
92}
93struct Change {
94 container: Ref<Styles>,
95 index: usize,
96 old: Rc<dyn IStyle>,
97 new: Rc<dyn IStyle>,
98 current: Rc<RefCell<Weak<dyn IStyle>>>,
99 started: bool,
100}
101impl PreparedReload for Change {
102 fn commit(&mut self) -> Result<(), ReloadError> {
103 let entries = self.container.snapshot();
104 if entries
105 .get(self.index)
106 .is_none_or(|entry| !style_ptr_eq(entry, &self.old))
107 {
108 return Err(ReloadError::RestartRequired(
109 "Owned style contribution changed after preparation".into(),
110 ));
111 }
112 self.started = true;
113 self.container.set(self.index, self.new.clone());
115 Ok(())
116 }
117 fn rollback(&mut self) {
118 if self.started {
119 let entries = self.container.snapshot();
122 if let Some(index) = entries
123 .iter()
124 .position(|entry| style_ptr_eq(entry, &self.new))
125 {
126 self.container.set(index, self.old.clone());
127 } else if !entries.iter().any(|entry| style_ptr_eq(entry, &self.old)) {
128 self.container
129 .insert(self.index.min(entries.len()), self.old.clone());
130 }
131 }
132 }
133 fn finish(&mut self) {
134 *self.current.borrow_mut() = Rc::downgrade(&self.new);
135 }
136}
137impl ReloadTarget for StyleReloadTarget {
138 fn documents(&self) -> Vec<String> {
139 let mut documents = self.dependencies.clone();
140 documents.push(self.uri.clone());
141 documents
142 }
143 fn prepare(&self, sources: Rc<Sources>) -> Result<Box<dyn PreparedReload>, ReloadError> {
144 self.prepare_change(sources)
145 .map(|change| Box::new(change) as Box<dyn PreparedReload>)
146 }
147 fn resource_preparation(
148 self: Rc<Self>,
149 sources: Rc<Sources>,
150 ) -> Result<Option<crate::ResourcePreparation>, ReloadError> {
151 let old = self.current.borrow().upgrade().ok_or_else(|| {
152 ReloadError::RestartRequired("Owned resource/style contribution was removed".into())
153 })?;
154 let original: Rc<dyn ferroui_base::controls::IResourceNode> = old;
155 Ok(Some(crate::ResourcePreparation::new(original, move || {
156 let change = self.prepare_change(sources.clone())?;
157 let replacement: Rc<dyn ferroui_base::controls::IResourceNode> = change.new.clone();
158 Ok((replacement, Box::new(change)))
159 })))
160 }
161}
162impl StyleReloadTarget {
163 fn prepare_change(&self, sources: Rc<Sources>) -> Result<Change, ReloadError> {
164 let container = self
165 .container
166 .upgrade()
167 .ok_or_else(|| ReloadError::Failed("Style container closed".into()))?;
168 let old = self.current.borrow().upgrade().ok_or_else(|| {
169 ReloadError::RestartRequired("Owned style contribution was removed".into())
170 })?;
171 let entries = container.snapshot();
172 let mut matches = entries
173 .iter()
174 .enumerate()
175 .filter(|(_, entry)| style_ptr_eq(entry, &old));
176 let index = matches.next().map(|(index, _)| index).ok_or_else(|| {
177 ReloadError::RestartRequired("Owned style contribution was replaced".into())
178 })?;
179 if matches.next().is_some() {
180 return Err(ReloadError::RestartRequired(
181 "Ambiguous style contribution ownership".into(),
182 ));
183 }
184 let source = sources
185 .get(&self.uri)
186 .ok_or_else(|| ReloadError::Failed("Missing style source".into()))?;
187 let xml = roxmltree::Document::parse(source)
188 .map_err(|error| ReloadError::Failed(error.to_string()))?;
189 let root = xml.root_element();
190 if !matches!(root.tag_name().name(), "Styles" | "Style")
191 || root.tag_name().namespace() != Some("https://github.com/ferroui")
192 || crate::DocumentContract::parse(source)?.class.is_some()
193 {
194 return Err(ReloadError::RestartRequired(
195 "Custom style roots require an explicit adapter".into(),
196 ));
197 }
198 let uri = ferroui_base::utilities::Uri::try_create(
199 &self.uri,
200 ferroui_base::utilities::UriKind::Absolute,
201 )
202 .ok_or_else(|| ReloadError::Failed("Invalid style URI".into()))?;
203 let candidate = ferroui_markup_xaml_loader::FerroRuntimeXamlLoader::load_document(
204 RuntimeXamlLoaderDocument::with_base_uri(Some(uri), source),
205 Some(self.configuration.clone()),
206 )
207 .map_err(|error| ReloadError::Failed(error.to_string()))?;
208 let new = Rc::<dyn IStyle>::from_xaml_object(&candidate)
209 .ok_or_else(|| ReloadError::Failed("Expected style".into()))?;
210 Ok(Change {
211 container,
212 index,
213 old,
214 new,
215 current: self.current.clone(),
216 started: false,
217 })
218 }
219}