diff --git a/api/sixtyfps-cpp/include/sixtyfps.h b/api/sixtyfps-cpp/include/sixtyfps.h index 76a0574d3..ab71b7000 100644 --- a/api/sixtyfps-cpp/include/sixtyfps.h +++ b/api/sixtyfps-cpp/include/sixtyfps.h @@ -271,11 +271,15 @@ inline FocusEventResult process_focus_event(ComponentRef component, int64_t &foc using cbindgen_private::grid_layout_info; using cbindgen_private::GridLayoutCellData; using cbindgen_private::GridLayoutData; +using cbindgen_private::BoxLayoutData; +using cbindgen_private::BoxLayoutCellData; using cbindgen_private::LayoutInfo; using cbindgen_private::Padding; using cbindgen_private::PathLayoutData; using cbindgen_private::PathLayoutItemData; using cbindgen_private::solve_grid_layout; +using cbindgen_private::solve_box_layout; +using cbindgen_private::box_layout_info; using cbindgen_private::solve_path_layout; // models diff --git a/examples/todo/ui/todo.60 b/examples/todo/ui/todo.60 index a077e9a81..77af6cec0 100644 --- a/examples/todo/ui/todo.60 +++ b/examples/todo/ui/todo.60 @@ -64,7 +64,7 @@ MainWindow := Window { } } HorizontalLayout { - Rectangle {} // Fixme: remove and use alignement instead + Rectangle { horizontal-stretch: 1; } // Fixme: remove and use alignement instead Button { text: "Remove Done Items"; clicked => { root.remove_done(); } diff --git a/sixtyfps_compiler/generator/cpp.rs b/sixtyfps_compiler/generator/cpp.rs index f081f7243..869f2fafc 100644 --- a/sixtyfps_compiler/generator/cpp.rs +++ b/sixtyfps_compiler/generator/cpp.rs @@ -1529,11 +1529,11 @@ impl crate::layout::gen::Language for CppLanguageLayoutGen { let mut creation_code = cells; creation_code.insert( 0, - format!(" sixtyfps::GridLayoutCellData {}_data[] = {{", cell_ref_variable,), + format!(" sixtyfps::BoxLayoutCellData {}_data[] = {{", cell_ref_variable,), ); creation_code.push(" };".to_owned()); creation_code.push(format!( - " const sixtyfps::Slice {cv}{{{cv}_data, std::size({cv}_data)}};", + " const sixtyfps::Slice {cv}{{{cv}_data, std::size({cv}_data)}};", cv = cell_ref_variable )); diff --git a/sixtyfps_compiler/layout.rs b/sixtyfps_compiler/layout.rs index a7e75ba2c..9fa95a6e6 100644 --- a/sixtyfps_compiler/layout.rs +++ b/sixtyfps_compiler/layout.rs @@ -119,6 +119,8 @@ pub struct LayoutConstraints { pub maximum_width: Option, pub minimum_height: Option, pub maximum_height: Option, + pub horizontal_stretch: Option, + pub vertical_stretch: Option, } impl LayoutConstraints { @@ -128,6 +130,8 @@ impl LayoutConstraints { maximum_width: binding_reference(&element, "maximum_width"), minimum_height: binding_reference(&element, "minimum_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.minimum_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, &'static str); 4] { + pub fn for_each_restrictions<'a>(&'a self) -> [(&Option, &'static str); 6] { [ (&self.minimum_width, "min_width"), (&self.maximum_width, "max_width"), (&self.minimum_height, "min_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)) { self.maximum_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.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)); } } diff --git a/sixtyfps_compiler/typeregister.rs b/sixtyfps_compiler/typeregister.rs index 2f3e70b34..2c2f34106 100644 --- a/sixtyfps_compiler/typeregister.rs +++ b/sixtyfps_compiler/typeregister.rs @@ -30,6 +30,8 @@ pub fn reserved_property(name: &str) -> Type { ("padding_right", Type::Length), ("padding_top", Type::Length), ("padding_bottom", Type::Length), + ("horizontal_stretch", Type::Float32), + ("vertical_stretch", Type::Float32), ("clip", Type::Bool), ("opacity", Type::Float32), ("visible", Type::Bool), diff --git a/sixtyfps_runtime/corelib/items.rs b/sixtyfps_runtime/corelib/items.rs index 8f45d9e84..b1c243c6a 100644 --- a/sixtyfps_runtime/corelib/items.rs +++ b/sixtyfps_runtime/corelib/items.rs @@ -466,12 +466,7 @@ impl Item for Text { let font = fc.find_font(&font_family, font_size); let width = font.text_width(&text); let height = font.height(); - LayoutInfo { - min_width: width, - max_width: f32::MAX, - min_height: height, - max_height: height, - } + LayoutInfo { min_width: width, min_height: height, ..LayoutInfo::default() } }) } @@ -1019,12 +1014,16 @@ impl Item for TextInput { } 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) = - TextInput::with_font(self, window, |font| (font.text_width(&text), font.height())); - - LayoutInfo { min_width: width, max_width: f32::MAX, min_height: height, max_height: height } + LayoutInfo { + min_width: width, + min_height: height, + horizontal_stretch: 1., + ..LayoutInfo::default() + } } fn input_event( diff --git a/sixtyfps_runtime/corelib/layout.rs b/sixtyfps_runtime/corelib/layout.rs index 476a2c591..a7a97526a 100644 --- a/sixtyfps_runtime/corelib/layout.rs +++ b/sixtyfps_runtime/corelib/layout.rs @@ -27,11 +27,23 @@ pub struct LayoutInfo { pub min_height: f32, /// The maximum height for the item. pub max_height: f32, + + /// the horizontal stretch factor + pub horizontal_stretch: f32, + /// the vertical stretch factor + pub vertical_stretch: f32, } impl Default for LayoutInfo { 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), min_height: self.min_height.max(other.min_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 { 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 }, max_size: Size { width: max, height: Dimension::Auto }, size: Size { width: pref, height: Dimension::Auto }, - flex_grow: 1., + flex_grow: cell.stretch, margin, ..Default::default() }; @@ -238,6 +252,7 @@ pub extern "C" fn solve_grid_layout(data: &GridLayoutData) { rdata.max = rdata.max.min(row_max); rdata.min = rdata.min.max(row_min); 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); @@ -249,9 +264,17 @@ pub extern "C" fn solve_grid_layout(data: &GridLayoutData) { cdata.max = cdata.max.min(col_max); cdata.min = cdata.min.max(col_min); cdata.pref = cdata.pref.max(col_pref); + cdata.stretch = cdata.stretch.min(cell.constraint.horizontal_stretch); } } + let normalize_stretch = |v: &mut Vec| { + let s = v.iter().map(|x| x.stretch).sum::(); + 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( &mut row_layout_data, data.y + data.padding.top, @@ -299,7 +322,7 @@ pub extern "C" fn grid_layout_info<'a>( } 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]; @@ -313,6 +336,8 @@ pub extern "C" fn grid_layout_info<'a>( cdata.min = cdata.min.max(cell.constraint.min_width); rdata.pref = rdata.pref.max(cell.constraint.min_height); 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; @@ -335,7 +360,17 @@ pub extern "C" fn grid_layout_info<'a>( + padding.left + padding.right; - LayoutInfo { min_width, max_width, min_height, max_height } + let horizontal_stretch = col_layout_data.iter().map(|data| data.stretch).sum::(); + let vertical_stretch = row_layout_data.iter().map(|data| data.stretch).sum::(); + + LayoutInfo { + min_width, + max_width, + min_height, + max_height, + horizontal_stretch, + vertical_stretch, + } } #[repr(C)] @@ -381,6 +416,15 @@ pub extern "C" fn solve_box_layout(data: &BoxLayoutData, is_horizontal: bool) { ..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::(); + let flex_box = stretch.new_node(box_style, vec![]).unwrap(); 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 { min_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, align_self: AlignSelf::Stretch, ..Default::default() @@ -452,7 +501,7 @@ pub extern "C" fn box_layout_info<'a>( ) -> LayoutInfo { let count = cells.len(); 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); @@ -467,7 +516,17 @@ pub extern "C" fn box_layout_info<'a>( + padding.bottom; let min_width = cells.iter().map(|c| c.constraint.min_width).sum::() + extra_w; let max_width = cells.iter().map(|c| c.constraint.max_width).sum::() + extra_w; - LayoutInfo { min_width, max_width, min_height, max_height } + let horizontal_stretch = cells.iter().map(|c| c.constraint.horizontal_stretch).sum::(); + 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 { 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; let min_height = cells.iter().map(|c| c.constraint.min_height).sum::() + extra_h; let max_height = cells.iter().map(|c| c.constraint.max_height).sum::() + 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::(); + LayoutInfo { + min_width, + max_width, + min_height, + max_height, + horizontal_stretch, + vertical_stretch, + } } } diff --git a/sixtyfps_runtime/interpreter/dynamic_component.rs b/sixtyfps_runtime/interpreter/dynamic_component.rs index 4bed8a9fe..c88749f86 100644 --- a/sixtyfps_runtime/interpreter/dynamic_component.rs +++ b/sixtyfps_runtime/interpreter/dynamic_component.rs @@ -988,6 +988,8 @@ fn fill_layout_info_constraints( 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.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>( diff --git a/sixtyfps_runtime/rendering_backends/qt/widgets.rs b/sixtyfps_runtime/rendering_backends/qt/widgets.rs index d2c2f64ba..3ab3f9d14 100644 --- a/sixtyfps_runtime/rendering_backends/qt/widgets.rs +++ b/sixtyfps_runtime/rendering_backends/qt/widgets.rs @@ -176,8 +176,6 @@ impl Item for NativeButton { LayoutInfo { min_width: size.width as f32, min_height: size.height as f32, - max_width: size.width as f32, - max_height: size.height as f32, ..LayoutInfo::default() } } @@ -299,6 +297,7 @@ impl Item for NativeCheckBox { min_width: size.width as f32, min_height: size.height as f32, max_height: size.height as f32, + horizontal_stretch: 1., ..LayoutInfo::default() } } @@ -452,6 +451,7 @@ impl Item for NativeSpinBox { min_width: size.width as f32, min_height: size.height as f32, max_height: size.height as f32, + horizontal_stretch: 1., ..LayoutInfo::default() } } @@ -649,6 +649,7 @@ impl Item for NativeSlider { min_width: size.width as f32, min_height: size.height as f32, max_height: size.height as f32, + horizontal_stretch: 1., ..LayoutInfo::default() } } @@ -895,7 +896,13 @@ impl Item for NativeGroupBox { 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 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( @@ -1036,7 +1043,12 @@ impl Item for NativeLineEdit { 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 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( @@ -1268,7 +1280,13 @@ impl Item for NativeScrollView { 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 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( diff --git a/tests/cases/layout/nested_boxes.60 b/tests/cases/layout/nested_boxes.60 index e2ffa92ee..9ace13f0d 100644 --- a/tests/cases/layout/nested_boxes.60 +++ b/tests/cases/layout/nested_boxes.60 @@ -33,8 +33,8 @@ TestCase := Rectangle { } } rect_blue := Rectangle { + horizontal-stretch: 1; color: blue; - minimum_width: 100phx; // FIXME: replace with a smaller stretch factor } } rect_yellow := Rectangle { @@ -131,7 +131,6 @@ let instance = TestCase::new(); let instance = instance.as_ref(); use sixtyfps::re_exports::Component; instance.compute_layout(); -println!("{:?}", instance.get_debug_()); assert!(instance.get_rect_blue_ok()); assert!(instance.get_rect_orange_ok()); assert!(instance.get_rect_red_ok());