Skip to main content

i_slint_compiler/passes/
lower_tabwidget.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// cSpell: ignore tabwidget
5
6//! This pass lowers the TabWidget to create the tabbar.
7//!
8//! Must be done before inlining and many other passes because the lowered code must
9//! be further inlined as it may expand to a native widget that needs inlining
10
11use crate::diagnostics::BuildDiagnostics;
12use crate::expression_tree::{BindingExpression, Expression, MinMaxOp, NamedReference, Unit};
13use crate::langtype::{ElementType, Type};
14use crate::object_tree::*;
15use smol_str::{SmolStr, format_smolstr};
16use std::collections::HashSet;
17use std::rc::Rc;
18
19pub async fn lower_tabwidget(
20    doc: &Document,
21    type_loader: &mut crate::typeloader::TypeLoader,
22    diag: &mut BuildDiagnostics,
23) {
24    // Collect before lowering: lowering rewrites base_type, which would hide
25    // other TabWidget elements from builtin_type() (an instance and the style
26    // wrapper both match). Dedup a sub-component root visited more than once.
27    // The empty check below also avoids loading std-widgets.slint when unused.
28    let mut seen = HashSet::new();
29    let mut tab_widgets = Vec::new();
30    doc.visit_all_used_components(|component| {
31        recurse_elem_including_sub_components_no_borrow(component, &(), &mut |elem, _| {
32            if matches!(&elem.borrow().builtin_type(), Some(b) if b.name == "TabWidget")
33                && seen.insert(Rc::as_ptr(elem))
34            {
35                tab_widgets.push(elem.clone());
36            }
37        })
38    });
39
40    if tab_widgets.is_empty() {
41        return;
42    }
43
44    // If an element's base is itself a collected TabWidget that already has its own Tab
45    // children (e.g. `component Xyz inherits TabWidget { Tab { ... } }`), it's just
46    // instantiating Xyz rather than defining new Tabs, so skip lowering it here. Lowering it
47    // too would overwrite its base_type and lose the Tabs already lowered on Xyz (issue
48    // #8394); leaving it alone lets inlining pull Xyz's own Tabs in later.
49    //
50    // Any Tab children or an `orientation` override set directly at such an instantiation
51    // site can't be folded into the base, since that would mean moving elements across
52    // component boundaries. Diagnose both cases instead of silently dropping or ignoring them.
53    let tab_widgets: Vec<_> = tab_widgets
54        .into_iter()
55        .filter(|elem| {
56            if !base_chain_declares_tabs(&elem.borrow().base_type, &seen) {
57                return true;
58            }
59            if !elem.borrow().children.is_empty() {
60                diag.push_error(
61                    "Cannot add Tab elements when instantiating a TabWidget subclass that already declares its own Tab; declare them on the subclass instead".to_owned(),
62                    &*elem.borrow(),
63                );
64            }
65            if let Some(orientation) = elem.borrow().binding("orientation") {
66                diag.push_error(
67                    "Cannot set orientation when instantiating a TabWidget subclass that already declares its own Tab; declare it on the subclass instead".to_owned(),
68                    &orientation.span,
69                );
70            }
71            false
72        })
73        .collect();
74
75    // Ignore import errors
76    let mut build_diags_to_ignore = BuildDiagnostics::default();
77    let tabwidget_impl = type_loader
78        .import_component("std-widgets-impl.slint", "TabWidgetImpl", &mut build_diags_to_ignore)
79        .await
80        .expect("can't load TabWidgetImpl from std-widgets-impl.slint");
81    let tab_impl = type_loader
82        .import_component("std-widgets-impl.slint", "TabImpl", &mut build_diags_to_ignore)
83        .await
84        .expect("can't load TabImpl from std-widgets-impl.slint");
85    let tabbar_horizontal_impl = type_loader
86        .import_component(
87            "std-widgets-impl.slint",
88            "TabBarHorizontalImpl",
89            &mut build_diags_to_ignore,
90        )
91        .await
92        .expect("can't load TabBarHorizontalImpl from std-widgets-impl.slint");
93    let tabbar_vertical_impl = type_loader
94        .import_component(
95            "std-widgets-impl.slint",
96            "TabBarVerticalImpl",
97            &mut build_diags_to_ignore,
98        )
99        .await
100        .expect("can't load TabBarVerticalImpl from std-widgets-impl.slint");
101    let empty_type = type_loader.global_type_registry.borrow().empty_type();
102
103    for elem in &tab_widgets {
104        process_tabwidget(
105            elem,
106            ElementType::Component(tabwidget_impl.clone()),
107            ElementType::Component(tab_impl.clone()),
108            ElementType::Component(tabbar_horizontal_impl.clone()),
109            ElementType::Component(tabbar_vertical_impl.clone()),
110            &empty_type,
111            diag,
112        );
113    }
114}
115
116/// Walks a chain of `component Sub inherits Base { ... }` steps (as `base_type`s of
117/// components collected as TabWidgets), skipping empty pass-through subclasses, to check
118/// whether some component along the way already declares its own Tab children. Stops (and
119/// returns `false`) as soon as the chain leaves the collected TabWidgets, e.g. at the
120/// style-selected builtin TabWidget itself.
121fn base_chain_declares_tabs(
122    base_type: &ElementType,
123    tab_widget_roots: &HashSet<*const std::cell::RefCell<Element>>,
124) -> bool {
125    let mut base_type = base_type.clone();
126    loop {
127        let ElementType::Component(c) = &base_type else { return false };
128        if !tab_widget_roots.contains(&Rc::as_ptr(&c.root_element)) {
129            return false;
130        }
131        if !c.root_element.borrow().children.is_empty() {
132            return true;
133        }
134        let next = c.root_element.borrow().base_type.clone();
135        base_type = next;
136    }
137}
138
139fn process_tabwidget(
140    elem: &ElementRc,
141    tabwidget_impl: ElementType,
142    tab_impl: ElementType,
143    tabbar_horizontal_impl: ElementType,
144    tabbar_vertical_impl: ElementType,
145    empty_type: &ElementType,
146    diag: &mut BuildDiagnostics,
147) {
148    elem.borrow_mut().base_type = tabwidget_impl;
149    let mut children = std::mem::take(&mut elem.borrow_mut().children);
150    let num_tabs = children.len();
151    let mut tabs = Vec::new();
152    for child in &mut children {
153        if child.borrow().repeated.is_some() {
154            diag.push_error(
155                "dynamic tabs ('if' or 'for') are currently not supported".into(),
156                &*child.borrow(),
157            );
158            continue;
159        }
160        if child.borrow().base_type.to_string() != "Tab" {
161            assert!(diag.has_errors());
162            continue;
163        }
164        let index = tabs.len();
165        child.borrow_mut().base_type = empty_type.clone();
166        child
167            .borrow_mut()
168            .property_declarations
169            .insert(SmolStr::new_static("title"), Type::String.into());
170        set_geometry_prop(elem, child, "x", diag);
171        set_geometry_prop(elem, child, "y", diag);
172        set_geometry_prop(elem, child, "width", diag);
173        set_geometry_prop(elem, child, "height", diag);
174        let condition = Expression::BinaryExpression {
175            lhs: Expression::PropertyReference(NamedReference::new(
176                elem,
177                SmolStr::new_static("current-index"),
178            ))
179            .into(),
180            rhs: Expression::NumberLiteral(index as _, Unit::None).into(),
181            op: '=',
182            source_location: None,
183        };
184        let old = child.borrow_mut().set_binding(SmolStr::new_static("visible"), condition.into());
185        if let Some(old) = old {
186            diag.push_error(
187                "The property 'visible' cannot be set for Tabs inside a TabWidget".to_owned(),
188                &old,
189            );
190        }
191        let role = crate::typeregister::BUILTIN
192            .enums
193            .AccessibleRole
194            .clone()
195            .try_value_from_string("tab-panel")
196            .unwrap();
197        let old = child.borrow_mut().set_binding(
198            SmolStr::new_static("accessible-role"),
199            Expression::EnumerationValue(role).into(),
200        );
201        if let Some(old) = old {
202            diag.push_error(
203                "The property 'accessible-role' cannot be set for Tabs inside a TabWidget"
204                    .to_owned(),
205                &old,
206            );
207        }
208        let title_ref =
209            Expression::PropertyReference(NamedReference::new(child, "title".into())).into();
210        let old = child.borrow_mut().set_binding("accessible-label".into(), title_ref);
211        if let Some(old) = old {
212            diag.push_error(
213                "The property 'accessible-label' cannot be set for Tabs inside a TabWidget"
214                    .to_owned(),
215                &old,
216            );
217        }
218
219        let mut tab = Element {
220            id: format_smolstr!("{}-tab{}", elem.borrow().id, index),
221            base_type: tab_impl.clone(),
222            enclosing_component: elem.borrow().enclosing_component.clone(),
223            ..Default::default()
224        };
225        tab.set_binding(
226            SmolStr::new_static("title"),
227            BindingExpression::new_two_way(
228                NamedReference::new(child, SmolStr::new_static("title")).into(),
229            ),
230        );
231        tab.set_binding(
232            SmolStr::new_static("current"),
233            BindingExpression::new_two_way(
234                NamedReference::new(elem, SmolStr::new_static("current-index")).into(),
235            ),
236        );
237        tab.set_binding(
238            SmolStr::new_static("current-focused"),
239            BindingExpression::new_two_way(
240                NamedReference::new(elem, SmolStr::new_static("current-focused")).into(),
241            ),
242        );
243        tab.set_binding(
244            SmolStr::new_static("tab-index"),
245            Expression::NumberLiteral(index as _, Unit::None).into(),
246        );
247        tab.set_binding(
248            SmolStr::new_static("num-tabs"),
249            Expression::NumberLiteral(num_tabs as _, Unit::None).into(),
250        );
251        tabs.push(Element::make_rc(tab));
252    }
253
254    let mut tabbar_impl = tabbar_horizontal_impl;
255    if let Some(orientation) = elem.borrow().binding("orientation") {
256        if let Expression::EnumerationValue(val) = orientation.value_expression() {
257            if val.value == 1 {
258                tabbar_impl = tabbar_vertical_impl;
259            }
260        } else {
261            diag.push_error(
262                "The orientation property only supports constants at the moment".into(),
263                &orientation.span,
264            );
265        }
266    }
267    let tabbar = Element {
268        id: format_smolstr!("{}-tabbar", elem.borrow().id),
269        base_type: tabbar_impl,
270        enclosing_component: elem.borrow().enclosing_component.clone(),
271        children: tabs,
272        ..Default::default()
273    };
274    let tabbar = Element::make_rc(tabbar);
275    set_tabbar_geometry_prop(elem, &tabbar, "x");
276    set_tabbar_geometry_prop(elem, &tabbar, "y");
277    set_tabbar_geometry_prop(elem, &tabbar, "width");
278    set_tabbar_geometry_prop(elem, &tabbar, "height");
279    tabbar.borrow_mut().set_binding(
280        SmolStr::new_static("num-tabs"),
281        Expression::NumberLiteral(num_tabs as _, Unit::None).into(),
282    );
283    tabbar.borrow_mut().set_binding(
284        SmolStr::new_static("current"),
285        BindingExpression::new_two_way(
286            NamedReference::new(elem, SmolStr::new_static("current-index")).into(),
287        ),
288    );
289    elem.borrow_mut().set_binding(
290        SmolStr::new_static("current-focused"),
291        BindingExpression::new_two_way(
292            NamedReference::new(&tabbar, SmolStr::new_static("current-focused")).into(),
293        ),
294    );
295    elem.borrow_mut().set_binding(
296        SmolStr::new_static("tabbar-preferred-width"),
297        BindingExpression::new_two_way(
298            NamedReference::new(&tabbar, SmolStr::new_static("preferred-width")).into(),
299        ),
300    );
301    elem.borrow_mut().set_binding(
302        SmolStr::new_static("tabbar-preferred-height"),
303        BindingExpression::new_two_way(
304            NamedReference::new(&tabbar, SmolStr::new_static("preferred-height")).into(),
305        ),
306    );
307
308    if let Some(expr) = children
309        .iter()
310        .map(|x| {
311            Expression::PropertyReference(NamedReference::new(x, SmolStr::new_static("min-width")))
312        })
313        .reduce(|lhs, rhs| crate::builtin_macros::min_max_expression(lhs, rhs, MinMaxOp::Max))
314    {
315        elem.borrow_mut().set_binding("content-min-width".into(), expr.into());
316    };
317    if let Some(expr) = children
318        .iter()
319        .map(|x| {
320            Expression::PropertyReference(NamedReference::new(x, SmolStr::new_static("min-height")))
321        })
322        .reduce(|lhs, rhs| crate::builtin_macros::min_max_expression(lhs, rhs, MinMaxOp::Max))
323    {
324        elem.borrow_mut().set_binding("content-min-height".into(), expr.into());
325    };
326
327    elem.borrow_mut().children = std::iter::once(tabbar).chain(children).collect();
328}
329
330fn set_geometry_prop(
331    tab_widget: &ElementRc,
332    content: &ElementRc,
333    prop: &str,
334    diag: &mut BuildDiagnostics,
335) {
336    let old = content.borrow_mut().set_binding(
337        prop.into(),
338        Expression::PropertyReference(NamedReference::new(
339            tab_widget,
340            format_smolstr!("content-{}", prop),
341        ))
342        .into(),
343    );
344    if let Some(old) = old {
345        diag.push_error(
346            format!("The property '{prop}' cannot be set for Tabs inside a TabWidget"),
347            &old,
348        );
349    }
350}
351
352fn set_tabbar_geometry_prop(tab_widget: &ElementRc, tabbar: &ElementRc, prop: &str) {
353    tabbar.borrow_mut().set_binding(
354        prop.into(),
355        Expression::PropertyReference(NamedReference::new(
356            tab_widget,
357            format_smolstr!("tabbar-{}", prop),
358        ))
359        .into(),
360    );
361}