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