Introduce layout stretching

This commit is contained in:
Olivier Goffart 2020-10-26 16:33:38 +01:00
parent 7ff0b4b73f
commit 47be71e16d
10 changed files with 134 additions and 33 deletions

View file

@ -271,11 +271,15 @@ inline FocusEventResult process_focus_event(ComponentRef component, int64_t &foc
using cbindgen_private::grid_layout_info; using cbindgen_private::grid_layout_info;
using cbindgen_private::GridLayoutCellData; using cbindgen_private::GridLayoutCellData;
using cbindgen_private::GridLayoutData; using cbindgen_private::GridLayoutData;
using cbindgen_private::BoxLayoutData;
using cbindgen_private::BoxLayoutCellData;
using cbindgen_private::LayoutInfo; using cbindgen_private::LayoutInfo;
using cbindgen_private::Padding; using cbindgen_private::Padding;
using cbindgen_private::PathLayoutData; using cbindgen_private::PathLayoutData;
using cbindgen_private::PathLayoutItemData; using cbindgen_private::PathLayoutItemData;
using cbindgen_private::solve_grid_layout; using cbindgen_private::solve_grid_layout;
using cbindgen_private::solve_box_layout;
using cbindgen_private::box_layout_info;
using cbindgen_private::solve_path_layout; using cbindgen_private::solve_path_layout;
// models // models

View file

@ -64,7 +64,7 @@ MainWindow := Window {
} }
} }
HorizontalLayout { HorizontalLayout {
Rectangle {} // Fixme: remove and use alignement instead Rectangle { horizontal-stretch: 1; } // Fixme: remove and use alignement instead
Button { Button {
text: "Remove Done Items"; text: "Remove Done Items";
clicked => { root.remove_done(); } clicked => { root.remove_done(); }

View file

@ -1529,11 +1529,11 @@ impl crate::layout::gen::Language for CppLanguageLayoutGen {
let mut creation_code = cells; let mut creation_code = cells;
creation_code.insert( creation_code.insert(
0, 0,
format!(" sixtyfps::GridLayoutCellData {}_data[] = {{", cell_ref_variable,), format!(" sixtyfps::BoxLayoutCellData {}_data[] = {{", cell_ref_variable,),
); );
creation_code.push(" };".to_owned()); creation_code.push(" };".to_owned());
creation_code.push(format!( creation_code.push(format!(
" const sixtyfps::Slice<sixtyfps::GridLayoutCellData> {cv}{{{cv}_data, std::size({cv}_data)}};", " const sixtyfps::Slice<sixtyfps::BoxLayoutCellData> {cv}{{{cv}_data, std::size({cv}_data)}};",
cv = cell_ref_variable cv = cell_ref_variable
)); ));

View file

@ -119,6 +119,8 @@ pub struct LayoutConstraints {
pub maximum_width: Option<NamedReference>, pub maximum_width: Option<NamedReference>,
pub minimum_height: Option<NamedReference>, pub minimum_height: Option<NamedReference>,
pub maximum_height: Option<NamedReference>, pub maximum_height: Option<NamedReference>,
pub horizontal_stretch: Option<NamedReference>,
pub vertical_stretch: Option<NamedReference>,
} }
impl LayoutConstraints { impl LayoutConstraints {
@ -128,6 +130,8 @@ impl LayoutConstraints {
maximum_width: binding_reference(&element, "maximum_width"), maximum_width: binding_reference(&element, "maximum_width"),
minimum_height: binding_reference(&element, "minimum_height"), minimum_height: binding_reference(&element, "minimum_height"),
maximum_height: binding_reference(&element, "maximum_height"), maximum_height: binding_reference(&element, "maximum_height"),
horizontal_stretch: binding_reference(&element, "horizontal_stretch"),
vertical_stretch: binding_reference(&element, "vertical_stretch"),
} }
} }
@ -136,24 +140,28 @@ impl LayoutConstraints {
|| self.maximum_width.is_some() || self.maximum_width.is_some()
|| self.minimum_height.is_some() || self.minimum_height.is_some()
|| self.maximum_height.is_some() || self.maximum_height.is_some()
|| self.horizontal_stretch.is_some()
|| self.vertical_stretch.is_some()
} }
pub fn for_each_restrictions<'a>(&'a self) -> [(&Option<NamedReference>, &'static str); 4] { pub fn for_each_restrictions<'a>(&'a self) -> [(&Option<NamedReference>, &'static str); 6] {
[ [
(&self.minimum_width, "min_width"), (&self.minimum_width, "min_width"),
(&self.maximum_width, "max_width"), (&self.maximum_width, "max_width"),
(&self.minimum_height, "min_height"), (&self.minimum_height, "min_height"),
(&self.maximum_height, "max_height"), (&self.maximum_height, "max_height"),
(&self.horizontal_stretch, "horizontal_stretch"),
(&self.vertical_stretch, "vertical_stretch"),
] ]
} }
}
impl LayoutConstraints {
fn visit_named_references(&mut self, visitor: &mut impl FnMut(&mut NamedReference)) { fn visit_named_references(&mut self, visitor: &mut impl FnMut(&mut NamedReference)) {
self.maximum_width.as_mut().map(|e| visitor(&mut *e)); self.maximum_width.as_mut().map(|e| visitor(&mut *e));
self.minimum_width.as_mut().map(|e| visitor(&mut *e)); self.minimum_width.as_mut().map(|e| visitor(&mut *e));
self.maximum_height.as_mut().map(|e| visitor(&mut *e)); self.maximum_height.as_mut().map(|e| visitor(&mut *e));
self.minimum_height.as_mut().map(|e| visitor(&mut *e)); self.minimum_height.as_mut().map(|e| visitor(&mut *e));
self.horizontal_stretch.as_mut().map(|e| visitor(&mut *e));
self.vertical_stretch.as_mut().map(|e| visitor(&mut *e));
} }
} }

