use fresh_core::api::{TreeNode, WidgetSpec};
use fresh_core::text_property::TextPropertyEntry;
pub fn find_widget_by_key<'a>(spec: &'a WidgetSpec, target: &str) -> Option<&'a WidgetSpec> {
if target.is_empty() {
return None;
}
if leaf_key_matches(spec, target) {
return Some(spec);
}
spec.children().find_map(|c| find_widget_by_key(c, target))
}
fn leaf_key_matches(spec: &WidgetSpec, target: &str) -> bool {
match spec {
WidgetSpec::Toggle { key: Some(k), .. }
| WidgetSpec::Button { key: Some(k), .. }
| WidgetSpec::Text { key: Some(k), .. }
| WidgetSpec::List { key: Some(k), .. }
| WidgetSpec::Tree { key: Some(k), .. } => k == target,
_ => false,
}
}
pub fn set_toggle_checked_in_spec(
spec: &mut WidgetSpec,
widget_key: &str,
new_checked: bool,
) -> bool {
if widget_key.is_empty() {
return false;
}
if let WidgetSpec::Toggle { checked, key, .. } = spec {
if key.as_deref() == Some(widget_key) {
*checked = new_checked;
return true;
}
}
spec.children_mut()
.any(|c| set_toggle_checked_in_spec(c, widget_key, new_checked))
}
pub fn set_list_items_in_spec(
spec: &mut WidgetSpec,
widget_key: &str,
new_items: Vec<TextPropertyEntry>,
new_item_keys: Vec<String>,
) -> bool {
if widget_key.is_empty() {
return false;
}
if let WidgetSpec::List {
items,
item_keys,
key,
..
} = spec
{
if key.as_deref() == Some(widget_key) {
*items = new_items;
*item_keys = new_item_keys;
return true;
}
}
for c in spec.children_mut() {
if c.contains_key(widget_key) {
return set_list_items_in_spec(c, widget_key, new_items, new_item_keys);
}
}
false
}
pub fn set_tree_nodes_in_spec(
spec: &mut WidgetSpec,
widget_key: &str,
new_nodes: Vec<TreeNode>,
new_item_keys: Vec<String>,
) -> bool {
if widget_key.is_empty() {
return false;
}
if let WidgetSpec::Tree {
nodes,
item_keys,
key,
..
} = spec
{
if key.as_deref() == Some(widget_key) {
*nodes = new_nodes;
*item_keys = new_item_keys;
return true;
}
}
for c in spec.children_mut() {
if c.contains_key(widget_key) {
return set_tree_nodes_in_spec(c, widget_key, new_nodes, new_item_keys);
}
}
false
}
pub fn append_tree_nodes_in_spec(
spec: &mut WidgetSpec,
widget_key: &str,
new_nodes: Vec<TreeNode>,
new_item_keys: Vec<String>,
) -> bool {
if widget_key.is_empty() {
return false;
}
if let WidgetSpec::Tree {
nodes,
item_keys,
key,
..
} = spec
{
if key.as_deref() == Some(widget_key) {
nodes.extend(new_nodes);
item_keys.extend(new_item_keys);
return true;
}
}
for c in spec.children_mut() {
if c.contains_key(widget_key) {
return append_tree_nodes_in_spec(c, widget_key, new_nodes, new_item_keys);
}
}
false
}
pub fn set_raw_entries_in_spec(
spec: &mut WidgetSpec,
widget_key: &str,
new_entries: Vec<TextPropertyEntry>,
) -> bool {
if widget_key.is_empty() {
return false;
}
if let WidgetSpec::Raw { entries, key } = spec {
if key.as_deref() == Some(widget_key) {
*entries = new_entries;
return true;
}
}
for c in spec.children_mut() {
if c.contains_key(widget_key) {
return set_raw_entries_in_spec(c, widget_key, new_entries);
}
}
false
}
pub fn set_tree_checked_keys_in_spec(
spec: &mut WidgetSpec,
widget_key: &str,
checked: bool,
keys: &[String],
) -> bool {
if widget_key.is_empty() {
return false;
}
if let WidgetSpec::Tree {
nodes,
item_keys,
key,
..
} = spec
{
if key.as_deref() == Some(widget_key) {
let target: std::collections::HashSet<&str> = keys.iter().map(String::as_str).collect();
for (i, node) in nodes.iter_mut().enumerate() {
if node.checked.is_none() {
continue;
}
let item_key = item_keys.get(i).map(String::as_str).unwrap_or("");
if !item_key.is_empty() && target.contains(item_key) {
node.checked = Some(checked);
}
}
return true;
}
}
spec.children_mut()
.any(|c| set_tree_checked_keys_in_spec(c, widget_key, checked, keys))
}
pub fn tree_parent_index(nodes: &[TreeNode], child_idx: usize) -> Option<usize> {
let child = nodes.get(child_idx)?;
if child.depth == 0 {
return None;
}
let target_depth = child.depth - 1;
nodes[..child_idx]
.iter()
.enumerate()
.rev()
.find(|(_, n)| n.depth == target_depth)
.map(|(i, _)| i)
}
trait ContainsKey {
fn contains_key(&self, widget_key: &str) -> bool;
}
impl ContainsKey for WidgetSpec {
fn contains_key(&self, widget_key: &str) -> bool {
let direct = match self {
WidgetSpec::Toggle { key, .. }
| WidgetSpec::Button { key, .. }
| WidgetSpec::Text { key, .. }
| WidgetSpec::List { key, .. }
| WidgetSpec::Tree { key, .. }
| WidgetSpec::Raw { key, .. } => key.as_deref() == Some(widget_key),
_ => false,
};
direct || self.children().any(|c| c.contains_key(widget_key))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn toggle_with_key(k: &str) -> WidgetSpec {
WidgetSpec::Toggle {
checked: false,
label: "T".into(),
focused: false,
key: Some(k.into()),
}
}
#[test]
fn find_widget_by_key_finds_top_level_match() {
let spec = toggle_with_key("a");
assert!(find_widget_by_key(&spec, "a").is_some());
assert!(find_widget_by_key(&spec, "b").is_none());
}
#[test]
fn find_widget_by_key_recurses_into_row() {
let spec = WidgetSpec::Row {
children: vec![toggle_with_key("a"), toggle_with_key("b")],
key: None,
};
assert!(find_widget_by_key(&spec, "b").is_some());
}
#[test]
fn find_widget_by_key_returns_none_for_empty_target() {
let spec = toggle_with_key("a");
assert!(find_widget_by_key(&spec, "").is_none());
}
fn node(text: &str, depth: u32, has_children: bool) -> TreeNode {
TreeNode {
text: TextPropertyEntry::text(text),
depth,
has_children,
checked: None,
}
}
#[test]
fn tree_parent_index_top_level_returns_none() {
let nodes = vec![node("root", 0, true)];
assert!(tree_parent_index(&nodes, 0).is_none());
}
#[test]
fn tree_parent_index_finds_immediate_parent() {
let nodes = vec![
node("root", 0, true),
node("child", 1, false),
node("child2", 1, false),
];
assert_eq!(tree_parent_index(&nodes, 1), Some(0));
assert_eq!(tree_parent_index(&nodes, 2), Some(0));
}
#[test]
fn tree_parent_index_skips_intermediate_siblings() {
let nodes = vec![
node("root", 0, true),
node("child", 1, true),
node("grand", 2, false),
];
assert_eq!(tree_parent_index(&nodes, 2), Some(1));
}
#[test]
fn tree_parent_index_finds_parent_across_unrelated_subtree() {
let nodes = vec![
node("a", 0, true),
node("a.0", 1, false),
node("b", 0, true),
node("b.0", 1, false),
];
assert_eq!(tree_parent_index(&nodes, 3), Some(2));
}
#[test]
fn set_tree_nodes_in_spec_replaces_nodes() {
let mut spec = WidgetSpec::Tree {
nodes: vec![node("old", 0, false)],
item_keys: vec!["k0".into()],
selected_index: -1,
visible_rows: 5,
expanded_keys: vec![],
checkable: false,
key: Some("t".into()),
};
let new_nodes = vec![node("new1", 0, false), node("new2", 0, false)];
let new_keys = vec!["a".to_string(), "b".to_string()];
let ok = set_tree_nodes_in_spec(&mut spec, "t", new_nodes.clone(), new_keys.clone());
assert!(ok);
match &spec {
WidgetSpec::Tree {
nodes, item_keys, ..
} => {
assert_eq!(nodes.len(), 2);
assert_eq!(item_keys, &new_keys);
}
_ => unreachable!(),
}
}
#[test]
fn set_tree_checked_keys_in_spec_flips_only_named_keys() {
let mut a = node("a", 0, false);
a.checked = Some(true);
let mut b = node("b", 0, false);
b.checked = Some(true);
let mut c = node("c", 0, false);
c.checked = Some(true);
let mut spec = WidgetSpec::Tree {
nodes: vec![a, b, c],
item_keys: vec!["k_a".into(), "k_b".into(), "k_c".into()],
selected_index: -1,
visible_rows: 5,
expanded_keys: vec![],
checkable: true,
key: Some("t".into()),
};
let ok = set_tree_checked_keys_in_spec(
&mut spec,
"t",
false,
&["k_a".to_string(), "k_c".to_string()],
);
assert!(ok);
match &spec {
WidgetSpec::Tree { nodes, .. } => {
assert_eq!(nodes[0].checked, Some(false));
assert_eq!(nodes[1].checked, Some(true), "untouched");
assert_eq!(nodes[2].checked, Some(false));
}
_ => unreachable!(),
}
}
#[test]
fn set_tree_checked_keys_in_spec_skips_nodes_without_checkbox() {
let n_with = {
let mut n = node("checked", 0, false);
n.checked = Some(true);
n
};
let n_without = node("no-checkbox", 0, false); let mut spec = WidgetSpec::Tree {
nodes: vec![n_with, n_without],
item_keys: vec!["k0".into(), "k1".into()],
selected_index: -1,
visible_rows: 5,
expanded_keys: vec![],
checkable: true,
key: Some("t".into()),
};
let _ok = set_tree_checked_keys_in_spec(
&mut spec,
"t",
false,
&["k0".to_string(), "k1".to_string()],
);
match &spec {
WidgetSpec::Tree { nodes, .. } => {
assert_eq!(nodes[0].checked, Some(false));
assert_eq!(nodes[1].checked, None);
}
_ => unreachable!(),
}
}
#[test]
fn set_tree_nodes_in_spec_returns_false_for_unknown_key() {
let mut spec = WidgetSpec::Tree {
nodes: vec![node("a", 0, false)],
item_keys: vec!["k".into()],
selected_index: -1,
visible_rows: 5,
expanded_keys: vec![],
checkable: false,
key: Some("real".into()),
};
assert!(!set_tree_nodes_in_spec(&mut spec, "wrong", vec![], vec![]));
}
}