Skip to main content

i_slint_compiler/passes/
lower_popups.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4//! This pass transforms the PopupWindow element into a component
5
6use crate::diagnostics::{BuildDiagnostics, SourceLocation};
7use crate::expression_tree::{BindingExpression, Expression, NamedReference};
8use crate::langtype::{ElementType, EnumerationValue, Type};
9use crate::object_tree::*;
10use crate::typeregister::TypeRegister;
11use smol_str::{SmolStr, format_smolstr};
12use std::cell::RefCell;
13use std::rc::{Rc, Weak};
14
15const CLOSE_ON_CLICK: &str = "close-on-click";
16const CLOSE_POLICY: &str = "close-policy";
17
18pub fn lower_popups(
19    component: &Rc<Component>,
20    type_register: &TypeRegister,
21    diag: &mut BuildDiagnostics,
22) {
23    let window_type = type_register.lookup_builtin_element("Window").unwrap();
24
25    recurse_elem_including_sub_components_no_borrow(
26        component,
27        &None,
28        &mut |elem, parent_element: &Option<ElementRc>| {
29            if is_popup_window(elem) {
30                lower_popup_window(elem, parent_element.as_ref(), &window_type, diag);
31            }
32            Some(elem.clone())
33        },
34    )
35}
36
37pub fn is_popup_window(element: &ElementRc) -> bool {
38    match &element.borrow().base_type {
39        ElementType::Builtin(base_type) => base_type.name == "PopupWindow",
40        ElementType::Component(base_type) => base_type.inherits_popup_window.get(),
41        _ => false,
42    }
43}
44
45fn lower_popup_window(
46    popup_window_element: &ElementRc,
47    parent_element: Option<&ElementRc>,
48    window_type: &ElementType,
49    diag: &mut BuildDiagnostics,
50) {
51    if let Some(binding) = popup_window_element.borrow().bindings.get(CLOSE_ON_CLICK) {
52        if popup_window_element.borrow().bindings.contains_key(CLOSE_POLICY) {
53            diag.push_error(
54                "close-policy and close-on-click cannot be set at the same time".into(),
55                &binding.borrow().span,
56            );
57        } else {
58            diag.push_property_deprecation_warning(
59                CLOSE_ON_CLICK,
60                CLOSE_POLICY,
61                &binding.borrow().span,
62            );
63            if !matches!(
64                super::ignore_debug_hooks(&binding.borrow().expression),
65                Expression::BoolLiteral(_)
66            ) {
67                report_const_error(CLOSE_ON_CLICK, &binding.borrow().span, diag);
68            }
69        }
70    } else if let Some(binding) = popup_window_element.borrow().bindings.get(CLOSE_POLICY)
71        && !matches!(
72            super::ignore_debug_hooks(&binding.borrow().expression),
73            Expression::EnumerationValue(_)
74        )
75    {
76        report_const_error(CLOSE_POLICY, &binding.borrow().span, diag);
77    }
78
79    let parent_component = popup_window_element.borrow().enclosing_component.upgrade().unwrap();
80    let parent_element = match parent_element {
81        None => {
82            if matches!(popup_window_element.borrow().base_type, ElementType::Builtin(_)) {
83                popup_window_element.borrow_mut().base_type = window_type.clone();
84            }
85            parent_component.inherits_popup_window.set(true);
86            return;
87        }
88        Some(parent_element) => parent_element,
89    };
90
91    if Rc::ptr_eq(&parent_component.root_element, popup_window_element) {
92        diag.push_error(
93            "PopupWindow cannot be directly repeated or conditional".into(),
94            &*popup_window_element.borrow(),
95        );
96        return;
97    }
98
99    // Remove the popup_window_element from its parent
100    let mut parent_element_borrowed = parent_element.borrow_mut();
101    let index = parent_element_borrowed
102        .children
103        .iter()
104        .position(|child| Rc::ptr_eq(child, popup_window_element))
105        .expect("PopupWindow must be a child of its parent");
106    parent_element_borrowed.children.remove(index);
107    parent_element_borrowed.has_popup_child = true;
108    drop(parent_element_borrowed);
109    if let Some(parent_cip) = &mut *parent_component.child_insertion_point.borrow_mut()
110        && Rc::ptr_eq(&parent_cip.parent, parent_element)
111        && parent_cip.insertion_index > index
112    {
113        parent_cip.insertion_index -= 1;
114    }
115
116    let map_close_on_click_value = |b: &BindingExpression| {
117        let Expression::BoolLiteral(v) = super::ignore_debug_hooks(&b.expression) else {
118            assert!(diag.has_errors());
119            return None;
120        };
121        let enum_ty = crate::typeregister::BUILTIN.with(|e| e.enums.PopupClosePolicy.clone());
122        let s = if *v { "close-on-click" } else { "no-auto-close" };
123        Some(EnumerationValue {
124            value: enum_ty.values.iter().position(|v| v == s).unwrap(),
125            enumeration: enum_ty,
126        })
127    };
128
129    let close_policy =
130        popup_window_element.borrow_mut().bindings.remove(CLOSE_POLICY).and_then(|b| {
131            let b = b.into_inner();
132            if let Expression::EnumerationValue(v) = super::ignore_debug_hooks(&b.expression) {
133                Some(v.clone())
134            } else {
135                assert!(diag.has_errors());
136                None
137            }
138        });
139    let close_policy = close_policy
140        .or_else(|| {
141            popup_window_element
142                .borrow_mut()
143                .bindings
144                .remove(CLOSE_ON_CLICK)
145                .and_then(|b| map_close_on_click_value(&b.borrow()))
146        })
147        .or_else(|| {
148            // check bases
149            let mut base = popup_window_element.borrow().base_type.clone();
150            while let ElementType::Component(b) = base {
151                let base_policy = b
152                    .root_element
153                    .borrow()
154                    .bindings
155                    .get(CLOSE_POLICY)
156                    .and_then(|b| {
157                        let b = b.borrow();
158                        if let Expression::EnumerationValue(v) = &b.expression {
159                            return Some(v.clone());
160                        }
161                        assert!(diag.has_errors());
162                        None
163                    })
164                    .or_else(|| {
165                        b.root_element
166                            .borrow()
167                            .bindings
168                            .get(CLOSE_ON_CLICK)
169                            .and_then(|b| map_close_on_click_value(&b.borrow()))
170                    });
171                if let Some(base_policy) = base_policy {
172                    return Some(base_policy);
173                }
174                base = b.root_element.borrow().base_type.clone();
175            }
176            None
177        })
178        .unwrap_or_else(|| EnumerationValue {
179            value: 0,
180            enumeration: crate::typeregister::BUILTIN.with(|e| e.enums.PopupClosePolicy.clone()),
181        });
182
183    let popup_comp = Rc::new(Component {
184        root_element: popup_window_element.clone(),
185        parent_element: RefCell::new(Rc::downgrade(parent_element)),
186        ..Component::default()
187    });
188
189    let weak = Rc::downgrade(&popup_comp);
190    recurse_elem(&popup_comp.root_element, &(), &mut |e, _| {
191        e.borrow_mut().enclosing_component = weak.clone()
192    });
193
194    // The PopupWindow's `is-open` property is read from the *parent* component (for example to rotate a
195    // ComboBox's arrow while the dropdown is shown). Since the popup now lives in its own component and
196    // is only instantiated while shown, that read cannot resolve into the popup. Instead, synthesize a
197    // property on the parent and redirect every `<popup>.is-open` reference to it. The runtime keeps
198    // this property in sync (true on show, false on close); see the generators and window.rs.
199    let is_open = {
200        let mut referenced = false;
201        visit_all_named_references(&parent_component, &mut |nr| {
202            if Rc::ptr_eq(&nr.element(), popup_window_element) && nr.name() == "is-open" {
203                referenced = true;
204            }
205        });
206        if referenced {
207            let name = format_smolstr!("popup-{}-is-open", popup_window_element.borrow().id);
208            parent_component
209                .root_element
210                .borrow_mut()
211                .property_declarations
212                .insert(name.clone(), Type::Bool.into());
213            let is_open_ref = NamedReference::new(&parent_component.root_element, name);
214            // The runtime writes this property through a generated setter that the (LLR) optimizer
215            // cannot see, so mark it as set to keep it from being constant-folded to its default.
216            is_open_ref.mark_as_set();
217            let target = is_open_ref.clone();
218            visit_all_named_references(&parent_component, &mut |nr| {
219                if Rc::ptr_eq(&nr.element(), popup_window_element) && nr.name() == "is-open" {
220                    *nr = target.clone();
221                }
222            });
223            Some(is_open_ref)
224        } else {
225            None
226        }
227    };
228
229    // Take a reference to the x/y coordinates, to be read when calling show_popup(), and
230    // converted to absolute coordinates in the run-time library.
231    let coord_x = NamedReference::new(&popup_comp.root_element, SmolStr::new_static("x"));
232    let coord_y = NamedReference::new(&popup_comp.root_element, SmolStr::new_static("y"));
233
234    // Meanwhile, set the geometry x/y to zero, because we'll be shown as a top-level and
235    // children should be rendered starting with a (0, 0) offset.
236    {
237        let mut popup_mut = popup_comp.root_element.borrow_mut();
238        let name = format_smolstr!("popup-{}-dummy", popup_mut.id);
239        popup_mut.property_declarations.insert(name.clone(), Type::LogicalLength.into());
240        drop(popup_mut);
241        let dummy1 = NamedReference::new(&popup_comp.root_element, name.clone());
242        let dummy2 = NamedReference::new(&popup_comp.root_element, name.clone());
243        let mut popup_mut = popup_comp.root_element.borrow_mut();
244        popup_mut.geometry_props.as_mut().unwrap().x = dummy1;
245        popup_mut.geometry_props.as_mut().unwrap().y = dummy2;
246    }
247
248    check_no_reference_to_popup(popup_window_element, &parent_component, &weak, &coord_x, diag);
249
250    if matches!(popup_window_element.borrow().base_type, ElementType::Builtin(_)) {
251        popup_window_element.borrow_mut().base_type = window_type.clone();
252    }
253
254    super::focus_handling::call_focus_on_init(&popup_comp);
255
256    parent_component.popup_windows.borrow_mut().push(PopupWindow {
257        component: popup_comp,
258        x: coord_x,
259        y: coord_y,
260        close_policy,
261        parent_element: parent_element.clone(),
262        is_tooltip: popup_window_element.borrow().is_tooltip,
263        is_open,
264    });
265}
266
267fn report_const_error(prop: &str, span: &Option<SourceLocation>, diag: &mut BuildDiagnostics) {
268    diag.push_error(format!("The {prop} property only supports constants at the moment"), span);
269}
270
271/// Throw error when accessing the popup from outside
272// FIXME:
273// - the span is the span of the PopupWindow, that's wrong, we should have the span of the reference
274// - There are other object reference than in the NamedReference
275// - Maybe this should actually be allowed
276pub fn check_no_reference_to_popup(
277    popup_window_element: &ElementRc,
278    parent_component: &Rc<Component>,
279    new_weak: &Weak<Component>,
280    random_valid_ref: &NamedReference,
281    diag: &mut BuildDiagnostics,
282) {
283    visit_all_named_references(parent_component, &mut |nr| {
284        let element = &nr.element();
285        if check_element(element, new_weak, diag, popup_window_element, nr.name()) {
286            // just set it to whatever is a valid NamedReference, otherwise we'll panic later
287            *nr = random_valid_ref.clone();
288        }
289    });
290    visit_all_expressions(parent_component, |exp, _| {
291        exp.visit_recursive_mut(&mut |exp| {
292            if let Expression::ElementReference(element) = exp {
293                let elem = element.upgrade().unwrap();
294                if !Rc::ptr_eq(&elem, popup_window_element) {
295                    check_element(&elem, new_weak, diag, popup_window_element, "");
296                }
297            }
298        });
299    });
300}
301
302fn check_element(
303    element: &ElementRc,
304    popup_comp: &Weak<Component>,
305    diag: &mut BuildDiagnostics,
306    popup_window_element: &ElementRc,
307    prop_name: &str,
308) -> bool {
309    if Weak::ptr_eq(&element.borrow().enclosing_component, popup_comp) {
310        let element_name = popup_window_element
311            .borrow()
312            .builtin_type()
313            .map(|t| t.name.clone())
314            .unwrap_or_else(|| SmolStr::new_static("PopupWindow"));
315        let id = element.borrow().id.clone();
316        let what = if prop_name.is_empty() {
317            if id.is_empty() { "something".into() } else { format!("element '{id}'") }
318        } else if id.is_empty() {
319            format!("property or callback '{prop_name}'")
320        } else {
321            format!("property or callback '{id}.{prop_name}'")
322        };
323
324        diag.push_error(
325            format!("Cannot access {what} inside of a {element_name} from enclosing component"),
326            &*popup_window_element.borrow(),
327        );
328        true
329    } else {
330        false
331    }
332}