Skip to main content

banished_ui/
tree.rs

1//! Finding nodes in the tree, changing their text, icons and visibility, and building nodes in code
2//! (groups, ribbons, text, lists, sliders).
3
4use super::*;
5
6impl Ui {
7    /// Depth-first search for a node by element name below `root`.
8    pub fn find(&self, root: NodeId, name: &str) -> Option<NodeId> {
9        if self.nodes[root].name == name {
10            return Some(root);
11        }
12        self.child_ids(root)
13            .into_iter()
14            .find_map(|c| self.find(c, name))
15    }
16
17    pub(super) fn child_ids(&self, id: NodeId) -> Vec<NodeId> {
18        let n = &self.nodes[id];
19        let mut out = n.children.clone();
20        out.extend(n.border);
21        if let Kind::Control(c) = &n.kind {
22            out.extend(c.children());
23        }
24        if let Kind::Button {
25            states, content, ..
26        } = &n.kind
27        {
28            out.extend(states.iter().flatten());
29            out.extend(content.iter());
30        }
31        out
32    }
33
34    /// Return every node rendered or owned directly below this node, including
35    /// control and button content that is stored outside `Node::children`.
36    pub fn children(&self, id: NodeId) -> Vec<NodeId> {
37        self.child_ids(id)
38    }
39
40    /// Replace the text of a Text/Label node, or of a Button's content.
41    pub fn set_text(&mut self, id: NodeId, text: &str) {
42        let target = match &self.nodes[id].kind {
43            Kind::Button {
44                content: Some(c), ..
45            } => *c,
46            _ => id,
47        };
48        match &mut self.nodes[target].kind {
49            Kind::Text(t) | Kind::Label { text: t, .. } => t.text = text.to_string(),
50            Kind::Control(Control::Edit(e)) => {
51                e.text.text = text.to_string();
52                // Display-only number elements (`NumberElement`, no max length) never scroll.
53                e.cursor = if e.max_length > 0 {
54                    text.chars().count()
55                } else {
56                    0
57                };
58            }
59            Kind::Control(Control::Document(d)) => d.text = text.to_string(),
60            Kind::Control(Control::Combo { button, .. } | Control::Check { button, .. }) => {
61                let b = *button;
62                self.set_text(b, text);
63            }
64            _ => log::warn!("set_text on non-text node {}", self.nodes[target].name),
65        }
66    }
67
68    /// `LabelElement::SetSprite` on a label, or on a combo's label (`ComboElement::UpdateText`
69    /// sets the chosen item's sprite when it has one).
70    pub fn set_icon(&mut self, id: NodeId, sprite: SpriteRef) {
71        let target = match &self.nodes[id].kind {
72            Kind::Control(Control::Combo { button, .. }) => *button,
73            _ => id,
74        };
75        let mut stack = vec![target];
76        while let Some(n) = stack.pop() {
77            if let Kind::Label { icon, .. } = &mut self.nodes[n].kind {
78                *icon = Some(sprite);
79                return;
80            }
81            stack.extend(self.nodes[n].children.iter().rev());
82        }
83    }
84
85    /// A combo's item `index`: the list's selection, the label's text and the item's sprite.
86    pub fn select_combo_item(&mut self, combo: NodeId, index: usize) {
87        let Kind::Control(Control::Combo { list, .. }) = self.nodes[combo].kind else {
88            return;
89        };
90        let (text, icon) = match &self.nodes[list].kind {
91            Kind::List(l) => (
92                l.items.get(index).cloned(),
93                l.icons.get(index).copied().flatten(),
94            ),
95            _ => (None, None),
96        };
97        let Some(text) = text else { return };
98        self.list_select(list, Some(index));
99        self.set_text(combo, &text);
100        if let Some(icon) = icon {
101            self.set_icon(combo, icon);
102        }
103    }
104
105    /// Show or hide an element.
106    pub fn set_visible(&mut self, id: NodeId, visible: bool) {
107        let f = &mut self.nodes[id].flags;
108        *f = if visible {
109            *f & !flags::HIDDEN
110        } else {
111            *f | flags::HIDDEN
112        };
113    }
114
115    /// Add an empty group (built in code, not from a resource).
116    pub fn group(&mut self) -> NodeId {
117        self.add(Node::new(Kind::Group))
118    }
119
120    /// Add an empty ribbon: a row, or a column when `vertical`.
121    pub fn ribbon(&mut self, vertical: bool, spacing: i32) -> NodeId {
122        self.add(Node::new(Kind::Ribbon {
123            vertical,
124            uniform: false,
125            spacing,
126            cells: Vec::new(),
127        }))
128    }
129
130    /// Add a text element with a font reference and a text.
131    pub fn text(&mut self, font: &str, text: &str) -> NodeId {
132        let font = self.res.font(font);
133        let data = TextData {
134            font,
135            text: text.to_string(),
136            max_width: 0,
137            justify: 0,
138            wrap: false,
139            shadow: false,
140            ellipsis_width: 0,
141            layout: TextLayout::default(),
142        };
143        self.add(Node::new(Kind::Text(data)))
144    }
145
146    /// Add a viewport: space that the application draws into.
147    pub fn viewport(&mut self) -> NodeId {
148        self.add(Node::new(Kind::Viewport))
149    }
150
151    /// Add a picture: an application texture, drawn letterboxed.
152    pub fn picture(&mut self) -> NodeId {
153        self.add(Node::new(Kind::Picture {
154            texture: 0,
155            size: [0, 0],
156        }))
157    }
158
159    /// A scrolling list using the game font and the `White` dialog sprite for highlights.
160    pub fn list(&mut self, font: &str) -> NodeId {
161        let font_id = self.res.font(font);
162        let row_height = font_id.map_or(16, |f| self.res.fonts[f].font.size + 4);
163        let highlight = self
164            .res
165            .sprite(Some(r"Dialog\SpriteSheet.rsc:resource"), "White");
166        self.add(Node::new(Kind::List(ListData {
167            items: Vec::new(),
168            icons: Vec::new(),
169            selected: None,
170            hovered: None,
171            scroll: 0,
172            font: font_id,
173            row_height,
174            highlight,
175            rows: Vec::new(),
176            reveal: false,
177        })))
178    }
179
180    /// Create a simple horizontal slider with a built-in solid track and knob.
181    pub fn slider(&mut self, range: [f32; 2]) -> NodeId {
182        self.add(Node::new(Kind::Control(Control::Slider(SliderData {
183            sprites: vec![None; 5],
184            range,
185            value: range[0],
186            steps: 0,
187            inset: 8,
188            track_y: 0,
189            number: None,
190            over_knob: false,
191            drag: None,
192        }))))
193    }
194
195    /// Set the rows of a list.
196    pub fn set_list_items(&mut self, id: NodeId, items: Vec<String>) {
197        self.set_list_icons(id, items, Vec::new());
198    }
199
200    /// [`Self::set_list_items`] with each item's sprite.
201    pub fn set_list_icons(
202        &mut self,
203        id: NodeId,
204        items: Vec<String>,
205        icons: Vec<Option<SpriteRef>>,
206    ) {
207        if let Kind::List(l) = &mut self.nodes[id].kind {
208            l.icons = icons;
209            l.items = items;
210            l.selected = None;
211            l.hovered = None;
212            l.scroll = 0;
213            l.rows.clear();
214        }
215    }
216
217    /// Select a row of a list, or none.
218    pub fn list_select(&mut self, id: NodeId, index: Option<usize>) {
219        if let Kind::List(l) = &mut self.nodes[id].kind {
220            l.selected = index.filter(|&i| i < l.items.len());
221            l.reveal = true;
222        }
223    }
224
225    pub(super) fn reveal_selection(&mut self, id: NodeId) {
226        let h = self.nodes[id].rect[3] - self.nodes[id].rect[1];
227        if let Kind::List(l) = &mut self.nodes[id].kind {
228            if let (true, Some(i)) = (l.reveal && h > 0, l.selected) {
229                let top = i as i32 * l.row_height;
230                if top < l.scroll {
231                    l.scroll = top;
232                } else if top + l.row_height > l.scroll + h {
233                    l.scroll = top + l.row_height - h;
234                }
235                l.reveal = false;
236            }
237        }
238    }
239}