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().binding(CLOSE_ON_CLICK) {
52        if popup_window_element.borrow().binding(CLOSE_POLICY).is_some() {
53            diag.push_error(
54                "close-policy and close-on-click cannot be set at the same time".into(),
55                &binding.span,
56            );
57        } else {
58            diag.push_property_deprecation_warning(CLOSE_ON_CLICK, CLOSE_POLICY, &binding.span);
59            if !matches!(binding.value_expression(), Expression::BoolLiteral(_)) {
60                report_const_error(CLOSE_ON_CLICK, &binding.span, diag);
61            }
62        }
63    } else if let Some(binding) = popup_window_element.borrow().binding(CLOSE_POLICY)
64        && !matches!(binding.value_expression(), Expression::EnumerationValue(_))
65    {
66        report_const_error(CLOSE_POLICY, &binding.span, diag);
67    }
68
69    let parent_component = popup_window_element.borrow().enclosing_component.upgrade().unwrap();
70    let parent_element = match parent_element {
71        None => {
72            if matches!(popup_window_element.borrow().base_type, ElementType::Builtin(_)) {
73                popup_window_element.borrow_mut().base_type = window_type.clone();
74            }
75            parent_component.inherits_popup_window.set(true);
76            return;
77        }
78        Some(parent_element) => parent_element,
79    };
80
81    if Rc::ptr_eq(&parent_component.root_element, popup_window_element) {
82        diag.push_error(
83            "PopupWindow cannot be directly repeated or conditional".into(),
84            &*popup_window_element.borrow(),
85        );
86        return;
87    }
88
89    // Remove the popup_window_element from its parent
90    let mut parent_element_borrowed = parent_element.borrow_mut();
91    let index = parent_element_borrowed
92        .children
93        .iter()
94        .position(|child| Rc::ptr_eq(child, popup_window_element))
95        .expect("PopupWindow must be a child of its parent");
96    parent_element_borrowed.children.remove(index);
97    parent_element_borrowed.has_popup_child = true;
98    drop(parent_element_borrowed);
99    for parent_cip in parent_component.child_insertion_points.borrow_mut().values_mut() {
100        if Rc::ptr_eq(&parent_cip.parent, parent_element) && parent_cip.insertion_index > index {
101            parent_cip.insertion_index -= 1;
102        }
103    }
104
105    let map_close_on_click_value = |b: &BindingExpression| {
106        let Expression::BoolLiteral(v) = b.expression.ignore_debug_hooks() else {
107            assert!(diag.has_errors());
108            return None;
109        };
110        let enum_ty = crate::typeregister::BUILTIN.enums.PopupClosePolicy.clone();
111        let s = if *v { "close-on-click" } else { "no-auto-close" };
112        Some(EnumerationValue {
113            value: enum_ty.values.iter().position(|v| v == s).unwrap(),
114            enumeration: enum_ty,
115        })
116    };
117
118    let close_policy = popup_window_element.borrow_mut().take_binding(CLOSE_POLICY).and_then(|b| {
119        if let Expression::EnumerationValue(v) = b.expression.ignore_debug_hooks() {
120            Some(v.clone())
121        } else {
122            assert!(diag.has_errors());
123            None
124        }
125    });
126    let close_policy = close_policy
127        .or_else(|| {
128            popup_window_element
129                .borrow_mut()
130                .take_binding(CLOSE_ON_CLICK)
131                .and_then(|b| map_close_on_click_value(&b))
132        })
133        .or_else(|| {
134            // check bases
135            let mut base = popup_window_element.borrow().base_type.clone();
136            while let ElementType::Component(b) = base {
137                let base_policy = b
138                    .root_element
139                    .borrow()
140                    .binding(CLOSE_POLICY)
141                    .and_then(|b| {
142                        if let Expression::EnumerationValue(v) = b.value_expression() {
143                            return Some(v.clone());
144                        }
145                        assert!(diag.has_errors());
146                        None
147                    })
148                    .or_else(|| {
149                        b.root_element
150                            .borrow()
151                            .binding(CLOSE_ON_CLICK)
152                            .and_then(|b| map_close_on_click_value(&b))
153                    });
154                if let Some(base_policy) = base_policy {
155                    return Some(base_policy);
156                }
157                base = b.root_element.borrow().base_type.clone();
158            }
159            None
160        })
161        .unwrap_or_else(|| EnumerationValue {
162            value: 0,
163            enumeration: crate::typeregister::BUILTIN.enums.PopupClosePolicy.clone(),
164        });
165
166    let popup_comp = Rc::new(Component {
167        root_element: popup_window_element.clone(),
168        parent_element: RefCell::new(Rc::downgrade(parent_element)),
169        ..Component::default()
170    });
171
172    let weak = Rc::downgrade(&popup_comp);
173    recurse_elem(&popup_comp.root_element, &(), &mut |e, _| {
174        e.borrow_mut().enclosing_component = weak.clone()
175    });
176
177    // The PopupWindow's `is-open` property is read from the *parent* component (for example to rotate a
178    // ComboBox's arrow while the dropdown is shown). Since the popup now lives in its own component and
179    // is only instantiated while shown, that read cannot resolve into the popup. Instead, synthesize a
180    // property on the parent and redirect every `<popup>.is-open` reference to it. The runtime keeps
181    // this property in sync (true on show, false on close); see the generators and window.rs.
182    let is_open = {
183        let mut referenced = false;
184        visit_all_named_references(&parent_component, &mut |nr| {
185            if Rc::ptr_eq(&nr.element(), popup_window_element) && nr.name() == "is-open" {
186                referenced = true;
187            }
188        });
189        if referenced {
190            // A base component may already have a popup with the same id
191            let is_open_ref = crate::layout::create_new_prop(
192                &parent_component.root_element,
193                format_smolstr!("popup-{}-is-open", popup_window_element.borrow().id),
194                Type::Bool,
195            );
196            // The runtime writes this property through a generated setter that the (LLR) optimizer
197            // cannot see, so mark it as set to keep it from being constant-folded to its default.
198            is_open_ref.mark_as_set();
199            let target = is_open_ref.clone();
200            visit_all_named_references(&parent_component, &mut |nr| {
201                if Rc::ptr_eq(&nr.element(), popup_window_element) && nr.name() == "is-open" {
202                    *nr = target.clone();
203                }
204            });
205            Some(is_open_ref)
206        } else {
207            None
208        }
209    };
210
211    // Take a reference to the x/y coordinates, to be read when calling show_popup(), and
212    // converted to absolute coordinates in the run-time library.
213    let coord_x = NamedReference::new(&popup_comp.root_element, SmolStr::new_static("x"));
214    let coord_y = NamedReference::new(&popup_comp.root_element, SmolStr::new_static("y"));
215
216    // Meanwhile, set the geometry x/y to zero, because we'll be shown as a top-level and
217    // children should be rendered starting with a (0, 0) offset.
218    {
219        let mut popup_mut = popup_comp.root_element.borrow_mut();
220        let name = format_smolstr!("popup-{}-dummy", popup_mut.id);
221        popup_mut.property_declarations.insert(name.clone(), Type::LogicalLength.into());
222        drop(popup_mut);
223        let dummy1 = NamedReference::new(&popup_comp.root_element, name.clone());
224        let dummy2 = NamedReference::new(&popup_comp.root_element, name.clone());
225        let mut popup_mut = popup_comp.root_element.borrow_mut();
226        popup_mut.geometry_props.as_mut().unwrap().x = dummy1;
227        popup_mut.geometry_props.as_mut().unwrap().y = dummy2;
228    }
229
230    check_no_reference_to_popup(popup_window_element, &parent_component, &weak, &coord_x, diag);
231
232    if matches!(popup_window_element.borrow().base_type, ElementType::Builtin(_)) {
233        popup_window_element.borrow_mut().base_type = window_type.clone();
234    }
235
236    super::focus_handling::call_focus_on_init(&popup_comp);
237
238    parent_component.popup_windows.borrow_mut().push(PopupWindow {
239        component: popup_comp,
240        x: coord_x,
241        y: coord_y,
242        close_policy,
243        parent_element: parent_element.clone(),
244        is_tooltip: popup_window_element.borrow().is_tooltip,
245        is_open,
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
253/// Throw error when accessing the popup from outside
254// FIXME:
255// - the span is the span of the PopupWindow, that's wrong, we should have the span of the reference
256// - There are other object reference than in the NamedReference
257// - Maybe this should actually be allowed
258pub fn check_no_reference_to_popup(
259    popup_window_element: &ElementRc,
260    parent_component: &Rc<Component>,
261    new_weak: &Weak<Component>,
262    random_valid_ref: &NamedReference,
263    diag: &mut BuildDiagnostics,
264) {
265    visit_all_named_references(parent_component, &mut |nr| {
266        let element = &nr.element();
267        if check_element(element, new_weak, diag, popup_window_element, nr.name()) {
268            // just set it to whatever is a valid NamedReference, otherwise we'll panic later
269            *nr = random_valid_ref.clone();
270        }
271    });
272    visit_all_expressions(parent_component, |exp, _| {
273        exp.visit_recursive_mut(&mut |exp| {
274            if let Expression::ElementReference(element) = exp {
275                let elem = element.upgrade().unwrap();
276                if !Rc::ptr_eq(&elem, popup_window_element) {
277                    check_element(&elem, new_weak, diag, popup_window_element, "");
278                }
279            }
280        });
281    });
282}
283
284fn check_element(
285    element: &ElementRc,
286    popup_comp: &Weak<Component>,
287    diag: &mut BuildDiagnostics,
288    popup_window_element: &ElementRc,
289    prop_name: &str,
290) -> bool {
291    if Weak::ptr_eq(&element.borrow().enclosing_component, popup_comp) {
292        let element_name = popup_window_element
293            .borrow()
294            .builtin_type()
295            .map(|t| t.name.clone())
296            .unwrap_or_else(|| SmolStr::new_static("PopupWindow"));
297        let id = element.borrow().id.clone();
298        let what = if prop_name.is_empty() {
299            if id.is_empty() { "something".into() } else { format!("element '{id}'") }
300        } else if id.is_empty() {
301            format!("property or callback '{prop_name}'")
302        } else {
303            format!("property or callback '{id}.{prop_name}'")
304        };
305
306        diag.push_error(
307            format!("Cannot access {what} inside of a {element_name} from enclosing component"),
308            &*popup_window_element.borrow(),
309        );
310        true
311    } else {
312        false
313    }
314}