Skip to main content

ferroui_hot_reload/
component.rs

1//! Opt-in adapter for declarative class components with a stable content root.
2use 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 (&current, content) {
55        (Some(current), Some(content)) => Rc::ptr_eq(current, content),
56        (None, None) => true,
57        _ => false,
58    }
59}
60/// Prepares application reference/subscription changes without mutating the live component.
61/// Commit runs after content and namescope replacement. Rollback runs after restoring the
62/// previous graph; finish runs only after every target commits. Retain old subscriptions
63/// until finish, dispose new subscriptions on rollback, and never rerun constructors.
64/// As with ReloadTarget, the application must provide reversible, non-panicking cleanup.
65pub trait ComponentRefresh {
66    fn prepare(
67        &self,
68        root: &Ref<StyledElement>,
69        scope: Option<&NameScopeRef>,
70    ) -> Result<Box<dyn PreparedReload>, ReloadError>;
71}
72/// Explicitly opt in for declarative content. Use `with_refresh` for cached references and
73/// application child subscriptions. The original component is never reconstructed. Root
74/// assignments can be claimed with the literal, static-resource and binding ownership
75/// methods. `with_owned_resources` and `with_owned_styles` claim inline
76/// Resources and Styles. `with_owned_handlers` claims routed XAML subscriptions.
77pub 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    /// Declares resources/styles outside the document's inline includes, such as
93    /// application resources consumed through StaticResource. Their revisions are
94    /// prepared and committed in the same transaction as this target.
95    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    /// Claims the current root Resources contributions immediately after population.
150    /// Preserves later application keys; conflicting writes/provider ownership require restart.
151    /// Use `with_owned_styles` for Styles; custom resource side effects need an adapter.
152    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    /// Claims the current inline styles and Styles.Resources after initial
157    /// population. Later application style entries retain their identity/order.
158    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    /// Claims routed XAML subscriptions from initial population. Framework
163    /// metadata events without a removable token require an explicit adapter.
164    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    /// Add application state transfer for named/keyed replacement children.
169    /// The preserved component root is not reconstructed or restored by this hook.
170    pub fn with_state_adapter(mut self, adapter: Rc<dyn crate::ControlStateAdapter>) -> Self {
171        self.state_adapters.push(adapter);
172        self
173    }
174    /// Claims XAML ownership of this literal root attribute. Call immediately after
175    /// initial population, before application overrides. Binding/animation changes
176    /// and externally changed values require restart. Custom property callbacks must
177    /// support reversible assignment; no arbitrary user side effects are rolled back.
178    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    /// Claims a root literal or StaticResource assignment, including Theme/Template.
195    /// Re-resolves the candidate value on dependency revisions even when the markup
196    /// attribute is unchanged. Call immediately after initial population. Dynamic
197    /// resources, bindings and application overrides require another adapter or restart.
198    /// Property callbacks must support reversible assignment.
199    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    /// Claims a root literal or synchronous reflection/compiled binding. Register
219    /// immediately after population, before application assignments. Preparation
220    /// defers the candidate root binding and recreates it for the preserved root;
221    /// its new namescope belongs to the candidate children. Custom binding kinds,
222    /// delayed/asynchronous update modes and lifecycle properties need an adapter.
223    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            // Explicit Content/Children syntax owns the same graph as implicit
286            // content. Other root property elements still need their own adapter.
287            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    // Removing x:Class constructs only the framework root declared by the document. This
300    // adapter deliberately never runs the application class constructor a second time.
301    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        // Map lazy declarations before detaching candidate resources. Publish
485        // their new weak context before resource/style notifications can build them.
486        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}