View file

@ -30,6 +30,8 @@ pub fn reserved_property(name: &str) -> Type {
("padding_right", Type::Length), ("padding_right", Type::Length),
("padding_top", Type::Length), ("padding_top", Type::Length),
("padding_bottom", Type::Length), ("padding_bottom", Type::Length),
("horizontal_stretch", Type::Float32),
("vertical_stretch", Type::Float32),
("clip", Type::Bool), ("clip", Type::Bool),
("opacity", Type::Float32), ("opacity", Type::Float32),
("visible", Type::Bool), ("visible", Type::Bool),

View file

@ -466,12 +466,7 @@ impl Item for Text {
let font = fc.find_font(&font_family, font_size); let font = fc.find_font(&font_family, font_size);
let width = font.text_width(&text); let width = font.text_width(&text);
let height = font.height(); let height = font.height();
LayoutInfo { LayoutInfo { min_width: width, min_height: height, ..LayoutInfo::default() }
min_width: width,
max_width: f32::MAX,
min_height: height,
max_height: height,
}
}) })
} }
@ -1019,12 +1014,16 @@ impl Item for TextInput {
} }
fn layouting_info(self: Pin<&Self>, window: &ComponentWindow) -> LayoutInfo { fn layouting_info(self: Pin<&Self>, window: &ComponentWindow) -> LayoutInfo {
let text = Self::FIELD_OFFSETS.text.apply_pin(self).get(); let (width, height) = TextInput::with_font(self, window, |font| {
(font.text_width("********************"), font.height())
});
let (width, height) = LayoutInfo {
TextInput::with_font(self, window, |font| (font.text_width(&text), font.height())); min_width: width,
min_height: height,
LayoutInfo { min_width: width, max_width: f32::MAX, min_height: height, max_height: height } horizontal_stretch: 1.,
..LayoutInfo::default()
}
} }
fn input_event( fn input_event(

View file

@ -27,11 +27,23 @@ pub struct LayoutInfo {
pub min_height: f32, pub min_height: f32,
/// The maximum height for the item. /// The maximum height for the item.
pub max_height: f32, pub max_height: f32,
/// the horizontal stretch factor
pub horizontal_stretch: f32,
/// the vertical stretch factor
pub vertical_stretch: f32,
} }
impl Default for LayoutInfo { impl Default for LayoutInfo {
fn default() -> Self { fn default() -> Self {
LayoutInfo { min_width: 0., max_width: f32::MAX, min_height: 0., max_height: f32::MAX } LayoutInfo {
min_width: 0.,
max_width: f32::MAX,
min_height: 0.,
max_height: f32::MAX,
horizontal_stretch: 0.,
vertical_stretch: 0.,
}
} }
} }
@ -43,6 +55,8 @@ impl LayoutInfo {
max_width: self.max_width.min(other.max_width), max_width: self.max_width.min(other.max_width),
min_height: self.min_height.max(other.min_height), min_height: self.min_height.max(other.min_height),
max_height: self.max_height.min(other.max_height), max_height: self.max_height.min(other.max_height),
horizontal_stretch: self.horizontal_stretch.min(other.horizontal_stretch),
vertical_stretch: self.vertical_stretch.min(other.vertical_stretch),
} }
} }
} }
@ -65,7 +79,7 @@ mod grid_internal {
impl Default for LayoutData { impl Default for LayoutData {
fn default() -> Self { fn default() -> Self {
LayoutData { min: 0., max: Coord::MAX, pref: 0., stretch: 1., pos: 0., size: 0. } LayoutData { min: 0., max: Coord::MAX, pref: 0., stretch: f32::MAX, pos: 0., size: 0. }
} }
} }
@ -111,7 +125,7 @@ mod grid_internal {
min_size: Size { width: min, height: Dimension::Auto }, min_size: Size { width: min, height: Dimension::Auto },
max_size: Size { width: max, height: Dimension::Auto }, max_size: Size { width: max, height: Dimension::Auto },
size: Size { width: pref, height: Dimension::Auto }, size: Size { width: pref, height: Dimension::Auto },
flex_grow: 1., flex_grow: cell.stretch,
margin, margin,
..Default::default() ..Default::default()
}; };
@ -238,6 +252,7 @@ pub extern "C" fn solve_grid_layout(data: &GridLayoutData) {
rdata.max = rdata.max.min(row_max); rdata.max = rdata.max.min(row_max);
rdata.min = rdata.min.max(row_min); rdata.min = rdata.min.max(row_min);
rdata.pref = rdata.pref.max(row_pref); rdata.pref = rdata.pref.max(row_pref);
rdata.stretch = rdata.stretch.min(cell.constraint.vertical_stretch);
} }
let col_max = cell.constraint.max_width / (cell.colspan as f32); let col_max = cell.constraint.max_width / (cell.colspan as f32);
@ -249,9 +264,17 @@ pub extern "C" fn solve_grid_layout(data: &GridLayoutData) {
cdata.max = cdata.max.min(col_max); cdata.max = cdata.max.min(col_max);
cdata.min = cdata.min.max(col_min); cdata.min = cdata.min.max(col_min);
cdata.pref = cdata.pref.max(col_pref); cdata.pref = cdata.pref.max(col_pref);
cdata.stretch = cdata.stretch.min(cell.constraint.horizontal_stretch);
} }
} }
let normalize_stretch = |v: &mut Vec<grid_internal::LayoutData>| {
let s = v.iter().map(|x| x.stretch).sum::<f32>();
v.iter_mut().for_each(|x| x.stretch = if s == 0. { 1. } else { x.stretch / s })
};
normalize_stretch(&mut row_layout_data);
normalize_stretch(&mut col_layout_data);
grid_internal::layout_items( grid_internal::layout_items(
&mut row_layout_data, &mut row_layout_data,
data.y + data.padding.top, data.y + data.padding.top,
@ -299,7 +322,7 @@ pub extern "C" fn grid_layout_info<'a>(
} }
if num_col < 1 || num_row < 1 { if num_col < 1 || num_row < 1 {
return LayoutInfo { min_width: 0., max_width: 0., min_height: 0., max_height: 0. }; return LayoutInfo { max_width: 0., max_height: 0., ..LayoutInfo::default() };
}; };
let mut row_layout_data = vec![grid_internal::LayoutData::default(); num_row as usize]; let mut row_layout_data = vec![grid_internal::LayoutData::default(); num_row as usize];
@ -313,6 +336,8 @@ pub extern "C" fn grid_layout_info<'a>(
cdata.min = cdata.min.max(cell.constraint.min_width); cdata.min = cdata.min.max(cell.constraint.min_width);
rdata.pref = rdata.pref.max(cell.constraint.min_height); rdata.pref = rdata.pref.max(cell.constraint.min_height);
cdata.pref = cdata.pref.max(cell.constraint.min_width); cdata.pref = cdata.pref.max(cell.constraint.min_width);
rdata.stretch = rdata.stretch.min(cell.constraint.vertical_stretch);
cdata.stretch = cdata.stretch.min(cell.constraint.horizontal_stretch);
} }
let spacing_h = spacing * (num_row - 1) as Coord; let spacing_h = spacing * (num_row - 1) as Coord;
@ -335,7 +360,17 @@ pub extern "C" fn grid_layout_info<'a>(
+ padding.left + padding.left
+ padding.right; + padding.right;
LayoutInfo { min_width, max_width, min_height, max_height } let horizontal_stretch = col_layout_data.iter().map(|data| data.stretch).sum::<Coord>();
let vertical_stretch = row_layout_data.iter().map(|data| data.stretch).sum::<Coord>();
LayoutInfo {
min_width,
max_width,
min_height,
max_height,
horizontal_stretch,
vertical_stretch,
}
} }
#[repr(C)] #[repr(C)]
@ -381,6 +416,15 @@ pub extern "C" fn solve_box_layout(data: &BoxLayoutData, is_horizontal: bool) {
..Default::default() ..Default::default()
}; };
let stretch_factor = |cell: &BoxLayoutCellData| {
if is_horizontal {
cell.constraint.horizontal_stretch
} else {
cell.constraint.vertical_stretch
}
};
let stretch_factor_sum = data.cells.iter().map(stretch_factor).sum::<Coord>();
let flex_box = stretch.new_node(box_style, vec![]).unwrap(); let flex_box = stretch.new_node(box_style, vec![]).unwrap();
for (index, cell) in data.cells.iter().enumerate() { for (index, cell) in data.cells.iter().enumerate() {
@ -409,7 +453,12 @@ pub extern "C" fn solve_box_layout(data: &BoxLayoutData, is_horizontal: bool) {
let cell_style = Style { let cell_style = Style {
min_size, min_size,
max_size, max_size,
flex_grow: 1., flex_grow: if stretch_factor_sum == 0. {
1.
} else {
stretch_factor(cell) / stretch_factor_sum
},
flex_basis: if is_horizontal { min_size.width } else { min_size.height },
margin, margin,
align_self: AlignSelf::Stretch, align_self: AlignSelf::Stretch,
..Default::default() ..Default::default()
@ -452,7 +501,7 @@ pub extern "C" fn box_layout_info<'a>(
) -> LayoutInfo { ) -> LayoutInfo {
let count = cells.len(); let count = cells.len();
if count < 1 { if count < 1 {
return LayoutInfo { min_width: 0., max_width: 0., min_height: 0., max_height: 0. }; return LayoutInfo { max_width: 0., max_height: 0., ..LayoutInfo::default() };
}; };
let order_float = |a: &Coord, b: &Coord| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal); let order_float = |a: &Coord, b: &Coord| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal);
@ -467,7 +516,17 @@ pub extern "C" fn box_layout_info<'a>(
+ padding.bottom; + padding.bottom;
let min_width = cells.iter().map(|c| c.constraint.min_width).sum::<Coord>() + extra_w; let min_width = cells.iter().map(|c| c.constraint.min_width).sum::<Coord>() + extra_w;
let max_width = cells.iter().map(|c| c.constraint.max_width).sum::<Coord>() + extra_w; let max_width = cells.iter().map(|c| c.constraint.max_width).sum::<Coord>() + extra_w;
LayoutInfo { min_width, max_width, min_height, max_height } let horizontal_stretch = cells.iter().map(|c| c.constraint.horizontal_stretch).sum::<f32>();
let vertical_stretch =
cells.iter().map(|c| c.constraint.vertical_stretch).min_by(order_float).unwrap();
LayoutInfo {
min_width,
max_width,
min_height,
max_height,
horizontal_stretch,
vertical_stretch,
}
} else { } else {
let extra_h = padding.top + padding.bottom + spacing * (count - 1) as Coord; let extra_h = padding.top + padding.bottom + spacing * (count - 1) as Coord;
@ -479,7 +538,17 @@ pub extern "C" fn box_layout_info<'a>(
+ padding.right; + padding.right;
let min_height = cells.iter().map(|c| c.constraint.min_height).sum::<Coord>() + extra_h; let min_height = cells.iter().map(|c| c.constraint.min_height).sum::<Coord>() + extra_h;
let max_height = cells.iter().map(|c| c.constraint.max_height).sum::<Coord>() + extra_h; let max_height = cells.iter().map(|c| c.constraint.max_height).sum::<Coord>() + extra_h;
LayoutInfo { min_width, max_width, min_height, max_height } let horizontal_stretch =
cells.iter().map(|c| c.constraint.horizontal_stretch).min_by(order_float).unwrap();
let vertical_stretch = cells.iter().map(|c| c.constraint.vertical_stretch).sum::<f32>();
LayoutInfo {
min_width,
max_width,
min_height,
max_height,
horizontal_stretch,
vertical_stretch,
}
} }
} }

