Skip to main content

ferroui_hot_reload/
panel.rs

1//! Replacement of an explicitly owned child range in a stable panel.
2use crate::{ComponentRefresh, PreparedReload, ReloadError, ReloadTarget};
3use ferroui_base::{
4    controls::{NameScope, NameScopeRef},
5    BoxedValue, Ref, WeakRef,
6};
7use ferroui_controls::{Control, Panel};
8use ferroui_markup_xaml::{
9    source_overlay::Sources, FromXamlObject, RuntimeXamlLoaderConfiguration,
10    RuntimeXamlLoaderDocument,
11};
12use std::{cell::RefCell, ops::Range, rc::Rc};
13
14struct Owned {
15    children: Vec<WeakRef<Control>>,
16    index: usize,
17}
18/// Opt-in adapter for framework panels. Only `owned` children are replaced;
19/// application children outside that range retain their identity and ordering.
20/// The root namescope must belong to this XAML document. Application children
21/// needing names must own separate namescopes. Root assignments and routed handlers
22/// require explicit ownership opt-in; constructors are never rerun on the live root.
23pub struct PanelReloadTarget {
24    root: WeakRef<Panel>,
25    uri: String,
26    dependencies: Vec<String>,
27    baseline: Rc<RefCell<String>>,
28    owned: Rc<RefCell<Owned>>,
29    configuration: RuntimeXamlLoaderConfiguration,
30    refresh: Option<Rc<dyn ComponentRefresh>>,
31    properties: Vec<Box<dyn crate::root_properties::OwnedRootProperty>>,
32    resources: Option<crate::root_resources::OwnedResources>,
33    styles: Option<crate::root_styles::OwnedStyles>,
34    handlers: Option<crate::handlers::OwnedHandlers>,
35    state_adapters: Vec<Rc<dyn crate::ControlStateAdapter>>,
36    restoration: crate::control_state::StateRestoration,
37}
38impl PanelReloadTarget {
39    /// Declares resources/styles outside the document's inline includes, such as
40    /// application resources consumed through StaticResource. Their revisions are
41    /// prepared and committed in the same transaction as this target.
42    pub fn with_dependencies(mut self, dependencies: impl IntoIterator<Item = String>) -> Self {
43        self.dependencies = dependencies
44            .into_iter()
45            .map(|uri| crate::document_key(&uri))
46            .collect();
47        self
48    }
49
50    pub fn new(
51        root: &Ref<Panel>,
52        owned: Range<usize>,
53        uri: String,
54        baseline: String,
55        configuration: RuntimeXamlLoaderConfiguration,
56    ) -> Result<Self, ReloadError> {
57        let children = root.children().to_vec();
58        let range = children
59            .get(owned.clone())
60            .ok_or_else(|| ReloadError::Failed("Invalid owned panel child range".into()))?;
61        if root.is_items_host() {
62            return Err(ReloadError::RestartRequired(
63                "Items-host panels require an items adapter".into(),
64            ));
65        }
66        Ok(Self {
67            root: root.downgrade(),
68            uri: crate::document_key(&uri),
69            dependencies: Vec::new(),
70            baseline: Rc::new(RefCell::new(baseline)),
71            owned: Rc::new(RefCell::new(Owned {
72                children: range.iter().map(Ref::downgrade).collect(),
73                index: owned.start,
74            })),
75            configuration,
76            refresh: None,
77            properties: Vec::new(),
78            resources: None,
79            styles: None,
80            handlers: None,
81            state_adapters: Vec::new(),
82            restoration: Default::default(),
83        })
84    }
85    pub fn with_refresh(mut self, refresh: Rc<dyn ComponentRefresh>) -> Self {
86        self.refresh = Some(refresh);
87        self
88    }
89    /// Claims existing XAML Resources contributions; see ComponentReloadTarget.
90    pub fn with_owned_resources(mut self) -> Self {
91        self.resources = self.root.upgrade().map(|root| crate::root_resources::OwnedResources::capture(&root.upcast()));
92        self
93    }
94    /// Claims inline styles and Styles.Resources after initial XAML population.
95    pub fn with_owned_styles(mut self) -> Self {
96        self.styles = self.root.upgrade().map(|root| crate::root_styles::OwnedStyles::capture(&root.upcast()));
97        self
98    }
99    /// Claims the document's routed XAML handlers. Call after population and
100    /// before adding application children; the owned range must cover the document.
101    pub fn with_owned_handlers(mut self) -> Result<Self, ReloadError> {
102        let root = self.root.upgrade().ok_or_else(|| ReloadError::Failed("Panel closed".into()))?;
103        let owned = self.owned.borrow();
104        if owned.index != 0 || owned.children.len() != root.children().count() {
105            return Err(ReloadError::RestartRequired("Handler ownership requires the complete XAML child range".into()));
106        }
107        self.handlers = Some(crate::handlers::OwnedHandlers::capture(&root.upcast()));
108        drop(owned);
109        Ok(self)
110    }
111    /// Add application state transfer for uniquely identified controls in the
112    /// panel's namescope. Application children must use separate scopes.
113    pub fn with_state_adapter(mut self, adapter: Rc<dyn crate::ControlStateAdapter>) -> Self {
114        self.state_adapters.push(adapter);
115        self
116    }
117    /// Claims an initially populated literal root attribute for reversible reload.
118    /// See ComponentReloadTarget::with_owned_root_property for the ownership contract.
119    pub fn with_owned_root_property<T: ferroui_base::PropertyValue>(
120        mut self,
121        property: &'static ferroui_base::StyledProperty<T>,
122    ) -> Self {
123        if let Some(root) = self.root.upgrade() {
124            if !self
125                .properties
126                .iter()
127                .any(|owned| owned.name() == property.name())
128            {
129                self.properties
130                    .push(crate::root_properties::own(&root, property));
131            }
132        }
133        self
134    }
135    /// Claims a root literal or StaticResource assignment, including Theme/Template.
136    /// Re-resolves the candidate value on dependency revisions even when the markup
137    /// attribute is unchanged. Call immediately after initial population. Dynamic
138    /// resources, bindings and application overrides require another adapter or restart.
139    /// Property callbacks must support reversible assignment.
140    pub fn with_owned_root_static_resource_property<T: ferroui_base::PropertyValue>(
141        mut self,
142        property: &'static ferroui_base::StyledProperty<T>,
143    ) -> Self {
144        if let Some(root) = self.root.upgrade() {
145            self.properties
146                .retain(|owned| owned.name() != property.name());
147            self.properties
148                .push(crate::root_properties::own_static_resource(&root, property));
149        }
150        self
151    }
152    pub fn is_alive(&self) -> bool {
153        self.root.upgrade().is_some()
154    }
155
156    /// Claims a literal or synchronous root binding with the same ownership and
157    /// context contract as ComponentReloadTarget::with_owned_root_binding_property.
158    pub fn with_owned_root_binding_property<T: ferroui_base::PropertyValue>(
159        mut self,
160        property: &'static ferroui_base::StyledProperty<T>,
161    ) -> Self {
162        if let Some(root) = self.root.upgrade() {
163            self.properties.retain(|owned| owned.name() != property.name());
164            self.properties.push(crate::root_properties::own_binding(&root, property));
165        }
166        self
167    }
168}
169struct Change {
170    restoration: crate::control_state::StateRestoration,
171    root: Ref<Panel>,
172    original: Vec<Ref<Control>>,
173    old: Vec<Ref<Control>>,
174    new: Vec<Ref<Control>>,
175    index: usize,
176    old_scope: Option<NameScopeRef>,
177    new_scope: Option<NameScopeRef>,
178    baseline: Rc<RefCell<String>>,
179    owned: Rc<RefCell<Owned>>,
180    before: String,
181    after: String,
182    state: Rc<crate::state::ViewState>,
183    refresh: Option<Box<dyn PreparedReload>>,
184    properties: Vec<Box<dyn PreparedReload>>,
185    handlers: Option<Box<dyn PreparedReload>>,
186    started: bool,
187    detached_contexts: Vec<crate::detached_context::DetachedContext>,
188}
189impl PreparedReload for Change {
190    fn commit(&mut self) -> Result<(), ReloadError> {
191        if self.root.children().to_vec() != self.original {
192            return Err(ReloadError::RestartRequired(
193                "Panel children changed after preparation".into(),
194            ));
195        }
196        if let Some(handlers) = &mut self.handlers { handlers.commit()?; }
197        for property in &mut self.properties {
198            property.commit()?;
199        }
200        self.detached_contexts = self
201            .old
202            .iter()
203            .filter_map(|child| {
204                crate::detached_context::DetachedContext::freeze(&Some(Control::boxed(child)))
205            })
206            .collect();
207        self.started = true;
208        for child in &self.old {
209            self.root.children().remove(child.clone());
210        }
211        self.root
212            .children()
213            .insert_range(self.index, self.new.clone());
214        NameScope::set_name_scope(&self.root, self.new_scope.clone());
215        if let Some(refresh) = &mut self.refresh {
216            refresh.commit()?;
217        }
218        Ok(())
219    }
220    fn rollback(&mut self) {
221        for property in self.properties.iter_mut().rev() {
222            property.rollback();
223        }
224        if !self.started {
225            if let Some(handlers) = &mut self.handlers { handlers.rollback(); }
226            return;
227        }
228        let _retired: Vec<_> = self
229            .new
230            .iter()
231            .filter_map(|child| {
232                crate::detached_context::DetachedContext::freeze(&Some(Control::boxed(child)))
233            })
234            .collect();
235        // Also handles a partially completed removal/insertion before a setter panics.
236        for child in self.new.iter().chain(&self.old) {
237            self.root.children().remove(child.clone());
238        }
239        self.root
240            .children()
241            .insert_range(self.index, self.old.clone());
242        for context in self.detached_contexts.drain(..) {
243            context.restore();
244        }
245        NameScope::set_name_scope(&self.root, self.old_scope.clone());
246        if let Some(refresh) = &mut self.refresh {
247            refresh.rollback();
248        }
249        if let Some(handlers) = &mut self.handlers { handlers.rollback(); }
250    }
251    fn finish(&mut self) {
252        for property in &mut self.properties {
253            property.finish();
254        }
255        *self.baseline.borrow_mut() = self.after.clone();
256        *self.owned.borrow_mut() = Owned {
257            children: self.new.iter().map(Ref::downgrade).collect(),
258            index: self.index,
259        };
260        if let Some(refresh) = &mut self.refresh {
261            refresh.finish();
262        }
263        if let Some(handlers) = &mut self.handlers { handlers.finish(); }
264        let root = self.root.downgrade();
265        let scope = self.new_scope.clone();
266        let state = self.state.clone();
267        let before = self.before.clone();
268        let after = self.after.clone();
269        let children: Vec<_> = self.new.iter().map(Ref::downgrade).collect();
270        let index = self.index;
271        self.restoration.schedule(
272            move || {
273                if let Some(root) = root.upgrade() {
274                    let current = || {
275                        NameScope::get_name_scope(&root) == scope
276                            && root.children().to_vec().get(index..index + children.len())
277                                .is_some_and(|current| current.iter().zip(&children).all(|(actual, expected)| {
278                                    expected.upgrade().is_some_and(|expected| expected.ptr_eq(actual))
279                                }))
280                    };
281                    if current() {
282                        root.update_layout();
283                    }
284                    if current() {
285                        let value: BoxedValue = Rc::new(root);
286                        state.restore(&value, &before, &after);
287                    }
288                }
289            },
290        );
291    }
292}
293impl ReloadTarget for PanelReloadTarget {
294    fn complete_pending_state(&self) {
295        self.restoration.complete();
296    }
297    fn documents(&self) -> Vec<String> {
298        let mut documents = self.dependencies.clone();
299        documents.push(self.uri.clone());
300        documents
301    }
302    fn prepare(&self, sources: Rc<Sources>) -> Result<Box<dyn PreparedReload>, ReloadError> {
303        let root = self
304            .root
305            .upgrade()
306            .ok_or_else(|| ReloadError::Failed("Panel closed".into()))?;
307        if root.is_items_host() {
308            return Err(ReloadError::RestartRequired(
309                "Items-host panels require an items adapter".into(),
310            ));
311        }
312        let original = root.children().to_vec();
313        let owned = self.owned.borrow();
314        let old = owned
315            .children
316            .iter()
317            .map(|child| {
318                child.upgrade().ok_or_else(|| {
319                    ReloadError::RestartRequired("Owned panel child was removed".into())
320                })
321            })
322            .collect::<Result<Vec<_>, _>>()?;
323        let index = old
324            .first()
325            .and_then(|first| original.iter().position(|child| child == first))
326            .unwrap_or(owned.index);
327        if original.get(index..index + old.len()) != Some(old.as_slice()) {
328            return Err(ReloadError::RestartRequired(
329                "Owned panel children are no longer contiguous".into(),
330            ));
331        }
332        let source = sources
333            .get(&self.uri)
334            .ok_or_else(|| ReloadError::Failed("Missing panel source".into()))?;
335        let before = self.baseline.borrow().clone();
336        let prepared = crate::component::shadow_source(
337            source,
338            &before,
339            &[
340                "Panel",
341                "StackPanel",
342                "WrapPanel",
343                "DockPanel",
344                "Canvas",
345                "Grid",
346                "UniformGrid",
347            ],
348            "Children",
349            &self
350                .properties
351                .iter()
352                .map(|property| property.name())
353                .collect::<Vec<_>>(),
354            match (self.resources.is_some(), self.styles.is_some()) {
355                (true, true) => &["Resources", "Styles"],
356                (true, false) => &["Resources"],
357                (false, true) => &["Styles"],
358                _ => &[],
359            },
360            self.handlers.is_some(),
361        )?;
362        let uri = ferroui_base::utilities::Uri::try_create(
363            &self.uri,
364            ferroui_base::utilities::UriKind::Absolute,
365        )
366        .ok_or_else(|| ReloadError::Failed("Invalid panel URI".into()))?;
367        let bindings = ferroui_markup_xaml_loader::binding_capture::BindingCaptureScope::new(
368            self.uri.clone(),
369            self.properties.iter().filter_map(|property| property.binding_property()),
370        );
371        let mut document = RuntimeXamlLoaderDocument::with_base_uri(Some(uri), &prepared);
372        document.service_provider = Some(crate::root_resources::services(&root.clone().upcast(), self.resources.as_ref(), self.styles.as_ref()));
373        let candidate_root = (self.styles.is_some() || self.resources.is_some() || self.handlers.is_some()).then(|| crate::root_preview::create(&prepared)).transpose()?;
374        let _preview = candidate_root.as_ref().map(|candidate| crate::root_preview::install(
375            &root.clone().upcast(), candidate, self.resources.as_ref(), self.styles.as_ref(),
376        ));
377        let code = self.handlers.as_ref().map(|_| ferroui_markup_xaml::reload_events::ReloadCodeBehind::new(
378            &root.clone().upcast::<ferroui_base::StyledElement>().upcast(), &candidate_root.as_ref().unwrap().clone().upcast(),
379        ));
380        if let Some(code) = &code { document.service_provider = Some(code.services(document.service_provider.take().unwrap())); }
381        document.root_instance = candidate_root.map(|candidate| Rc::new(candidate) as BoxedValue);
382        let deferred = ferroui_markup_xaml::xaml_il::runtime::DeferredContentReloadScope::new();
383        let dynamic = ferroui_markup_xaml::reload_resources::DynamicResourceReloadScope::new();
384        let candidate = ferroui_markup_xaml_loader::FerroRuntimeXamlLoader::load_document(
385            document,
386            Some(self.configuration.clone()),
387        )
388        .map_err(|error| ReloadError::Failed(error.to_string()))?;
389        let candidate = Ref::<Panel>::from_xaml_object(&candidate)
390            .ok_or_else(|| ReloadError::RestartRequired("Document is not a panel".into()))?;
391        if self.resources.is_some() || self.styles.is_some() {
392            ferroui_markup_xaml::reload_validation::prepare_candidate(&candidate)
393                .map_err(|error| ReloadError::Failed(error.to_string()))?;
394            crate::root_styles::prepare_styling(&candidate.clone().upcast(), &root.clone().upcast())?;
395        }
396        let mut properties = Vec::new();
397        for property in &self.properties {
398            if let Some(change) = property.prepare(
399                &root.clone().upcast(),
400                &candidate.clone().upcast(),
401                &before,
402                source,
403                &bindings,
404            )? {
405                properties.push(change);
406            }
407        }
408        properties.insert(0, crate::deferred_context::prepare(
409            &deferred, &candidate.clone().upcast(), &root.clone().upcast(), self.resources.is_some(), self.styles.is_some(),
410        ));
411        let new = candidate.children().to_vec();
412        let new_scope = NameScope::get_name_scope(&candidate);
413        candidate.children().clear();
414        NameScope::set_name_scope(&candidate, None);
415        if let Some(resources) = &self.resources {
416            properties.insert(1, resources.prepare(&candidate.clone().upcast())?);
417        }
418        let mut dynamic_index = if self.resources.is_some() { 2 } else { 1 };
419        if let Some(styles) = &self.styles {
420            properties.insert(dynamic_index, styles.prepare(&candidate.clone().upcast())?);
421            dynamic_index += 1;
422        }
423        properties.insert(dynamic_index, crate::deferred_context::prepare_dynamic(
424            &dynamic, &candidate.clone().upcast(), &root.clone().upcast(),
425        ));
426        let handlers = self.handlers.as_ref().zip(code.as_ref()).map(|(owner, code)| owner.prepare(code)).transpose()?;
427        let refresh = self
428            .refresh
429            .as_ref()
430            .map(|refresh| refresh.prepare(&root.clone().upcast(), new_scope.as_ref()))
431            .transpose()?;
432        let value: BoxedValue = Rc::new(root.clone());
433        Ok(Box::new(Change {
434            restoration: self.restoration.clone(),
435            state: Rc::new(crate::state::ViewState::capture_document(
436                Some(&value), Some(&self.uri), &before, &self.state_adapters,
437            )),
438            old_scope: NameScope::get_name_scope(&root),
439            root,
440            original,
441            old,
442            new,
443            index,
444            new_scope,
445            baseline: self.baseline.clone(),
446            owned: self.owned.clone(),
447            before,
448            after: source.clone(),
449            refresh,
450            properties,
451            handlers,
452            started: false,
453            detached_contexts: Vec::new(),
454        }))
455    }
456}