Skip to main content

apace_dom/
radio.rs

1//! Live radio association. No registry of retired controls or group history.
2use crate::{BaseDocument, local_name, node::SpecialElementData, traversal::TreeTraverser};
3use apace_traits::node_id::NodeId;
4use style_dom::ElementState;
5
6impl BaseDocument {
7    fn check_kind(&self, id: NodeId) -> Option<&str> {
8        let element = self.get_node(id)?.element_data()?;
9        if element.name.local != local_name!("input") {
10            return None;
11        }
12        element
13            .attr(local_name!("type"))
14            .filter(|kind| matches!(*kind, "checkbox" | "radio"))
15    }
16    fn radio_root(&self, mut id: NodeId) -> Option<NodeId> {
17        while let Some(parent) = self.get_node(id)?.parent {
18            id = parent;
19        }
20        Some(id)
21    }
22    fn radio_form(&self, id: NodeId, root: NodeId) -> Option<NodeId> {
23        let element = self.get_node(id)?.element_data()?;
24        if let Some(name) = element.attr(local_name!("form")) {
25            // An explicit unresolved form does not fall back to an ancestor.
26            return TreeTraverser::new_with_root(self, root).find(|candidate| {
27                self.get_node(*candidate)
28                    .and_then(|n| n.element_data())
29                    .is_some_and(|e| {
30                        e.name.local == local_name!("form")
31                            && e.attr(local_name!("id")) == Some(name)
32                    })
33            });
34        }
35        let mut cursor = self.get_node(id)?.parent;
36        while let Some(id) = cursor {
37            let node = self.get_node(id)?;
38            if node.data.is_element_with_tag_name(&local_name!("form")) {
39                return Some(id);
40            }
41            cursor = node.parent;
42        }
43        None
44    }
45    /// Members in current physical tree order, including disabled members. An
46    /// empty/missing name is a singleton; tree and form owners isolate groups.
47    /// Only the live host tree is traversed, never an application's data source.
48    pub fn radio_group(&self, id: NodeId) -> Vec<NodeId> {
49        if self.check_kind(id) != Some("radio") {
50            return Vec::new();
51        }
52        let Some(name) = self
53            .get_node(id)
54            .and_then(|n| n.attr(local_name!("name")))
55            .filter(|s| !s.is_empty())
56        else {
57            return vec![id];
58        };
59        let root = self.radio_root(id).unwrap();
60        let form = self.radio_form(id, root);
61        TreeTraverser::new_with_root(self, root)
62            .filter(|peer| {
63                self.check_kind(*peer) == Some("radio")
64                    && self
65                        .get_node(*peer)
66                        .and_then(|n| n.attr(local_name!("name")))
67                        == Some(name)
68                    && self.radio_form(*peer, root) == form
69            })
70            .collect()
71    }
72    fn live_checked(&self, id: NodeId) -> bool {
73        let element = self.get_node(id).unwrap().element_data().unwrap();
74        element
75            .checkbox_input_checked()
76            .unwrap_or_else(|| element.element_state.contains(ElementState::CHECKED))
77    }
78    fn write_checked(&mut self, id: NodeId, checked: bool) -> bool {
79        let before = self.live_checked(id);
80        let initialized = self
81            .get_node(id)
82            .unwrap()
83            .element_data()
84            .unwrap()
85            .checkbox_input_checked()
86            .is_some();
87        if before == checked && initialized {
88            return false;
89        }
90        self.snapshot_node_and(id, ElementState::CHECKED, |node| {
91            let element = node.element_data_mut().unwrap();
92            // Property writes before initial layout must survive construction.
93            if !matches!(element.special_data, SpecialElementData::CheckboxInput(_)) {
94                element.special_data = SpecialElementData::CheckboxInput(checked);
95            }
96            element.set_checkbox_input_checked(checked);
97            node.mark_ancestors_dirty();
98            node.insert_damage(crate::layout::damage::ALL_DAMAGE);
99        });
100        true
101    }
102    /// The live checked property; does not rewrite the default checked attribute.
103    /// Reasserting true on a radio also withdraws its peers without input events.
104    pub fn set_input_checked(&mut self, id: NodeId, checked: bool) -> bool {
105        let Some(kind) = self.check_kind(id) else {
106            return false;
107        };
108        let peers = if kind == "radio" && checked {
109            self.radio_group(id)
110        } else {
111            Vec::new()
112        };
113        let mut changed = self.write_checked(id, checked);
114        for peer in peers {
115            if peer != id && self.live_checked(peer) {
116                changed |= self.write_checked(peer, false);
117            }
118        }
119        if changed {
120            self.shell_provider.request_redraw();
121        }
122        true
123    }
124    /// None is unsupported; false is an already-selected radio. Native input
125    /// events belong only to the newly selected target, never its old peers.
126    pub fn activate_radio(&mut self, id: NodeId) -> Option<bool> {
127        if self.check_kind(id) != Some("radio") {
128            return None;
129        }
130        let changed = !self.live_checked(id);
131        self.set_input_checked(id, true);
132        Some(changed)
133    }
134    pub(crate) fn reconcile_radio(&mut self, id: NodeId) {
135        if self.check_kind(id) == Some("radio") && self.live_checked(id) {
136            self.set_input_checked(id, true);
137        }
138    }
139    pub(crate) fn reconcile_radio_subtrees(&mut self, roots: &[NodeId]) {
140        // Capture initial checkedness before any peer is withdrawn, so bulk
141        // insertion has the same last-checked-member behavior as ordered inserts.
142        let radios: Vec<_> = roots
143            .iter()
144            .flat_map(|root| TreeTraverser::new_with_root(self, *root))
145            .filter(|id| self.check_kind(*id) == Some("radio") && self.live_checked(*id))
146            .collect();
147        for id in radios {
148            self.set_input_checked(id, true);
149        }
150    }
151}