1use crate::{PreparedReload, ReloadError, ReloadTarget};
3use ferroui_base::{
4 controls::{NameScope, NameScopeRef},
5 BoxedValue, Ref, StyledElement, WeakRef,
6};
7use ferroui_controls::{ContentControl, ContentPage};
8use ferroui_markup_xaml::{
9 source_overlay::Sources, FromXamlObject, RuntimeXamlLoaderConfiguration,
10 RuntimeXamlLoaderDocument,
11};
12use std::{cell::RefCell, rc::Rc};
13
14enum Root {
15 Control(WeakRef<ContentControl>),
16 Page(WeakRef<ContentPage>),
17}
18#[derive(Clone)]
19enum LiveRoot {
20 Control(Ref<ContentControl>),
21 Page(Ref<ContentPage>),
22}
23impl Root {
24 fn upgrade(&self) -> Option<LiveRoot> {
25 match self {
26 Self::Control(root) => root.upgrade().map(LiveRoot::Control),
27 Self::Page(root) => root.upgrade().map(LiveRoot::Page),
28 }
29 }
30}
31impl LiveRoot {
32 fn element(&self) -> Ref<StyledElement> {
33 match self {
34 Self::Control(root) => root.clone().upcast(),
35 Self::Page(root) => root.clone().upcast(),
36 }
37 }
38 fn content(&self) -> Option<BoxedValue> {
39 match self {
40 Self::Control(root) => root.content(),
41 Self::Page(root) => root.content(),
42 }
43 }
44 fn set_content(&self, value: Option<BoxedValue>) {
45 match self {
46 Self::Control(root) => root.set_content(value),
47 Self::Page(root) => root.set_content(value),
48 }
49 }
50}
51fn matches_replacement(root: &Ref<StyledElement>, scope: &Option<NameScopeRef>, content: &Option<BoxedValue>) -> bool {
52 let current = root.cast::<ContentControl>().and_then(|root| root.content())
53 .or_else(|| root.cast::<ContentPage>().and_then(|root| root.content()));
54 NameScope::get_name_scope(root) == *scope && match (¤t, content) {
55 (Some(current), Some(content)) => Rc::ptr_eq(current, content),
56 (None, None) => true,
57 _ => false,
58 }
59}
60pub trait ComponentRefresh {
66 fn prepare(
67 &self,
68 root: &Ref<StyledElement>,
69 scope: Option<&NameScopeRef>,
70 ) -> Result<Box<dyn PreparedReload>, ReloadError>;
71}
72pub struct ComponentReloadTarget {
78 root: Root,
79 uri: String,
80 dependencies: Vec<String>,
81 baseline: Rc<RefCell<String>>,
82 configuration: RuntimeXamlLoaderConfiguration,
83 refresh: Option<Rc<dyn ComponentRefresh>>,
84 properties: Vec<Box<dyn crate::root_properties::OwnedRootProperty>>,
85 resources: Option<crate::root_resources::OwnedResources>,
86 styles: Option<crate::root_styles::OwnedStyles>,
87 handlers: Option<crate::handlers::OwnedHandlers>,
88 state_adapters: Vec<Rc<dyn crate::ControlStateAdapter>>,
89 restoration: crate::control_state::StateRestoration,
90}
91impl ComponentReloadTarget {
92 pub fn with_dependencies(mut self, dependencies: impl IntoIterator<Item = String>) -> Self {
96 self.dependencies = dependencies
97 .into_iter()
98 .map(|uri| crate::document_key(&uri))
99 .collect();
100 self
101 }
102
103 pub fn content_control(
104 root: &Ref<ContentControl>,
105 uri: String,
106 baseline: String,
107 configuration: RuntimeXamlLoaderConfiguration,
108 ) -> Self {
109 Self {
110 root: Root::Control(root.downgrade()),
111 uri: crate::document_key(&uri),
112 dependencies: Vec::new(),
113 baseline: Rc::new(RefCell::new(baseline)),
114 configuration,
115 refresh: None,
116 properties: Vec::new(),
117 resources: None,
118 styles: None,
119 handlers: None,
120 state_adapters: Vec::new(),
121 restoration: Default::default(),
122 }
123 }
124 pub fn content_page(
125 root: &Ref<ContentPage>,
126 uri: String,
127 baseline: String,
128 configuration: RuntimeXamlLoaderConfiguration,
129 ) -> Self {
130 Self {
131 root: Root::Page(root.downgrade()),
132 uri: crate::document_key(&uri),
133 dependencies: Vec::new(),
134 baseline: Rc::new(RefCell::new(baseline)),
135 configuration,
136 refresh: None,
137 properties: Vec::new(),
138 resources: None,
139 styles: None,
140 handlers: None,
141 state_adapters: Vec::new(),
142 restoration: Default::default(),
143 }
144 }
145 pub fn with_refresh(mut self, refresh: Rc<dyn ComponentRefresh>) -> Self {
146 self.refresh = Some(refresh);
147 self
148 }
149 pub fn with_owned_resources(mut self) -> Self {
153 self.resources = self.root.upgrade().map(|root| crate::root_resources::OwnedResources::capture(&root.element()));
154 self
155 }
156 pub fn with_owned_styles(mut self) -> Self {
159 self.styles = self.root.upgrade().map(|root| crate::root_styles::OwnedStyles::capture(&root.element()));
160 self
161 }
162 pub fn with_owned_handlers(mut self) -> Self {
165 self.handlers = self.root.upgrade().map(|root| crate::handlers::OwnedHandlers::capture(&root.element().upcast()));
166 self
167 }
168 pub fn with_state_adapter(mut self, adapter: Rc<dyn crate::ControlStateAdapter>) -> Self {
171 self.state_adapters.push(adapter);
172 self
173 }
174 pub fn with_owned_root_property<T: ferroui_base::PropertyValue>(
179 mut self,
180 property: &'static ferroui_base::StyledProperty<T>,
181 ) -> Self {
182 if let Some(root) = self.root.upgrade() {
183 if !self
184 .properties
185 .iter()
186 .any(|owned| owned.name() == property.name())
187 {
188 self.properties
189 .push(crate::root_properties::own(&root.element(), property));
190 }
191 }
192 self
193 }
194 pub fn with_owned_root_static_resource_property<T: ferroui_base::PropertyValue>(
200 mut self,
201 property: &'static ferroui_base::StyledProperty<T>,
202 ) -> Self {
203 if let Some(root) = self.root.upgrade() {
204 self.properties
205 .retain(|owned| owned.name() != property.name());
206 self.properties
207 .push(crate::root_properties::own_static_resource(
208 &root.element(),
209 property,
210 ));
211 }
212 self
213 }
214 pub fn is_alive(&self) -> bool {
215 self.root.upgrade().is_some()
216 }
217
218 pub fn with_owned_root_binding_property<T: ferroui_base::PropertyValue>(
224 mut self,
225 property: &'static ferroui_base::StyledProperty<T>,
226 ) -> Self {
227 if let Some(root) = self.root.upgrade() {
228 self.properties.retain(|owned| owned.name() != property.name());
229 self.properties.push(crate::root_properties::own_binding(&root.element(), property));
230 }
231 self
232 }
233}
234pub(crate) fn shadow_source(
235 source: &str,
236 baseline: &str,
237 supported: &[&str],
238 content_member: &str,
239 owned_properties: &[&str],
240 owned_members: &[&str],
241 owned_handlers: bool,
242) -> Result<String, ReloadError> {
243 const XAML: &str = "http://schemas.microsoft.com/winfx/2006/xaml";
244 let parse = |text: &str| crate::DocumentContract::parse(text);
245 if !parse(source)?.events.is_empty() {
246 if !owned_handlers { return Err(ReloadError::RestartRequired("Component handlers require an ownership adapter".into())); }
247 crate::contract::validate_routed_events(source)?;
248 }
249 let current =
250 roxmltree::Document::parse(source).map_err(|e| ReloadError::Failed(e.to_string()))?;
251 let before =
252 roxmltree::Document::parse(baseline).map_err(|e| ReloadError::Failed(e.to_string()))?;
253 let root = current.root_element();
254 let old = before.root_element();
255 let attributes = |node: roxmltree::Node<'_, '_>| {
256 let mut attrs: Vec<_> = node
257 .attributes()
258 .filter(|a| a.namespace().is_some() || !owned_properties.contains(&a.name()))
259 .map(|a| {
260 (
261 a.namespace().unwrap_or("").to_owned(),
262 a.name().to_owned(),
263 a.value().to_owned(),
264 )
265 })
266 .collect();
267 attrs.sort();
268 attrs
269 };
270 if root.tag_name() != old.tag_name() || attributes(root) != attributes(old) {
271 return Err(ReloadError::RestartRequired(
272 "Root assignment changes require an ownership-aware adapter".into(),
273 ));
274 }
275 if root.attribute("Name").is_some()
276 || root.attribute((XAML, "Name")).is_some()
277 || root.attribute((crate::KEY_NAMESPACE, "Key")).is_some()
278 {
279 return Err(ReloadError::RestartRequired(
280 "Named component roots require an explicit namescope adapter".into(),
281 ));
282 }
283 for node in root.children().filter(|node| node.is_element()) {
284 if let Some((owner, member)) = node.tag_name().name().split_once('.') {
285 if (member == content_member || owned_members.contains(&member))
288 && (supported.contains(&owner) || (owned_members.contains(&member) && matches!(owner, "StyledElement" | "Control")))
289 && node.tag_name().namespace() == Some("https://github.com/ferroui")
290 && node.attributes().len() == 0
291 {
292 continue;
293 }
294 return Err(ReloadError::RestartRequired(
295 "Root property-element assignments require an ownership-aware adapter".into(),
296 ));
297 }
298 }
299 if !supported.contains(&root.tag_name().name())
302 || root.tag_name().namespace() != Some("https://github.com/ferroui")
303 {
304 return Err(ReloadError::RestartRequired(
305 "Unsupported component root".into(),
306 ));
307 }
308 let mut result = source.to_owned();
309 if let Some(class) = root
310 .attributes()
311 .find(|a| a.namespace() == Some(XAML) && a.name() == "Class")
312 {
313 result.replace_range(class.range(), "");
314 }
315 crate::identity::runtime_source(&result)
316}
317struct Change {
318 restoration: crate::control_state::StateRestoration,
319 root: LiveRoot,
320 old_content: Option<BoxedValue>,
321 new_content: Option<BoxedValue>,
322 old_scope: Option<NameScopeRef>,
323 new_scope: Option<NameScopeRef>,
324 state: Rc<crate::state::ViewState>,
325 before: String,
326 after: String,
327 baseline: Rc<RefCell<String>>,
328 refresh: Option<Box<dyn PreparedReload>>,
329 properties: Vec<Box<dyn PreparedReload>>,
330 handlers: Option<Box<dyn PreparedReload>>,
331 detached_context: Option<crate::detached_context::DetachedContext>,
332}
333impl PreparedReload for Change {
334 fn commit(&mut self) -> Result<(), ReloadError> {
335 if let Some(handlers) = &mut self.handlers { handlers.commit()?; }
336 for property in &mut self.properties {
337 property.commit()?;
338 }
339 self.detached_context = crate::detached_context::DetachedContext::freeze(&self.old_content);
340 self.root.set_content(self.new_content.clone());
341 NameScope::set_name_scope(&self.root.element(), self.new_scope.clone());
342 if let Some(refresh) = &mut self.refresh {
343 refresh.commit()?;
344 }
345 Ok(())
346 }
347 fn rollback(&mut self) {
348 for property in self.properties.iter_mut().rev() {
349 property.rollback();
350 }
351 let _retired = crate::detached_context::DetachedContext::freeze(&self.new_content);
352 self.root.set_content(self.old_content.clone());
353 if let Some(context) = self.detached_context.take() {
354 context.restore();
355 }
356 NameScope::set_name_scope(&self.root.element(), self.old_scope.clone());
357 if let Some(refresh) = &mut self.refresh {
358 refresh.rollback();
359 }
360 if let Some(handlers) = &mut self.handlers { handlers.rollback(); }
361 }
362 fn finish(&mut self) {
363 for property in &mut self.properties {
364 property.finish();
365 }
366 *self.baseline.borrow_mut() = self.after.clone();
367 if let Some(refresh) = &mut self.refresh {
368 refresh.finish();
369 }
370 if let Some(handlers) = &mut self.handlers { handlers.finish(); }
371 let root = self.root.element().downgrade();
372 let content = self.new_content.clone();
373 let scope = self.new_scope.clone();
374 let state = self.state.clone();
375 let before = self.before.clone();
376 let after = self.after.clone();
377 self.restoration.schedule(
378 move || {
379 if let Some(root) = root.upgrade() {
380 if matches_replacement(&root, &scope, &content) {
381 if let Some(layout) = root.cast::<ferroui_base::layout::Layoutable>() {
382 layout.update_layout();
383 }
384 }
385 if matches_replacement(&root, &scope, &content) {
386 state.restore(&(Rc::new(root.clone()) as BoxedValue), &before, &after);
387 }
388 }
389 },
390 );
391 }
392}
393impl ReloadTarget for ComponentReloadTarget {
394 fn complete_pending_state(&self) {
395 self.restoration.complete();
396 }
397 fn documents(&self) -> Vec<String> {
398 let mut documents = self.dependencies.clone();
399 documents.push(self.uri.clone());
400 documents
401 }
402 fn prepare(&self, sources: Rc<Sources>) -> Result<Box<dyn PreparedReload>, ReloadError> {
403 let root = self
404 .root
405 .upgrade()
406 .ok_or_else(|| ReloadError::Failed("Component closed".into()))?;
407 let source = sources
408 .get(&self.uri)
409 .ok_or_else(|| ReloadError::Failed("Missing component source".into()))?;
410 let before = self.baseline.borrow().clone();
411 let prepared = shadow_source(
412 source,
413 &before,
414 &["ContentControl", "UserControl", "ContentPage"],
415 "Content",
416 &self
417 .properties
418 .iter()
419 .map(|property| property.name())
420 .collect::<Vec<_>>(),
421 match (self.resources.is_some(), self.styles.is_some()) {
422 (true, true) => &["Resources", "Styles"],
423 (true, false) => &["Resources"],
424 (false, true) => &["Styles"],
425 _ => &[],
426 },
427 self.handlers.is_some(),
428 )?;
429 let uri = ferroui_base::utilities::Uri::try_create(
430 &self.uri,
431 ferroui_base::utilities::UriKind::Absolute,
432 )
433 .ok_or_else(|| ReloadError::Failed("Invalid component URI".into()))?;
434 let bindings = ferroui_markup_xaml_loader::binding_capture::BindingCaptureScope::new(
435 self.uri.clone(),
436 self.properties.iter().filter_map(|property| property.binding_property()),
437 );
438 let mut document = RuntimeXamlLoaderDocument::with_base_uri(Some(uri), &prepared);
439 document.service_provider = Some(crate::root_resources::services(&root.element(), self.resources.as_ref(), self.styles.as_ref()));
440 let candidate_root = (self.styles.is_some() || self.resources.is_some() || self.handlers.is_some()).then(|| crate::root_preview::create(&prepared)).transpose()?;
441 let _preview = candidate_root.as_ref().map(|candidate| crate::root_preview::install(
442 &root.element(), candidate, self.resources.as_ref(), self.styles.as_ref(),
443 ));
444 let code = self.handlers.as_ref().map(|_| ferroui_markup_xaml::reload_events::ReloadCodeBehind::new(
445 &root.element().upcast(), &candidate_root.as_ref().unwrap().clone().upcast(),
446 ));
447 if let Some(code) = &code { document.service_provider = Some(code.services(document.service_provider.take().unwrap())); }
448 document.root_instance = candidate_root.map(|candidate| Rc::new(candidate) as BoxedValue);
449 let deferred = ferroui_markup_xaml::xaml_il::runtime::DeferredContentReloadScope::new();
450 let dynamic = ferroui_markup_xaml::reload_resources::DynamicResourceReloadScope::new();
451 let candidate = ferroui_markup_xaml_loader::FerroRuntimeXamlLoader::load_document(
452 document,
453 Some(self.configuration.clone()),
454 )
455 .map_err(|e| ReloadError::Failed(e.to_string()))?;
456 let candidate = match &root {
457 LiveRoot::Control(_) => {
458 Ref::<ContentControl>::from_xaml_object(&candidate).map(LiveRoot::Control)
459 }
460 LiveRoot::Page(_) => {
461 Ref::<ContentPage>::from_xaml_object(&candidate).map(LiveRoot::Page)
462 }
463 }
464 .ok_or_else(|| ReloadError::RestartRequired("Component content kind changed".into()))?;
465 if self.resources.is_some() || self.styles.is_some() {
466 ferroui_markup_xaml::reload_validation::prepare_candidate(&candidate.element())
467 .map_err(|error| ReloadError::Failed(error.to_string()))?;
468 crate::root_styles::prepare_styling(&candidate.element(), &root.element())?;
469 ferroui_markup_xaml::reload_validation::validate_root_template(&candidate.element())
470 .map_err(|error| ReloadError::Failed(error.to_string()))?;
471 }
472 let mut properties = Vec::new();
473 for property in &self.properties {
474 if let Some(change) = property.prepare(
475 &root.element().upcast(),
476 &candidate.element().upcast(),
477 &before,
478 source,
479 &bindings,
480 )? {
481 properties.push(change);
482 }
483 }
484 properties.insert(0, crate::deferred_context::prepare(
487 &deferred, &candidate.element(), &root.element(), self.resources.is_some(), self.styles.is_some(),
488 ));
489 let new_content = candidate.content();
490 let new_scope = NameScope::get_name_scope(&candidate.element());
491 candidate.set_content(None);
492 NameScope::set_name_scope(&candidate.element(), None);
493 if let Some(resources) = &self.resources {
494 properties.insert(1, resources.prepare(&candidate.element())?);
495 }
496 let mut dynamic_index = if self.resources.is_some() { 2 } else { 1 };
497 if let Some(styles) = &self.styles {
498 properties.insert(dynamic_index, styles.prepare(&candidate.element())?);
499 dynamic_index += 1;
500 }
501 properties.insert(dynamic_index, crate::deferred_context::prepare_dynamic(
502 &dynamic, &candidate.element(), &root.element(),
503 ));
504 let handlers = self.handlers.as_ref().zip(code.as_ref()).map(|(owner, code)| owner.prepare(code)).transpose()?;
505 let refresh = self
506 .refresh
507 .as_ref()
508 .map(|refresh| refresh.prepare(&root.element(), new_scope.as_ref()))
509 .transpose()?;
510 Ok(Box::new(Change {
511 restoration: self.restoration.clone(),
512 old_content: root.content(),
513 state: Rc::new(crate::state::ViewState::capture_document(
514 Some(&(Rc::new(root.element()) as BoxedValue)), Some(&self.uri),
515 &before,
516 &self.state_adapters,
517 )),
518 old_scope: NameScope::get_name_scope(&root.element()),
519 root,
520 new_content,
521 new_scope,
522 before,
523 after: source.clone(),
524 baseline: self.baseline.clone(),
525 refresh,
526 properties,
527 handlers,
528 detached_context: None,
529 }))
530 }
531}