Skip to main content

banished_ui/
input.rs

1use crate::controls::{intersect, scroll_thumb};
2use crate::*;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5/// A key the interface reacts to.
6pub enum Key {
7    /// Tab: the next control.
8    Tab,
9    /// Shift and Tab: the previous control.
10    BackTab,
11    /// Enter.
12    Enter,
13    /// Escape.
14    Escape,
15    /// The space bar.
16    Space,
17    /// The left arrow.
18    Left,
19    /// The right arrow.
20    Right,
21    /// The up arrow.
22    Up,
23    /// The down arrow.
24    Down,
25    /// Home.
26    Home,
27    /// End.
28    End,
29    /// Page Up.
30    PageUp,
31    /// Page Down.
32    PageDown,
33    /// Backspace.
34    Backspace,
35    /// Delete.
36    Delete,
37    /// A character key.
38    Character(char),
39}
40
41impl Ui {
42    /// The element that has the keyboard focus.
43    pub fn focused(&self) -> Option<NodeId> {
44        self.focus
45    }
46    /// The pointer in UI units, as last moved.
47    pub fn pointer(&self) -> (i32, i32) {
48        self.pointer
49    }
50    /// Whether the current pointer target is a control that consumes pointer input.
51    pub fn pointer_over_interactive(&self) -> bool {
52        self.hot.is_some_and(|id| self.interactive(id))
53    }
54    /// `DialogInstance::GetMouseCursorIndex`: the cursor index of `id`, unless one of its
55    /// visible children holds `(x, y)`, then that child's, the last such child winning.
56    pub fn cursor_index(&self, id: NodeId, x: i32, y: i32) -> i32 {
57        let n = &self.nodes[id];
58        let mut cursor = n.cursor;
59        for &c in &n.children {
60            let child = &self.nodes[c];
61            if child.visible() && child.contains(x, y) {
62                cursor = self.cursor_index(c, x, y);
63            }
64        }
65        cursor
66    }
67
68    /// Whether key input should be reserved for an active text edit control.
69    pub fn text_edit_focused(&self) -> bool {
70        self.focus
71            .is_some_and(|id| matches!(&self.nodes[id].kind, Kind::Control(Control::Edit(_))))
72    }
73    /// The events since the last call, in order. (A click is returned by
74    /// [`pointer_button`](Self::pointer_button) instead.)
75    pub fn drain_events(&mut self) -> Vec<Event> {
76        std::mem::take(&mut self.pending)
77    }
78    /// Set the key that clicks an element: an upper-case character code, or 13 (Enter) or 27
79    /// (Escape).
80    pub fn set_shortcut(&mut self, id: NodeId, key: u32) {
81        self.nodes[id].shortcut = Some(key);
82    }
83    /// Forget the pointer, the focus and the pending events, for example when the window loses the
84    /// focus.
85    pub fn clear_input(&mut self) {
86        self.focus = None;
87        self.hot = None;
88        self.pressed = None;
89        self.pointer_down = false;
90        self.pending.clear();
91        self.input_root = None;
92        self.tooltip_instance = None;
93    }
94
95    /// Move the pointer to `(x, y)` over the tree at `root`.
96    pub fn pointer_move(&mut self, x: i32, y: i32, root: NodeId) {
97        self.input_root = Some(root);
98        self.pointer = (x, y);
99        // Open dropdown lists sit above the tree, so they take the pointer first.
100        let popup = self
101            .open_lists(root)
102            .into_iter()
103            .rev()
104            .find_map(|l| self.hit(l, x, y, None));
105        self.hot = popup.or_else(|| self.hit(root, x, y, None));
106        if self.pointer_down {
107            if let Some(id) = self.pressed {
108                self.drag_control(id, x, y);
109                // `ButtonElement::Update`: leaving a pressed button lets it go; coming back
110                // before the release does not press it again.
111                let button = matches!(
112                    self.nodes[id].kind,
113                    Kind::Button { .. }
114                        | Kind::Control(Control::Check { .. } | Control::Combo { .. })
115                ) || self.nodes[id].flags & banished_formats::ui::flags::MOUSE != 0;
116                if button && self.hot != Some(id) {
117                    self.pressed = None;
118                }
119            }
120        }
121        for n in &mut self.nodes {
122            if let Kind::Control(Control::Slider(s)) = &mut n.kind {
123                let k = s.knob(n.rect);
124                s.over_knob = k[0] <= x && x <= k[2] && k[1] <= y && y <= k[3];
125            }
126        }
127        self.update_states(root);
128        for n in &mut self.nodes {
129            if let Kind::List(l) = &mut n.kind {
130                l.hovered = None;
131            }
132            if let Kind::Control(Control::Document(d)) = &mut n.kind {
133                d.hovered_link = None;
134            }
135        }
136        if let Some(h) = self.hot {
137            let rect = self.nodes[h].rect;
138            if let Kind::List(l) = &mut self.nodes[h].kind {
139                let row = ((y - rect[1] + l.scroll) / l.row_height.max(1)).max(0) as usize;
140                l.hovered = (row < l.items.len()).then_some(row);
141            }
142            if let Kind::Control(Control::Document(d)) = &mut self.nodes[h].kind {
143                d.hovered_link = d
144                    .links
145                    .iter()
146                    .position(|(_, r)| contains(*r, x - rect[0], y - rect[1]));
147            }
148        }
149        self.update_tooltip(root);
150    }
151
152    /// Press or release the pointer's button. Returns the click when a button is released over
153    /// the element it was pressed on.
154    pub fn pointer_button(&mut self, down: bool, root: NodeId) -> Option<Event> {
155        // A dropdown is modal with respect to pointer input; an outside click closes it.
156        let open: Vec<_> = self
157            .active_nodes(root)
158            .into_iter()
159            .filter(|&id| {
160                matches!(
161                    self.nodes[id].kind,
162                    Kind::Control(Control::Combo { open: true, .. })
163                )
164            })
165            .collect();
166        if down {
167            for id in open {
168                let Kind::Control(Control::Combo { button, list, .. }) = self.nodes[id].kind else {
169                    continue;
170                };
171                if self.hot != Some(id) && self.hot != Some(button) && self.hot != Some(list) {
172                    if let Kind::Control(Control::Combo { open, .. }) = &mut self.nodes[id].kind {
173                        *open = false;
174                    }
175                }
176            }
177        }
178        self.pointer_down = down;
179        let mut event = None;
180        if down {
181            self.pressed = self.hot;
182            if let Some(f) = self.focus.filter(|&f| Some(f) != self.hot) {
183                if matches!(self.nodes[f].kind, Kind::Control(Control::Edit(_))) {
184                    self.commit_edit(f);
185                }
186            }
187            self.focus = self.hot;
188            // `ButtonElement::Update`: `Play(4)` on press (a list plays nothing; a slider or a
189            // scroll bar, below).
190            if self.hot.is_some_and(|id| {
191                self.interactive(id)
192                    && self.nodes[id].usable()
193                    && !matches!(
194                        self.nodes[id].kind,
195                        Kind::List(_)
196                            | Kind::Control(
197                                Control::Slider(_) | Control::Scroll { .. } | Control::Edit(_)
198                            )
199                    )
200            }) {
201                self.sounds.push(4);
202            }
203            if let Some(id) = self.hot {
204                let rect = self.nodes[id].rect;
205                match &mut self.nodes[id].kind {
206                    Kind::List(l) => {
207                        if let Some(row) = l.hovered {
208                            l.selected = Some(row);
209                            event = Some(Event::Selected(id, row));
210                        }
211                    }
212                    // `EditElement::Update`: a press puts the cursor before the first
213                    // character whose right edge reaches the pointer (at the end past the
214                    // text, or left of it), and shows the caret.
215                    Kind::Control(Control::Edit(e)) => {
216                        let target = self.pointer.0 - rect[0];
217                        let len = e.text.text.chars().count();
218                        e.cursor = (0..len)
219                            .find(|&i| {
220                                let prefix = e.text.text.chars().take(i + 1).collect::<String>();
221                                let x = e.text.font.map_or(0, |f| {
222                                    TextLayout::compute(&self.res, f, &prefix, None, 0).width
223                                });
224                                0 <= target && target <= x
225                            })
226                            .unwrap_or(len);
227                        e.caret = true;
228                        e.blink = 0.0;
229                    }
230                    Kind::Control(Control::Document(d)) => d.pressed_link = d.hovered_link,
231                    // `ScrollElement::Update`: on a bar that scrolls, a press on the thumb
232                    // starts a drag, one past it pages the view (`page / (1 - page)` of the
233                    // range) that way; either plays 4.
234                    Kind::Control(Control::Scroll {
235                        sprites,
236                        vertical,
237                        value,
238                        page,
239                        inset,
240                        drag,
241                        ..
242                    }) if *page < 1.0 => {
243                        let (x, y) = self.pointer;
244                        let thumb = scroll_thumb(rect, *vertical, *value, *page, *inset, sprites);
245                        let step = *page / (1.0 - *page);
246                        if thumb[0] <= x && x <= thumb[2] && thumb[1] <= y && y <= thumb[3] {
247                            *drag = Some((*value, if *vertical { y } else { x }));
248                            self.sounds.push(4);
249                        } else if (*vertical && thumb[3] < y) || thumb[2] < x {
250                            let v = *value + step;
251                            self.set_value(id, v);
252                            self.sounds.push(4);
253                            self.pending.push(Event::Changed(id));
254                            self.scroll_parent(id);
255                        } else if (*vertical && y < thumb[1]) || x < thumb[0] {
256                            let v = *value - step;
257                            self.set_value(id, v);
258                            self.sounds.push(4);
259                            self.pending.push(Event::Changed(id));
260                            self.scroll_parent(id);
261                        }
262                    }
263                    // `SliderElement::Update`: a press on the knob starts a drag; one beside it
264                    // on the slider moves the value a step towards the pointer (event `0x17`).
265                    // Either plays 4.
266                    Kind::Control(Control::Slider(sl)) => {
267                        let (x, y) = self.pointer;
268                        let knob = sl.knob(rect);
269                        if knob[0] <= x && x <= knob[2] && knob[1] <= y && y <= knob[3] {
270                            sl.drag = Some((sl.value, x));
271                            self.sounds.push(4);
272                        } else if knob[0] <= x && x <= knob[2] {
273                        } else {
274                            let step = if knob[2] < x { sl.step() } else { -sl.step() };
275                            let value = sl.value + step;
276                            self.set_value(id, value);
277                            self.sounds.push(4);
278                            event = Some(Event::Changed(id));
279                        }
280                    }
281                    _ => {
282                        self.drag_control(id, self.pointer.0, self.pointer.1);
283                    }
284                }
285            }
286        } else {
287            // `SliderElement::Update` / `ScrollElement::Update`: the drag ends; within 32 px it
288            // plays 5.
289            if let Some(id) = self.pressed {
290                let rect = self.nodes[id].rect;
291                let (x, y) = self.pointer;
292                let drag = match &mut self.nodes[id].kind {
293                    Kind::Control(Control::Slider(sl)) => sl.drag.take(),
294                    Kind::Control(Control::Scroll { drag, .. }) => drag.take(),
295                    _ => None,
296                };
297                if let Some((start, _)) = drag {
298                    let near = rect[0] - 32 <= x
299                        && x <= rect[2] + 32
300                        && rect[1] - 32 <= y
301                        && y <= rect[3] + 32;
302                    if near {
303                        self.sounds.push(5);
304                    } else {
305                        // Deviation: the original keeps the drag flag after a release far
306                        // away (until a release near it); the port ends the drag there.
307                        self.set_value(id, start);
308                        self.scroll_parent(id);
309                    }
310                }
311            }
312            if let Some(id) = self.pressed.filter(|p| Some(*p) == self.hot) {
313                event = self.activate(id).or(event);
314                // `Play(5)` with the click event (`0x12`).
315                if event.is_some() {
316                    self.sounds.push(5);
317                }
318            }
319            self.pressed = None;
320        }
321        if let Some(e) = event {
322            event = Some(self.route_event(root, e));
323        }
324        if !down {
325            for n in &mut self.nodes {
326                if let Kind::Control(Control::Document(d)) = &mut n.kind {
327                    d.pressed_link = None;
328                }
329            }
330        }
331        self.update_states(root);
332        event
333    }
334
335    fn drag_control(&mut self, id: NodeId, x: i32, y: i32) {
336        let rect = self.nodes[id].rect;
337        let mut next = None;
338        match &self.nodes[id].kind {
339            // `SliderElement::Update`: a drag from the knob follows the pointer while it stays
340            // within 32 px of the slider and puts the value back while it is further away
341            // (without an event).
342            Kind::Control(Control::Slider(s)) => {
343                let Some((start, _)) = s.drag else {
344                    return;
345                };
346                let near = rect[0] - 32 <= x
347                    && x <= rect[2] + 32
348                    && rect[1] - 32 <= y
349                    && y <= rect[3] + 32;
350                if !near {
351                    self.set_value(id, start);
352                    return;
353                }
354                next = s.dragged(rect, x);
355            }
356            // `ScrollElement::Update`: a drag from the thumb moves the value by the pointer's
357            // travel over the bar's length less the thumb's share; more than 32 px away the
358            // value goes back.
359            Kind::Control(Control::Scroll {
360                vertical,
361                page,
362                drag,
363                ..
364            }) => {
365                let Some((start, from)) = *drag else {
366                    return;
367                };
368                let near = rect[0] - 32 <= x
369                    && x <= rect[2] + 32
370                    && rect[1] - 32 <= y
371                    && y <= rect[3] + 32;
372                if !near {
373                    next = Some(start);
374                } else {
375                    let axis = if *vertical { 1 } else { 0 };
376                    let p = if *vertical { y } else { x };
377                    let length = (rect[axis + 2] - rect[axis]) as f32;
378                    next = Some(start - (from - p) as f32 / (length - length * *page));
379                }
380            }
381            _ => {}
382        }
383        if let Some(v) = next {
384            let before = self.value(id);
385            self.set_value(id, v);
386            if before != self.value(id) {
387                self.pending.push(Event::Changed(id));
388                self.scroll_parent(id);
389            }
390        }
391    }
392
393    fn scroll_parent(&mut self, bar: NodeId) {
394        let value = self.value(bar).unwrap_or(0.0);
395        let parent=(0..self.nodes.len()).find(|&id|matches!(&self.nodes[id].kind,Kind::Control(Control::Container{bars,..}) if bars.contains(&Some(bar))));
396        if let Some(parent) = parent {
397            let Kind::Control(Control::Container {
398                bars,
399                content,
400                view,
401                ..
402            }) = self.nodes[parent].kind.clone()
403            else {
404                return;
405            };
406            let axis = if bars[0] == Some(bar) { 0 } else { 1 };
407            let size = content.map_or(0, |n| self.nodes[n].size[axis]);
408            let max = (size - (view[axis + 2] - view[axis])).max(0);
409            if let Kind::Control(Control::Container { offset, .. }) = &mut self.nodes[parent].kind {
410                offset[axis] = (value * max as f32) as i32;
411            }
412        }
413    }
414
415    fn activate(&mut self, id: NodeId) -> Option<Event> {
416        if !self.nodes[id].usable() || !self.nodes[id].visible() {
417            return None;
418        }
419        if self.nodes[id].flags & banished_formats::ui::flags::MOUSE != 0 {
420            return Some(Event::Clicked(id));
421        }
422        match &mut self.nodes[id].kind {
423            Kind::Button { .. } => Some(Event::Clicked(id)),
424            Kind::Control(Control::Check { checked, .. }) => {
425                *checked = !*checked;
426                Some(Event::Changed(id))
427            }
428            Kind::Control(Control::Combo { open, .. }) => {
429                *open = !*open;
430                Some(Event::Clicked(id))
431            }
432            Kind::Control(Control::Document(d)) => d
433                .hovered_link
434                .filter(|&i| {
435                    d.pressed_link
436                        .and_then(|p| d.links.get(p))
437                        .is_some_and(|p| p.0 == d.links[i].0)
438                })
439                .map(|i| Event::Link(id, i)),
440            _ => None,
441        }
442    }
443
444    fn route_event(&mut self, root: NodeId, event: Event) -> Event {
445        let id = event_node(event);
446        for parent in self.active_nodes(root) {
447            let mut selected_text = None;
448            let spinner_current = match &self.nodes[parent].kind {
449                Kind::Control(Control::Spinner { parts, .. }) => {
450                    parts[0].and_then(|n| self.value(n))
451                }
452                _ => None,
453            };
454            // `NumberEditElement::SetValue` clamps to the edit's range (`SpinnerElement::SetMinMax`).
455            let spinner_range = match &self.nodes[parent].kind {
456                Kind::Control(Control::Spinner { parts, .. }) => parts[0]
457                    .and_then(|n| match &self.nodes[n].kind {
458                        Kind::Control(Control::Edit(e)) => e.range,
459                        _ => None,
460                    })
461                    .map(|r| (r[0].min(r[1]) as i32, r[0].max(r[1]) as i32)),
462                _ => None,
463            };
464            let routed = match &mut self.nodes[parent].kind {
465                Kind::Control(Control::Tabs { tabs, selected, .. }) => {
466                    if let Some(i) = tabs.iter().position(|&(a, _)| a == id) {
467                        *selected = i;
468                        Some(Event::Selected(parent, i))
469                    } else {
470                        None
471                    }
472                }
473                Kind::Control(Control::Radio { children, selected }) => {
474                    if let Some(i) = children.iter().position(|&n| n == id) {
475                        *selected = i;
476                        Some(Event::Selected(parent, i))
477                    } else {
478                        None
479                    }
480                }
481                Kind::Control(Control::Check {
482                    button, checked, ..
483                }) if *button == id => {
484                    *checked = !*checked;
485                    Some(Event::Changed(parent))
486                }
487                Kind::Control(Control::Combo { button, list, open }) => {
488                    if *button == id {
489                        *open = !*open;
490                        Some(Event::Clicked(parent))
491                    } else if let Event::Selected(n, i) = event {
492                        if n == *list {
493                            // `ComboDialog::OnElementEvent`: the choice plays 5 and closes.
494                            selected_text = Some((*button, *list, i));
495                            *open = false;
496                            self.sounds.push(5);
497                            Some(Event::Selected(parent, i))
498                        } else {
499                            None
500                        }
501                    } else {
502                        None
503                    }
504                }
505                Kind::Control(Control::Spinner { parts, value, step }) => {
506                    let delta = if parts[1] == Some(id) {
507                        *step
508                    } else if parts[2] == Some(id) {
509                        -*step
510                    } else {
511                        0
512                    };
513                    if delta != 0 {
514                        *value =
515                            (spinner_current.unwrap_or(*value as f32) as i32).saturating_add(delta);
516                        if let Some((lo, hi)) = spinner_range {
517                            *value = (*value).clamp(lo, hi);
518                        }
519                        selected_text = parts[0].map(|n| (n, usize::MAX, *value as usize));
520                        Some(Event::Changed(parent))
521                    } else if parts[0] == Some(id) {
522                        *value = spinner_current.unwrap_or(*value as f32) as i32;
523                        Some(Event::Changed(parent))
524                    } else {
525                        None
526                    }
527                }
528                _ => None,
529            };
530            if let Some((button, list, i)) = selected_text {
531                let text = if list == usize::MAX {
532                    self.set_value(button, i as i32 as f32);
533                    (i as i32).to_string()
534                } else {
535                    match &self.nodes[list].kind {
536                        Kind::List(l) => l.items.get(i).cloned().unwrap_or_default(),
537                        _ => String::new(),
538                    }
539                };
540                if list != usize::MAX {
541                    self.set_text(button, &text);
542                    let icon = match &self.nodes[list].kind {
543                        Kind::List(l) => l.icons.get(i).copied().flatten(),
544                        _ => None,
545                    };
546                    if let Some(icon) = icon {
547                        self.set_icon(button, icon);
548                    }
549                }
550            }
551            if let Some(routed) = routed {
552                return routed;
553            }
554        }
555        event
556    }
557
558    /// `ContainerElement::Update`: the wheel over a container queues 7 frames of scrolling
559    /// (up for a positive `delta`), which [`Ui::update_containers`] plays out.
560    pub fn wheel(&mut self, delta: i32) -> bool {
561        if delta == 0 {
562            return false;
563        }
564        let (x, y) = self.pointer;
565        // The innermost visible container under the pointer.
566        let active = self
567            .input_root
568            .map(|r| self.active_nodes(r))
569            .unwrap_or_default();
570        let hovered = active
571            .into_iter()
572            .filter_map(|id| {
573                let n = &self.nodes[id];
574                matches!(n.kind, Kind::Control(Control::Container { .. }))
575                    .then_some(n.rect)
576                    .filter(|r| contains(*r, x, y))
577                    .map(|r| (id, (r[2] - r[0]) * (r[3] - r[1])))
578            })
579            .min_by_key(|c| c.1);
580        if let Some((id, _)) = hovered {
581            if let Kind::Control(Control::Container { wheel, .. }) = &mut self.nodes[id].kind {
582                *wheel += if 0 < delta { -7 } else { 7 };
583            }
584            return true;
585        }
586        // A list outside any container (the port's own popups) scrolls directly.
587        if let Some(h) = self.hot {
588            let height = self.nodes[h].rect[3] - self.nodes[h].rect[1];
589            if let Kind::List(l) = &mut self.nodes[h].kind {
590                let max = (l.items.len() as i32 * l.row_height - height).max(0);
591                l.scroll = (l.scroll - delta).clamp(0, max);
592                return true;
593            }
594        }
595        false
596    }
597
598    /// `ContainerElement::Update`, each frame: one queued wheel frame moves the vertical bar a
599    /// seventh of a third of the view (`0.33 / 7`), within the content.
600    pub fn update_containers(&mut self) {
601        for id in 0..self.nodes.len() {
602            let Kind::Control(Control::Container {
603                content,
604                bars,
605                view,
606                wheel,
607                ..
608            }) = self.nodes[id].kind
609            else {
610                continue;
611            };
612            if wheel == 0 || bars[1].is_none() {
613                continue;
614            }
615            let height = (view[3] - view[1]) as f32;
616            let max = content.map_or(0.0, |n| self.nodes[n].size[1] as f32 - height);
617            if max <= 0.0 {
618                continue;
619            }
620            if let Kind::Control(Control::Container {
621                offset,
622                wheel,
623                fine,
624                ..
625            }) = &mut self.nodes[id].kind
626            {
627                if *fine as i32 != offset[1] {
628                    *fine = offset[1] as f32;
629                }
630                let mut step = (max / (max / height)) / 7.0 * 0.33;
631                if *wheel < 0 {
632                    step = -step;
633                    *wheel += 1;
634                } else {
635                    *wheel -= 1;
636                }
637                *fine = (*fine + step).clamp(0.0, max);
638                offset[1] = *fine as i32;
639            }
640        }
641    }
642
643    pub(crate) fn active_nodes(&self, root: NodeId) -> Vec<NodeId> {
644        fn visit(ui: &Ui, id: NodeId, out: &mut Vec<NodeId>) {
645            let n = &ui.nodes[id];
646            if !n.visible() || !n.enabled {
647                return;
648            }
649            // A locked element is passed by, its children kept.
650            if n.usable() {
651                out.push(id);
652            }
653            let children = match &n.kind {
654                Kind::Control(c) => c.active_children(),
655                Kind::Button { .. } => Vec::new(),
656                _ => n.children.clone(),
657            };
658            for c in children {
659                visit(ui, c, out);
660            }
661        }
662        let mut out = Vec::new();
663        visit(self, root, &mut out);
664        out
665    }
666    fn interactive(&self, id: NodeId) -> bool {
667        if self.nodes[id].flags & banished_formats::ui::flags::MOUSE != 0 {
668            return true;
669        }
670        match &self.nodes[id].kind {
671            Kind::Button { .. } | Kind::List(_) => true,
672            Kind::Control(
673                Control::Slider(_)
674                | Control::Scroll { .. }
675                | Control::Check { .. }
676                | Control::Combo { .. },
677            ) => true,
678            Kind::Control(Control::Edit(e)) => e.max_length > 0,
679            Kind::Control(Control::Document(d)) => !d.links.is_empty(),
680            _ => false,
681        }
682    }
683    fn hit(&self, id: NodeId, x: i32, y: i32, clip: Option<[i32; 4]>) -> Option<NodeId> {
684        let n = &self.nodes[id];
685        if !n.visible() || !n.enabled || clip.is_some_and(|r| !contains(r, x, y)) {
686            return None;
687        }
688        let mut child_clip = clip;
689        if let Kind::Control(Control::Document(d)) = &n.kind {
690            return d
691                .links
692                .iter()
693                .any(|(_, r)| contains(*r, x - n.rect[0], y - n.rect[1]))
694                .then_some(id);
695        }
696        let children = match &n.kind {
697            Kind::Control(Control::Container {
698                content,
699                bars,
700                view,
701                ..
702            }) => {
703                for c in bars.iter().flatten().rev() {
704                    if let Some(h) = self.hit(*c, x, y, clip) {
705                        return Some(h);
706                    }
707                }
708                if let Some(c) = content {
709                    return self.hit(*c, x, y, intersect(clip, *view));
710                }
711                return None;
712            }
713            Kind::Control(Control::Clip { .. }) => {
714                child_clip = intersect(clip, n.rect);
715                if let Kind::Control(c) = &n.kind {
716                    c.active_children()
717                } else {
718                    Vec::new()
719                }
720            }
721            Kind::Control(Control::Check { .. } | Control::Combo { .. }) => {
722                if let Kind::Control(Control::Combo {
723                    list, open: true, ..
724                }) = &n.kind
725                {
726                    if let Some(h) = self.hit(*list, x, y, None) {
727                        return Some(h);
728                    }
729                }
730                return n.contains(x, y).then_some(id);
731            }
732            Kind::Control(c) => c.active_children(),
733            _ => n.children.clone(),
734        };
735        for child in children.into_iter().rev() {
736            if let Some(h) = self.hit(child, x, y, child_clip) {
737                return Some(h);
738            }
739        }
740        (self.interactive(id) && n.contains(x, y)).then_some(id)
741    }
742
743    /// Press a key. Returns whether the interface used it.
744    pub fn key(&mut self, root: NodeId, key: Key) -> bool {
745        let active = self.active_nodes(root);
746        if self.focus.is_some_and(|id| !active.contains(&id)) {
747            self.focus = None;
748        }
749        if matches!(key, Key::Tab | Key::BackTab) {
750            let focusable: Vec<_> = active
751                .iter()
752                .copied()
753                .filter(|&id| self.interactive(id))
754                .collect();
755            if focusable.is_empty() {
756                return false;
757            }
758            let current = self
759                .focus
760                .and_then(|id| focusable.iter().position(|&n| n == id));
761            let i = match (current, key) {
762                (Some(i), Key::BackTab) => (i + focusable.len() - 1) % focusable.len(),
763                (Some(i), _) => (i + 1) % focusable.len(),
764                (None, Key::BackTab) => focusable.len() - 1,
765                _ => 0,
766            };
767            self.focus = Some(focusable[i]);
768            return true;
769        }
770        if let Some(id) = self.focus {
771            // A combo's popup retains focus while arrows browse; Enter confirms and Escape
772            // closes it. These keys must not fall through to the asset browser's navigation.
773            let combo = active
774                .iter()
775                .find_map(|&parent| match self.nodes[parent].kind {
776                    Kind::Control(Control::Combo { list, .. }) if parent == id || list == id => {
777                        Some((parent, list))
778                    }
779                    _ => None,
780                });
781            if let Some((parent, list)) = combo {
782                if matches!(key, Key::Up | Key::Down | Key::Home | Key::End) {
783                    if let Kind::Control(Control::Combo { open, .. }) = &mut self.nodes[parent].kind
784                    {
785                        *open = true;
786                    }
787                    if let Kind::List(l) = &mut self.nodes[list].kind {
788                        if !l.items.is_empty() {
789                            let last = l.items.len() - 1;
790                            let i = match key {
791                                Key::Home => 0,
792                                Key::End => last,
793                                Key::Up => l.selected.unwrap_or(0).saturating_sub(1),
794                                _ => l.selected.map_or(0, |i| (i + 1).min(last)),
795                            };
796                            l.selected = Some(i);
797                            l.reveal = true;
798                        }
799                    }
800                    self.focus = Some(list);
801                    return true;
802                }
803                if key == Key::Escape {
804                    if let Kind::Control(Control::Combo { open, .. }) = &mut self.nodes[parent].kind
805                    {
806                        *open = false;
807                    }
808                    self.focus = Some(parent);
809                    return true;
810                }
811                if id == list && matches!(key, Key::Enter | Key::Space) {
812                    if let Kind::List(l) = &self.nodes[list].kind {
813                        if let Some(i) = l.selected {
814                            let event = self.route_event(root, Event::Selected(list, i));
815                            self.pending.push(event);
816                        }
817                    }
818                    self.focus = Some(parent);
819                    return true;
820                }
821            }
822            if let Kind::Control(Control::Edit(e)) = &mut self.nodes[id].kind {
823                let len = e.text.text.chars().count();
824                e.cursor = e.cursor.min(len);
825                let mut changed = false;
826                match key {
827                    Key::Left => e.cursor = e.cursor.saturating_sub(1),
828                    Key::Right => e.cursor = (e.cursor + 1).min(len),
829                    Key::Home => e.cursor = 0,
830                    Key::End => e.cursor = len,
831                    Key::Backspace if e.cursor > 0 => {
832                        let at = byte_index(&e.text.text, e.cursor);
833                        let prev = byte_index(&e.text.text, e.cursor - 1);
834                        e.text.text.replace_range(prev..at, "");
835                        e.cursor -= 1;
836                        changed = true;
837                    }
838                    Key::Delete if e.cursor < len => {
839                        let at = byte_index(&e.text.text, e.cursor);
840                        let next = byte_index(&e.text.text, e.cursor + 1);
841                        e.text.text.replace_range(at..next, "");
842                        changed = true;
843                    }
844                    // `EditElement::Update`: Enter or Escape gives up the focus.
845                    Key::Enter | Key::Escape => {
846                        self.commit_edit(id);
847                        return true;
848                    }
849                    Key::Character(_) | Key::Space => return false,
850                    _ => return true,
851                }
852                if changed {
853                    self.pending.push(Event::Changed(id));
854                }
855                return true;
856            }
857            if let Kind::Control(Control::Slider(s)) = &self.nodes[id].kind {
858                let step = s.step();
859                let value = match key {
860                    Key::Left | Key::Down => s.value - step,
861                    Key::Right | Key::Up => s.value + step,
862                    Key::Home => s.range[0],
863                    Key::End => s.range[1],
864                    _ => f32::NAN,
865                };
866                if value.is_finite() {
867                    self.set_value(id, value);
868                    self.pending.push(Event::Changed(id));
869                    return true;
870                }
871            }
872            if let Kind::List(l) = &mut self.nodes[id].kind {
873                let last = l.items.len().saturating_sub(1);
874                let current = l.selected.unwrap_or(0);
875                let selected = match key {
876                    Key::Up => Some(current.saturating_sub(1)),
877                    Key::Down => Some(l.selected.map_or(0, |i| (i + 1).min(last))),
878                    Key::Home => Some(0),
879                    Key::End => Some(last),
880                    Key::PageUp => Some(current.saturating_sub(10)),
881                    Key::PageDown => Some((current + 10).min(last)),
882                    _ => None,
883                };
884                if let Some(i) = selected.filter(|_| !l.items.is_empty()) {
885                    l.selected = Some(i);
886                    l.reveal = true;
887                    let e = self.route_event(root, Event::Selected(id, i));
888                    self.pending.push(e);
889                    return true;
890                }
891            }
892            if matches!(key, Key::Enter | Key::Space) {
893                if let Some(event) = self.activate(id) {
894                    let event = self.route_event(root, event);
895                    self.pending.push(event);
896                    return true;
897                }
898            }
899        }
900        if key == Key::Escape {
901            let mut closed = false;
902            for id in &active {
903                if let Kind::Control(Control::Combo { open, .. }) = &mut self.nodes[*id].kind {
904                    closed |= *open;
905                    *open = false;
906                }
907            }
908            if closed {
909                return true;
910            }
911        }
912        let code = match key {
913            Key::Enter => 13,
914            Key::Escape => 27,
915            Key::Space => 32,
916            Key::Character(c) => c.to_ascii_uppercase() as u32,
917            _ => 0,
918        };
919        if code != 0 {
920            if let Some(id) = active
921                .into_iter()
922                .find(|&id| self.nodes[id].shortcut == Some(code))
923            {
924                if let Some(e) = self.activate(id) {
925                    let e = self.route_event(root, e);
926                    self.pending.push(e);
927                    return true;
928                }
929            }
930        }
931        false
932    }
933
934    /// Type text into the focused edit box. Returns whether it was used.
935    pub fn text_input(&mut self, text: &str) -> bool {
936        let Some(id) = self.focus else {
937            return false;
938        };
939        let Kind::Control(Control::Edit(e)) = &mut self.nodes[id].kind else {
940            return false;
941        };
942        if e.max_length == 0 {
943            return false;
944        }
945        let mut changed = false;
946        // `EditElement::SelectCharacter`: the keys of a US keyboard give printable ASCII but
947        // `:` and `;`; each goes in while the text is shorter than its most and the edit
948        // takes it. Deviation: an edit whose allowed characters name `:` or `;` takes them (the
949        // Multiplayer window's relay address); the original's edits never name them.
950        for ch in text.chars() {
951            let typable =
952                (' '..='~').contains(&ch) && (!matches!(ch, ':' | ';') || e.allowed.contains(ch));
953            if !typable
954                || e.text.text.chars().count() >= e.max_length
955                || (!e.allowed.is_empty() && !e.allowed.contains(ch))
956            {
957                continue;
958            }
959            let at = byte_index(&e.text.text, e.cursor);
960            e.text.text.insert(at, ch);
961            e.cursor += 1;
962            changed = true;
963        }
964        if changed {
965            self.pending.push(Event::Changed(id));
966        }
967        true
968    }
969
970    /// `EditElement::Update` losing the focus (Enter, Escape or a press elsewhere): a
971    /// NumberEdit's text becomes its number within its range (`NumberEditElement::
972    /// FinalizeText`), and event `0x15` goes.
973    pub(crate) fn commit_edit(&mut self, id: NodeId) {
974        if let Kind::Control(Control::Edit(e)) = &mut self.nodes[id].kind {
975            if let Some(range) = e.range {
976                let value = to_integer(&e.text.text)
977                    .clamp(range[0].min(range[1]) as i32, range[0].max(range[1]) as i32);
978                e.text.text = value.to_string();
979                e.cursor = e.cursor.min(e.text.text.chars().count());
980            }
981            self.pending.push(Event::Submitted(id));
982        }
983        if self.focus == Some(id) {
984            self.focus = None;
985        }
986    }
987
988    /// The caret's blink: every half second while an edit has the focus.
989    pub(crate) fn update_caret(&mut self, dt: f32) {
990        let Some(id) = self.focus else { return };
991        if let Kind::Control(Control::Edit(e)) = &mut self.nodes[id].kind {
992            e.blink += dt;
993            if 0.5 < e.blink {
994                e.caret = !e.caret;
995                e.blink = 0.0;
996            }
997        }
998    }
999
1000    fn update_states(&mut self, _root: NodeId) {
1001        for id in 0..self.nodes.len() {
1002            let hot = self.hot == Some(id) || self.focus == Some(id);
1003            let pressed = self.pressed == Some(id) && self.pointer_down;
1004            let enabled = self.nodes[id].usable();
1005            if let Kind::Button { state, .. } = &mut self.nodes[id].kind {
1006                *state = if !enabled {
1007                    1
1008                } else if hot && pressed {
1009                    3
1010                } else if hot {
1011                    2
1012                } else {
1013                    0
1014                };
1015            }
1016            let button = match &self.nodes[id].kind {
1017                Kind::Control(Control::Check { button, .. } | Control::Combo { button, .. }) => {
1018                    Some(*button)
1019                }
1020                _ => None,
1021            };
1022            if let Some(button) = button {
1023                if let Kind::Button { state, .. } = &mut self.nodes[button].kind {
1024                    *state = if !enabled {
1025                        1
1026                    } else if hot && pressed {
1027                        3
1028                    } else if hot {
1029                        2
1030                    } else {
1031                        0
1032                    };
1033                }
1034            }
1035        }
1036    }
1037
1038    /// The tooltip text of the element under the pointer.
1039    pub fn hovered_tooltip(&self) -> Option<&str> {
1040        self.hot
1041            .map(|h| self.nodes[h].tooltip.as_str())
1042            .filter(|t| !t.is_empty())
1043    }
1044
1045    /// The element whose tooltip shows: the topmost element under the pointer (an open
1046    /// dropdown list first, else the last drawn) or the nearest of its ancestors with tooltip
1047    /// text. Elements covered by a later dialog do not show theirs.
1048    fn tooltip_source(&self, root: NodeId) -> Option<NodeId> {
1049        let (x, y) = self.pointer;
1050        let under = |id: NodeId| {
1051            let n = &self.nodes[id];
1052            n.contains(x, y) && n.rect[2] > n.rect[0] && n.rect[3] > n.rect[1]
1053        };
1054        let popup = self.open_lists(root).into_iter().rev().find(|&l| under(l));
1055        let nodes: Vec<NodeId> = match popup {
1056            Some(l) => self.active_nodes(l),
1057            None => self.active_nodes(root),
1058        };
1059        let under: Vec<NodeId> = nodes.into_iter().filter(|&id| under(id)).collect();
1060        let top = *under.last()?;
1061        under
1062            .into_iter()
1063            .rev()
1064            .find(|&id| !self.nodes[id].tooltip.is_empty() && self.descendant(id, top))
1065    }
1066
1067    fn update_tooltip(&mut self, root: NodeId) {
1068        let Some(source) = self.tooltip_source(root) else {
1069            return;
1070        };
1071        // `UserInterface::UpdateToolTip` (0x180123300): the open tooltip follows the pointer.
1072        if self.tooltip_instance.is_some_and(|(s, _)| s == source) {
1073            self.place_tooltip();
1074            return;
1075        }
1076        // `Element::IsToolTipValid` (0x180036140): a tooltip needs the element's own tooltip
1077        // dialog as well as its text.
1078        let Some(reference) = self.nodes[source].tooltip_dialog.clone() else {
1079            return;
1080        };
1081        if let Some(dialog) = self.instantiate_dialog(&reference) {
1082            let text = self.nodes[source].tooltip.clone();
1083            for name in ["textToolTip", "text", "textDescription"] {
1084                if let Some(id) = self.find(dialog.root, name) {
1085                    self.set_parameters(id, &[(0, &text)]);
1086                    break;
1087                }
1088            }
1089            let controllers: Vec<u32> = dialog.bindings.iter().map(|b| b.controller).collect();
1090            // `HotKeyToolTipUI` (`FUN_14006b2e0`): with a key binding, its name is parameter 1
1091            // of `textToolTip` and parameter 0 of `textHotKey`.
1092            if controllers.contains(&tooltip_controller::HOT_KEY) {
1093                if let Some(key) = self.nodes[source].hotkey.clone() {
1094                    if let Some(id) = self.find(dialog.root, "textToolTip") {
1095                        self.set_parameters(id, &[(1, &key)]);
1096                    }
1097                    if let Some(id) = self.find(dialog.root, "textHotKey") {
1098                        self.set_parameters(id, &[(0, &key)]);
1099                    }
1100                }
1101            }
1102            // `TitleDescToolTipUI` (`FUN_140080c00`): the element's title and description.
1103            if controllers.contains(&tooltip_controller::TITLE_DESC) {
1104                if let Some((title, desc)) = self.nodes[source].title_desc.clone() {
1105                    for (name, value) in [("textTitle", title), ("textDesc", desc)] {
1106                        if let Some(id) = self.find(dialog.root, name) {
1107                            self.set_parameters(id, &[(0, &value)]);
1108                        }
1109                    }
1110                }
1111            }
1112            self.tooltip_instance = Some((source, dialog.root));
1113            self.tooltip_opened = Some((source, dialog.root, controllers));
1114            self.place_tooltip();
1115        }
1116    }
1117
1118    /// Size and place the open tooltip dialog on the pointer (again after the application
1119    /// filled it).
1120    pub fn place_tooltip(&mut self) {
1121        let Some((_, root)) = self.tooltip_instance else {
1122            return;
1123        };
1124        self.begin_size_pass();
1125        self.compute_size(root);
1126        let [w, h] = self.nodes[root].size;
1127        // `BeginToolTip` / `UpdateToolTip`: `SetUpperLeft(x, y − height, false)`, the tooltip
1128        // standing on the pointer.
1129        let (x, y) = (self.pointer.0, self.pointer.1 - h);
1130        self.layout_dialog(root, [x, y, x + w, y + h]);
1131    }
1132
1133    /// The tooltip dialog opened since the last call: source element, dialog root and the
1134    /// dialog's controller ids. Application controllers fill it, then call `place_tooltip`.
1135    pub fn new_tooltip(&mut self) -> Option<(NodeId, NodeId, Vec<u32>)> {
1136        self.tooltip_opened.take()
1137    }
1138
1139    /// `UI::TextElement::SetParameter` on a text or label whose text still holds its template.
1140    pub fn set_parameters(&mut self, id: NodeId, values: &[(usize, &str)]) {
1141        let current = match &self.nodes[id].kind {
1142            Kind::Text(t) | Kind::Label { text: t, .. } => t.text.clone(),
1143            _ => return,
1144        };
1145        let mut out = current;
1146        for (i, v) in values {
1147            out = out.replace(&format!("@{i}"), v);
1148        }
1149        self.set_text(id, &out);
1150    }
1151    /// The open tooltip dialog, while its element is still the tooltip source under the
1152    /// pointer within the input root (a modal dialog hides the tooltips of what it covers).
1153    pub(crate) fn draw_tooltip(&self, root: NodeId, out: &mut Vec<Quad>) {
1154        if self.pointer_down {
1155            return;
1156        }
1157        let Some((source, tip)) = self.tooltip_instance else {
1158            return;
1159        };
1160        let input = self.input_root.unwrap_or(root);
1161        if tip < self.nodes.len() && self.tooltip_source(input) == Some(source) {
1162            self.draw_node(tip, 0xFFFFFFFF, None, out);
1163        }
1164    }
1165}
1166
1167fn contains(r: [i32; 4], x: i32, y: i32) -> bool {
1168    x >= r[0] && x < r[2] && y >= r[1] && y < r[3]
1169}
1170fn byte_index(s: &str, cursor: usize) -> usize {
1171    s.char_indices().nth(cursor).map_or(s.len(), |p| p.0)
1172}
1173/// `System::String::ToInteger`: an optional sign and the digits after it, 0 without.
1174fn to_integer(s: &str) -> i32 {
1175    let s = s.trim_start();
1176    let (sign, digits) = match s.strip_prefix('-') {
1177        Some(rest) => (-1i64, rest),
1178        None => (1, s.strip_prefix('+').unwrap_or(s)),
1179    };
1180    let mut v: i64 = 0;
1181    for c in digits.chars().take_while(|c| c.is_ascii_digit()) {
1182        v = (v * 10 + c as i64 - '0' as i64).min(i32::MAX as i64 + 1);
1183    }
1184    (sign * v).clamp(i32::MIN as i64, i32::MAX as i64) as i32
1185}
1186
1187pub(crate) fn event_node(e: Event) -> NodeId {
1188    match e {
1189        Event::Clicked(n)
1190        | Event::Selected(n, _)
1191        | Event::Changed(n)
1192        | Event::Submitted(n)
1193        | Event::Link(n, _) => n,
1194    }
1195}
1196
1197/// The type ids (CRC-32 of the class name) of the game's tooltip controllers that the interface
1198/// handles itself.
1199pub mod tooltip_controller {
1200    /// `HotKeyToolTipUI`: a tooltip that shows the element's key.
1201    pub const HOT_KEY: u32 = 0x5dd8_3c93;
1202    /// `TitleDescToolTipUI`: a tooltip with a title and a description.
1203    pub const TITLE_DESC: u32 = 0x33e6_1003;
1204    /// The tooltip that lists what a building needs.
1205    pub const BUILD_REQUIREMENT: u32 = 0xc3fe_f549;
1206}