mirror of
https://github.com/slint-ui/slint.git
synced 2025-09-28 12:54:45 +00:00
Allow for fitting paths into a given bounding rectangle
... by applying a transformation. This allows designing a path in some other path design tool and then make it fit using bindings.
This commit is contained in:
parent
79ba943882
commit
992f990fa8
12 changed files with 162 additions and 36 deletions
|
@ -74,6 +74,8 @@ fn main() {
|
|||
let path_primitive = rendering_primitives_builder.create(RenderingPrimitive::Path {
|
||||
x: 50.,
|
||||
y: 300.,
|
||||
width: 0.,
|
||||
height: 0.,
|
||||
elements: PathData::Elements(SharedArray::from(TRIANGLE_PATH)),
|
||||
fill_color: Color::from_rgb(0, 128, 255),
|
||||
stroke_color: Color::BLACK,
|
||||
|
|
|
@ -929,11 +929,19 @@ fn compute_layout(component: &Rc<Component>) -> Vec<String> {
|
|||
" auto y = {};",
|
||||
compile_expression(&path_layout.y_reference, component)
|
||||
));
|
||||
res.push(format!(
|
||||
" auto width = {};",
|
||||
compile_expression(&path_layout.width_reference, component)
|
||||
));
|
||||
res.push(format!(
|
||||
" auto height = {};",
|
||||
compile_expression(&path_layout.height_reference, component)
|
||||
));
|
||||
|
||||
res.push(" sixtyfps::PathLayoutData pl { ".into());
|
||||
res.push(" &path,".to_owned());
|
||||
res.push(" {items, std::size(items)},".to_owned());
|
||||
res.push(" x, y".to_owned());
|
||||
res.push(" x, y, width, height".to_owned());
|
||||
res.push(" };".to_owned());
|
||||
res.push(" sixtyfps::solve_path_layout(&pl);".to_owned());
|
||||
res.push("}".to_owned());
|
||||
|
|
|
@ -771,6 +771,8 @@ fn compute_layout(component: &Rc<Component>) -> TokenStream {
|
|||
|
||||
let x_pos = compile_expression(&*path_layout.x_reference, &component);
|
||||
let y_pos = compile_expression(&*path_layout.y_reference, &component);
|
||||
let width = compile_expression(&*path_layout.width_reference, &component);
|
||||
let height = compile_expression(&*path_layout.width_reference, &component);
|
||||
|
||||
layouts.push(quote! {
|
||||
solve_path_layout(&PathLayoutData {
|
||||
|
@ -778,6 +780,8 @@ fn compute_layout(component: &Rc<Component>) -> TokenStream {
|
|||
elements: &#path,
|
||||
x: #x_pos,
|
||||
y: #y_pos,
|
||||
width: #width,
|
||||
height: #height,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -53,11 +53,15 @@ pub struct PathLayout {
|
|||
pub elements: Vec<ElementRc>,
|
||||
pub x_reference: Box<Expression>,
|
||||
pub y_reference: Box<Expression>,
|
||||
pub width_reference: Box<Expression>,
|
||||
pub height_reference: Box<Expression>,
|
||||
}
|
||||
|
||||
impl ExpressionFieldsVisitor for PathLayout {
|
||||
fn visit_expressions(&mut self, mut visitor: impl FnMut(&mut Expression)) {
|
||||
visitor(&mut self.x_reference);
|
||||
visitor(&mut self.y_reference);
|
||||
visitor(&mut self.width_reference);
|
||||
visitor(&mut self.height_reference);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,17 +29,16 @@ pub fn lower_layouts(component: &Rc<Component>, diag: &mut Diagnostics) {
|
|||
false
|
||||
};
|
||||
|
||||
let ref_child = child.clone();
|
||||
let prop_ref = move |name: &'static str| {
|
||||
Box::new(Expression::PropertyReference(NamedReference {
|
||||
element: Rc::downgrade(&ref_child),
|
||||
name: name.into(),
|
||||
}))
|
||||
};
|
||||
|
||||
let (x_reference, y_reference) = if is_grid_layout || is_path_layout {
|
||||
(
|
||||
Box::new(Expression::PropertyReference(NamedReference {
|
||||
element: Rc::downgrade(&child),
|
||||
name: "x".into(),
|
||||
})),
|
||||
Box::new(Expression::PropertyReference(NamedReference {
|
||||
element: Rc::downgrade(&child),
|
||||
name: "y".into(),
|
||||
})),
|
||||
)
|
||||
(prop_ref("x"), prop_ref("y"))
|
||||
} else {
|
||||
(Box::new(Expression::Invalid), Box::new(Expression::Invalid))
|
||||
};
|
||||
|
@ -101,6 +100,8 @@ pub fn lower_layouts(component: &Rc<Component>, diag: &mut Diagnostics) {
|
|||
path: path_elements_expr,
|
||||
x_reference,
|
||||
y_reference,
|
||||
width_reference: prop_ref("width"),
|
||||
height_reference: prop_ref("height"),
|
||||
});
|
||||
continue;
|
||||
} else {
|
||||
|
|
|
@ -280,6 +280,8 @@ impl TypeRegister {
|
|||
let mut path = BuiltinElement::new("Path");
|
||||
path.properties.insert("x".to_owned(), Type::Float32);
|
||||
path.properties.insert("y".to_owned(), Type::Float32);
|
||||
path.properties.insert("width".to_owned(), Type::Float32);
|
||||
path.properties.insert("height".to_owned(), Type::Float32);
|
||||
path.properties.insert("fill_color".to_owned(), Type::Color);
|
||||
path.properties.insert("stroke_color".to_owned(), Type::Color);
|
||||
path.properties.insert("stroke_width".to_owned(), Type::Float32);
|
||||
|
@ -322,6 +324,8 @@ impl TypeRegister {
|
|||
let mut path_layout = BuiltinElement::new("PathLayout");
|
||||
path_layout.properties.insert("x".to_owned(), Type::Float32);
|
||||
path_layout.properties.insert("y".to_owned(), Type::Float32);
|
||||
path_layout.properties.insert("width".to_owned(), Type::Float32);
|
||||
path_layout.properties.insert("height".to_owned(), Type::Float32);
|
||||
path_layout.properties.insert("commands".to_owned(), Type::String);
|
||||
path_elements.iter().for_each(|elem| {
|
||||
path_layout
|
||||
|
|
|
@ -378,13 +378,38 @@ impl<'a> Iterator for ToLyonPathEventIterator<'a> {
|
|||
|
||||
impl<'a> ExactSizeIterator for ToLyonPathEventIterator<'a> {}
|
||||
|
||||
struct TransformedLyonPathIterator<EventIt> {
|
||||
it: EventIt,
|
||||
transform: lyon::math::Transform,
|
||||
}
|
||||
|
||||
impl<EventIt: Iterator<Item = lyon::path::Event<lyon::math::Point, lyon::math::Point>>> Iterator
|
||||
for TransformedLyonPathIterator<EventIt>
|
||||
{
|
||||
type Item = lyon::path::Event<lyon::math::Point, lyon::math::Point>;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.it.next().map(|ev| ev.transformed(&self.transform))
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
self.it.size_hint()
|
||||
}
|
||||
}
|
||||
|
||||
impl<EventIt: Iterator<Item = lyon::path::Event<lyon::math::Point, lyon::math::Point>>>
|
||||
ExactSizeIterator for TransformedLyonPathIterator<EventIt>
|
||||
{
|
||||
}
|
||||
|
||||
/// LyonPathIterator is a data structure that acts as starting point for iterating
|
||||
/// through the low-level events of a path. If the path was constructed from said
|
||||
/// events, then it is a very thin abstraction. If the path was created from higher-level
|
||||
/// elements, then an intermediate lyon path is required/built.
|
||||
pub struct LyonPathIterator<'a> {
|
||||
it: LyonPathIteratorVariant<'a>,
|
||||
transform: Option<lyon::math::Transform>,
|
||||
}
|
||||
|
||||
enum LyonPathIteratorVariant<'a> {
|
||||
FromPath(lyon::path::Path),
|
||||
FromEvents(&'a crate::SharedArray<PathEvent>, &'a crate::SharedArray<Point>),
|
||||
|
@ -397,17 +422,42 @@ impl<'a> LyonPathIterator<'a> {
|
|||
) -> Box<dyn Iterator<Item = lyon::path::Event<lyon::math::Point, lyon::math::Point>> + 'a>
|
||||
{
|
||||
match &self.it {
|
||||
LyonPathIteratorVariant::FromPath(path) => Box::new(path.iter()),
|
||||
LyonPathIteratorVariant::FromPath(path) => self.apply_transform(path.iter()),
|
||||
LyonPathIteratorVariant::FromEvents(events, coordinates) => {
|
||||
Box::new(ToLyonPathEventIterator {
|
||||
Box::new(self.apply_transform(ToLyonPathEventIterator {
|
||||
events_it: events.iter(),
|
||||
coordinates_it: coordinates.iter(),
|
||||
first: coordinates.first(),
|
||||
last: coordinates.last(),
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This function changes the iterator to be one that applies a transformation to make the path fit into the specified bounds.
|
||||
pub fn fitted(mut self, width: f32, height: f32) -> LyonPathIterator<'a> {
|
||||
if width > 0. || height > 0. {
|
||||
let br = lyon::algorithms::aabb::bounding_rect(self.iter());
|
||||
self.transform = Some(lyon::algorithms::fit::fit_rectangle(
|
||||
&br,
|
||||
&Rect::from_size(Size::new(width, height)),
|
||||
lyon::algorithms::fit::FitStyle::Min,
|
||||
));
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
fn apply_transform(
|
||||
&'a self,
|
||||
event_it: impl Iterator<Item = lyon::path::Event<lyon::math::Point, lyon::math::Point>> + 'a,
|
||||
) -> Box<dyn Iterator<Item = lyon::path::Event<lyon::math::Point, lyon::math::Point>> + 'a>
|
||||
{
|
||||
match self.transform {
|
||||
Some(transform) => Box::new(TransformedLyonPathIterator { it: event_it, transform }),
|
||||
None => Box::new(event_it),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
|
@ -443,6 +493,7 @@ impl PathData {
|
|||
LyonPathIteratorVariant::FromEvents(events, coordinates)
|
||||
}
|
||||
},
|
||||
transform: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -554,6 +605,8 @@ pub enum RenderingPrimitive {
|
|||
Path {
|
||||
x: f32,
|
||||
y: f32,
|
||||
width: f32,
|
||||
height: f32,
|
||||
elements: crate::PathData,
|
||||
fill_color: Color,
|
||||
stroke_color: Color,
|
||||
|
|
|
@ -268,6 +268,8 @@ pub use crate::abi::datastructures::TouchAreaVTable;
|
|||
pub struct Path {
|
||||
pub x: Property<f32>,
|
||||
pub y: Property<f32>,
|
||||
pub width: Property<f32>,
|
||||
pub height: Property<f32>,
|
||||
pub elements: Property<PathData>,
|
||||
pub fill_color: Property<Color>,
|
||||
pub stroke_color: Property<Color>,
|
||||
|
@ -291,6 +293,8 @@ impl Item for Path {
|
|||
RenderingPrimitive::Path {
|
||||
x: Self::field_offsets().x.apply_pin(self).get(context),
|
||||
y: Self::field_offsets().y.apply_pin(self).get(context),
|
||||
width: Self::field_offsets().width.apply_pin(self).get(context),
|
||||
height: Self::field_offsets().height.apply_pin(self).get(context),
|
||||
elements: Self::field_offsets().elements.apply_pin(self).get(context),
|
||||
fill_color: Self::field_offsets().fill_color.apply_pin(self).get(context),
|
||||
stroke_color: Self::field_offsets().stroke_color.apply_pin(self).get(context),
|
||||
|
|
|
@ -163,6 +163,8 @@ pub struct PathLayoutData<'a> {
|
|||
pub items: Slice<'a, PathLayoutItemData<'a>>,
|
||||
pub x: Coord,
|
||||
pub y: Coord,
|
||||
pub width: Coord,
|
||||
pub height: Coord,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
|
@ -178,7 +180,7 @@ pub extern "C" fn solve_path_layout(data: &PathLayoutData) {
|
|||
use lyon::geom::*;
|
||||
use lyon::path::iterator::PathIterator;
|
||||
|
||||
let path_iter = data.elements.iter();
|
||||
let path_iter = data.elements.iter().fitted(data.width, data.height);
|
||||
|
||||
let tolerance = 0.01;
|
||||
|
||||
|
|
|
@ -557,6 +557,12 @@ unsafe extern "C" fn compute_layout(component: ComponentRefPin, eval_context: &E
|
|||
let component_type =
|
||||
&*(component.get_vtable() as *const ComponentVTable as *const ComponentDescription);
|
||||
|
||||
let resolve_prop_ref = |prop_ref: &expression_tree::Expression| {
|
||||
eval::eval_expression(&prop_ref, &component_type, eval_context)
|
||||
.try_into()
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
for it in &component_type.original.layout_constraints.borrow().grids {
|
||||
use sixtyfps_corelib::layout::*;
|
||||
|
||||
|
@ -604,21 +610,13 @@ unsafe extern "C" fn compute_layout(component: ComponentRefPin, eval_context: &E
|
|||
.unwrap()
|
||||
};
|
||||
|
||||
let x = eval::eval_expression(&it.x_reference, &component_type, eval_context)
|
||||
.try_into()
|
||||
.unwrap_or_default();
|
||||
|
||||
let y = eval::eval_expression(&it.y_reference, &component_type, eval_context)
|
||||
.try_into()
|
||||
.unwrap_or_default();
|
||||
|
||||
solve_grid_layout(&GridLayoutData {
|
||||
row_constraint: Slice::from(row_constraint.as_slice()),
|
||||
col_constraint: Slice::from(col_constraint.as_slice()),
|
||||
width: within_prop("width"),
|
||||
height: within_prop("height"),
|
||||
x,
|
||||
y,
|
||||
x: resolve_prop_ref(&it.x_reference),
|
||||
y: resolve_prop_ref(&it.y_reference),
|
||||
cells: Slice::from(cells.as_slice()),
|
||||
});
|
||||
}
|
||||
|
@ -643,19 +641,13 @@ unsafe extern "C" fn compute_layout(component: ComponentRefPin, eval_context: &E
|
|||
|
||||
let path_elements = eval::convert_path(&it.path, component_type, eval_context);
|
||||
|
||||
let x = eval::eval_expression(&it.x_reference, &component_type, eval_context)
|
||||
.try_into()
|
||||
.unwrap_or_default();
|
||||
|
||||
let y = eval::eval_expression(&it.y_reference, &component_type, eval_context)
|
||||
.try_into()
|
||||
.unwrap_or_default();
|
||||
|
||||
solve_path_layout(&PathLayoutData {
|
||||
items: Slice::from(items.as_slice()),
|
||||
elements: &path_elements,
|
||||
x,
|
||||
y,
|
||||
x: resolve_prop_ref(&it.x_reference),
|
||||
y: resolve_prop_ref(&it.y_reference),
|
||||
width: resolve_prop_ref(&it.width_reference),
|
||||
height: resolve_prop_ref(&it.height_reference),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -347,6 +347,8 @@ impl RenderingPrimitivesBuilder for GLRenderingPrimitivesBuilder {
|
|||
RenderingPrimitive::Path {
|
||||
x: _,
|
||||
y: _,
|
||||
width,
|
||||
height,
|
||||
elements,
|
||||
fill_color,
|
||||
stroke_color,
|
||||
|
@ -354,7 +356,7 @@ impl RenderingPrimitivesBuilder for GLRenderingPrimitivesBuilder {
|
|||
} => {
|
||||
let mut primitives = SmallVec::new();
|
||||
|
||||
let path_iter = elements.iter();
|
||||
let path_iter = elements.iter().fitted(*width, *height);
|
||||
|
||||
if *fill_color != Color::TRANSPARENT {
|
||||
primitives.extend(
|
||||
|
|
50
tests/cases/path_fit.60
Normal file
50
tests/cases/path_fit.60
Normal file
|
@ -0,0 +1,50 @@
|
|||
TestCase := Rectangle {
|
||||
|
||||
Text {
|
||||
text: "The path and the items below should both fit into the red rectangle";
|
||||
color: black;
|
||||
}
|
||||
|
||||
fit_rect := Rectangle {
|
||||
x: 0;
|
||||
y: 100;
|
||||
color: red;
|
||||
width: 700;
|
||||
height: 500;
|
||||
|
||||
Path {
|
||||
x: 0;
|
||||
y: 0;
|
||||
width: fit_rect.width;
|
||||
height: fit_rect.height;
|
||||
commands: "M 100 300 Q 150 50 1100 400 Q 1450 500 750 500 Q 1000 600 950 600 C 325 575 350 450 150 550 Q 0 600 100 800 C 250 850 300 600 550 850 C 800 850 850 650 2000 700 ";
|
||||
stroke_color: black;
|
||||
stroke_width: 2;
|
||||
}
|
||||
|
||||
PathLayout {
|
||||
x: 0;
|
||||
y: 0;
|
||||
width: fit_rect.width;
|
||||
height: fit_rect.height;
|
||||
commands: "M 100 300 Q 150 50 1100 400 Q 1450 500 750 500 Q 1000 600 950 600 C 325 575 350 450 150 550 Q 0 600 100 800 C 250 850 300 600 550 850 C 800 850 850 650 2000 700 ";
|
||||
|
||||
Text {
|
||||
text: "First item";
|
||||
color: black;
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Second item";
|
||||
color: black;
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Third item";
|
||||
color: black;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue