diff --git a/compositor_api/src/types/component.rs b/compositor_api/src/types/component.rs index 549a45e4d..c3a928eed 100644 --- a/compositor_api/src/types/component.rs +++ b/compositor_api/src/types/component.rs @@ -92,6 +92,18 @@ pub struct View { /// List of box shadows. pub box_shadow: Option>, + + /// (**default=`0.0`**) Padding on top side in pixels. + pub padding_top: Option, + + /// (**default=`0.0`**) Padding on right side in pixels. + pub padding_right: Option, + + /// (**default=`0.0`**) Padding on bottom side in pixels. + pub padding_bottom: Option, + + /// (**default=`0.0`**) Padding on left side in pixels. + pub padding_left: Option, } #[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)] diff --git a/compositor_api/src/types/from_component.rs b/compositor_api/src/types/from_component.rs index 339f115c0..13988454f 100644 --- a/compositor_api/src/types/from_component.rs +++ b/compositor_api/src/types/from_component.rs @@ -86,6 +86,17 @@ impl TryFrom for scene::ViewComponent { Some(Overflow::Fit) => scene::Overflow::Fit, None => scene::Overflow::Hidden, }; + let padding = scene::Padding { + top: view.padding_top.unwrap_or(0.0), + right: view.padding_right.unwrap_or(0.0), + bottom: view.padding_bottom.unwrap_or(0.0), + left: view.padding_left.unwrap_or(0.0), + }; + + if padding.top < 0.0 || padding.right < 0.0 || padding.bottom < 0.0 || padding.left < 0.0 { + return Err(TypeError::new("Padding values cannot be negative.")); + } + Ok(Self { id: view.id.map(Into::into), children: view @@ -114,6 +125,7 @@ impl TryFrom for scene::ViewComponent { .into_iter() .map(TryInto::try_into) .collect::>()?, + padding, }) } } diff --git a/compositor_render/src/scene/components.rs b/compositor_render/src/scene/components.rs index 1ad769a45..8de07b373 100644 --- a/compositor_render/src/scene/components.rs +++ b/compositor_render/src/scene/components.rs @@ -148,6 +148,8 @@ pub struct ViewComponent { pub border_color: RGBAColor, pub box_shadow: Vec, + + pub padding: Padding, } #[derive(Debug, Clone, Copy)] @@ -178,6 +180,24 @@ pub enum ViewChildrenDirection { Column, } +#[derive(Debug, Clone, Copy, Default)] +pub struct Padding { + pub top: f32, + pub right: f32, + pub bottom: f32, + pub left: f32, +} + +impl Padding { + pub fn horizontal(&self) -> f32 { + self.left + self.right + } + + pub fn vertical(&self) -> f32 { + self.top + self.bottom + } +} + #[derive(Debug, Clone)] pub struct RescalerComponent { pub id: Option, diff --git a/compositor_render/src/scene/components/position.rs b/compositor_render/src/scene/components/position.rs index 0525293d2..4bc789f64 100644 --- a/compositor_render/src/scene/components/position.rs +++ b/compositor_render/src/scene/components/position.rs @@ -1,6 +1,6 @@ use crate::scene::AbsolutePosition; -use super::Position; +use super::{Padding, Position}; impl Position { pub(crate) fn with_border(self, border_width: f32) -> Self { @@ -24,4 +24,26 @@ impl Position { }), } } + + pub(crate) fn with_padding(self, padding: Padding) -> Self { + match self { + Position::Static { width, height } => Self::Static { + width: width.map(|w| w + padding.horizontal()), + height: height.map(|h| h + padding.vertical()), + }, + Position::Absolute(AbsolutePosition { + width, + height, + position_horizontal, + position_vertical, + rotation_degrees, + }) => Self::Absolute(AbsolutePosition { + width: width.map(|w| w + padding.horizontal()), + height: height.map(|h| h + padding.vertical()), + position_horizontal, + position_vertical, + rotation_degrees, + }), + } + } } diff --git a/compositor_render/src/scene/layout.rs b/compositor_render/src/scene/layout.rs index 710661c46..028724c40 100644 --- a/compositor_render/src/scene/layout.rs +++ b/compositor_render/src/scene/layout.rs @@ -8,7 +8,7 @@ use crate::{ use super::{ rescaler_component::StatefulRescalerComponent, tiles_component::StatefulTilesComponent, view_component::StatefulViewComponent, AbsolutePosition, BorderRadius, ComponentId, - HorizontalPosition, Position, RGBAColor, Size, StatefulComponent, VerticalPosition, + HorizontalPosition, Padding, Position, RGBAColor, Size, StatefulComponent, VerticalPosition, }; #[derive(Debug, Clone)] @@ -161,18 +161,23 @@ impl StatefulLayoutComponent { child: &mut StatefulComponent, position: AbsolutePosition, parent_size: Size, + parent_padding: Padding, pts: Duration, ) -> NestedLayout { let width = position.width.unwrap_or(parent_size.width); let height = position.height.unwrap_or(parent_size.height); let top = match position.position_vertical { - VerticalPosition::TopOffset(top) => top, - VerticalPosition::BottomOffset(bottom) => parent_size.height - bottom - height, + VerticalPosition::TopOffset(top) => top + parent_padding.top, + VerticalPosition::BottomOffset(bottom) => { + parent_size.height - bottom - height - parent_padding.bottom + parent_padding.top + } }; let left = match position.position_horizontal { - HorizontalPosition::LeftOffset(left) => left, - HorizontalPosition::RightOffset(right) => parent_size.width - right - width, + HorizontalPosition::LeftOffset(left) => left + parent_padding.left, + HorizontalPosition::RightOffset(right) => { + parent_size.width - right - width - parent_padding.right + parent_padding.left + } }; let rotation_degrees = position.rotation_degrees; diff --git a/compositor_render/src/scene/types/interpolation.rs b/compositor_render/src/scene/types/interpolation.rs index ee4ea4693..7142c306c 100644 --- a/compositor_render/src/scene/types/interpolation.rs +++ b/compositor_render/src/scene/types/interpolation.rs @@ -1,3 +1,5 @@ +use crate::scene::Padding; + use super::{HorizontalPosition, VerticalPosition}; #[derive(Debug, Clone, Copy)] @@ -77,3 +79,14 @@ impl ContinuousValue for HorizontalPosition { } } } + +impl ContinuousValue for Padding { + fn interpolate(start: &Self, end: &Self, state: InterpolationState) -> Self { + Self { + top: ContinuousValue::interpolate(&start.top, &end.top, state), + right: ContinuousValue::interpolate(&start.right, &end.right, state), + bottom: ContinuousValue::interpolate(&start.bottom, &end.bottom, state), + left: ContinuousValue::interpolate(&start.left, &end.left, state), + } + } +} diff --git a/compositor_render/src/scene/view_component.rs b/compositor_render/src/scene/view_component.rs index ab133031b..7a3608234 100644 --- a/compositor_render/src/scene/view_component.rs +++ b/compositor_render/src/scene/view_component.rs @@ -8,7 +8,7 @@ use super::{ scene_state::BuildStateTreeCtx, transition::{TransitionOptions, TransitionState}, types::interpolation::ContinuousValue, - BorderRadius, BoxShadow, Component, ComponentId, IntermediateNode, Overflow, Position, + BorderRadius, BoxShadow, Component, ComponentId, IntermediateNode, Overflow, Padding, Position, RGBAColor, SceneError, Size, StatefulComponent, }; @@ -37,6 +37,8 @@ struct ViewComponentParam { border_color: RGBAColor, box_shadow: Vec, + + padding: Padding, } impl StatefulViewComponent { @@ -56,10 +58,12 @@ impl StatefulViewComponent { self.children.iter_mut().collect() } - /// External position of a component (includes border) + /// External position of a component (includes border and padding) pub(super) fn position(&self, pts: Duration) -> Position { let view = self.view(pts); - view.position.with_border(view.border_width) + view.position + .with_border(view.border_width) + .with_padding(view.padding) } pub(super) fn component_id(&self) -> Option<&ComponentId> { @@ -130,6 +134,7 @@ impl ViewComponent { border_width: self.border_width, border_color: self.border_color, box_shadow: self.box_shadow, + padding: self.padding, }, transition, children: self diff --git a/compositor_render/src/scene/view_component/interpolation.rs b/compositor_render/src/scene/view_component/interpolation.rs index e5afa293f..d6d298315 100644 --- a/compositor_render/src/scene/view_component/interpolation.rs +++ b/compositor_render/src/scene/view_component/interpolation.rs @@ -22,6 +22,7 @@ impl ContinuousValue for ViewComponentParam { ), border_color: end.border_color, box_shadow: ContinuousValue::interpolate(&start.box_shadow, &end.box_shadow, state), + padding: ContinuousValue::interpolate(&start.padding, &end.padding, state), } } } diff --git a/compositor_render/src/scene/view_component/layout.rs b/compositor_render/src/scene/view_component/layout.rs index 8b356f1a3..2b2563d17 100644 --- a/compositor_render/src/scene/view_component/layout.rs +++ b/compositor_render/src/scene/view_component/layout.rs @@ -38,6 +38,7 @@ impl ViewComponentParam { width: f32::max(size.width - 2.0 * self.border_width, 0.0), height: f32::max(size.height - 2.0 * self.border_width, 0.0), }; + let static_child_size = self.static_child_size(content_size, children, pts); let (scale, crop, mask) = match self.overflow { Overflow::Visible => (1.0, None, None), @@ -99,7 +100,11 @@ impl ViewComponentParam { } Position::Absolute(position) => { StatefulLayoutComponent::layout_absolute_position_child( - child, position, size, pts, + child, + position, + size, + self.padding, + pts, ) } } @@ -132,24 +137,34 @@ impl ViewComponentParam { pts: Duration, ) -> (NestedLayout, f32) { let mut static_offset = opts.static_offset; + + let (static_width, static_height) = match self.direction { + ViewChildrenDirection::Row => (opts.static_child_size, opts.parent_size.height), + ViewChildrenDirection::Column => (opts.parent_size.width, opts.static_child_size), + }; + + // Parent padding can shrink the child if it doesn't have width/height provided + let static_width = static_width - self.padding.horizontal(); + let static_height = static_height - self.padding.vertical(); + + let width = opts.width.unwrap_or(static_width); + let height = opts.height.unwrap_or(static_height); + let (top, left, width, height) = match self.direction { ViewChildrenDirection::Row => { - let width = opts.width.unwrap_or(opts.static_child_size); - let height = opts.height.unwrap_or(opts.parent_size.height); - let top = opts.parent_border_width; - let left = static_offset; + let top = opts.parent_border_width + self.padding.top; + let left = static_offset + self.padding.left; static_offset += width; (top, left, width, height) } ViewChildrenDirection::Column => { - let height = opts.height.unwrap_or(opts.static_child_size); - let width = opts.width.unwrap_or(opts.parent_size.width); - let top = static_offset; - let left = opts.parent_border_width; + let top = static_offset + self.padding.top; + let left = opts.parent_border_width + self.padding.left; static_offset += height; (top, left, width, height) } }; + let layout = match child { StatefulComponent::Layout(layout_component) => { let children_layouts = layout_component.layout(Size { width, height }, pts); diff --git a/integration_tests/examples/tiles.rs b/integration_tests/examples/tiles.rs index cfbb12600..4ada7bbb5 100644 --- a/integration_tests/examples/tiles.rs +++ b/integration_tests/examples/tiles.rs @@ -39,7 +39,7 @@ fn client_code() -> Result<()> { .map(|_| { json!({ "type": "input_stream", - "input_id": "input_1", + "input_id": "input_1" }) }) .collect(); diff --git a/schemas/scene.schema.json b/schemas/scene.schema.json index bb0f320cf..94f57f08d 100644 --- a/schemas/scene.schema.json +++ b/schemas/scene.schema.json @@ -251,6 +251,38 @@ "items": { "$ref": "#/definitions/BoxShadow" } + }, + "padding_top": { + "description": "(**default=`0.0`**) Padding on top side in pixels.", + "type": [ + "number", + "null" + ], + "format": "float" + }, + "padding_right": { + "description": "(**default=`0.0`**) Padding on right side in pixels.", + "type": [ + "number", + "null" + ], + "format": "float" + }, + "padding_bottom": { + "description": "(**default=`0.0`**) Padding on bottom side in pixels.", + "type": [ + "number", + "null" + ], + "format": "float" + }, + "padding_left": { + "description": "(**default=`0.0`**) Padding on left side in pixels.", + "type": [ + "number", + "null" + ], + "format": "float" } }, "additionalProperties": false diff --git a/snapshot_tests/snapshots b/snapshot_tests/snapshots index 57fbdbaa6..d66c571a0 160000 --- a/snapshot_tests/snapshots +++ b/snapshot_tests/snapshots @@ -1 +1 @@ -Subproject commit 57fbdbaa6cf83e4cfe907c04af409221e5a1d566 +Subproject commit d66c571a0902fe3ab2f323da97e583088a9b5b57 diff --git a/snapshot_tests/view/column_view_padding_static_children.scene.json b/snapshot_tests/view/column_view_padding_static_children.scene.json new file mode 100644 index 000000000..de34427c2 --- /dev/null +++ b/snapshot_tests/view/column_view_padding_static_children.scene.json @@ -0,0 +1,49 @@ +{ + "video": { + "root": { + "type": "view", + "background_color": "#000000FF", + "children": [ + { + "type": "view", + "background_color": "#FF0000FF", + "direction": "column", + "width": 300, + "height": 300, + "children": [ + { + "type": "view", + "padding_top": 10, + "padding_left": 20, + "border_width": 4, + "border_color": "#FF00FFFF", + "background_color": "#0000FFFF", + "children": [] + }, + { + "type": "view", + "padding_top": 10, + "padding_bottom": 30, + "padding_right": 10, + "padding_left": 20, + "border_width": 4, + "border_color": "#0F0FF0FF", + "background_color": "#00FF00FF", + "children": [ + { + "type": "view", + "width": 250, + "height": 250, + "border_width": 4, + "border_color": "#FF00FFFF", + "background_color": "#00FFFFFF", + "children": [] + } + ] + } + ] + } + ] + } + } +} diff --git a/snapshot_tests/view/nested_padding_static_children.scene.json b/snapshot_tests/view/nested_padding_static_children.scene.json new file mode 100644 index 000000000..0b6a65fce --- /dev/null +++ b/snapshot_tests/view/nested_padding_static_children.scene.json @@ -0,0 +1,59 @@ +{ + "video": { + "root": { + "type": "view", + "background_color": "#000000FF", + "children": [ + { + "type": "view", + "background_color": "#FF0000FF", + "direction": "row", + "width": 300, + "height": 300, + "children": [ + { + "type": "view", + "padding_top": 10, + "padding_left": 5, + "border_width": 4, + "border_color": "#FF00FFFF", + "background_color": "#0000FFFF", + "children": [] + }, + { + "type": "view", + "padding_top": 10, + "padding_left": 15, + "border_width": 4, + "border_color": "#00FF00FF", + "children": [ + { + "type": "view", + "padding_top": 10, + "padding_bottom": 20, + "padding_right": 10, + "border_width": 4, + "border_color": "#0000FFFF", + "padding_left": 20, + "background_color": "#00FF00FF", + "children": [ + { + "type": "view", + "width": 250, + "height": 250, + "padding_top": 40, + "border_width": 4, + "border_color": "#FF0000FF", + "background_color": "#00FFFFFF", + "children": [] + } + ] + } + ] + } + ] + } + ] + } + } +} diff --git a/snapshot_tests/view/padding_absolute_left_children.scene.json b/snapshot_tests/view/padding_absolute_left_children.scene.json new file mode 100644 index 000000000..fc72b44f0 --- /dev/null +++ b/snapshot_tests/view/padding_absolute_left_children.scene.json @@ -0,0 +1,50 @@ +{ + "video": { + "root": { + "type": "view", + "background_color": "#000000FF", + "children": [ + { + "type": "view", + "background_color": "#FF0000FF", + "direction": "row", + "width": 300, + "height": 300, + "children": [ + { + "type": "view", + "padding_top": 10, + "padding_left": 5, + "background_color": "#0000FFFF", + "children": [] + }, + { + "type": "view", + "padding_top": 10, + "padding_left": 15, + "children": [ + { + "type": "view", + "background_color": "#00FF00FF", + "top": 10, + "left": 20, + "padding_top": 10, + "padding_left": 20, + "children": [ + { + "type": "view", + "width": 250, + "height": 250, + "background_color": "#FF00FFFF", + "children": [] + } + ] + } + ] + } + ] + } + ] + } + } +} diff --git a/snapshot_tests/view/padding_absolute_right_children.scene.json b/snapshot_tests/view/padding_absolute_right_children.scene.json new file mode 100644 index 000000000..6cb1ac566 --- /dev/null +++ b/snapshot_tests/view/padding_absolute_right_children.scene.json @@ -0,0 +1,48 @@ +{ + "video": { + "root": { + "type": "view", + "background_color": "#000000FF", + "children": [ + { + "type": "view", + "background_color": "#FF0000FF", + "direction": "row", + "width": 300, + "height": 300, + "children": [ + { + "type": "view", + "padding_top": 10, + "padding_left": 5, + "background_color": "#0000FFFF", + "children": [] + }, + { + "type": "view", + "padding_top": 10, + "padding_left": 15, + "children": [ + { + "type": "view", + "background_color": "#00FF00FF", + "top": 10, + "right": 30, + "padding_top": 20, + "padding_left": 20, + "padding_right": 20, + "children": [ + { + "type": "input_stream", + "input_id": "input_1" + } + ] + } + ] + } + ] + } + ] + } + } +} diff --git a/snapshot_tests/view/row_view_padding_static_children.scene.json b/snapshot_tests/view/row_view_padding_static_children.scene.json new file mode 100644 index 000000000..53c2a39d7 --- /dev/null +++ b/snapshot_tests/view/row_view_padding_static_children.scene.json @@ -0,0 +1,42 @@ +{ + "video": { + "root": { + "type": "view", + "background_color": "#000000FF", + "children": [ + { + "type": "view", + "background_color": "#FF0000FF", + "direction": "row", + "width": 300, + "height": 300, + "children": [ + { + "type": "view", + "padding_left": 20, + "background_color": "#0000FFFF", + "children": [] + }, + { + "type": "view", + "padding_bottom": 30, + "padding_left": 20, + "padding_top": 10, + "padding_right": 10, + "background_color": "#00FF00FF", + "children": [ + { + "type": "view", + "width": 250, + "height": 250, + "background_color": "#00FFFFFF", + "children": [] + } + ] + } + ] + } + ] + } + } +} diff --git a/src/snapshot_tests/view_tests.rs b/src/snapshot_tests/view_tests.rs index 225983484..24eaa1f50 100644 --- a/src/snapshot_tests/view_tests.rs +++ b/src/snapshot_tests/view_tests.rs @@ -214,6 +214,41 @@ fn view_tests() { )), ..default.clone() }); + runner.add(TestCase { + name: "view/column_view_padding_static_children", + scene_updates: scene_from_json(include_str!( + "../../snapshot_tests/view/column_view_padding_static_children.scene.json" + )), + ..default.clone() + }); + runner.add(TestCase { + name: "view/row_view_padding_static_children", + scene_updates: scene_from_json(include_str!( + "../../snapshot_tests/view/row_view_padding_static_children.scene.json" + )), + ..default.clone() + }); + runner.add(TestCase { + name: "view/nested_padding_static_children", + scene_updates: scene_from_json(include_str!( + "../../snapshot_tests/view/nested_padding_static_children.scene.json" + )), + ..default.clone() + }); + runner.add(TestCase { + name: "view/padding_absolute_children_left", + scene_updates: scene_from_json(include_str!( + "../../snapshot_tests/view/padding_absolute_left_children.scene.json" + )), + ..default.clone() + }); + runner.add(TestCase { + name: "view/padding_absolute_children_right", + scene_updates: scene_from_json(include_str!( + "../../snapshot_tests/view/padding_absolute_right_children.scene.json" + )), + ..default.clone() + }); runner.run() } diff --git a/src/snapshot_tests/yuv_tests.rs b/src/snapshot_tests/yuv_tests.rs index f877f40e0..793b12dcb 100644 --- a/src/snapshot_tests/yuv_tests.rs +++ b/src/snapshot_tests/yuv_tests.rs @@ -98,6 +98,7 @@ fn yuv_test_uniform_color() { border_width: 0.0, border_color: RGBAColor(0, 0, 0, 0), box_shadow: vec![], + padding: Default::default(), })], resolution: Resolution { width, height }, ..Default::default() diff --git a/ts/live-compositor/src/api.generated.ts b/ts/live-compositor/src/api.generated.ts index c2dfaa05f..9b73a865e 100644 --- a/ts/live-compositor/src/api.generated.ts +++ b/ts/live-compositor/src/api.generated.ts @@ -328,6 +328,22 @@ export type Component = * List of box shadows. */ box_shadow?: BoxShadow[] | null; + /** + * (**default=`0.0`**) Padding on top side in pixels. + */ + padding_top?: number | null; + /** + * (**default=`0.0`**) Padding on right side in pixels. + */ + padding_right?: number | null; + /** + * (**default=`0.0`**) Padding on bottom side in pixels. + */ + padding_bottom?: number | null; + /** + * (**default=`0.0`**) Padding on left side in pixels. + */ + padding_left?: number | null; } | { type: "web_view"; diff --git a/ts/live-compositor/src/components/View.ts b/ts/live-compositor/src/components/View.ts index c779c363c..d8a3cb702 100644 --- a/ts/live-compositor/src/components/View.ts +++ b/ts/live-compositor/src/components/View.ts @@ -77,6 +77,34 @@ export type ViewStyleProps = { * Properties of the BoxShadow applied to the container. */ boxShadow?: Api.BoxShadow[]; + /** + * (**default=`0.0`**) Sets padding for all sides of the component. + */ + padding?: number; + /** + * (**default=`0.0`**) Sets padding for the top and bottom of the component. + */ + paddingVertical?: number; + /** + * (**default=`0.0`**) Sets padding for the left and right of the component. + */ + paddingHorizontal?: number; + /** + * (**default=`0.0`**) Sets padding for the top of the component. + */ + paddingTop?: number; + /** + * (**default=`0.0`**) Sets padding for the right of the component. + */ + paddingRight?: number; + /** + * (**default=`0.0`**) Sets padding for the bottom of the component. + */ + paddingBottom?: number; + /** + * (**default=`0.0`**) Sets padding for the left of the component. + */ + paddingLeft?: number; }; export type ViewProps = ComponentBaseProps & { @@ -120,6 +148,11 @@ function sceneBuilder( border_color: style.borderColor, box_shadow: style.boxShadow, + + padding_top: style.paddingTop ?? style.paddingVertical ?? style.padding, + padding_bottom: style.paddingBottom ?? style.paddingVertical ?? style.padding, + padding_right: style.paddingRight ?? style.paddingHorizontal ?? style.padding, + padding_left: style.paddingLeft ?? style.paddingHorizontal ?? style.padding, }; }