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    // Take a reference to the x/y coordinates, to be read when calling show_popup(), and
195    // converted to absolute coordinates in the run-time library.
196    let coord_x = NamedReference::new(&popup_comp.root_element, SmolStr::new_static("x"));
197    let coord_y = NamedReference::new(&popup_comp.root_element, SmolStr::new_static("y"));
198
199    // Meanwhile, set the geometry x/y to zero, because we'll be shown as a top-level and
200    // children should be rendered starting with a (0, 0) offset.
201    {
202        let mut popup_mut = popup_comp.root_element.borrow_mut();
203        let name = format_smolstr!("popup-{}-dummy", popup_mut.id);
204        popup_mut.property_declarations.insert(name.clone(), Type::LogicalLength.into());
205        drop(popup_mut);
206        let dummy1 = NamedReference::new(&popup_comp.root_element, name.clone());
207        let dummy2 = NamedReference::new(&popup_comp.root_element, name.clone());
208        let mut popup_mut = popup_comp.root_element.borrow_mut();
209        popup_mut.geometry_props.as_mut().unwrap().x = dummy1;
210        popup_mut.geometry_props.as_mut().unwrap().y = dummy2;
211    }
212
213    check_no_reference_to_popup(popup_window_element, &parent_component, &weak, &coord_x, diag);
214
215    if matches!(popup_window_element.borrow().base_type, ElementType::Builtin(_)) {
216        popup_window_element.borrow_mut().base_type = window_type.clone();
217    }
218
219    super::focus_handling::call_focus_on_init(&popup_comp);
220
221    parent_component.popup_windows.borrow_mut().push(PopupWindow {
222        component: popup_comp,
223        x: coord_x,
224        y: coord_y,
225        close_policy,
226        parent_element: parent_element.clone(),
227    });
228}
229
230fn report_const_error(prop: &str, span: &Option<SourceLocation>, diag: &mut BuildDiagnostics) {
231    diag.push_error(format!("The {prop} property only supports constants at the moment"), span);
232}
233
234/// Throw error when accessing the popup from outside
235// FIXME:
236// - the span is the span of the PopupWindow, that's wrong, we should have the span of the reference
237// - There are other object reference than in the NamedReference
238// - Maybe this should actually be allowed
239pub fn check_no_reference_to_popup(
240    popup_window_element: &ElementRc,
241    parent_component: &Rc<Component>,
242    new_weak: &Weak<Component>,
243    random_valid_ref: &NamedReference,
244    diag: &mut BuildDiagnostics,
245) {
246    visit_all_named_references(parent_component, &mut |nr| {
247        let element = &nr.element();
248        if check_element(element, new_weak, diag, popup_window_element, nr.name()) {
249            // just set it to whatever is a valid NamedReference, otherwise we'll panic later
250            *nr = random_valid_ref.clone();
251        }
252    });
253    visit_all_expressions(parent_component, |exp, _| {
254        exp.visit_recursive_mut(&mut |exp| {
255            if let Expression::ElementReference(element) = exp {
256                let elem = element.upgrade().unwrap();
257                if !Rc::ptr_eq(&elem, popup_window_element) {
258                    check_element(&elem, new_weak, diag, popup_window_element, "");
259                }
260            }
261        });
262    });
263}
264
265fn check_element(
266    element: &ElementRc,
267    popup_comp: &Weak<Component>,
268    diag: &mut BuildDiagnostics,
269    popup_window_element: &ElementRc,
270    prop_name: &str,
271) -> bool {
272    if Weak::ptr_eq(&element.borrow().enclosing_component, popup_comp) {
273        let element_name = popup_window_element
274            .borrow()
275            .builtin_type()
276            .map(|t| t.name.clone())
277            .unwrap_or_else(|| SmolStr::new_static("PopupWindow"));
278        let id = element.borrow().id.clone();
279        let what = if prop_name.is_empty() {
280            if id.is_empty() { "something".into() } else { format!("element '{id}'") }
281        } else if id.is_empty() {
282            format!("property or callback '{prop_name}'")
283        } else {
284            format!("property or callback '{id}.{prop_name}'")
285        };
286
287        diag.push_error(
288            format!("Cannot access {what} inside of a {element_name} from enclosing component"),
289            &*popup_window_element.borrow(),
290        );
291        true
292    } else {
293        false
294    }
295}