#[cfg(not(alloc_frugal))]
use crate::compat::BTreeSet;
use crate::compat::{String, Vec};
#[cfg(not(alloc_frugal))]
use crate::core::ObjectId;
#[cfg(not(alloc_frugal))]
use crate::widget::runtime::{children_of, read_widget, with_widget_mut};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SavedState {
pub key: String,
pub version: u16,
pub bytes: Vec<u8>,
}
impl SavedState {
pub fn new(key: impl Into<String>, version: u16, bytes: Vec<u8>) -> Self {
Self { key: key.into(), version, bytes }
}
}
pub trait SaveState {
fn state_version(&self) -> u16;
fn save_state(&self) -> Vec<u8>;
fn restore_state(&mut self, state: &[u8]) -> bool;
}
#[cfg(not(alloc_frugal))]
pub fn collect_save_state(
root: ObjectId,
key_of: impl Fn(ObjectId) -> Option<String>,
) -> Vec<SavedState> {
let mut collected = Vec::new();
walk(root, &mut |id| {
let Some(key) = key_of(id) else {
return;
};
if key.is_empty() {
return;
}
if collected.iter().any(|entry: &SavedState| entry.key == key) {
log::warn!(
"save-state key '{key}' is declared by more than one widget; keeping the first"
);
return;
}
let saved = read_widget(id, |widget| {
widget.save_state_dyn().map(|participant| SavedState {
key,
version: participant.state_version(),
bytes: participant.save_state(),
})
})
.flatten();
if let Some(saved) = saved {
collected.push(saved);
}
});
collected.sort_by(|left, right| left.key.cmp(&right.key));
collected
}
#[cfg(not(alloc_frugal))]
pub fn restore_save_state(
root: ObjectId,
states: &[SavedState],
key_of: impl Fn(ObjectId) -> Option<String>,
) -> usize {
let mut applied = 0;
walk(root, &mut |id| {
let Some(key) = key_of(id) else {
return;
};
let Some(entry) = states.iter().find(|entry| entry.key == key) else {
return;
};
let restored = with_widget_mut(id, |widget| {
let Some(participant) = widget.save_state_dyn_mut() else {
return false;
};
if participant.state_version() != entry.version {
return false;
}
participant.restore_state(&entry.bytes)
})
.unwrap_or(false);
if restored {
applied += 1;
}
});
applied
}
#[cfg(not(alloc_frugal))]
fn walk(root: ObjectId, visit: &mut impl FnMut(ObjectId)) {
let mut stack = Vec::new();
stack.push(root);
let mut visited = BTreeSet::new();
while let Some(id) = stack.pop() {
if !visited.insert(id) {
continue;
}
visit(id);
let mut children = children_of(id);
children.extend(crate::widget::runtime::surfaces_of(id));
children.reverse();
stack.extend(children);
}
}
#[cfg(all(test, not(alloc_frugal), widgets_unstripped))]
mod tests {
use super::*;
use crate::core::Rect;
use crate::widget::container_widgets::groupbox::GroupBox;
use crate::widget::container_widgets::splitter::Splitter;
use crate::widget::container_widgets::tabwidget::TabWidget;
use crate::widget::runtime::{register, unregister};
struct Fixture {
root: ObjectId,
splitter: ObjectId,
tabs: ObjectId,
}
impl Fixture {
fn new() -> Self {
let root = register(Box::new(GroupBox::new(Rect::new(0, 0, 400, 300))))
.expect("a test thread has a registry");
let splitter = register(Box::new(Splitter::new(Rect::new(0, 0, 200, 200))))
.expect("a test thread has a registry");
let tabs = register(Box::new(TabWidget::new(Rect::new(0, 0, 200, 200))))
.expect("a test thread has a registry");
with_widget_mut(root, |parent| {
parent.add_child(splitter);
parent.add_child(tabs);
});
with_widget_mut(splitter, |widget| {
let splitter = (widget as &mut dyn core::any::Any)
.downcast_mut::<Splitter>()
.expect("splitter");
splitter.add_pane(0, 1);
splitter.add_pane(0, 1);
assert!(splitter.set_ratios(&[0.3, 0.7]));
});
with_widget_mut(tabs, |widget| {
let tabs =
(widget as &mut dyn core::any::Any).downcast_mut::<TabWidget>().expect("tabs");
tabs.add_tab("One".to_string(), None);
tabs.add_tab("Two".to_string(), None);
});
Self { root, splitter, tabs }
}
fn key_of(&self) -> impl Fn(ObjectId) -> Option<String> {
let splitter = self.splitter;
let tabs = self.tabs;
move |id| {
if id == splitter {
Some("workspace.split".to_string())
} else if id == tabs {
Some("workspace.tabs".to_string())
} else {
None
}
}
}
}
impl Drop for Fixture {
fn drop(&mut self) {
unregister(self.splitter);
unregister(self.tabs);
unregister(self.root);
}
}
fn splitter_ratios(id: ObjectId) -> Vec<f32> {
read_widget(id, |widget| {
(widget as &dyn core::any::Any)
.downcast_ref::<Splitter>()
.map(|splitter| splitter.ratios())
})
.flatten()
.expect("the id addresses a splitter")
}
fn tab_index(id: ObjectId) -> usize {
read_widget(id, |widget| {
(widget as &dyn core::any::Any)
.downcast_ref::<TabWidget>()
.map(|tabs| tabs.selected_index().unwrap_or(0))
})
.flatten()
.expect("the id addresses a tab widget")
}
#[test]
fn collect_follows_a_surface_that_is_not_a_tree_child() {
let root = register(Box::new(GroupBox::new(Rect::new(0, 0, 400, 300))))
.expect("a test thread has a registry");
let tabs = register(Box::new(TabWidget::new(Rect::new(0, 0, 200, 200))))
.expect("a test thread has a registry");
with_widget_mut(tabs, |widget| {
let tabs =
(widget as &mut dyn core::any::Any).downcast_mut::<TabWidget>().expect("tabs");
tabs.add_tab("Only".to_string(), None);
});
assert!(crate::widget::runtime::set_surface_window(tabs, root));
let states =
collect_save_state(root, |id| (id == tabs).then(|| "surface.tabs".to_string()));
assert_eq!(states.len(), 1, "a mounted surface must not be skipped");
assert_eq!(states[0].key, "surface.tabs");
unregister(tabs);
unregister(root);
}
#[test]
fn collect_uses_the_host_keys_and_sorts_them() {
let fixture = Fixture::new();
let states = collect_save_state(fixture.root, fixture.key_of());
assert_eq!(states.len(), 2, "one entry per opted-in widget");
assert_eq!(states[0].key, "workspace.split", "sorted by key");
assert_eq!(states[1].key, "workspace.tabs");
assert!(states.iter().all(|entry| entry.version == 1));
assert!(!states[0].bytes.is_empty());
}
#[test]
fn a_widget_without_a_key_is_not_collected() {
let fixture = Fixture::new();
let states = collect_save_state(fixture.root, |_| None);
assert!(states.is_empty(), "an unnamed instance must not be persisted");
let states = collect_save_state(fixture.root, |_| Some(String::new()));
assert!(states.is_empty(), "an empty key is the same refusal");
}
#[test]
fn restore_applies_matching_keys() {
let fixture = Fixture::new();
let states = collect_save_state(fixture.root, fixture.key_of());
assert_eq!(states.len(), 2);
with_widget_mut(fixture.splitter, |widget| {
let splitter =
(widget as &mut dyn core::any::Any).downcast_mut::<Splitter>().expect("splitter");
assert!(splitter.set_ratios(&[0.9, 0.1]));
});
with_widget_mut(fixture.tabs, |widget| {
let tabs =
(widget as &mut dyn core::any::Any).downcast_mut::<TabWidget>().expect("tabs");
tabs.set_current_index(1);
});
assert_eq!(splitter_ratios(fixture.splitter), vec![0.9, 0.1]);
assert_eq!(tab_index(fixture.tabs), 1);
let applied = restore_save_state(fixture.root, &states, fixture.key_of());
assert_eq!(applied, 2);
assert_eq!(splitter_ratios(fixture.splitter), vec![0.3, 0.7]);
assert_eq!(tab_index(fixture.tabs), 0, "the saved selection comes back");
}
#[test]
fn restore_skips_a_version_mismatch() {
let fixture = Fixture::new();
let mut states = collect_save_state(fixture.root, fixture.key_of());
for entry in &mut states {
entry.version = entry.version.wrapping_add(1);
}
let applied = restore_save_state(fixture.root, &states, fixture.key_of());
assert_eq!(applied, 0, "an unknown version must be refused, not misread");
}
#[test]
fn restore_rejects_a_malformed_payload_without_counting_it() {
let fixture = Fixture::new();
let states = vec![SavedState::new("workspace.split", 1, vec![0xff, 0x00])];
let applied = restore_save_state(fixture.root, &states, fixture.key_of());
assert_eq!(applied, 0);
assert_eq!(splitter_ratios(fixture.splitter), vec![0.3, 0.7], "nothing was applied");
}
}