View file

@ -988,6 +988,8 @@ fn fill_layout_info_constraints(
constraints.maximum_width.as_ref().map(|e| layout_info.max_width = expr_eval(e)); constraints.maximum_width.as_ref().map(|e| layout_info.max_width = expr_eval(e));
constraints.minimum_height.as_ref().map(|e| layout_info.min_height = expr_eval(e)); constraints.minimum_height.as_ref().map(|e| layout_info.min_height = expr_eval(e));
constraints.maximum_height.as_ref().map(|e| layout_info.max_height = expr_eval(e)); constraints.maximum_height.as_ref().map(|e| layout_info.max_height = expr_eval(e));
constraints.horizontal_stretch.as_ref().map(|e| layout_info.horizontal_stretch = expr_eval(e));
constraints.vertical_stretch.as_ref().map(|e| layout_info.vertical_stretch = expr_eval(e));
} }
fn collect_layouts_recursively<'a, 'b>( fn collect_layouts_recursively<'a, 'b>(

View file

@ -176,8 +176,6 @@ impl Item for NativeButton {
LayoutInfo { LayoutInfo {
min_width: size.width as f32, min_width: size.width as f32,
min_height: size.height as f32, min_height: size.height as f32,
max_width: size.width as f32,
max_height: size.height as f32,
..LayoutInfo::default() ..LayoutInfo::default()
} }
} }
@ -299,6 +297,7 @@ impl Item for NativeCheckBox {
min_width: size.width as f32, min_width: size.width as f32,
min_height: size.height as f32, min_height: size.height as f32,
max_height: size.height as f32, max_height: size.height as f32,
horizontal_stretch: 1.,
..LayoutInfo::default() ..LayoutInfo::default()
} }
} }
@ -452,6 +451,7 @@ impl Item for NativeSpinBox {
min_width: size.width as f32, min_width: size.width as f32,
min_height: size.height as f32, min_height: size.height as f32,
max_height: size.height as f32, max_height: size.height as f32,
horizontal_stretch: 1.,
..LayoutInfo::default() ..LayoutInfo::default()
} }
} }
@ -649,6 +649,7 @@ impl Item for NativeSlider {
min_width: size.width as f32, min_width: size.width as f32,
min_height: size.height as f32, min_height: size.height as f32,
max_height: size.height as f32, max_height: size.height as f32,
horizontal_stretch: 1.,
..LayoutInfo::default() ..LayoutInfo::default()
} }
} }
@ -895,7 +896,13 @@ impl Item for NativeGroupBox {
let right = Self::FIELD_OFFSETS.native_padding_right.apply_pin(self).get(); let right = Self::FIELD_OFFSETS.native_padding_right.apply_pin(self).get();
let top = Self::FIELD_OFFSETS.native_padding_top.apply_pin(self).get(); let top = Self::FIELD_OFFSETS.native_padding_top.apply_pin(self).get();
let bottom = Self::FIELD_OFFSETS.native_padding_bottom.apply_pin(self).get(); let bottom = Self::FIELD_OFFSETS.native_padding_bottom.apply_pin(self).get();
LayoutInfo { min_width: left + right, min_height: top + bottom, ..LayoutInfo::default() } LayoutInfo {
min_width: left + right,
min_height: top + bottom,
horizontal_stretch: 1.,
vertical_stretch: 1.,
..LayoutInfo::default()
}
} }
fn input_event( fn input_event(
@ -1036,7 +1043,12 @@ impl Item for NativeLineEdit {
let right = Self::FIELD_OFFSETS.native_padding_right.apply_pin(self).get(); let right = Self::FIELD_OFFSETS.native_padding_right.apply_pin(self).get();
let top = Self::FIELD_OFFSETS.native_padding_top.apply_pin(self).get(); let top = Self::FIELD_OFFSETS.native_padding_top.apply_pin(self).get();
let bottom = Self::FIELD_OFFSETS.native_padding_bottom.apply_pin(self).get(); let bottom = Self::FIELD_OFFSETS.native_padding_bottom.apply_pin(self).get();
LayoutInfo { min_width: left + right, min_height: top + bottom, ..LayoutInfo::default() } LayoutInfo {
min_width: left + right,
min_height: top + bottom,
horizontal_stretch: 1.,
..LayoutInfo::default()
}
} }
fn input_event( fn input_event(
@ -1268,7 +1280,13 @@ impl Item for NativeScrollView {
let right = Self::FIELD_OFFSETS.native_padding_right.apply_pin(self).get(); let right = Self::FIELD_OFFSETS.native_padding_right.apply_pin(self).get();
let top = Self::FIELD_OFFSETS.native_padding_top.apply_pin(self).get(); let top = Self::FIELD_OFFSETS.native_padding_top.apply_pin(self).get();
let bottom = Self::FIELD_OFFSETS.native_padding_bottom.apply_pin(self).get(); let bottom = Self::FIELD_OFFSETS.native_padding_bottom.apply_pin(self).get();
LayoutInfo { min_width: left + right, min_height: top + bottom, ..LayoutInfo::default() } LayoutInfo {
min_width: left + right,
min_height: top + bottom,
horizontal_stretch: 1.,
vertical_stretch: 1.,
..LayoutInfo::default()
}
} }
fn input_event( fn input_event(

View file

@ -33,8 +33,8 @@ TestCase := Rectangle {
} }
} }
rect_blue := Rectangle { rect_blue := Rectangle {
horizontal-stretch: 1;
color: blue; color: blue;
minimum_width: 100phx; // FIXME: replace with a smaller stretch factor
} }
} }
rect_yellow := Rectangle { rect_yellow := Rectangle {
@ -131,7 +131,6 @@ let instance = TestCase::new();
let instance = instance.as_ref(); let instance = instance.as_ref();
use sixtyfps::re_exports::Component; use sixtyfps::re_exports::Component;
instance.compute_layout(); instance.compute_layout();
println!("{:?}", instance.get_debug_());
assert!(instance.get_rect_blue_ok()); assert!(instance.get_rect_blue_ok());
assert!(instance.get_rect_orange_ok()); assert!(instance.get_rect_orange_ok());
assert!(instance.get_rect_red_ok()); assert!(instance.get_rect_red_ok());