Skip to main content

i_slint_compiler/passes/
compile_paths.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 converts the verbose markup used for paths, such as
5//!    Path {
6//!        LineTo { ... } ArcTo { ... }
7//!    }
8//! to a vector of path elements (PathData) that is assigned to the
9//! elements property of the Path element. That way the generators have to deal
10//! with path embedding only as part of the property assignment.
11
12use crate::diagnostics::BuildDiagnostics;
13use crate::expression_tree::*;
14use crate::langtype::{BuiltinElement, BuiltinStruct, ElementType, Struct, Type};
15use crate::object_tree::*;
16use smol_str::SmolStr;
17use std::rc::Rc;
18use std::sync::Arc;
19
20pub fn compile_paths(
21    component: &Rc<Component>,
22    tr: &crate::typeregister::TypeRegister,
23    diag: &mut BuildDiagnostics,
24) {
25    let path_type = tr.lookup_element("Path").unwrap();
26    let path_type = path_type.as_builtin();
27
28    recurse_elem_including_sub_components_no_borrow(component, &(), &mut |elem_, _| {
29        if elem_.borrow().builtin_type().is_none_or(|bt| bt.name != "Path") {
30            return;
31        }
32
33        let commands_binding = elem_.borrow_mut().take_binding("commands");
34
35        let path_data_binding = if let Some(commands_expr) = commands_binding {
36            if let Some(path_child) = elem_
37                .borrow()
38                .children
39                .iter()
40                .find(|child| path_element_type(child, path_type).is_some())
41            {
42                diag.push_error(
43                    "Path elements cannot be mixed with the use of the SVG commands property"
44                        .into(),
45                    &*path_child.borrow(),
46                );
47                return;
48            }
49
50            match &commands_expr.expression {
51                Expression::StringLiteral(commands) => {
52                    match compile_path_from_string_literal(commands) {
53                        Ok(binding) => binding,
54                        Err(e) => {
55                            diag.push_error(
56                                format!("Error parsing SVG commands ({e})"),
57                                &commands_expr,
58                            );
59                            return;
60                        }
61                    }
62                }
63                expr if expr.ty() == Type::String => Expression::PathData(
64                    crate::expression_tree::Path::Commands(Box::new(commands_expr.expression)),
65                )
66                .into(),
67                _ => {
68                    diag.push_error(
69                        "The commands property only accepts strings".into(),
70                        &*elem_.borrow(),
71                    );
72                    return;
73                }
74            }
75        } else {
76            let mut elem = elem_.borrow_mut();
77            let enclosing_component = elem.enclosing_component.upgrade().unwrap();
78            let new_children = Vec::with_capacity(elem.children.len());
79            let old_children = std::mem::replace(&mut elem.children, new_children);
80
81            let mut path_data = Vec::new();
82
83            for child in old_children {
84                if let Some(element_type) = path_element_type(&child, path_type).cloned() {
85                    if child.borrow().repeated.is_some() {
86                        diag.push_error(
87                            "Path elements are not supported with `for`-`in` syntax, yet (https://github.com/slint-ui/slint/issues/754)".into(),
88                            &*child.borrow(),
89                        );
90                    } else {
91                        let mut bindings = std::collections::BTreeMap::new();
92                        {
93                            let mut child = child.borrow_mut();
94                            for k in element_type.properties.keys() {
95                                if let Some(binding) = child.take_binding(k) {
96                                    bindings.insert(k.clone(), binding.into());
97                                }
98                            }
99                        }
100                        path_data.push(PathElement { element_type, bindings });
101                        enclosing_component.optimized_elements.borrow_mut().push(child);
102                    }
103                } else {
104                    elem.children.push(child);
105                }
106            }
107
108            if path_data.is_empty() {
109                // Keep the elements a base component may have compiled already
110                return;
111            }
112
113            Expression::PathData(crate::expression_tree::Path::Elements(path_data)).into()
114        };
115
116        elem_.borrow_mut().set_binding(SmolStr::new_static("elements"), path_data_binding);
117    });
118}
119
120/// Reports path elements or `commands` given to an instance of a component whose `Path` base
121/// already declares path elements, and a children placeholder in such a `Path`.
122///
123/// Runs before inlining, while the path elements of the base are still its children.
124/// A `Path` is populated in one place only: a base that declares nothing can be filled by the
125/// instance, but there is no appending to what the base declares.
126pub fn check_derived_paths(
127    component: &Rc<Component>,
128    tr: &crate::typeregister::TypeRegister,
129    diag: &mut BuildDiagnostics,
130) {
131    let path_type = tr.lookup_element("Path").unwrap();
132    let path_type = path_type.as_builtin();
133
134    for (name, cip) in component.child_insertion_points.borrow().iter() {
135        if declares_path_elements(&cip.parent.borrow(), path_type) {
136            diag.push_error(
137                format!(
138                    "{} cannot be placed in a Path that already has path elements",
139                    slot_error_subject(name)
140                ),
141                &cip.node,
142            );
143        }
144    }
145
146    recurse_elem_including_sub_components_no_borrow(component, &(), &mut |elem, _| {
147        let elem = elem.borrow();
148        let ElementType::Component(base) = &elem.base_type else { return };
149        if elem.children.is_empty() && elem.binding("commands").is_none() {
150            return;
151        }
152        if base.child_insertion_points.borrow().contains_key(DEFAULT_SLOT_NAME) {
153            // The children go to the placeholder, which is checked above
154            return;
155        }
156        if declares_path_elements(&base.root_element.borrow(), path_type) {
157            diag.push_error(
158                "The Path was already populated in the base type and it can't be re-populated again"
159                    .into(),
160                &*elem,
161            );
162        }
163    });
164}
165
166/// Whether `elem`, or the root of a component it derives from, has path element children
167fn declares_path_elements(elem: &Element, path_type: &BuiltinElement) -> bool {
168    elem.builtin_type().is_some_and(|builtin| builtin.name == path_type.name)
169        && elem.any_in_inheritance_chain(|e| {
170            e.children.iter().any(|child| path_element_type(child, path_type).is_some())
171        })
172}
173
174/// The path element type of `child` when it is a `MoveTo`, `LineTo`, ... element
175fn path_element_type<'a>(
176    child: &ElementRc,
177    path_type: &'a BuiltinElement,
178) -> Option<&'a Rc<BuiltinElement>> {
179    let builtin = child.borrow().builtin_type()?;
180    path_type.additional_accepted_child_types.get(&builtin.native_class.class_name)
181}
182
183fn compile_path_from_string_literal(
184    commands: &str,
185) -> Result<BindingExpression, lyon_extra::parser::ParseError> {
186    let mut builder = lyon_path::Path::builder();
187    let mut parser = lyon_extra::parser::PathParser::new();
188    parser.parse(
189        &lyon_extra::parser::ParserOptions::DEFAULT,
190        &mut lyon_extra::parser::Source::new(commands.chars()),
191        &mut builder,
192    )?;
193    let path = builder.build();
194
195    let event_enum = crate::typeregister::BUILTIN.enums.PathEvent.clone();
196    let point_type = Arc::new(Struct::new(
197        IntoIterator::into_iter([
198            (SmolStr::new_static("x"), Type::Float32),
199            (SmolStr::new_static("y"), Type::Float32),
200        ])
201        .collect(),
202        BuiltinStruct::Point,
203    ));
204
205    let mut points = Vec::new();
206    let events = path
207        .into_iter()
208        .map(|event| {
209            Expression::EnumerationValue(match event {
210                lyon_path::Event::Begin { at } => {
211                    points.push(at);
212                    event_enum.clone().try_value_from_string("begin").unwrap()
213                }
214                lyon_path::Event::Line { from, to } => {
215                    points.push(from);
216                    points.push(to);
217
218                    event_enum.clone().try_value_from_string("line").unwrap()
219                }
220                lyon_path::Event::Quadratic { from, ctrl, to } => {
221                    points.push(from);
222                    points.push(ctrl);
223                    points.push(to);
224
225                    event_enum.clone().try_value_from_string("quadratic").unwrap()
226                }
227                lyon_path::Event::Cubic { from, ctrl1, ctrl2, to } => {
228                    points.push(from);
229                    points.push(ctrl1);
230                    points.push(ctrl2);
231                    points.push(to);
232                    event_enum.clone().try_value_from_string("cubic").unwrap()
233                }
234                lyon_path::Event::End { first: _, last: _, close } => {
235                    if close {
236                        event_enum.clone().try_value_from_string("end-closed").unwrap()
237                    } else {
238                        event_enum.clone().try_value_from_string("end-open").unwrap()
239                    }
240                }
241            })
242        })
243        .collect();
244
245    let points = points
246        .into_iter()
247        .map(|point| Expression::Struct {
248            ty: point_type.clone(),
249            values: IntoIterator::into_iter([
250                (SmolStr::new_static("x"), Expression::NumberLiteral(point.x as _, Unit::None)),
251                (SmolStr::new_static("y"), Expression::NumberLiteral(point.y as _, Unit::None)),
252            ])
253            .collect(),
254        })
255        .collect();
256
257    Ok(Expression::PathData(Path::Events(events, points)).into())
258}