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::{format_smolstr, 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        if !matches!(
71            super::ignore_debug_hooks(&binding.borrow().expression),
72            Expression::EnumerationValue(_)
73        ) {
74            report_const_error(CLOSE_POLICY, &binding.borrow().span, diag);
75        }
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        if Rc::ptr_eq(&parent_cip.parent, parent_element) && parent_cip.insertion_index > index {
110            parent_cip.insertion_index -= 1;
111        }
112    }
113
114    if matches!(popup_window_element.borrow().base_type, ElementType::Builtin(_)) {
115        popup_window_element.borrow_mut().base_type = window_type.clone();
116    }
117
118    let map_close_on_click_value = |b: &BindingExpression| {
119        let Expression::BoolLiteral(v) = super::ignore_debug_hooks(&b.expression) else {
120            assert!(diag.has_errors());
121            return None;
122        };
123        let enum_ty = crate::typeregister::BUILTIN.with(|e| e.enums.PopupClosePolicy.clone());
124        let s = if *v { "close-on-click" } else { "no-auto-close" };
125        Some(EnumerationValue {
126            value: enum_ty.values.iter().position(|v| v == s).unwrap(),
127            enumeration: enum_ty,
128        })
129    };
130
131    let close_policy =
132        popup_window_element.borrow_mut().bindings.remove(CLOSE_POLICY).and_then(|b| {
133            let b = b.into_inner();
134            if let Expression::EnumerationValue(v) = super::ignore_debug_hooks(&b.expression) {
135                Some(v.clone())
136            } else {
137                assert!(diag.has_errors());
138                None
139            }
140        });
141    let close_policy = close_policy
142        .or_else(|| {
143            popup_window_element
144                .borrow_mut()
145                .bindings
146                .remove(CLOSE_ON_CLICK)
147                .and_then(|b| map_close_on_click_value(&b.borrow()))
148        })
149        .or_else(|| {
150            // check bases
151            let mut base = popup_window_element.borrow().base_type.clone();
152            while let ElementType::Component(b) = base {
153                let base_policy = b
154                    .root_element
155                    .borrow()
156                    .bindings
157                    .get(CLOSE_POLICY)
158                    .and_then(|b| {
159                        let b = b.borrow();
160                        if let Expression::EnumerationValue(v) = &b.expression {
161                            return Some(v.clone());
162                        }
163                        assert!(diag.has_errors());
164                        None
165                    })
166                    .or_else(|| {
167                        b.root_element
168                            .borrow()
169                            .bindings
170                            .get(CLOSE_ON_CLICK)
171                            .and_then(|b| map_close_on_click_value(&b.borrow()))
172                    });
173                if let Some(base_policy) = base_policy {
174                    return Some(base_policy);
175                }
176                base = b.root_element.borrow().base_type.clone();
177            }
178            None
179        })
180        .unwrap_or_else(|| EnumerationValue {
181            value: 0,
182            enumeration: crate::typeregister::BUILTIN.with(|e| e.enums.PopupClosePolicy.clone()),
183        });
184
185    let popup_comp = Rc::new(Component {
186        root_element: popup_window_element.clone(),
187        parent_element: Rc::downgrade(parent_element),
188        ..Component::default()
189    });
190
191    let weak = Rc::downgrade(&popup_comp);
192    recurse_elem(&popup_comp.root_element, &(), &mut |e, _| {
193        e.borrow_mut().enclosing_component = weak.clone()
194    });
195
196    // Take a reference to the x/y coordinates, to be read when calling show_popup(), and
197    // converted to absolute coordinates in the run-time library.
198    let coord_x = NamedReference::new(&popup_comp.root_element, SmolStr::new_static("x"));
199    let coord_y = NamedReference::new(&popup_comp.root_element, SmolStr::new_static("y"));
200
201    // Meanwhile, set the geometry x/y to zero, because we'll be shown as a top-level and
202    // children should be rendered starting with a (0, 0) offset.
203    {
204        let mut popup_mut = popup_comp.root_element.borrow_mut();
205        let name = format_smolstr!("popup-{}-dummy", popup_mut.id);
206        popup_mut.property_declarations.insert(name.clone(), Type::LogicalLength.into());
207        drop(popup_mut);
208        let dummy1 = NamedReference::new(&popup_comp.root_element, name.clone());
209        let dummy2 = NamedReference::new(&popup_comp.root_element, name.clone());
210        let mut popup_mut = popup_comp.root_element.borrow_mut();
211        popup_mut.geometry_props.as_mut().unwrap().x = dummy1;
212        popup_mut.geometry_props.as_mut().unwrap().y = dummy2;
213    }
214
215    // Throw error when accessing the popup from outside
216    // FIXME:
217    // - the span is the span of the PopupWindow, that's wrong, we should have the span of the reference
218    // - There are other object reference than in the NamedReference
219    // - Maybe this should actually be allowed
220    visit_all_named_references(&parent_component, &mut |nr| {
221        let element = &nr.element();
222        if check_element(element, &weak, diag, popup_window_element) {
223            // just set it to whatever is a valid NamedReference, otherwise we'll panic later
224            *nr = coord_x.clone();
225        }
226    });
227    visit_all_expressions(&parent_component, |exp, _| {
228        exp.visit_recursive_mut(&mut |exp| {
229            if let Expression::ElementReference(ref element) = exp {
230                let elem = element.upgrade().unwrap();
231                if !Rc::ptr_eq(&elem, popup_window_element) {
232                    check_element(&elem, &weak, diag, popup_window_element);
233                }
234            }
235        });
236    });
237
238    super::focus_handling::call_focus_on_init(&popup_comp);
239
240    parent_component.popup_windows.borrow_mut().push(PopupWindow {
241        component: popup_comp,
242        x: coord_x,
243        y: coord_y,
244        close_policy,
245        parent_element: parent_element.clone(),
246    });
247}
248
249fn report_const_error(prop: &str, span: &Option<SourceLocation>, diag: &mut BuildDiagnostics) {
250    diag.push_error(format!("The {prop} property only supports constants at the moment"), span);
251}
252
253fn check_element(
254    element: &ElementRc,
255    popup_comp: &Weak<Component>,
256    diag: &mut BuildDiagnostics,
257    popup_window_element: &ElementRc,
258) -> bool {
259    if Weak::ptr_eq(&element.borrow().enclosing_component, popup_comp) {
260        diag.push_error(
261            "Cannot access the inside of a PopupWindow from enclosing component".into(),
262            &*popup_window_element.borrow(),
263        );
264        true
265    } else {
266        false
267    }
268}