diff --git a/compositor_api/src/types/component.rs b/compositor_api/src/types/component.rs index b08762531..d8cac4430 100644 --- a/compositor_api/src/types/component.rs +++ b/compositor_api/src/types/component.rs @@ -36,14 +36,14 @@ pub struct View { /// List of component's children. pub children: Option>, - /// Width of a component in pixels. Exact behavior might be different based on the parent - /// component: + /// Width of a component in pixels (without a border). Exact behavior might be different + /// based on the parent component: /// - If the parent component is a layout, check sections "Absolute positioning" and "Static /// positioning" of that component. /// - If the parent component is not a layout, then this field is required. pub width: Option, - /// Height of a component in pixels. Exact behavior might be different based on the parent - /// component: + /// Height of a component in pixels (without a border). Exact behavior might be different + /// based on the parent component: /// - If the parent component is a layout, check sections "Absolute positioning" and "Static /// positioning" of that component. /// - If the parent component is not a layout, then this field is required. @@ -52,16 +52,16 @@ pub struct View { /// Direction defines how static children are positioned inside a View component. pub direction: Option, - /// Distance in pixels between this component's top edge and its parent's top edge. + /// Distance in pixels between this component's top edge and its parent's top edge (including a border). /// If this field is defined, then the component will ignore a layout defined by its parent. pub top: Option, - /// Distance in pixels between this component's left edge and its parent's left edge. + /// Distance in pixels between this component's left edge and its parent's left edge (including a border). /// If this field is defined, this element will be absolutely positioned, instead of being /// laid out by its parent. pub left: Option, - /// Distance in pixels between the bottom edge of this component and the bottom edge of its parent. - /// If this field is defined, this element will be absolutely positioned, instead of being - /// laid out by its parent. + /// Distance in pixels between the bottom edge of this component and the bottom edge of its + /// parent (including a border). If this field is defined, this element will be absolutely + /// positioned, instead of being laid out by its parent. pub bottom: Option, /// Distance in pixels between this component's right edge and its parent's right edge. /// If this field is defined, this element will be absolutely positioned, instead of being @@ -80,6 +80,27 @@ pub struct View { /// (**default=`"#00000000"`**) Background color in a `"#RRGGBBAA"` format. pub background_color_rgba: Option, + + /// (**default=`0.0`**) Radius of a rounded corner. + pub border_radius: Option, + + /// (**default=`0.0`**) Border width. + pub border_width: Option, + + /// (**default=`"#00000000"`**) Border color in a `"#RRGGBBAA"` format. + pub border_color_rgba: Option, + + /// List of box shadows. + pub box_shadow: Option>, +} + +#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct BoxShadow { + pub offset_x: Option, + pub offset_y: Option, + pub color_rgba: Option, + pub blur_radius: Option, } #[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)] @@ -126,29 +147,29 @@ pub struct Rescaler { /// (**default=`"center"`**) Vertical alignment. pub vertical_align: Option, - /// Width of a component in pixels. Exact behavior might be different based on the parent - /// component: + /// Width of a component in pixels (without a border). Exact behavior might be different + /// based on the parent component: /// - If the parent component is a layout, check sections "Absolute positioning" and "Static /// positioning" of that component. /// - If the parent component is not a layout, then this field is required. pub width: Option, - /// Height of a component in pixels. Exact behavior might be different based on the parent - /// component: + /// Height of a component in pixels (without a border). Exact behavior might be different + /// based on the parent component: /// - If the parent component is a layout, check sections "Absolute positioning" and "Static /// positioning" of that component. /// - If the parent component is not a layout, then this field is required. pub height: Option, - /// Distance in pixels between this component's top edge and its parent's top edge. + /// Distance in pixels between this component's top edge and its parent's top edge (including a border). /// If this field is defined, then the component will ignore a layout defined by its parent. pub top: Option, - /// Distance in pixels between this component's left edge and its parent's left edge. + /// Distance in pixels between this component's left edge and its parent's left edge (including a border). /// If this field is defined, this element will be absolutely positioned, instead of being /// laid out by its parent. pub left: Option, - /// Distance in pixels between this component's bottom edge and its parent's bottom edge. - /// If this field is defined, this element will be absolutely positioned, instead of being - /// laid out by its parent. + /// Distance in pixels between the bottom edge of this component and the bottom edge of its + /// parent (including a border). If this field is defined, this element will be absolutely + /// positioned, instead of being laid out by its parent. pub bottom: Option, /// Distance in pixels between this component's right edge and its parent's right edge. /// If this field is defined, this element will be absolutely positioned, instead of being @@ -161,6 +182,18 @@ pub struct Rescaler { /// Defines how this component will behave during a scene update. This will only have an /// effect if the previous scene already contained a `Rescaler` component with the same id. pub transition: Option, + + /// (**default=`0.0`**) Radius of a rounded corner. + pub border_radius: Option, + + /// (**default=`0.0`**) Border width. + pub border_width: Option, + + /// (**default=`"#00000000"`**) Border color in a `"#RRGGBBAA"` format. + pub border_color_rgba: Option, + + /// List of box shadows. + pub box_shadow: Option>, } #[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)] @@ -183,7 +216,8 @@ pub struct WebView { /// List of component's children. pub children: Option>, - /// Id of a web renderer instance. It identifies an instance registered using a [`register web renderer`](../routes.md#register-web-renderer-instance) request. + /// Id of a web renderer instance. It identifies an instance registered using a + /// [`register web renderer`](../routes.md#register-web-renderer-instance) request. /// /// :::warning /// You can only refer to specific instances in one Component at a time. @@ -217,8 +251,10 @@ pub struct Shader { /// @group(1) @binding(0) var /// ``` /// :::note - /// This object's structure must match the structure defined in a shader source code. Currently, we do not handle memory layout automatically. - /// To achieve the correct memory alignment, you might need to pad your data with additional fields. See [WGSL documentation](https://www.w3.org/TR/WGSL/#alignment-and-size) for more details. + /// This object's structure must match the structure defined in a shader source code. + /// Currently, we do not handle memory layout automatically. To achieve the correct memory + /// alignment, you might need to pad your data with additional fields. See + /// [WGSL documentation](https://www.w3.org/TR/WGSL/#alignment-and-size) for more details. /// ::: pub shader_param: Option, /// Resolution of a texture where shader will be executed. @@ -378,4 +414,6 @@ pub struct Tiles { /// Defines how this component will behave during a scene update. This will only have an /// effect if the previous scene already contained a `Tiles` component with the same id. pub transition: Option, + + pub border_radius: Option, } diff --git a/compositor_api/src/types/from_component.rs b/compositor_api/src/types/from_component.rs index 66a4346ac..5bc2ad9fb 100644 --- a/compositor_api/src/types/from_component.rs +++ b/compositor_api/src/types/from_component.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use compositor_render::scene; +use compositor_render::scene::BorderRadius; use compositor_render::scene::Position; use compositor_render::MAX_NODE_RESOLUTION; @@ -101,6 +102,18 @@ impl TryFrom for scene::ViewComponent { .map(TryInto::try_into) .unwrap_or(Ok(scene::RGBAColor(0, 0, 0, 0)))?, transition: view.transition.map(TryInto::try_into).transpose()?, + border_radius: BorderRadius::new_with_radius(view.border_radius.unwrap_or(0.0)), + border_width: view.border_width.unwrap_or(0.0), + border_color: view + .border_color_rgba + .map(TryInto::try_into) + .unwrap_or(Ok(scene::RGBAColor(0, 0, 0, 0)))?, + box_shadow: view + .box_shadow + .unwrap_or_default() + .into_iter() + .map(TryInto::try_into) + .collect::>()?, }) } } @@ -165,6 +178,18 @@ impl TryFrom for scene::RescalerComponent { .unwrap_or(VerticalAlign::Center) .into(), transition: rescaler.transition.map(TryInto::try_into).transpose()?, + border_radius: BorderRadius::new_with_radius(rescaler.border_radius.unwrap_or(0.0)), + border_width: rescaler.border_width.unwrap_or(0.0), + border_color: rescaler + .border_color_rgba + .map(TryInto::try_into) + .unwrap_or(Ok(scene::RGBAColor(0, 0, 0, 0)))?, + box_shadow: rescaler + .box_shadow + .unwrap_or_default() + .into_iter() + .map(TryInto::try_into) + .collect::>()?, }) } } @@ -339,3 +364,19 @@ impl TryFrom for scene::TilesComponent { Ok(result) } } + +impl TryFrom for scene::BoxShadow { + type Error = TypeError; + + fn try_from(value: BoxShadow) -> Result { + Ok(Self { + offset_x: value.offset_x.unwrap_or(0.0), + offset_y: value.offset_y.unwrap_or(0.0), + blur_radius: value.blur_radius.unwrap_or(0.0), + color: value + .color_rgba + .map(TryInto::try_into) + .unwrap_or(Ok(scene::RGBAColor(255, 255, 255, 255)))?, + }) + } +} diff --git a/compositor_render/src/scene/components.rs b/compositor_render/src/scene/components.rs index e0396c054..1ad769a45 100644 --- a/compositor_render/src/scene/components.rs +++ b/compositor_render/src/scene/components.rs @@ -3,10 +3,12 @@ use std::{fmt::Display, sync::Arc, time::Duration}; use crate::{InputId, RendererId}; use super::{ - AbsolutePosition, Component, HorizontalAlign, InterpolationKind, RGBAColor, Size, VerticalAlign, + AbsolutePosition, BorderRadius, BoxShadow, Component, HorizontalAlign, InterpolationKind, + RGBAColor, Size, VerticalAlign, }; mod interpolation; +mod position; #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ComponentId(pub Arc); @@ -140,6 +142,12 @@ pub struct ViewComponent { pub overflow: Overflow, pub background_color: RGBAColor, + + pub border_radius: BorderRadius, + pub border_width: f32, + pub border_color: RGBAColor, + + pub box_shadow: Vec, } #[derive(Debug, Clone, Copy)] @@ -181,6 +189,12 @@ pub struct RescalerComponent { pub mode: RescaleMode, pub horizontal_align: HorizontalAlign, pub vertical_align: VerticalAlign, + + pub border_radius: BorderRadius, + pub border_width: f32, + pub border_color: RGBAColor, + + pub box_shadow: Vec, } #[derive(Debug, Clone, Copy)] diff --git a/compositor_render/src/scene/components/interpolation.rs b/compositor_render/src/scene/components/interpolation.rs index d2872457b..a01e95c6e 100644 --- a/compositor_render/src/scene/components/interpolation.rs +++ b/compositor_render/src/scene/components/interpolation.rs @@ -1,4 +1,7 @@ -use crate::scene::types::interpolation::{ContinuousValue, InterpolationState}; +use crate::scene::{ + types::interpolation::{ContinuousValue, InterpolationState}, + BorderRadius, BoxShadow, +}; use super::{AbsolutePosition, Position}; @@ -46,3 +49,42 @@ impl ContinuousValue for AbsolutePosition { } } } + +impl ContinuousValue for BorderRadius { + fn interpolate(start: &Self, end: &Self, state: InterpolationState) -> Self { + Self { + top_left: ContinuousValue::interpolate(&start.top_left, &end.top_left, state), + top_right: ContinuousValue::interpolate(&start.top_right, &end.top_right, state), + bottom_right: ContinuousValue::interpolate( + &start.bottom_right, + &end.bottom_right, + state, + ), + bottom_left: ContinuousValue::interpolate(&start.bottom_left, &end.bottom_left, state), + } + } +} + +impl ContinuousValue for Vec { + fn interpolate(start: &Self, end: &Self, state: InterpolationState) -> Self { + start + .iter() + .zip(end.iter()) + // interpolate as long both lists have entries + .map(|(start, end)| ContinuousValue::interpolate(start, end, state)) + // add remaining elements if end is longer + .chain(end.iter().skip(usize::min(start.len(), end.len())).copied()) + .collect() + } +} + +impl ContinuousValue for BoxShadow { + fn interpolate(start: &Self, end: &Self, state: InterpolationState) -> Self { + Self { + offset_x: ContinuousValue::interpolate(&start.offset_x, &end.offset_x, state), + offset_y: ContinuousValue::interpolate(&start.offset_y, &end.offset_y, state), + blur_radius: ContinuousValue::interpolate(&start.blur_radius, &end.blur_radius, state), + color: end.color, + } + } +} diff --git a/compositor_render/src/scene/components/position.rs b/compositor_render/src/scene/components/position.rs new file mode 100644 index 000000000..0525293d2 --- /dev/null +++ b/compositor_render/src/scene/components/position.rs @@ -0,0 +1,27 @@ +use crate::scene::AbsolutePosition; + +use super::Position; + +impl Position { + pub(crate) fn with_border(self, border_width: f32) -> Self { + match self { + Position::Static { width, height } => Self::Static { + width: width.map(|w| w + 2.0 * border_width), + height: height.map(|h| h + 2.0 * border_width), + }, + Position::Absolute(AbsolutePosition { + width, + height, + position_horizontal, + position_vertical, + rotation_degrees, + }) => Self::Absolute(AbsolutePosition { + width: width.map(|w| w + 2.0 * border_width), + height: height.map(|h| h + 2.0 * border_width), + position_horizontal, + position_vertical, + rotation_degrees, + }), + } + } +} diff --git a/compositor_render/src/scene/layout.rs b/compositor_render/src/scene/layout.rs index 8a2ab7d09..710661c46 100644 --- a/compositor_render/src/scene/layout.rs +++ b/compositor_render/src/scene/layout.rs @@ -7,8 +7,8 @@ use crate::{ use super::{ rescaler_component::StatefulRescalerComponent, tiles_component::StatefulTilesComponent, - view_component::StatefulViewComponent, AbsolutePosition, ComponentId, HorizontalPosition, - Position, Size, StatefulComponent, VerticalPosition, + view_component::StatefulViewComponent, AbsolutePosition, BorderRadius, ComponentId, + HorizontalPosition, Position, RGBAColor, Size, StatefulComponent, VerticalPosition, }; #[derive(Debug, Clone)] @@ -49,6 +49,7 @@ impl StatefulLayoutComponent { } } + // External position of a component (includes border, padding, ...) pub(super) fn position(&self, pts: Duration) -> Position { match self { StatefulLayoutComponent::View(view) => view.position(pts), @@ -177,6 +178,7 @@ impl StatefulLayoutComponent { let rotation_degrees = position.rotation_degrees; let content = Self::layout_content(child, 0); let crop = None; + let mask = None; match child { StatefulComponent::Layout(layout_component) => { @@ -194,10 +196,15 @@ impl StatefulLayoutComponent { scale_x: 1.0, scale_y: 1.0, crop, + mask, content, child_nodes_count, children: vec![children_layouts], + border_width: 0.0, + border_color: RGBAColor(0, 0, 0, 0), + border_radius: BorderRadius::ZERO, + box_shadow: vec![], } } _non_layout_components => { @@ -215,10 +222,15 @@ impl StatefulLayoutComponent { scale_x: 1.0, scale_y: 1.0, crop, + mask, content, child_nodes_count, children: vec![], + border_width: 0.0, + border_color: RGBAColor(0, 0, 0, 0), + border_radius: BorderRadius::ZERO, + box_shadow: vec![], } } } diff --git a/compositor_render/src/scene/rescaler_component.rs b/compositor_render/src/scene/rescaler_component.rs index 259dcf60e..4bd40d8d5 100644 --- a/compositor_render/src/scene/rescaler_component.rs +++ b/compositor_render/src/scene/rescaler_component.rs @@ -8,8 +8,8 @@ use super::{ scene_state::BuildStateTreeCtx, transition::{TransitionOptions, TransitionState}, types::interpolation::ContinuousValue, - Component, ComponentId, HorizontalAlign, IntermediateNode, Position, RescaleMode, SceneError, - Size, StatefulComponent, VerticalAlign, + BorderRadius, BoxShadow, Component, ComponentId, HorizontalAlign, IntermediateNode, Position, + RGBAColor, RescaleMode, SceneError, Size, StatefulComponent, VerticalAlign, }; mod interpolation; @@ -31,6 +31,12 @@ struct RescalerComponentParam { mode: RescaleMode, horizontal_align: HorizontalAlign, vertical_align: VerticalAlign, + + border_radius: BorderRadius, + border_width: f32, + border_color: RGBAColor, + + box_shadow: Vec, } impl StatefulRescalerComponent { @@ -52,7 +58,8 @@ impl StatefulRescalerComponent { } pub(super) fn position(&self, pts: Duration) -> Position { - self.transition_snapshot(pts).position + let rescaler = self.transition_snapshot(pts); + rescaler.position.with_border(rescaler.border_width) } pub(super) fn component_id(&self) -> Option<&ComponentId> { @@ -107,7 +114,7 @@ impl RescalerComponent { previous_state.and_then(|s| s.transition.clone()), ctx.last_render_pts, ); - let view = StatefulRescalerComponent { + let rescaler = StatefulRescalerComponent { start, end: RescalerComponentParam { id: self.id, @@ -115,12 +122,16 @@ impl RescalerComponent { mode: self.mode, horizontal_align: self.horizontal_align, vertical_align: self.vertical_align, + border_radius: self.border_radius, + border_width: self.border_width, + border_color: self.border_color, + box_shadow: self.box_shadow, }, transition, child: Box::new(Component::stateful_component(*self.child, ctx)?), }; Ok(StatefulComponent::Layout( - StatefulLayoutComponent::Rescaler(view), + StatefulLayoutComponent::Rescaler(rescaler), )) } } diff --git a/compositor_render/src/scene/rescaler_component/interpolation.rs b/compositor_render/src/scene/rescaler_component/interpolation.rs index 019100c9b..78dc02482 100644 --- a/compositor_render/src/scene/rescaler_component/interpolation.rs +++ b/compositor_render/src/scene/rescaler_component/interpolation.rs @@ -10,6 +10,18 @@ impl ContinuousValue for RescalerComponentParam { mode: end.mode, horizontal_align: end.horizontal_align, vertical_align: end.vertical_align, + border_radius: ContinuousValue::interpolate( + &start.border_radius, + &end.border_radius, + state, + ), + border_width: ContinuousValue::interpolate( + &start.border_width, + &end.border_width, + state, + ), + border_color: end.border_color, + box_shadow: ContinuousValue::interpolate(&start.box_shadow, &end.box_shadow, state), } } } diff --git a/compositor_render/src/scene/rescaler_component/layout.rs b/compositor_render/src/scene/rescaler_component/layout.rs index cdf71b5db..7127ff103 100644 --- a/compositor_render/src/scene/rescaler_component/layout.rs +++ b/compositor_render/src/scene/rescaler_component/layout.rs @@ -2,10 +2,10 @@ use std::time::Duration; use crate::{ scene::{ - layout::StatefulLayoutComponent, HorizontalAlign, RescaleMode, Size, StatefulComponent, - VerticalAlign, + layout::StatefulLayoutComponent, BorderRadius, HorizontalAlign, RGBAColor, RescaleMode, + Size, StatefulComponent, VerticalAlign, }, - transformations::layout::{Crop, LayoutContent, NestedLayout}, + transformations::layout::{LayoutContent, Mask, NestedLayout}, }; use super::RescalerComponentParam; @@ -17,50 +17,58 @@ impl RescalerComponentParam { child: &mut StatefulComponent, pts: Duration, ) -> NestedLayout { + let content_size = Size { + 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 child_width = child.width(pts); let child_height = child.height(pts); match (child_width, child_height) { - (None, None) => self.layout_with_scale(size, child, pts, 1.0), + (None, None) => self.layout_with_scale(content_size, child, pts, 1.0), (None, Some(child_height)) => { - self.layout_with_scale(size, child, pts, size.height / child_height) + self.layout_with_scale(content_size, child, pts, content_size.height / child_height) } (Some(child_width), None) => { - self.layout_with_scale(size, child, pts, size.width / child_width) + self.layout_with_scale(content_size, child, pts, content_size.width / child_width) } (Some(child_width), Some(child_height)) => { let scale = match self.mode { - RescaleMode::Fit => { - f32::min(size.width / child_width, size.height / child_height) - } - RescaleMode::Fill => { - f32::max(size.width / child_width, size.height / child_height) - } + RescaleMode::Fit => f32::min( + content_size.width / child_width, + content_size.height / child_height, + ), + RescaleMode::Fill => f32::max( + content_size.width / child_width, + content_size.height / child_height, + ), }; - self.layout_with_scale(size, child, pts, scale) + self.layout_with_scale(content_size, child, pts, scale) } } } fn layout_with_scale( &self, - size: Size, + max_size: Size, // without borders child: &mut StatefulComponent, pts: Duration, scale: f32, ) -> NestedLayout { + let child_width = child.width(pts); + let child_height = child.height(pts); let (content, children, child_nodes_count) = match child { StatefulComponent::Layout(layout_component) => { - let children_layouts = layout_component.layout( + let children_layout = layout_component.layout( Size { - width: size.width / scale, - height: size.height / scale, + width: child_width.unwrap_or(max_size.width / scale), + height: child_height.unwrap_or(max_size.height / scale), }, pts, ); - let child_nodes_count = children_layouts.child_nodes_count; + let child_nodes_count = children_layout.child_nodes_count; ( LayoutContent::None, - vec![children_layouts], + vec![children_layout], child_nodes_count, ) } @@ -71,63 +79,74 @@ impl RescalerComponentParam { VerticalAlign::Top => 0.0, VerticalAlign::Bottom => child .height(pts) - .map(|height| size.height - (height * scale)) + .map(|height| max_size.height - (height * scale)) .unwrap_or(0.0), VerticalAlign::Center | VerticalAlign::Justified => child .height(pts) - .map(|height| (size.height - (height * scale)) / 2.0) + .map(|height| (max_size.height - (height * scale)) / 2.0) .unwrap_or(0.0), }; let left = match self.horizontal_align { HorizontalAlign::Left => 0.0, HorizontalAlign::Right => child .width(pts) - .map(|width| (size.width - (width * scale))) + .map(|width| (max_size.width - (width * scale))) .unwrap_or(0.0), HorizontalAlign::Center | HorizontalAlign::Justified => child .width(pts) - .map(|width| (size.width - (width * scale)) / (2.0)) + .map(|width| (max_size.width - (width * scale)) / (2.0)) .unwrap_or(0.0), }; let width = child .width(pts) .map(|child_width| child_width * scale) - .unwrap_or(size.width); + .unwrap_or(max_size.width); let height = child .height(pts) .map(|child_height| child_height * scale) - .unwrap_or(size.height); + .unwrap_or(max_size.height); NestedLayout { top: 0.0, left: 0.0, - width: size.width, - height: size.height, + width: max_size.width + (self.border_width * 2.0), + height: max_size.height + (self.border_width * 2.0), rotation_degrees: 0.0, scale_x: 1.0, scale_y: 1.0, - crop: Some(Crop { - top: 0.0, - left: 0.0, - width: size.width, - height: size.height, + crop: None, + mask: Some(Mask { + radius: self.border_radius - self.border_width, + top: self.border_width, + left: self.border_width, + width: max_size.width, + height: max_size.height, }), content: LayoutContent::None, children: vec![NestedLayout { - top, - left, + top: top + self.border_width, + left: left + self.border_width, width, height, rotation_degrees: 0.0, scale_x: scale, scale_y: scale, crop: None, + mask: None, content, child_nodes_count, children, + border_width: 0.0, + border_color: RGBAColor(0, 0, 0, 0), + border_radius: BorderRadius::ZERO, + box_shadow: vec![], }], child_nodes_count, + border_width: self.border_width, + border_color: self.border_color, + border_radius: self.border_radius, + box_shadow: self.box_shadow.clone(), } } } diff --git a/compositor_render/src/scene/tiles_component/layout.rs b/compositor_render/src/scene/tiles_component/layout.rs index d7080be78..adc76c173 100644 --- a/compositor_render/src/scene/tiles_component/layout.rs +++ b/compositor_render/src/scene/tiles_component/layout.rs @@ -1,7 +1,7 @@ use std::time::Duration; use crate::{ - scene::{layout::StatefulLayoutComponent, RGBAColor, Size, StatefulComponent}, + scene::{layout::StatefulLayoutComponent, BorderRadius, RGBAColor, Size, StatefulComponent}, transformations::layout::{LayoutContent, NestedLayout}, }; @@ -29,9 +29,14 @@ pub(super) fn layout_tiles( scale_x: 1.0, scale_y: 1.0, crop: None, + mask: None, content: LayoutContent::Color(background_color), child_nodes_count: children.iter().map(|l| l.child_nodes_count).sum(), children, + border_width: 0.0, + border_color: RGBAColor(0, 0, 0, 0), + border_radius: BorderRadius::ZERO, + box_shadow: vec![], } } @@ -64,9 +69,14 @@ fn layout_child(child: &mut StatefulComponent, tile: Option, pts: Duration scale_x: 1.0, scale_y: 1.0, crop: None, + mask: None, content: LayoutContent::None, child_nodes_count: children_layouts.child_nodes_count, children: vec![children_layouts], + border_width: 0.0, + border_color: RGBAColor(0, 0, 0, 0), + border_radius: BorderRadius::ZERO, + box_shadow: vec![], } } _ => { @@ -81,9 +91,14 @@ fn layout_child(child: &mut StatefulComponent, tile: Option, pts: Duration scale_x: 1.0, scale_y: 1.0, crop: None, + mask: None, content: StatefulLayoutComponent::layout_content(child, 0), child_nodes_count: 1, children: vec![], + border_width: 0.0, + border_color: RGBAColor(0, 0, 0, 0), + border_radius: BorderRadius::ZERO, + box_shadow: vec![], } } } diff --git a/compositor_render/src/scene/types.rs b/compositor_render/src/scene/types.rs index 66ff62f83..e0f243a9f 100644 --- a/compositor_render/src/scene/types.rs +++ b/compositor_render/src/scene/types.rs @@ -1,3 +1,5 @@ +use std::ops::{Add, Div, Mul, Sub}; + mod convert; pub(crate) mod interpolation; @@ -74,3 +76,79 @@ pub enum InterpolationKind { Bounce, CubicBezier { x1: f64, y1: f64, x2: f64, y2: f64 }, } + +#[derive(Debug, Clone, Copy)] +pub struct BorderRadius { + pub top_left: f32, + pub top_right: f32, + pub bottom_right: f32, + pub bottom_left: f32, +} + +impl BorderRadius { + pub const ZERO: BorderRadius = BorderRadius { + top_left: 0.0, + top_right: 0.0, + bottom_right: 0.0, + bottom_left: 0.0, + }; + + pub fn new_with_radius(radius: f32) -> Self { + Self { + top_left: radius, + top_right: radius, + bottom_right: radius, + bottom_left: radius, + } + } +} + +impl Mul for BorderRadius { + type Output = BorderRadius; + + fn mul(self, rhs: f32) -> Self::Output { + Self { + top_left: self.top_left * rhs, + top_right: self.top_right * rhs, + bottom_right: self.bottom_right * rhs, + bottom_left: self.bottom_left * rhs, + } + } +} + +impl Div for BorderRadius { + type Output = BorderRadius; + + fn div(self, rhs: f32) -> Self::Output { + self * (1.0 / rhs) + } +} + +impl Add for BorderRadius { + type Output = BorderRadius; + + fn add(self, rhs: f32) -> Self::Output { + Self { + top_left: f32::max(self.top_left + rhs, 0.0), + top_right: f32::max(self.top_right + rhs, 0.0), + bottom_right: f32::max(self.bottom_right + rhs, 0.0), + bottom_left: f32::max(self.bottom_left + rhs, 0.0), + } + } +} + +impl Sub for BorderRadius { + type Output = BorderRadius; + + fn sub(self, rhs: f32) -> Self::Output { + self + (-rhs) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct BoxShadow { + pub offset_x: f32, + pub offset_y: f32, + pub blur_radius: f32, + pub color: RGBAColor, +} diff --git a/compositor_render/src/scene/view_component.rs b/compositor_render/src/scene/view_component.rs index 885cdd42e..ab133031b 100644 --- a/compositor_render/src/scene/view_component.rs +++ b/compositor_render/src/scene/view_component.rs @@ -8,8 +8,8 @@ use super::{ scene_state::BuildStateTreeCtx, transition::{TransitionOptions, TransitionState}, types::interpolation::ContinuousValue, - Component, ComponentId, IntermediateNode, Overflow, Position, RGBAColor, SceneError, Size, - StatefulComponent, + BorderRadius, BoxShadow, Component, ComponentId, IntermediateNode, Overflow, Position, + RGBAColor, SceneError, Size, StatefulComponent, }; mod interpolation; @@ -32,6 +32,11 @@ struct ViewComponentParam { overflow: Overflow, background_color: RGBAColor, + border_radius: BorderRadius, + border_width: f32, + border_color: RGBAColor, + + box_shadow: Vec, } impl StatefulViewComponent { @@ -51,8 +56,10 @@ impl StatefulViewComponent { self.children.iter_mut().collect() } + /// External position of a component (includes border) pub(super) fn position(&self, pts: Duration) -> Position { - self.view(pts).position + let view = self.view(pts); + view.position.with_border(view.border_width) } pub(super) fn component_id(&self) -> Option<&ComponentId> { @@ -119,6 +126,10 @@ impl ViewComponent { position: self.position, background_color: self.background_color, overflow: self.overflow, + border_radius: self.border_radius, + border_width: self.border_width, + border_color: self.border_color, + box_shadow: self.box_shadow, }, transition, children: self diff --git a/compositor_render/src/scene/view_component/interpolation.rs b/compositor_render/src/scene/view_component/interpolation.rs index b1fb958f6..e5afa293f 100644 --- a/compositor_render/src/scene/view_component/interpolation.rs +++ b/compositor_render/src/scene/view_component/interpolation.rs @@ -10,6 +10,18 @@ impl ContinuousValue for ViewComponentParam { position: ContinuousValue::interpolate(&start.position, &end.position, state), background_color: end.background_color, overflow: end.overflow, + border_radius: ContinuousValue::interpolate( + &start.border_radius, + &end.border_radius, + state, + ), + border_width: ContinuousValue::interpolate( + &start.border_width, + &end.border_width, + state, + ), + border_color: end.border_color, + box_shadow: ContinuousValue::interpolate(&start.box_shadow, &end.box_shadow, state), } } } diff --git a/compositor_render/src/scene/view_component/layout.rs b/compositor_render/src/scene/view_component/layout.rs index 2ace2fbaa..8b356f1a3 100644 --- a/compositor_render/src/scene/view_component/layout.rs +++ b/compositor_render/src/scene/view_component/layout.rs @@ -2,10 +2,10 @@ use std::time::Duration; use crate::{ scene::{ - layout::StatefulLayoutComponent, Overflow, Position, Size, StatefulComponent, - ViewChildrenDirection, + layout::StatefulLayoutComponent, BorderRadius, Overflow, Position, RGBAColor, Size, + StatefulComponent, ViewChildrenDirection, }, - transformations::layout::{Crop, LayoutContent, NestedLayout}, + transformations::layout::{LayoutContent, Mask, NestedLayout}, }; use super::ViewComponentParam; @@ -22,36 +22,52 @@ struct StaticChildLayoutOpts { /// For direction=column defines height of a static component static_child_size: f32, parent_size: Size, + /// border width before rescaling, it is used to calculate top/left offset correctly + /// when `overflow: fit` is set + parent_border_width: f32, } impl ViewComponentParam { pub(super) fn layout( &self, - size: Size, + size: Size, // how much size component has available(includes space for border) children: &mut [StatefulComponent], pts: Duration, ) -> NestedLayout { - let static_child_size = self.static_child_size(size, children, pts); - let (scale, crop) = match self.overflow { - Overflow::Visible => (1.0, None), + let content_size = Size { + 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), Overflow::Hidden => ( 1.0, - Some(Crop { - top: 0.0, - left: 0.0, - width: size.width, - height: size.height, + None, + Some(Mask { + radius: self.border_radius - self.border_width, + top: self.border_width, + left: self.border_width, + width: content_size.width, + height: content_size.height, }), ), Overflow::Fit => ( - self.scale_factor_for_overflow_fit(size, children, pts), + self.scale_factor_for_overflow_fit(content_size, children, pts), None, + Some(Mask { + radius: self.border_radius - self.border_width, + top: self.border_width, + left: self.border_width, + width: content_size.width, + height: content_size.height, + }), ), }; // offset along x or y direction (depends on self.direction) where next // child component should be placed - let mut static_offset = 0.0; + let mut static_offset = self.border_width / scale; let children: Vec<_> = children .iter_mut() @@ -72,7 +88,8 @@ impl ViewComponentParam { height, static_offset, static_child_size, - parent_size: size, + parent_size: content_size, + parent_border_width: self.border_width / scale, }, pts, ); @@ -97,9 +114,14 @@ impl ViewComponentParam { scale_x: scale, scale_y: scale, crop, + mask, content: LayoutContent::Color(self.background_color), child_nodes_count: children.iter().map(|l| l.child_nodes_count).sum(), children, + border_width: self.border_width, + border_color: self.border_color, + border_radius: self.border_radius, + box_shadow: self.box_shadow.clone(), } } @@ -114,18 +136,18 @@ impl ViewComponentParam { ViewChildrenDirection::Row => { let width = opts.width.unwrap_or(opts.static_child_size); let height = opts.height.unwrap_or(opts.parent_size.height); - let top = 0.0; + let top = opts.parent_border_width; let left = static_offset; static_offset += width; - (top as f32, left, width, height) + (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 = 0.0; + let left = opts.parent_border_width; static_offset += height; - (top, left as f32, width, height) + (top, left, width, height) } }; let layout = match child { @@ -140,9 +162,14 @@ impl ViewComponentParam { scale_x: 1.0, scale_y: 1.0, crop: None, + mask: None, content: LayoutContent::None, child_nodes_count: children_layouts.child_nodes_count, children: vec![children_layouts], + border_width: 0.0, + border_color: RGBAColor(0, 0, 0, 0), + border_radius: BorderRadius::ZERO, + box_shadow: vec![], } } _ => NestedLayout { @@ -154,9 +181,14 @@ impl ViewComponentParam { scale_x: 1.0, scale_y: 1.0, crop: None, + mask: None, content: StatefulLayoutComponent::layout_content(child, 0), child_nodes_count: 1, children: vec![], + border_width: 0.0, + border_color: RGBAColor(0, 0, 0, 0), + border_radius: BorderRadius::ZERO, + box_shadow: vec![], }, }; (layout, static_offset) @@ -165,6 +197,8 @@ impl ViewComponentParam { /// Calculate a size of a static child component that does not have it explicitly defined. /// Returned value represents width if the direction is `ViewChildrenDirection::Row` or /// height if the direction is `ViewChildrenDirection::Column`. + /// + /// size represents dimensions of content (without a border). fn static_child_size(&self, size: Size, children: &[StatefulComponent], pts: Duration) -> f32 { let max_size = match self.direction { super::ViewChildrenDirection::Row => size.width, @@ -190,7 +224,7 @@ impl ViewComponentParam { fn scale_factor_for_overflow_fit( &self, - size: Size, + content_size: Size, children: &[StatefulComponent], pts: Duration, ) -> f32 { @@ -198,8 +232,8 @@ impl ViewComponentParam { .sum_static_children_sizes(children, pts) .max(0.000000001); // avoid division by 0 let (max_size, max_alternative_size) = match self.direction { - super::ViewChildrenDirection::Row => (size.width, size.height), - super::ViewChildrenDirection::Column => (size.height, size.width), + super::ViewChildrenDirection::Row => (content_size.width, content_size.height), + super::ViewChildrenDirection::Column => (content_size.height, content_size.width), }; let max_alternative_size_for_child = Self::static_children_iter(children, pts) .map(|child| match self.direction { diff --git a/compositor_render/src/transformations/layout.rs b/compositor_render/src/transformations/layout.rs index 3a5ff2828..d09904b41 100644 --- a/compositor_render/src/transformations/layout.rs +++ b/compositor_render/src/transformations/layout.rs @@ -1,7 +1,7 @@ use std::{sync::Arc, time::Duration}; use crate::{ - scene::{RGBAColor, Size}, + scene::{BorderRadius, BoxShadow, RGBAColor, Size}, state::RenderCtx, wgpu::texture::NodeTexture, Resolution, @@ -11,16 +11,12 @@ mod flatten; mod layout_renderer; mod params; mod shader; -mod transformation_matrices; -use self::{ - params::{LayoutNodeParams, ParamsBuffer}, - shader::LayoutShader, -}; +use self::shader::LayoutShader; pub(crate) use layout_renderer::LayoutRenderer; + use log::error; -pub(crate) use transformation_matrices::{vertices_transformation_matrix, Position}; pub(crate) trait LayoutProvider: Send { fn layouts(&mut self, pts: Duration, inputs: &[Option]) -> NestedLayout; @@ -30,9 +26,10 @@ pub(crate) trait LayoutProvider: Send { pub(crate) struct LayoutNode { layout_provider: Box, shader: Arc, - params: ParamsBuffer, } +/// When rendering we cut this fragment from texture and stretch it on +/// the expected position #[derive(Debug, Clone)] pub struct Crop { pub top: f32, @@ -41,20 +38,55 @@ pub struct Crop { pub height: f32, } +#[derive(Debug, Clone)] +pub struct Mask { + pub radius: BorderRadius, + // position of parent on the output frame + pub top: f32, + pub left: f32, + pub width: f32, + pub height: f32, +} + #[derive(Debug, Clone)] struct RenderLayout { + // top-left corner, includes border top: f32, left: f32, + + // size on the output texture, includes border width: f32, height: f32, + + // Defines what should be cut from the content. + // - for texture defines part of the texture that will be stretched to + // the `self.width/self.height`. It might cut off border radius. + // - for box shadow + + // Rotated around the center rotation_degrees: f32, + // border radius needs to applied before cropping, so we can't just make it a part of a parent + // mask + border_radius: BorderRadius, + masks: Vec, content: RenderLayoutContent, } #[derive(Debug, Clone)] enum RenderLayoutContent { - Color(RGBAColor), - ChildNode { index: usize, crop: Crop }, + Color { + color: RGBAColor, + border_color: RGBAColor, + border_width: f32, + }, + ChildNode { + index: usize, + border_color: RGBAColor, + border_width: f32, + crop: Crop, + }, + #[allow(dead_code)] + BoxShadow { color: RGBAColor, blur_radius: f32 }, } #[derive(Debug, Clone)] @@ -66,19 +98,44 @@ pub enum LayoutContent { #[derive(Debug, Clone)] pub struct NestedLayout { + // top-left corner, includes border of current element + // (0, 0) represents top-left corner of a parent (inner corner if parent has border too) + // + // e.g. if parent layout and current layout have border 10 and current layout is at (0, 0) then + // their top and left edges will be next to each other without overlapping pub top: f32, pub left: f32, + + // size on the output texture, includes border pub width: f32, pub height: f32, + pub rotation_degrees: f32, /// scale will affect content/children, but not the properties of current layout like - /// top/left/widht/height + /// top/left/width/height pub scale_x: f32, pub scale_y: f32, /// Crop is applied before scaling. + /// + /// If you need to scale before cropping use 2 nested layouts: + /// - child to scale + /// - parent to crop + /// + /// Depending on content + /// - For texture it describes what chunk of texture should be cut and stretched on + /// width/height + /// - For layout it cuts of part of it (defined in coordinates system of this component) pub crop: Option, + /// Everything outside this mask should not be rendered. Coordinates are relative to + /// the layouts top-left corner (and not to the 0,0 point that top-left are defined in) + pub mask: Option, pub content: LayoutContent, + pub border_width: f32, + pub border_color: RGBAColor, + pub border_radius: BorderRadius, + pub box_shadow: Vec, + pub(crate) children: Vec, /// Describes how many children of this component are nodes. This value also /// counts `layout` if its content is a `LayoutContent::ChildNode`. @@ -97,7 +154,6 @@ impl LayoutNode { Self { layout_provider, shader, - params: ParamsBuffer::new(ctx.wgpu_ctx, vec![]), } } @@ -113,39 +169,14 @@ impl LayoutNode { .map(|node_texture| node_texture.resolution()) .collect(); let output_resolution = self.layout_provider.resolution(pts); - let layouts = self - .layout_provider - .layouts(pts, &input_resolutions) - .flatten(&input_resolutions, output_resolution); - - let params: Vec = layouts - .iter() - .map(|layout| { - let (is_texture, background_color, input_resolution) = match layout.content { - RenderLayoutContent::ChildNode { index, .. } => ( - 1, - RGBAColor(0, 0, 0, 0), - *input_resolutions.get(index).unwrap_or(&None), - ), - RenderLayoutContent::Color(color) => (0, color, None), - }; - - LayoutNodeParams { - is_texture, - background_color, - transform_vertices_matrix: layout - .vertices_transformation_matrix(&output_resolution), - transform_texture_coords_matrix: layout - .texture_coords_transformation_matrix(&input_resolution), - } - }) - .collect(); - self.params.update(params, ctx.wgpu_ctx); + let layouts = self.layout_provider.layouts(pts, &input_resolutions); + let layouts = layouts.flatten(&input_resolutions, output_resolution); let textures: Vec> = layouts .iter() .map(|layout| match layout.content { - RenderLayoutContent::Color(_) => None, + RenderLayoutContent::BoxShadow { .. } => None, + RenderLayoutContent::Color { .. } => None, RenderLayoutContent::ChildNode { index, .. } => match sources.get(index) { Some(node_texture) => Some(*node_texture), None => { @@ -158,7 +189,7 @@ impl LayoutNode { let target = target.ensure_size(ctx.wgpu_ctx, output_resolution); self.shader - .render(ctx.wgpu_ctx, self.params.bind_group(), &textures, target); + .render(ctx.wgpu_ctx, output_resolution, layouts, &textures, target); } } @@ -176,9 +207,14 @@ impl NestedLayout { scale_x: 1.0, scale_y: 1.0, crop: None, + mask: None, content: LayoutContent::None, children: vec![], child_nodes_count, + border_width: 0.0, + border_color: RGBAColor(0, 0, 0, 0), + border_radius: BorderRadius::ZERO, + box_shadow: vec![], } } } diff --git a/compositor_render/src/transformations/layout/apply_layouts.wgsl b/compositor_render/src/transformations/layout/apply_layouts.wgsl index 7bfae09a1..8fb19918a 100644 --- a/compositor_render/src/transformations/layout/apply_layouts.wgsl +++ b/compositor_render/src/transformations/layout/apply_layouts.wgsl @@ -1,51 +1,372 @@ struct VertexInput { + // position in clip space [-1, -1] (bottom-left) X [1, 1] (top-right) @location(0) position: vec3, + // texture coordinates in texture coordiantes [0, 0] (top-left) X [1, 1] (bottom-right) @location(1) tex_coords: vec2, } struct VertexOutput { + // position in output in pixel coordinates [0, 0] (top-left) X [output_resolution.x, output_resolution.y] (bottom-right) @builtin(position) position: vec4, + // texture coordinates in texture coordiantes [0, 0] (top-left) X [1, 1] (bottom-right) @location(0) tex_coords: vec2, + // Position relative to center of the rectangle in [-rect_width/2, rect_width/2] X [-rect_height/2, height/2] + @location(2) center_position: vec2, } +struct BoxShadowParams { + border_radius: vec4, + color: vec4, + top: f32, + left: f32, + width: f32, + height: f32, + rotation_degrees: f32, + blur_radius: f32, +} + +struct TextureParams { + border_radius: vec4, + border_color: vec4, + // position + top: f32, + left: f32, + width: f32, + height: f32, + // texture crop + crop_top: f32, + crop_left: f32, + crop_width: f32, + crop_height: f32, + + rotation_degrees: f32, + // border size in pixels + border_width: f32, +} + +struct ColorParams { + border_radius: vec4, + border_color: vec4, + color: vec4, + + top: f32, + left: f32, + width: f32, + height: f32, -struct Layout { - vertices_transformation: mat4x4, - texture_coord_transformation: mat4x4, - color: vec4, // used only when is_texture == 0 - is_texture: u32, // 0 -> color, 1 -> texture + rotation_degrees: f32, + border_width: f32, } +struct ParentMask { + radius: vec4, + top: f32, + left: f32, + width: f32, + height: f32, +} + +struct LayoutInfo { + // 0 -> Texture, 1 -> Color, 2 -> BoxShadow + layout_type: u32, + index: u32, + masks_len: u32 +} + + @group(0) @binding(0) var texture: texture_2d; -@group(1) @binding(0) var layouts: array; -@group(2) @binding(0) var sampler_: sampler; -var layout_id: u32; +@group(1) @binding(0) var output_resolution: vec4; +@group(1) @binding(1) var texture_params: array; +@group(1) @binding(2) var color_params: array; +@group(1) @binding(3) var box_shadow_params: array; + +@group(2) @binding(0) var masks: array; + +@group(3) @binding(0) var sampler_: sampler; + +var layout_info: LayoutInfo; + +fn rotation_matrix(rotation: f32) -> mat4x4 { + // wgsl is column-major + let angle = radians(rotation); + let c = cos(angle); + let s = sin(angle); + return mat4x4( + vec4(c, s, 0.0, 0.0), + vec4(-s, c, 0.0, 0.0), + vec4(0.0, 0.0, 1.0, 0.0), + vec4(0.0, 0.0, 0.0, 1.0) + ); +} + +fn scale_matrix(scale: vec2) -> mat4x4 { + return mat4x4( + vec4(scale.x, 0.0, 0.0, 0.0), + vec4(0.0, scale.y, 0.0, 0.0), + vec4(0.0, 0.0, 1.0, 0.0), + vec4(0.0, 0.0, 0.0, 1.0) + ); +} + + +fn translation_matrix(translation: vec2) -> mat4x4 { + return mat4x4( + vec4(1.0, 0.0, 0.0, 0.0), + vec4(0.0, 1.0, 0.0, 0.0), + vec4(0.0, 0.0, 1.0, 0.0), + vec4(translation, 0.0, 1.0) + ); +} + +fn vertices_transformation_matrix(left: f32, top: f32, width: f32, height: f32, rotation: f32) -> mat4x4 { + let scale_to_size = vec2( + width / output_resolution.x, + height / output_resolution.y + ); + let scale_to_pixels = vec2( + output_resolution.x / 2.0, + output_resolution.y / 2.0 + ); + let scale_to_clip_space = vec2( + 1.0 / scale_to_pixels.x, + 1.0 / scale_to_pixels.y + ); + + let scale_to_pixels_mat = scale_matrix(scale_to_pixels * scale_to_size); + let scale_to_clip_space_mat = scale_matrix(scale_to_clip_space); + + let left_border_x = -(output_resolution.x / 2.0); + let distance_left_to_middle = left + width / 2.0; + let top_border_y = output_resolution.y / 2.0; + let distance_top_to_middle = top + height / 2.0; + let translation = vec2( + left_border_x + distance_left_to_middle, + top_border_y - distance_top_to_middle + ); + + let translation_mat = translation_matrix(translation); + let rotation_mat = rotation_matrix(rotation); + + return scale_to_clip_space_mat * translation_mat * rotation_mat * scale_to_pixels_mat; +} + +fn texture_coord_transformation_matrix(crop_left: f32, crop_top: f32, crop_width: f32, crop_height: f32) -> mat4x4 { + let dim = textureDimensions(texture); + let scale = vec2( + crop_width / f32(dim.x), + crop_height / f32(dim.y), + ); + + let translation = vec2( + crop_left / f32(dim.x), + crop_top / f32(dim.y), + ); + + return translation_matrix(translation) * scale_matrix(scale); +} @vertex fn vs_main(input: VertexInput) -> VertexOutput { var output: VertexOutput; - let vertices_transformation_matrix: mat4x4 = layouts[layout_id].vertices_transformation; - let texture_coord_transformation_matrix: mat4x4 = layouts[layout_id].texture_coord_transformation; + switch (layout_info.layout_type) { + // texture + case 0u: { + let vertices_transformation = vertices_transformation_matrix( + texture_params[layout_info.index].left, + texture_params[layout_info.index].top, + texture_params[layout_info.index].width, + texture_params[layout_info.index].height, + texture_params[layout_info.index].rotation_degrees + ); + let texture_transformation = texture_coord_transformation_matrix( + texture_params[layout_info.index].crop_left, + texture_params[layout_info.index].crop_top, + texture_params[layout_info.index].crop_width, + texture_params[layout_info.index].crop_height + ); + + output.position = vertices_transformation * vec4(input.position, 1.0); + output.tex_coords = (texture_transformation * vec4(input.tex_coords, 0.0, 1.0)).xy; + let rect_size = vec2(texture_params[layout_info.index].width, texture_params[layout_info.index].height); + output.center_position = input.position.xy / 2.0 * rect_size; + } + // color + case 1u: { + let vertices_transformation = vertices_transformation_matrix( + color_params[layout_info.index].left, + color_params[layout_info.index].top, + color_params[layout_info.index].width, + color_params[layout_info.index].height, + color_params[layout_info.index].rotation_degrees + ); + output.position = vertices_transformation * vec4(input.position, 1.0); + output.tex_coords = input.tex_coords; + let rect_size = vec2(color_params[layout_info.index].width, color_params[layout_info.index].height); + output.center_position = input.position.xy / 2.0 * rect_size; + } + // box shadow + case 2u: { + let width = box_shadow_params[layout_info.index].width + 2.0 * box_shadow_params[layout_info.index].blur_radius; + let height = box_shadow_params[layout_info.index].height + 2.0 * box_shadow_params[layout_info.index].blur_radius; - output.position = vec4(input.position, 1.0) * vertices_transformation_matrix; - output.tex_coords = (vec4(input.tex_coords, 0.0, 1.0) * texture_coord_transformation_matrix).xy; + let vertices_transformation = vertices_transformation_matrix( + box_shadow_params[layout_info.index].left - box_shadow_params[layout_info.index].blur_radius, + box_shadow_params[layout_info.index].top - box_shadow_params[layout_info.index].blur_radius, + width, + height, + box_shadow_params[layout_info.index].rotation_degrees + ); + output.position = vertices_transformation * vec4(input.position, 1.0); + output.tex_coords = input.tex_coords; + let rect_size = vec2(width, height); + output.center_position = input.position.xy / 2.0 * rect_size; + } + default {} + } return output; } +// Signed distance function for rounded rectangle https://iquilezles.org/articles/distfunctions +// adapted from https://www.shadertoy.com/view/4llXD7 +// Distance from outside is positive and inside it is negative +// +// dist - signed distance from the center of the rectangle in pixels +// size - size of the rectangle in pixels +// radius - radius of the corners in pixels [top-left, top-right, bottom-right, bottom-left] +// rotation - rotation of the rectangle in degrees +// WARNING - it doesn't work when border radius is > min(size.x, size.y) / 2 +fn roundedRectSDF(dist: vec2, size: vec2, radius: vec4, rotation: f32) -> f32 { + let half_size = size / 2.0; + + // wierd hack to get the radius of the nearest corner stored in r.x + var r: vec2 = vec2(0.0, 0.0); + r = select(radius.yz, radius.xw, dist.x < 0.0); + r.x = select(r.x, r.y, dist.y < 0.0); + + let q = abs(dist) - half_size + r.x; + return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0, 0.0))) - r.x; +} + @fragment fn fs_main(input: VertexOutput) -> @location(0) vec4 { - let current_layout = layouts[layout_id]; + let transparent = vec4(1.0, 1.0, 1.0, 0.0); - // sampling can't be conditional, so in case of plane_id == -1 - // sample textures[0], but ignore the result. - if (current_layout.is_texture == 0u) { - return current_layout.color; + var mask_alpha = 1.0; + + for (var i = 0; i < i32(layout_info.masks_len); i++) { + let radius = masks[i].radius; + let top = masks[i].top; + let left = masks[i].left; + let width = masks[i].width; + let height = masks[i].height; + let size = vec2(width, height); + + let distance = roundedRectSDF( + vec2(left, top) + (size / 2.0) - input.position.xy, + size, + radius, + 0.0, + ); + mask_alpha = mask_alpha * smoothstep(-0.5, 0.5 , -distance); } - // clamp transparent, when crop > input texture - let is_inside: f32 = round(f32(input.tex_coords.x < 1.0 && input.tex_coords.x > 0.0 && input.tex_coords.y > 0.0 && input.tex_coords.y < 1.0)); - return is_inside * textureSample(texture, sampler_, input.tex_coords); + switch layout_info.layout_type { + case 0u: { + let sample = textureSample(texture, sampler_, input.tex_coords); + + let width = texture_params[layout_info.index].width; + let height = texture_params[layout_info.index].height; + let border_radius = texture_params[layout_info.index].border_radius; + let rotation_degrees = texture_params[layout_info.index].rotation_degrees; + let border_width = texture_params[layout_info.index].border_width; + let border_color = texture_params[layout_info.index].border_color; + + let size = vec2(width, height); + let edge_distance = -roundedRectSDF( + input.center_position, + size, + border_radius, + rotation_degrees + ); + + if (border_width < 1.0) { + let content_alpha = smoothstep(-0.5, 0.5, edge_distance); + return vec4(sample.rgb, sample.a * content_alpha * mask_alpha); + } else if (mask_alpha < 0.01) { + return vec4(0, 0, 0, 0); + } else { + if (edge_distance > border_width / 2.0) { + // border <-> content + let border_alpha = smoothstep(border_width - 0.5, border_width + 0.5, edge_distance); + let border_or_content = mix(border_color, sample, border_alpha); + return vec4(border_or_content.rgb, border_or_content.a * mask_alpha); + } else { + // border <-> outside + let content_alpha = smoothstep(-0.5, 0.5, edge_distance); + return vec4(border_color.rgb, border_color.a * content_alpha * mask_alpha); + } + } + } + case 1u: { + let color = color_params[layout_info.index].color; + + let width = color_params[layout_info.index].width; + let height = color_params[layout_info.index].height; + let border_radius = color_params[layout_info.index].border_radius; + let rotation_degrees = color_params[layout_info.index].rotation_degrees; + let border_width = color_params[layout_info.index].border_width; + let border_color = color_params[layout_info.index].border_color; + + let size = vec2(width, height); + let edge_distance = -roundedRectSDF( + input.center_position, + size, + border_radius, + rotation_degrees + ); + + if (border_width < 1.0) { + let content_alpha = smoothstep(-0.5, 0.5, edge_distance); + return vec4(color.rgb, color.a * content_alpha * mask_alpha); + } else { + if (edge_distance > border_width / 2.0) { + // border <-> content + let border_alpha = smoothstep(border_width, border_width + 1.0, edge_distance); + let border_or_content = mix(border_color, color, border_alpha); + return vec4(border_or_content.rgb, border_or_content.a * mask_alpha); + } else { + // border <-> outside + let content_alpha = smoothstep(-0.5, 0.5, edge_distance); + return vec4(border_color.rgb, border_color.a * content_alpha * mask_alpha); + } + } + } + case 2u: { + let color = box_shadow_params[layout_info.index].color; + + let width = box_shadow_params[layout_info.index].width; + let height = box_shadow_params[layout_info.index].height; + let border_radius = box_shadow_params[layout_info.index].border_radius; + let rotation_degrees = box_shadow_params[layout_info.index].rotation_degrees; + let blur_radius = box_shadow_params[layout_info.index].blur_radius; + + let size = vec2(width, height); + let edge_distance = -roundedRectSDF( + input.center_position, + size, + border_radius, + rotation_degrees + ); + + let blur_alpha = smoothstep(0.0, blur_radius, edge_distance) * mask_alpha; + + return vec4(color.rgb, color.a * blur_alpha); + } + default { + return vec4(0.0, 0.0, 0.0, 0.0); + } + } } diff --git a/compositor_render/src/transformations/layout/flatten.rs b/compositor_render/src/transformations/layout/flatten.rs index 131a623fa..fe4215ab4 100644 --- a/compositor_render/src/transformations/layout/flatten.rs +++ b/compositor_render/src/transformations/layout/flatten.rs @@ -1,6 +1,10 @@ +use std::{iter, mem}; + use crate::{scene::RGBAColor, Resolution}; -use super::{Crop, LayoutContent, NestedLayout, RenderLayout, RenderLayoutContent}; +use super::{ + BoxShadow, Crop, LayoutContent, Mask, NestedLayout, RenderLayout, RenderLayoutContent, +}; impl NestedLayout { pub(super) fn flatten( @@ -8,14 +12,20 @@ impl NestedLayout { input_resolutions: &[Option], resolution: Resolution, ) -> Vec { - let layouts = self.inner_flatten(0); - layouts + let (shadow, layouts) = self.inner_flatten(0, vec![]); + shadow .into_iter() + .chain(layouts) .filter(|layout| Self::should_render(layout, input_resolutions, resolution)) + .map(NestedLayout::fix_final_render_layout) .collect() } - fn inner_flatten(mut self, child_index_offset: usize) -> Vec { + fn inner_flatten( + mut self, + child_index_offset: usize, + parent_masks: Vec, + ) -> (Vec, Vec) { let mut child_index_offset = child_index_offset; if let LayoutContent::ChildNode { index, size } = self.content { self.content = LayoutContent::ChildNode { @@ -24,20 +34,90 @@ impl NestedLayout { }; child_index_offset += 1 } - let layout = self.render_layout(); - let children: Vec<_> = std::mem::take(&mut self.children) + let layout = self.render_layout(&parent_masks); + // It is separated because box shadows of all siblings need to be rendered before + // this layout and it's siblings + let box_shadow_layouts = self + .box_shadow + .iter() + .map(|shadow| self.box_shadow_layout(shadow, &parent_masks)) + .collect(); + + let parent_masks = match &self.mask { + Some(mask) => parent_masks + .iter() + .chain(iter::once(mask)) + .cloned() + .collect(), + None => parent_masks.clone(), + }; + let parent_masks = self.child_parent_masks(&parent_masks); + + let (children_shadow, children_layouts): (Vec<_>, Vec<_>) = + std::mem::take(&mut self.children) + .into_iter() + .map(|child| { + let child_nodes_count = child.child_nodes_count; + let (shadows, layouts) = + child.inner_flatten(child_index_offset, parent_masks.clone()); + child_index_offset += child_nodes_count; + (shadows, layouts) + }) + .unzip(); + let children_shadow = children_shadow .into_iter() - .flat_map(|child| { - let child_nodes_count = child.child_nodes_count; - let layouts = child.inner_flatten(child_index_offset); - child_index_offset += child_nodes_count; - layouts - }) + .flatten() .map(|l| self.flatten_child(l)) .collect(); - [vec![layout], children].concat() + let children_layouts = children_layouts + .into_iter() + .flatten() + .map(|l| self.flatten_child(l)) + .collect(); + + ( + box_shadow_layouts, + [vec![layout], children_shadow, children_layouts].concat(), + ) } + // Final pass on each render layout, it applies following modifications: + // - If border_width is between 0 and 1 set it to 1. + // - Remove masks that don't do anything + fn fix_final_render_layout(mut layout: RenderLayout) -> RenderLayout { + fn filter_mask(layout: &RenderLayout, mask: Mask) -> Option { + let max_top_border = f32::max(mask.radius.top_left, mask.radius.top_right); + let max_bottom_border = f32::max(mask.radius.bottom_left, mask.radius.bottom_right); + let max_left_border = f32::max(mask.radius.top_left, mask.radius.bottom_left); + let max_right_border = f32::max(mask.radius.top_right, mask.radius.bottom_right); + let should_skip = mask.top + max_top_border <= layout.top + && mask.left + max_left_border <= layout.left + && mask.left + mask.width - max_right_border >= layout.left + layout.width + && mask.top + mask.height - max_bottom_border >= layout.top + layout.height; + match should_skip { + true => None, + false => Some(mask), + } + } + match &mut layout.content { + RenderLayoutContent::Color { border_width, .. } + | RenderLayoutContent::ChildNode { border_width, .. } => { + if *border_width < 1.0 { + *border_width = 0.0 + } + } + _ => (), + }; + layout.masks = mem::take(&mut layout.masks) + .into_iter() + .filter_map(|mask| filter_mask(&layout, mask)) + .collect(); + layout + } + + // Decides if layout will affect the output of the stream, if not this layout will not be + // passed to the shader. + // Layouts are in absolute units at this point. fn should_render( layout: &RenderLayout, input_resolutions: &[Option], @@ -51,9 +131,19 @@ impl NestedLayout { return false; } match &layout.content { - RenderLayoutContent::Color(RGBAColor(_, _, _, 0)) => false, - RenderLayoutContent::Color(_) => true, - RenderLayoutContent::ChildNode { crop, index } => { + RenderLayoutContent::Color { + color: RGBAColor(_, _, _, 0), + border_color: RGBAColor(_, _, _, border_alpha), + border_width, + } => *border_alpha != 0 || *border_width > 0.0, + RenderLayoutContent::Color { .. } => true, + RenderLayoutContent::ChildNode { + crop, + index, + border_color: RGBAColor(_, _, _, _), + border_width: _, + } => { + // TODO: handle a case when only border is visible (currently impossible) let size = input_resolutions.get(*index).copied().flatten(); if let Some(size) = size { if crop.left > size.width as f32 || crop.top > size.height as f32 { @@ -65,18 +155,59 @@ impl NestedLayout { } true } + RenderLayoutContent::BoxShadow { + color: RGBAColor(_, _, _, 0), + .. + } => false, + RenderLayoutContent::BoxShadow { .. } => true, } } - fn flatten_child(&self, layout: RenderLayout) -> RenderLayout { + // parent_masks - in self coordinates + fn flatten_child(&self, child: RenderLayout) -> RenderLayout { + // scale factor used if we need to scale something that can't be + // scaled separately for horizontal and vertical direction. + let unified_scale = f32::min(self.scale_x, self.scale_y); + match &self.crop { None => RenderLayout { - top: self.top + (layout.top * self.scale_y), - left: self.left + (layout.left * self.scale_x), - width: layout.width * self.scale_x, - height: layout.height * self.scale_y, - rotation_degrees: layout.rotation_degrees + self.rotation_degrees, // TODO: not exactly correct - content: layout.content, + top: self.top + (child.top * self.scale_y), + left: self.left + (child.left * self.scale_x), + width: child.width * self.scale_x, + height: child.height * self.scale_y, + rotation_degrees: child.rotation_degrees + self.rotation_degrees, // TODO: not exactly correct + content: match child.content { + RenderLayoutContent::Color { + color, + border_color, + border_width, + } => RenderLayoutContent::Color { + color, + border_color, + border_width: border_width * unified_scale, + }, + RenderLayoutContent::ChildNode { + index, + border_color, + border_width, + crop, + } => RenderLayoutContent::ChildNode { + index, + border_color, + border_width: border_width * unified_scale, + crop, + }, + RenderLayoutContent::BoxShadow { color, blur_radius } => { + RenderLayoutContent::BoxShadow { + color, + blur_radius: blur_radius * unified_scale, + } + } + }, + // TODO: This will not work correctly for layouts that are not proportionally + // scaled + border_radius: child.border_radius * unified_scale, + masks: self.parent_parent_masks(&child.masks), }, Some(crop) => { // Below values are only correct if `crop` is in the same coordinate @@ -86,37 +217,49 @@ impl NestedLayout { // Value in coordinates of `self` (relative to it's top-left corner). Represents // a position after cropping and translated back to (layout.top, layout.left). - let cropped_top = f32::max(layout.top - crop.top, 0.0); - let cropped_left = f32::max(layout.left - crop.left, 0.0); - let cropped_bottom = f32::min(layout.top + layout.height - crop.top, crop.height); - let cropped_right = f32::min(layout.left + layout.width - crop.left, crop.width); + let cropped_top = f32::max(child.top - crop.top, 0.0); + let cropped_left = f32::max(child.left - crop.left, 0.0); + let cropped_bottom = f32::min(child.top + child.height - crop.top, crop.height); + let cropped_right = f32::min(child.left + child.width - crop.left, crop.width); let cropped_width = cropped_right - cropped_left; let cropped_height = cropped_bottom - cropped_top; - match layout.content { - RenderLayoutContent::Color(color) => { + match child.content.clone() { + RenderLayoutContent::Color { + color, + border_color, + border_width, + } => { RenderLayout { top: self.top + (cropped_top * self.scale_y), left: self.left + (cropped_left * self.scale_x), width: cropped_width * self.scale_x, height: cropped_height * self.scale_y, - rotation_degrees: layout.rotation_degrees + self.rotation_degrees, // TODO: not exactly correct - content: RenderLayoutContent::Color(color), + rotation_degrees: child.rotation_degrees + self.rotation_degrees, // TODO: not exactly correct + content: RenderLayoutContent::Color { + color, + border_color, + border_width: border_width * unified_scale, + }, + border_radius: child.border_radius * unified_scale, + masks: self.parent_parent_masks(&child.masks), } } RenderLayoutContent::ChildNode { index, crop: child_crop, + border_color, + border_width, } => { // Calculate how much top/left coordinates changed when cropping. It represents // how much was removed in layout coordinates. Ignore the change of a position that // was a result of a translation after cropping. - let top_diff = f32::max(crop.top - layout.top, 0.0); - let left_diff = f32::max(crop.left - layout.left, 0.0); + let top_diff = f32::max(crop.top - child.top, 0.0); + let left_diff = f32::max(crop.left - child.left, 0.0); // Factor to translate from `layout` coordinates to child node coord. // The same factor holds for translations from `self.layout`. - let horizontal_scale_factor = child_crop.width / layout.width; - let vertical_scale_factor = child_crop.height / layout.height; + let horizontal_scale_factor = child_crop.width / child.width; + let vertical_scale_factor = child_crop.height / child.height; let crop = Crop { top: child_crop.top + (top_diff * vertical_scale_factor), @@ -130,8 +273,30 @@ impl NestedLayout { left: self.left + (cropped_left * self.scale_x), width: cropped_width * self.scale_x, height: cropped_height * self.scale_y, - rotation_degrees: layout.rotation_degrees + self.rotation_degrees, // TODO: not exactly correct - content: RenderLayoutContent::ChildNode { index, crop }, + rotation_degrees: child.rotation_degrees + self.rotation_degrees, // TODO: not exactly correct + content: RenderLayoutContent::ChildNode { + index, + crop, + border_color, + border_width, + }, + border_radius: child.border_radius * unified_scale, + masks: self.parent_parent_masks(&child.masks), + } + } + RenderLayoutContent::BoxShadow { color, blur_radius } => { + RenderLayout { + top: self.top + (cropped_top * self.scale_y), + left: self.left + (cropped_left * self.scale_x), + width: cropped_width * self.scale_x, + height: cropped_height * self.scale_y, + rotation_degrees: child.rotation_degrees + self.rotation_degrees, // TODO: not exactly correct + content: RenderLayoutContent::BoxShadow { + color, + blur_radius: blur_radius * unified_scale, + }, + border_radius: child.border_radius * unified_scale, + masks: self.parent_parent_masks(&child.masks), } } } @@ -139,7 +304,11 @@ impl NestedLayout { } } - fn render_layout(&self) -> RenderLayout { + /// Calculate RenderLayout for self (without children) + /// Resulting layout is in coordinates: + /// - relative self's parent top-left corner. + /// - before parent scaling is applied + fn render_layout(&self, parent_masks: &[Mask]) -> RenderLayout { RenderLayout { top: self.top, left: self.left, @@ -147,7 +316,11 @@ impl NestedLayout { height: self.height, rotation_degrees: self.rotation_degrees, content: match self.content { - LayoutContent::Color(color) => RenderLayoutContent::Color(color), + LayoutContent::Color(color) => RenderLayoutContent::Color { + color, + border_color: self.border_color, + border_width: self.border_width, + }, LayoutContent::ChildNode { index, size } => RenderLayoutContent::ChildNode { index, crop: Crop { @@ -156,9 +329,63 @@ impl NestedLayout { width: size.width, height: size.height, }, + border_color: self.border_color, + border_width: self.border_width, + }, + LayoutContent::None => RenderLayoutContent::Color { + color: RGBAColor(0, 0, 0, 0), + border_color: self.border_color, + border_width: self.border_width, }, - LayoutContent::None => RenderLayoutContent::Color(RGBAColor(0, 0, 0, 0)), }, + border_radius: self.border_radius, + masks: parent_masks.to_vec(), + } + } + + /// calculate RenderLayout for one of self box shadows + fn box_shadow_layout(&self, box_shadow: &BoxShadow, parent_masks: &[Mask]) -> RenderLayout { + RenderLayout { + top: self.top + box_shadow.offset_y - 0.5 * box_shadow.blur_radius, + left: self.left + box_shadow.offset_x - 0.5 * box_shadow.blur_radius, + width: self.width + box_shadow.blur_radius, + height: self.height + box_shadow.blur_radius, + rotation_degrees: self.rotation_degrees, // TODO: this is incorrect + border_radius: self.border_radius + box_shadow.blur_radius, + content: RenderLayoutContent::BoxShadow { + color: box_shadow.color, + blur_radius: box_shadow.blur_radius * 2.0, // TODO: 2.0 is empirically selected + // value + }, + masks: parent_masks.to_vec(), } } + + /// Calculate ParentMasks in coordinates of child NestedLayout. + fn child_parent_masks(&self, masks: &[Mask]) -> Vec { + masks + .iter() + .map(|mask| Mask { + radius: mask.radius / f32::min(self.scale_x, self.scale_y), + top: (mask.top - self.top) / self.scale_y, + left: (mask.left - self.left) / self.scale_x, + width: mask.width / self.scale_x, + height: mask.height / self.scale_y, + }) + .collect() + } + + /// Translates parent mask from child coordinates to parent. Reverse operation to `child_parent_masks`. + fn parent_parent_masks(&self, masks: &[Mask]) -> Vec { + masks + .iter() + .map(|mask| Mask { + radius: mask.radius * f32::min(self.scale_x, self.scale_y), + top: (mask.top * self.scale_y) + self.top, + left: (mask.left * self.scale_x) + self.left, + width: mask.width * self.scale_x, + height: mask.height * self.scale_y, + }) + .collect() + } } diff --git a/compositor_render/src/transformations/layout/params.rs b/compositor_render/src/transformations/layout/params.rs index 1c3fc7474..18f04ddd1 100644 --- a/compositor_render/src/transformations/layout/params.rs +++ b/compositor_render/src/transformations/layout/params.rs @@ -1,126 +1,366 @@ -use nalgebra_glm::Mat4; -use wgpu::util::DeviceExt; +use tracing::error; +use wgpu::{ + util::{BufferInitDescriptor, DeviceExt}, + BindGroupLayoutDescriptor, BufferUsages, +}; -use crate::{scene::RGBAColor, wgpu::WgpuCtx}; +use crate::{scene::RGBAColor, wgpu::WgpuCtx, Resolution}; -#[derive(Debug, Clone)] -pub(super) struct LayoutNodeParams { - pub(super) transform_vertices_matrix: Mat4, - pub(super) transform_texture_coords_matrix: Mat4, - pub(super) is_texture: u32, - pub(super) background_color: RGBAColor, +use super::{BorderRadius, RenderLayout}; + +const MAX_MASKS: usize = 20; +const MAX_LAYOUTS_COUNT: usize = 100; +const TEXTURE_PARAMS_BUFFER_SIZE: usize = MAX_LAYOUTS_COUNT * 80; +const COLOR_PARAMS_SIZE: usize = MAX_LAYOUTS_COUNT * 80; +const BOX_SHADOW_PARAMS_SIZE: usize = MAX_LAYOUTS_COUNT * 80; + +#[derive(Debug)] +pub struct LayoutInfo { + pub layout_type: u32, + pub index: u32, + pub masks_len: u32, } -impl Default for LayoutNodeParams { - fn default() -> Self { - Self { - transform_vertices_matrix: Mat4::identity(), - transform_texture_coords_matrix: Mat4::identity(), - is_texture: 0, - background_color: RGBAColor(0, 0, 0, 0), - } +impl LayoutInfo { + pub fn to_bytes(&self) -> [u8; 16] { + let mut result = [0u8; 16]; + result[0..4].copy_from_slice(&self.layout_type.to_le_bytes()); + result[4..8].copy_from_slice(&self.index.to_le_bytes()); + result[8..12].copy_from_slice(&self.masks_len.to_le_bytes()); + result } } -pub(super) struct ParamsBuffer { - bind_group: wgpu::BindGroup, - buffer: wgpu::Buffer, - content: bytes::Bytes, +#[derive(Debug)] +pub struct ParamsBindGroups { + pub bind_group_1: wgpu::BindGroup, + pub bind_group_1_layout: wgpu::BindGroupLayout, + output_resolution_buffer: wgpu::Buffer, + texture_params_buffer: wgpu::Buffer, + color_params_buffer: wgpu::Buffer, + box_shadow_params_buffer: wgpu::Buffer, + pub bind_groups_2: Vec<(wgpu::BindGroup, wgpu::Buffer)>, + pub bind_group_2_layout: wgpu::BindGroupLayout, } -impl ParamsBuffer { - pub fn new(wgpu_ctx: &WgpuCtx, params: Vec) -> Self { - let mut content = Self::shader_buffer_content(¶ms); - if content.is_empty() { - content = bytes::Bytes::copy_from_slice(&[0]); - } +impl ParamsBindGroups { + pub fn new(ctx: &WgpuCtx) -> ParamsBindGroups { + let output_resolution_buffer = create_buffer(ctx, 16); + let texture_params_buffer = create_buffer(ctx, TEXTURE_PARAMS_BUFFER_SIZE); + let color_params_buffer = create_buffer(ctx, COLOR_PARAMS_SIZE); + let box_shadow_params_buffer = create_buffer(ctx, BOX_SHADOW_PARAMS_SIZE); - let buffer = wgpu_ctx + let bind_group_1_layout = ctx .device - .create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("params buffer"), - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - contents: &content, + .create_bind_group_layout(&BindGroupLayoutDescriptor { + label: Some("Bind group 1 layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + count: None, + visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + count: None, + visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + count: None, + visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + }, + wgpu::BindGroupLayoutEntry { + binding: 3, + count: None, + visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + }, + ], }); - let bind_group = wgpu_ctx - .device - .create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("params bind group"), - layout: &wgpu_ctx.uniform_bgl, + let bind_group_1 = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("Bind group 1"), + layout: &bind_group_1_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: output_resolution_buffer.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: texture_params_buffer.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: color_params_buffer.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 3, + resource: box_shadow_params_buffer.as_entire_binding(), + }, + ], + }); + + let bind_group_2_layout = + ctx.device + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("Bind group 2 layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }); + + let mut bind_groups_2 = Vec::with_capacity(100); + for _ in 0..100 { + let buffer = create_buffer(ctx, 20 * 32); + + let bind_group_2 = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("Bind group 2"), + layout: &bind_group_2_layout, entries: &[wgpu::BindGroupEntry { binding: 0, resource: buffer.as_entire_binding(), }], }); + bind_groups_2.push((bind_group_2, buffer)); + } Self { - bind_group, - buffer, - content, + bind_group_1, + output_resolution_buffer, + texture_params_buffer, + color_params_buffer, + box_shadow_params_buffer, + bind_groups_2, + bind_group_1_layout, + bind_group_2_layout, } } - pub fn bind_group(&self) -> &wgpu::BindGroup { - &self.bind_group - } - - pub fn update(&mut self, params: Vec, wgpu_ctx: &WgpuCtx) { - let content = Self::shader_buffer_content(¶ms); - if self.content.len() != content.len() { - *self = Self::new(wgpu_ctx, params); + pub fn update( + &self, + ctx: &WgpuCtx, + output_resolution: Resolution, + layouts: Vec, + ) -> Vec { + if layouts.len() > MAX_LAYOUTS_COUNT { + error!( + "Max layouts count ({}) exceeded ({}). Skipping rendering some of them.", + MAX_LAYOUTS_COUNT, + layouts.len() + ) } - if self.content != content { - wgpu_ctx.queue.write_buffer(&self.buffer, 0, &content); + let mut output_resolution_bytes = [0u8; 8]; + output_resolution_bytes[0..4] + .copy_from_slice(&(output_resolution.width as f32).to_le_bytes()); + output_resolution_bytes[4..8] + .copy_from_slice(&(output_resolution.height as f32).to_le_bytes()); + + ctx.queue + .write_buffer(&self.output_resolution_buffer, 0, &output_resolution_bytes); + + let mut layout_infos = Vec::new(); + + let mut texture_params = Vec::new(); + let mut color_params = Vec::new(); + let mut box_shadow_params = Vec::new(); + + for (index, layout) in layouts.iter().enumerate().take(MAX_LAYOUTS_COUNT) { + let RenderLayout { + top, + left, + width, + height, + rotation_degrees, + border_radius, + masks, + content, + } = layout; + let border_radius_bytes = borders_radius_to_bytes(*border_radius); + + match content { + super::RenderLayoutContent::Color { + color, + border_color, + border_width, + } => { + let layout_info = LayoutInfo { + layout_type: 1, + index: color_params.len() as u32, + masks_len: masks.len() as u32, + }; + let mut color_params_bytes = [0u8; 80]; + color_params_bytes[0..16].copy_from_slice(&border_radius_bytes); + color_params_bytes[16..32].copy_from_slice(&color_to_bytes(*border_color)); + color_params_bytes[32..48].copy_from_slice(&color_to_bytes(*color)); + color_params_bytes[48..52].copy_from_slice(&top.to_le_bytes()); + color_params_bytes[52..56].copy_from_slice(&left.to_le_bytes()); + color_params_bytes[56..60].copy_from_slice(&width.to_le_bytes()); + color_params_bytes[60..64].copy_from_slice(&height.to_le_bytes()); + color_params_bytes[64..68].copy_from_slice(&rotation_degrees.to_le_bytes()); + color_params_bytes[68..72].copy_from_slice(&border_width.to_le_bytes()); + color_params.push(color_params_bytes); + layout_infos.push(layout_info); + } + super::RenderLayoutContent::ChildNode { + index: _, + crop, + border_color, + border_width, + } => { + let layout_info = LayoutInfo { + layout_type: 0, + index: texture_params.len() as u32, + masks_len: masks.len() as u32, + }; + let mut texture_params_bytes = [0u8; 80]; + texture_params_bytes[0..16].copy_from_slice(&border_radius_bytes); + texture_params_bytes[16..32].copy_from_slice(&color_to_bytes(*border_color)); + texture_params_bytes[32..36].copy_from_slice(&top.to_le_bytes()); + texture_params_bytes[36..40].copy_from_slice(&left.to_le_bytes()); + texture_params_bytes[40..44].copy_from_slice(&width.to_le_bytes()); + texture_params_bytes[44..48].copy_from_slice(&height.to_le_bytes()); + texture_params_bytes[48..52].copy_from_slice(&crop.top.to_le_bytes()); + texture_params_bytes[52..56].copy_from_slice(&crop.left.to_le_bytes()); + texture_params_bytes[56..60].copy_from_slice(&crop.width.to_le_bytes()); + texture_params_bytes[60..64].copy_from_slice(&crop.height.to_le_bytes()); + texture_params_bytes[64..68].copy_from_slice(&rotation_degrees.to_le_bytes()); + texture_params_bytes[68..72].copy_from_slice(&border_width.to_le_bytes()); + texture_params.push(texture_params_bytes); + layout_infos.push(layout_info); + } + super::RenderLayoutContent::BoxShadow { color, blur_radius } => { + let layout_info = LayoutInfo { + layout_type: 2, + index: box_shadow_params.len() as u32, + masks_len: masks.len() as u32, + }; + let mut box_shadow_params_bytes = [0u8; 64]; + box_shadow_params_bytes[0..16].copy_from_slice(&border_radius_bytes); + box_shadow_params_bytes[16..32].copy_from_slice(&color_to_bytes(*color)); + box_shadow_params_bytes[32..36].copy_from_slice(&top.to_le_bytes()); + box_shadow_params_bytes[36..40].copy_from_slice(&left.to_le_bytes()); + box_shadow_params_bytes[40..44].copy_from_slice(&width.to_le_bytes()); + box_shadow_params_bytes[44..48].copy_from_slice(&height.to_le_bytes()); + box_shadow_params_bytes[48..52] + .copy_from_slice(&rotation_degrees.to_le_bytes()); + box_shadow_params_bytes[52..56].copy_from_slice(&blur_radius.to_le_bytes()); + box_shadow_params.push(box_shadow_params_bytes); + layout_infos.push(layout_info); + } + } + if masks.len() > MAX_MASKS { + error!( + "Max parent border radiuses count ({}) exceeded ({}). Skipping rendering some og them.", + MAX_MASKS, + masks.len() + ); + } + + let mut masks_bytes = Vec::new(); + + for mask in masks.iter().take(20) { + let mut mask_bytes = [0u8; 32]; + mask_bytes[0..16].copy_from_slice(&borders_radius_to_bytes(mask.radius)); + mask_bytes[16..20].copy_from_slice(&mask.top.to_le_bytes()); + mask_bytes[20..24].copy_from_slice(&mask.left.to_le_bytes()); + mask_bytes[24..28].copy_from_slice(&mask.width.to_le_bytes()); + mask_bytes[28..32].copy_from_slice(&mask.height.to_le_bytes()); + + masks_bytes.push(mask_bytes); + } + + masks_bytes.resize_with(20, || [0u8; 32]); + match self.bind_groups_2.get(index) { + Some((_bg, buffer)) => { + ctx.queue.write_buffer(buffer, 0, &masks_bytes.concat()); + } + None => { + error!("Not enought parent border radiuses bind groups preallocated"); + } + } + + ctx.queue + .write_buffer(&self.bind_groups_2[index].1, 0, &masks_bytes.concat()); } - } + texture_params.resize_with(100, || [0u8; 80]); + color_params.resize_with(100, || [0u8; 80]); + box_shadow_params.resize_with(100, || [0u8; 64]); + + ctx.queue + .write_buffer(&self.texture_params_buffer, 0, &texture_params.concat()); + ctx.queue + .write_buffer(&self.color_params_buffer, 0, &color_params.concat()); + ctx.queue.write_buffer( + &self.box_shadow_params_buffer, + 0, + &box_shadow_params.concat(), + ); - fn shader_buffer_content(params: &[LayoutNodeParams]) -> bytes::Bytes { - // this should only be enabled on `wasm32`, but it needs to be enabled as a temporary fix - // (@wbarczynski has a PR fixing this in the works right now) - let params = { - // On WebGL we have to fill the whole array - const MAX_PARAMS_COUNT: usize = 100; - let mut params = params.to_vec(); - params.resize_with(MAX_PARAMS_COUNT, LayoutNodeParams::default); - params - }; - - params - .iter() - .map(LayoutNodeParams::shader_buffer_content) - .collect::>() - .concat() - .into() + layout_infos } } -impl LayoutNodeParams { - fn shader_buffer_content(&self) -> [u8; 160] { - let Self { - transform_vertices_matrix, - transform_texture_coords_matrix, - is_texture, - background_color, - } = self; - let mut result = [0; 160]; - fn from_u8_color(value: u8) -> [u8; 4] { - (value as f32 / 255.0).to_ne_bytes() - } +fn create_buffer(ctx: &WgpuCtx, size: usize) -> wgpu::Buffer { + ctx.device.create_buffer_init(&BufferInitDescriptor { + label: Some("params buffer"), + contents: &vec![0u8; size], + usage: BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }) +} - result[0..64].copy_from_slice(bytemuck::bytes_of(&transform_vertices_matrix.transpose())); - result[64..128].copy_from_slice(bytemuck::bytes_of( - &transform_texture_coords_matrix.transpose(), - )); - result[128..132].copy_from_slice(&from_u8_color(background_color.0)); - result[132..136].copy_from_slice(&from_u8_color(background_color.1)); - result[136..140].copy_from_slice(&from_u8_color(background_color.2)); - result[140..144].copy_from_slice(&from_u8_color(background_color.3)); +fn borders_radius_to_bytes(border_radius: BorderRadius) -> [u8; 16] { + let mut result = [0u8; 16]; + result[0..4].copy_from_slice(&border_radius.top_left.to_le_bytes()); + result[4..8].copy_from_slice(&border_radius.top_right.to_le_bytes()); + result[8..12].copy_from_slice(&border_radius.bottom_right.to_le_bytes()); + result[12..16].copy_from_slice(&border_radius.bottom_left.to_le_bytes()); + result +} - result[144..148].copy_from_slice(&is_texture.to_ne_bytes()); - // 12 bytes padding +fn color_to_bytes(color: RGBAColor) -> [u8; 16] { + let RGBAColor(r, g, b, a) = color; + let mut result = [0u8; 16]; + result[0..4].copy_from_slice(&srgb_to_linear(r).to_le_bytes()); + result[4..8].copy_from_slice(&srgb_to_linear(g).to_le_bytes()); + result[8..12].copy_from_slice(&srgb_to_linear(b).to_le_bytes()); + result[12..16].copy_from_slice(&(a as f32 / 255.0).to_le_bytes()); + result +} - result +fn srgb_to_linear(color: u8) -> f32 { + let color = color as f32 / 255.0; + if color < 0.04045 { + color / 12.92 + } else { + f32::powf((color + 0.055) / 1.055, 2.4) } } diff --git a/compositor_render/src/transformations/layout/shader.rs b/compositor_render/src/transformations/layout/shader.rs index 12fcc6243..55cbd00a6 100644 --- a/compositor_render/src/transformations/layout/shader.rs +++ b/compositor_render/src/transformations/layout/shader.rs @@ -1,16 +1,24 @@ use std::sync::Arc; -use crate::wgpu::{ - common_pipeline::{self, CreateShaderError, Sampler}, - texture::{NodeTexture, NodeTextureState}, - WgpuCtx, WgpuErrorScope, +use tracing::error; + +use crate::{ + wgpu::{ + common_pipeline::{self, CreateShaderError, Sampler}, + texture::{NodeTexture, NodeTextureState}, + WgpuCtx, WgpuErrorScope, + }, + Resolution, }; +use super::{params::ParamsBindGroups, RenderLayout}; + #[derive(Debug)] pub struct LayoutShader { pipeline: wgpu::RenderPipeline, sampler: Sampler, texture_bgl: wgpu::BindGroupLayout, + params_bind_groups: ParamsBindGroups, } impl LayoutShader { @@ -32,8 +40,8 @@ impl LayoutShader { shader_module: wgpu::ShaderModule, ) -> Result { let sampler = Sampler::new(&wgpu_ctx.device); - let texture_bgl = common_pipeline::create_single_texture_bgl(&wgpu_ctx.device); + let params_bind_groups = ParamsBindGroups::new(wgpu_ctx); let pipeline_layout = wgpu_ctx @@ -42,12 +50,13 @@ impl LayoutShader { label: Some("shader transformation pipeline layout"), bind_group_layouts: &[ &texture_bgl, - &wgpu_ctx.uniform_bgl, + ¶ms_bind_groups.bind_group_1_layout, + ¶ms_bind_groups.bind_group_2_layout, &sampler.bind_group_layout, ], push_constant_ranges: &[wgpu::PushConstantRange { stages: wgpu::ShaderStages::VERTEX_FRAGMENT, - range: 0..4, + range: 0..16, }], }); @@ -61,18 +70,31 @@ impl LayoutShader { pipeline, sampler, texture_bgl, + params_bind_groups, }) } pub fn render( &self, wgpu_ctx: &Arc, - params: &wgpu::BindGroup, + output_resolution: Resolution, + layouts: Vec, textures: &[Option<&NodeTexture>], target: &NodeTextureState, ) { + let layout_infos = self + .params_bind_groups + .update(wgpu_ctx, output_resolution, layouts); let input_texture_bgs: Vec = self.input_textures_bg(wgpu_ctx, textures); + if layout_infos.len() != input_texture_bgs.len() { + error!( + "Layout infos len ({:?}) and textures bind groups count ({:?}) mismatch", + layout_infos.len(), + input_texture_bgs.len() + ); + } + let mut encoder = wgpu_ctx.device.create_command_encoder(&Default::default()); { let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { @@ -91,18 +113,24 @@ impl LayoutShader { occlusion_query_set: None, }); - for (layout_id, texture_bg) in input_texture_bgs.iter().enumerate() { + for (index, (texture_bg, layout_info)) in input_texture_bgs + .iter() + .zip(layout_infos.iter()) + .take(100) + .enumerate() + { render_pass.set_pipeline(&self.pipeline); render_pass.set_push_constants( wgpu::ShaderStages::VERTEX_FRAGMENT, 0, - &(layout_id as u32).to_le_bytes(), + &layout_info.to_bytes(), ); render_pass.set_bind_group(0, texture_bg, &[]); - render_pass.set_bind_group(1, params, &[]); - render_pass.set_bind_group(2, &self.sampler.bind_group, &[]); + render_pass.set_bind_group(1, &self.params_bind_groups.bind_group_1, &[]); + render_pass.set_bind_group(2, &self.params_bind_groups.bind_groups_2[index].0, &[]); + render_pass.set_bind_group(3, &self.sampler.bind_group, &[]); wgpu_ctx.plane.draw(&mut render_pass); } diff --git a/compositor_render/src/transformations/text_renderer.rs b/compositor_render/src/transformations/text_renderer.rs index 49e2b84aa..a3ae0810b 100644 --- a/compositor_render/src/transformations/text_renderer.rs +++ b/compositor_render/src/transformations/text_renderer.rs @@ -100,7 +100,7 @@ impl TextRendererNode { let mut viewport = glyphon::Viewport::new(&renderer_ctx.wgpu_ctx.device, cache); viewport.update(&renderer_ctx.wgpu_ctx.queue, self.resolution.into()); - let swapchain_format = TextureFormat::Rgba8Unorm; + let swapchain_format = TextureFormat::Rgba8UnormSrgb; let mut atlas = TextAtlas::new( &renderer_ctx.wgpu_ctx.device, &renderer_ctx.wgpu_ctx.queue, diff --git a/compositor_render/src/transformations/web_renderer.rs b/compositor_render/src/transformations/web_renderer.rs index bd6fbc918..7f306a7c5 100644 --- a/compositor_render/src/transformations/web_renderer.rs +++ b/compositor_render/src/transformations/web_renderer.rs @@ -10,6 +10,8 @@ mod renderer; #[path = "web_renderer/disabled_renderer.rs"] mod renderer; +mod tranformation_matrices; + pub use renderer::*; pub mod chromium_context; diff --git a/compositor_render/src/transformations/web_renderer/browser_client.rs b/compositor_render/src/transformations/web_renderer/browser_client.rs index 52a707bd9..491f16491 100644 --- a/compositor_render/src/transformations/web_renderer/browser_client.rs +++ b/compositor_render/src/transformations/web_renderer/browser_client.rs @@ -1,16 +1,16 @@ use std::sync::{Arc, Mutex}; -use crate::{ - transformations::layout::{vertices_transformation_matrix, Position}, - Resolution, -}; +use crate::Resolution; use bytes::Bytes; use compositor_chromium::cef; use log::error; use crate::transformations::web_renderer::{FrameData, SourceTransforms}; -use super::GET_FRAME_POSITIONS_MESSAGE; +use super::{ + tranformation_matrices::{vertices_transformation_matrix, Position}, + GET_FRAME_POSITIONS_MESSAGE, +}; #[derive(Clone)] pub(super) struct BrowserClient { diff --git a/compositor_render/src/transformations/layout/transformation_matrices.rs b/compositor_render/src/transformations/web_renderer/tranformation_matrices.rs similarity index 54% rename from compositor_render/src/transformations/layout/transformation_matrices.rs rename to compositor_render/src/transformations/web_renderer/tranformation_matrices.rs index 0c1c26767..4168f1ed9 100644 --- a/compositor_render/src/transformations/layout/transformation_matrices.rs +++ b/compositor_render/src/transformations/web_renderer/tranformation_matrices.rs @@ -2,57 +2,8 @@ use nalgebra_glm::{rotate_z, scale, translate, vec3, Mat4, Vec3}; use crate::Resolution; -use super::RenderLayout; - -impl RenderLayout { - /// Returns matrix that transforms input plane vertices - /// (located in corners of clip space), to final position - pub(super) fn vertices_transformation_matrix(&self, output_resolution: &Resolution) -> Mat4 { - vertices_transformation_matrix( - &Position { - top: self.top, - left: self.left, - width: self.width, - height: self.height, - rotation_degrees: self.rotation_degrees, - }, - output_resolution, - ) - } - - pub(super) fn texture_coords_transformation_matrix( - &self, - input_resolution: &Option, - ) -> Mat4 { - let Some(input_resolution) = input_resolution else { - return Mat4::identity(); - }; - - match self.content { - super::RenderLayoutContent::Color(_) => Mat4::identity(), - super::RenderLayoutContent::ChildNode { ref crop, .. } => { - let x_scale = crop.width / input_resolution.width as f32; - let y_scale = crop.height / input_resolution.height as f32; - - let x_translate = crop.left / input_resolution.width as f32; - let y_translate = crop.top / input_resolution.height as f32; - - let mut transform_texture_matrix = Mat4::identity(); - transform_texture_matrix = translate( - &transform_texture_matrix, - &vec3(x_translate, y_translate, 0.0), - ); - transform_texture_matrix = - scale(&transform_texture_matrix, &vec3(x_scale, y_scale, 1.0)); - - transform_texture_matrix - } - } - } -} - #[derive(Debug)] -pub(crate) struct Position { +pub(super) struct Position { pub(crate) top: f32, pub(crate) left: f32, pub(crate) width: f32, @@ -60,7 +11,7 @@ pub(crate) struct Position { pub(crate) rotation_degrees: f32, } -pub(crate) fn vertices_transformation_matrix( +pub(super) fn vertices_transformation_matrix( position: &Position, output_resolution: &Resolution, ) -> Mat4 { diff --git a/compositor_render/src/wgpu/common_pipeline.rs b/compositor_render/src/wgpu/common_pipeline.rs index bf373deeb..5c22de2cd 100644 --- a/compositor_render/src/wgpu/common_pipeline.rs +++ b/compositor_render/src/wgpu/common_pipeline.rs @@ -118,7 +118,7 @@ pub fn create_render_pipeline( module: shader_module, entry_point: crate::wgpu::common_pipeline::FRAGMENT_ENTRYPOINT_NAME, targets: &[Some(wgpu::ColorTargetState { - format: wgpu::TextureFormat::Rgba8Unorm, + format: wgpu::TextureFormat::Rgba8UnormSrgb, write_mask: wgpu::ColorWrites::all(), blend: Some(wgpu::BlendState::ALPHA_BLENDING), })], diff --git a/compositor_render/src/wgpu/format/interleaved_yuv_to_rgba.rs b/compositor_render/src/wgpu/format/interleaved_yuv_to_rgba.rs index 821ad2fa7..5b7e9b909 100644 --- a/compositor_render/src/wgpu/format/interleaved_yuv_to_rgba.rs +++ b/compositor_render/src/wgpu/format/interleaved_yuv_to_rgba.rs @@ -42,7 +42,7 @@ impl InterleavedYuv422ToRgbaConverter { module: &shader_module, entry_point: "fs_main", targets: &[Some(wgpu::ColorTargetState { - format: wgpu::TextureFormat::Rgba8Unorm, + format: wgpu::TextureFormat::Rgba8UnormSrgb, write_mask: wgpu::ColorWrites::all(), blend: None, })], diff --git a/compositor_render/src/wgpu/format/interleaved_yuv_to_rgba.wgsl b/compositor_render/src/wgpu/format/interleaved_yuv_to_rgba.wgsl index 232e0ccc3..55dc34de0 100644 --- a/compositor_render/src/wgpu/format/interleaved_yuv_to_rgba.wgsl +++ b/compositor_render/src/wgpu/format/interleaved_yuv_to_rgba.wgsl @@ -21,6 +21,14 @@ fn vs_main(input: VertexInput) -> VertexOutput { @group(0) @binding(0) var texture: texture_2d; @group(1) @binding(0) var sampler_: sampler; +fn srgb_to_linear(srgb: vec3) -> vec3 { + let cutoff = step(srgb, vec3(0.04045)); + let higher = pow((srgb + vec3(0.055))/vec3(1.055), vec3(2.4)); + let lower = srgb/vec3(12.92); + + return mix(higher, lower, cutoff); +} + @fragment fn fs_main(input: VertexOutput) -> @location(0) vec4 { var dimensions = textureDimensions(texture); @@ -48,5 +56,6 @@ fn fs_main(input: VertexOutput) -> @location(0) vec4 { let g = y - 0.34414 * (u - 128.0 / 255.0) - 0.71414 * (v - 128.0 / 255.0); let b = y + 1.77200 * (u - 128.0 / 255.0); - return vec4(clamp(r, 0.0, 1.0), clamp(g, 0.0, 1.0), clamp(b, 0.0, 1.0), 1.0); + let srgb = vec3(clamp(r, 0.0, 1.0), clamp(g, 0.0, 1.0), clamp(b, 0.0, 1.0)); + return vec4(srgb_to_linear(srgb), 1.0); } diff --git a/compositor_render/src/wgpu/format/nv12_to_rgba.rs b/compositor_render/src/wgpu/format/nv12_to_rgba.rs index ea8686c7b..dddcb5091 100644 --- a/compositor_render/src/wgpu/format/nv12_to_rgba.rs +++ b/compositor_render/src/wgpu/format/nv12_to_rgba.rs @@ -41,7 +41,7 @@ impl Nv12ToRgbaConverter { module: &shader_module, entry_point: "fs_main", targets: &[Some(wgpu::ColorTargetState { - format: wgpu::TextureFormat::Rgba8Unorm, + format: wgpu::TextureFormat::Rgba8UnormSrgb, write_mask: wgpu::ColorWrites::all(), blend: None, })], diff --git a/compositor_render/src/wgpu/format/nv12_to_rgba.wgsl b/compositor_render/src/wgpu/format/nv12_to_rgba.wgsl index 17173d020..5883784b0 100644 --- a/compositor_render/src/wgpu/format/nv12_to_rgba.wgsl +++ b/compositor_render/src/wgpu/format/nv12_to_rgba.wgsl @@ -23,6 +23,14 @@ fn vs_main(input: VertexInput) -> VertexOutput { @group(1) @binding(0) var sampler_: sampler; +fn srgb_to_linear(srgb: vec3) -> vec3 { + let cutoff = step(srgb, vec3(0.04045)); + let higher = pow((srgb + vec3(0.055))/vec3(1.055), vec3(2.4)); + let lower = srgb/vec3(12.92); + + return mix(higher, lower, cutoff); +} + @fragment fn fs_main(input: VertexOutput) -> @location(0) vec4 { var y = textureSample(y_texture, sampler_, input.tex_coords).x; @@ -34,5 +42,6 @@ fn fs_main(input: VertexOutput) -> @location(0) vec4 { let g = y - 0.34414 * (u - 128.0 / 255.0) - 0.71414 * (v - 128.0 / 255.0); let b = y + 1.77200 * (u - 128.0 / 255.0); - return vec4(clamp(r, 0.0, 1.0), clamp(g, 0.0, 1.0), clamp(b, 0.0, 1.0), 1.0); + let srgb = vec3(clamp(r, 0.0, 1.0), clamp(g, 0.0, 1.0), clamp(b, 0.0, 1.0)); + return vec4(srgb_to_linear(srgb), 1.0); } diff --git a/compositor_render/src/wgpu/format/planar_yuv_to_rgba.rs b/compositor_render/src/wgpu/format/planar_yuv_to_rgba.rs index 6eb070f2d..79fed53a8 100644 --- a/compositor_render/src/wgpu/format/planar_yuv_to_rgba.rs +++ b/compositor_render/src/wgpu/format/planar_yuv_to_rgba.rs @@ -47,7 +47,7 @@ impl PlanarYuvToRgbaConverter { module: &shader_module, entry_point: "fs_main", targets: &[Some(wgpu::ColorTargetState { - format: wgpu::TextureFormat::Rgba8Unorm, + format: wgpu::TextureFormat::Rgba8UnormSrgb, write_mask: wgpu::ColorWrites::all(), blend: None, })], diff --git a/compositor_render/src/wgpu/format/planar_yuv_to_rgba.wgsl b/compositor_render/src/wgpu/format/planar_yuv_to_rgba.wgsl index 3b774312b..588f02cd1 100644 --- a/compositor_render/src/wgpu/format/planar_yuv_to_rgba.wgsl +++ b/compositor_render/src/wgpu/format/planar_yuv_to_rgba.wgsl @@ -32,6 +32,14 @@ struct PushConstantParams { var params: PushConstantParams; +fn srgb_to_linear(srgb: vec3) -> vec3 { + let cutoff = step(srgb, vec3(0.04045)); + let higher = pow((srgb + vec3(0.055))/vec3(1.055), vec3(2.4)); + let lower = srgb/vec3(12.92); + + return mix(higher, lower, cutoff); +} + @fragment fn fs_main(input: VertexOutput) -> @location(0) vec4 { var y = textureSample(y_texture, sampler_, input.tex_coords).x; @@ -50,5 +58,6 @@ fn fs_main(input: VertexOutput) -> @location(0) vec4 { let g = y - 0.34414 * (u - 128.0 / 255.0) - 0.71414 * (v - 128.0 / 255.0); let b = y + 1.77200 * (u - 128.0 / 255.0); - return vec4(clamp(r, 0.0, 1.0), clamp(g, 0.0, 1.0), clamp(b, 0.0, 1.0), 1.0); + let srgb = vec3(clamp(r, 0.0, 1.0), clamp(g, 0.0, 1.0), clamp(b, 0.0, 1.0)); + return vec4(srgb_to_linear(srgb), 1.0); } diff --git a/compositor_render/src/wgpu/format/rgba_to_yuv.wgsl b/compositor_render/src/wgpu/format/rgba_to_yuv.wgsl index 126eb1987..7fe4ff104 100644 --- a/compositor_render/src/wgpu/format/rgba_to_yuv.wgsl +++ b/compositor_render/src/wgpu/format/rgba_to_yuv.wgsl @@ -23,26 +23,35 @@ fn vs_main(input: VertexInput) -> VertexOutput { var plane_selector: u32; +fn linear_to_srgb(linear: vec3) -> vec3 { + let cutoff = step(linear, vec3(0.0031308)); + let higher = vec3(1.055)*pow(linear, vec3(1.0/2.4)) - vec3(0.055); + let lower = linear * vec3(12.92); + + return mix(higher, lower, cutoff); +} + @fragment fn fs_main(input: VertexOutput) -> @location(0) f32 { - let color = textureSample(texture, sampler_, input.tex_coords); - var conversion_weights: vec4; + let linear = textureSample(texture, sampler_, input.tex_coords); + let color = linear_to_srgb(linear.rgb); + var conversion_weights: vec3; var conversion_bias: f32; if(plane_selector == 0u) { // Y - conversion_weights = vec4(0.299, 0.587, 0.114, 0.0); + conversion_weights = vec3(0.299, 0.587, 0.114); conversion_bias = 0.0; } else if(plane_selector == 1u) { // U - conversion_weights = vec4(-0.168736, -0.331264, 0.5, 0.0); + conversion_weights = vec3(-0.168736, -0.331264, 0.5); conversion_bias = 128.0 / 255.0; } else if(plane_selector == 2u) { // V - conversion_weights = vec4(0.5, -0.418688, -0.081312, 0.0); + conversion_weights = vec3(0.5, -0.418688, -0.081312); conversion_bias = 128.0 / 255.0; } else { - conversion_weights = vec4(); + conversion_weights = vec3(); } return clamp(dot(color, conversion_weights) + conversion_bias, 0.0, 1.0); diff --git a/compositor_render/src/wgpu/texture/base.rs b/compositor_render/src/wgpu/texture/base.rs index 8cb6e415b..edd9f3f2e 100644 --- a/compositor_render/src/wgpu/texture/base.rs +++ b/compositor_render/src/wgpu/texture/base.rs @@ -94,7 +94,7 @@ impl Texture { height: 1, depth_or_array_layers: 1, }, - wgpu::TextureFormat::Rgba8Unorm, + wgpu::TextureFormat::Rgba8UnormSrgb, wgpu::TextureUsages::TEXTURE_BINDING, ) } diff --git a/compositor_render/src/wgpu/texture/bgra.rs b/compositor_render/src/wgpu/texture/bgra.rs index 00ed62648..99c444375 100644 --- a/compositor_render/src/wgpu/texture/bgra.rs +++ b/compositor_render/src/wgpu/texture/bgra.rs @@ -15,7 +15,7 @@ impl BGRATexture { height: resolution.height as u32, depth_or_array_layers: 1, }, - wgpu::TextureFormat::Rgba8Unorm, + wgpu::TextureFormat::Rgba8UnormSrgb, wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::TEXTURE_BINDING, )) } diff --git a/compositor_render/src/wgpu/texture/interleaved_yuv422.rs b/compositor_render/src/wgpu/texture/interleaved_yuv422.rs index f9631aa2e..f6956222d 100644 --- a/compositor_render/src/wgpu/texture/interleaved_yuv422.rs +++ b/compositor_render/src/wgpu/texture/interleaved_yuv422.rs @@ -27,7 +27,7 @@ impl InterleavedYuv422Texture { // g - y1 // b - v // a - y2 - wgpu::TextureFormat::Rgba8Unorm, + wgpu::TextureFormat::Rgba8UnormSrgb, wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::COPY_SRC diff --git a/compositor_render/src/wgpu/texture/rgba.rs b/compositor_render/src/wgpu/texture/rgba.rs index 26b399d22..e7c146253 100644 --- a/compositor_render/src/wgpu/texture/rgba.rs +++ b/compositor_render/src/wgpu/texture/rgba.rs @@ -18,7 +18,7 @@ impl RGBATexture { height: resolution.height as u32, depth_or_array_layers: 1, }, - wgpu::TextureFormat::Rgba8Unorm, + wgpu::TextureFormat::Rgba8UnormSrgb, wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::COPY_SRC diff --git a/compositor_web/Cargo.toml b/compositor_web/Cargo.toml index 9c39d284e..1ae49f2f9 100644 --- a/compositor_web/Cargo.toml +++ b/compositor_web/Cargo.toml @@ -36,3 +36,6 @@ glyphon = { workspace = true } [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { version = "0.2", features = ["js"] } + +[package.metadata.wasm-pack.profile.release] +wasm-opt = true diff --git a/compositor_web/src/wasm/input_uploader.rs b/compositor_web/src/wasm/input_uploader.rs index dec959680..66aa5df6a 100644 --- a/compositor_web/src/wasm/input_uploader.rs +++ b/compositor_web/src/wasm/input_uploader.rs @@ -97,8 +97,8 @@ impl InputUploader { mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8Unorm, - view_formats: &[wgpu::TextureFormat::Rgba8Unorm], + format: wgpu::TextureFormat::Rgba8UnormSrgb, + view_formats: &[wgpu::TextureFormat::Rgba8UnormSrgb], usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::COPY_SRC diff --git a/integration_tests/examples/raw_channel_input.rs b/integration_tests/examples/raw_channel_input.rs index 9a4272098..163442b01 100644 --- a/integration_tests/examples/raw_channel_input.rs +++ b/integration_tests/examples/raw_channel_input.rs @@ -205,12 +205,12 @@ fn create_texture(index: usize, device: &wgpu::Device, queue: &wgpu::Queue) -> A mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8Unorm, + format: wgpu::TextureFormat::Rgba8UnormSrgb, usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::TEXTURE_BINDING, - view_formats: &[wgpu::TextureFormat::Rgba8Unorm], + view_formats: &[wgpu::TextureFormat::Rgba8UnormSrgb], }); queue.write_texture( diff --git a/integration_tests/examples/rescaler_border_transition.rs b/integration_tests/examples/rescaler_border_transition.rs new file mode 100644 index 000000000..5e38a5b2e --- /dev/null +++ b/integration_tests/examples/rescaler_border_transition.rs @@ -0,0 +1,154 @@ +use anyhow::Result; +use compositor_api::types::Resolution; +use serde_json::json; +use std::{ + thread::{self}, + time::Duration, +}; + +use integration_tests::{ + examples::{self, run_example, TestSample}, + ffmpeg::{start_ffmpeg_receive, start_ffmpeg_send}, +}; + +const VIDEO_RESOLUTION: Resolution = Resolution { + width: 1280, + height: 720, +}; + +const IP: &str = "127.0.0.1"; +const INPUT_PORT: u16 = 8002; +const OUTPUT_PORT: u16 = 8004; + +fn main() { + run_example(client_code); +} + +fn client_code() -> Result<()> { + start_ffmpeg_receive(Some(OUTPUT_PORT), None)?; + + examples::post( + "input/input_1/register", + &json!({ + "type": "rtp_stream", + "port": INPUT_PORT, + "video": { + "decoder": "ffmpeg_h264" + } + }), + )?; + + examples::post( + "image/example_image/register", + &json!({ + "asset_type": "gif", + "url": "https://gifdb.com/images/high/rust-logo-on-fire-o41c0v9om8drr8dv.gif", + }), + )?; + + let scene1 = json!({ + "type": "view", + "background_color_rgba": "#42daf5ff", + "children": [ + { + "type": "rescaler", + "id": "resized", + "width": VIDEO_RESOLUTION.width, + "height": VIDEO_RESOLUTION.height, + "top": 0.0, + "right": 0.0, + "mode": "fill", + "border_color_rgba": "#FFFFFFFF", + "box_shadow": [ + { + "offset_y": 40, + "offset_x": 0, + "blur_radius": 40, + "color_rgba": "#00000088", + } + ], + "child": { + "type": "input_stream", + "input_id": "input_1" + } + } + ] + }); + + let scene2 = json!({ + "type": "view", + "background_color_rgba": "#42daf5ff", + "children": [ + { + "type": "rescaler", + "id": "resized", + "width": 300, + "height": 300, + "top": (VIDEO_RESOLUTION.height as f32 - 330.0) / 2.0 , + "right": (VIDEO_RESOLUTION.width as f32 - 330.0) / 2.0, + "mode": "fill", + "border_radius": 50, + "border_width": 15, + "border_color_rgba": "#FFFFFFFF", + "box_shadow": [ + { + "offset_y": 40, + "offset_x": 0, + "blur_radius": 40, + "color_rgba": "#00000088", + } + ], + "transition": { + "duration_ms": 1500, + "easing_function": { + "function_name": "cubic_bezier", + "points": [0.33, 1, 0.68, 1] + } + }, + "child": { + "type": "input_stream", + "input_id": "input_1" + } + } + ] + }); + + examples::post( + "output/output_1/register", + &json!({ + "type": "rtp_stream", + "ip": IP, + "port": OUTPUT_PORT, + "video": { + "resolution": { + "width": VIDEO_RESOLUTION.width, + "height": VIDEO_RESOLUTION.height, + }, + "encoder": { + "type": "ffmpeg_h264", + "preset": "ultrafast" + }, + "initial": { + "root": scene1 + } + } + }), + )?; + + examples::post("start", &json!({}))?; + + start_ffmpeg_send(IP, Some(INPUT_PORT), None, TestSample::TestPattern)?; + + thread::sleep(Duration::from_secs(5)); + + examples::post( + "output/output_1/update", + &json!({ + "video": { + "root": scene2, + } + }), + )?; + + Ok(()) +} diff --git a/integration_tests/examples/view_border_transition.rs b/integration_tests/examples/view_border_transition.rs new file mode 100644 index 000000000..72ed6f2fd --- /dev/null +++ b/integration_tests/examples/view_border_transition.rs @@ -0,0 +1,166 @@ +use anyhow::Result; +use compositor_api::types::Resolution; +use serde_json::json; +use std::{ + thread::{self}, + time::Duration, +}; + +use integration_tests::{ + examples::{self, run_example, TestSample}, + ffmpeg::{start_ffmpeg_receive, start_ffmpeg_send}, +}; + +const VIDEO_RESOLUTION: Resolution = Resolution { + width: 1280, + height: 720, +}; + +const IP: &str = "127.0.0.1"; +const INPUT_PORT: u16 = 8002; +const OUTPUT_PORT: u16 = 8004; + +fn main() { + run_example(client_code); +} + +fn client_code() -> Result<()> { + start_ffmpeg_receive(Some(OUTPUT_PORT), None)?; + + examples::post( + "input/input_1/register", + &json!({ + "type": "rtp_stream", + "port": INPUT_PORT, + "video": { + "decoder": "ffmpeg_h264" + } + }), + )?; + + examples::post( + "image/example_image/register", + &json!({ + "asset_type": "gif", + "url": "https://gifdb.com/images/high/rust-logo-on-fire-o41c0v9om8drr8dv.gif", + }), + )?; + + let scene1 = json!({ + "type": "view", + "background_color_rgba": "#42daf5ff", + "children": [ + { + "type": "view", + "id": "resized", + "width": VIDEO_RESOLUTION.width, + "height": VIDEO_RESOLUTION.height, + "top": 0.0, + "right": 0.0, + "background_color_rgba": "#0000FFFF", + "border_color_rgba": "#FFFFFFFF", + "box_shadow": [ + { + "offset_y": 40, + "offset_x": 0, + "blur_radius": 40, + "color_rgba": "#00000088", + } + ], + "children": [ + { + "type": "rescaler", + "mode": "fill", + "child": { + "type": "input_stream", + "input_id": "input_1" + } + } + ] + } + ] + }); + + let scene2 = json!({ + "type": "view", + "background_color_rgba": "#42daf5ff", + "children": [ + { + "type": "view", + "id": "resized", + "width": 300, + "height": 300, + "top": (VIDEO_RESOLUTION.height as f32 - 330.0) / 2.0 , + "right": (VIDEO_RESOLUTION.width as f32 - 330.0) / 2.0, + "border_radius": 50, + "border_width": 15, + "background_color_rgba": "#0000FFFF", + "border_color_rgba": "#FFFFFFFF", + "box_shadow": [ + { + "offset_y": 40, + "offset_x": 0, + "blur_radius": 40, + "color_rgba": "#00000088", + } + ], + "transition": { + "duration_ms": 1500, + "easing_function": { + "function_name": "cubic_bezier", + "points": [0.33, 1, 0.68, 1] + } + }, + "children": [ + { + "type": "rescaler", + "mode": "fill", + "child": { + "type": "input_stream", + "input_id": "input_1" + } + } + ] + } + ] + }); + + examples::post( + "output/output_1/register", + &json!({ + "type": "rtp_stream", + "ip": IP, + "port": OUTPUT_PORT, + "video": { + "resolution": { + "width": VIDEO_RESOLUTION.width, + "height": VIDEO_RESOLUTION.height, + }, + "encoder": { + "type": "ffmpeg_h264", + "preset": "ultrafast" + }, + "initial": { + "root": scene1 + } + } + }), + )?; + + examples::post("start", &json!({}))?; + + start_ffmpeg_send(IP, Some(INPUT_PORT), None, TestSample::TestPattern)?; + + thread::sleep(Duration::from_secs(5)); + + examples::post( + "output/output_1/update", + &json!({ + "video": { + "root": scene2, + } + }), + )?; + + Ok(()) +} diff --git a/schemas/scene.schema.json b/schemas/scene.schema.json index 0b2d97ccf..63b743f06 100644 --- a/schemas/scene.schema.json +++ b/schemas/scene.schema.json @@ -116,7 +116,7 @@ } }, "width": { - "description": "Width of a component in pixels. Exact behavior might be different based on the parent\ncomponent:\n- If the parent component is a layout, check sections \"Absolute positioning\" and \"Static\npositioning\" of that component.\n- If the parent component is not a layout, then this field is required.", + "description": "Width of a component in pixels (without a border). Exact behavior might be different\nbased on the parent component:\n- If the parent component is a layout, check sections \"Absolute positioning\" and \"Static\npositioning\" of that component.\n- If the parent component is not a layout, then this field is required.", "type": [ "number", "null" @@ -124,7 +124,7 @@ "format": "float" }, "height": { - "description": "Height of a component in pixels. Exact behavior might be different based on the parent\ncomponent:\n- If the parent component is a layout, check sections \"Absolute positioning\" and \"Static\npositioning\" of that component.\n- If the parent component is not a layout, then this field is required.", + "description": "Height of a component in pixels (without a border). Exact behavior might be different\nbased on the parent component:\n- If the parent component is a layout, check sections \"Absolute positioning\" and \"Static\npositioning\" of that component.\n- If the parent component is not a layout, then this field is required.", "type": [ "number", "null" @@ -143,7 +143,7 @@ ] }, "top": { - "description": "Distance in pixels between this component's top edge and its parent's top edge.\nIf this field is defined, then the component will ignore a layout defined by its parent.", + "description": "Distance in pixels between this component's top edge and its parent's top edge (including a border).\nIf this field is defined, then the component will ignore a layout defined by its parent.", "type": [ "number", "null" @@ -151,7 +151,7 @@ "format": "float" }, "left": { - "description": "Distance in pixels between this component's left edge and its parent's left edge.\nIf this field is defined, this element will be absolutely positioned, instead of being\nlaid out by its parent.", + "description": "Distance in pixels between this component's left edge and its parent's left edge (including a border).\nIf this field is defined, this element will be absolutely positioned, instead of being\nlaid out by its parent.", "type": [ "number", "null" @@ -159,7 +159,7 @@ "format": "float" }, "bottom": { - "description": "Distance in pixels between the bottom edge of this component and the bottom edge of its parent.\nIf this field is defined, this element will be absolutely positioned, instead of being\nlaid out by its parent.", + "description": "Distance in pixels between the bottom edge of this component and the bottom edge of its\nparent (including a border). If this field is defined, this element will be absolutely\npositioned, instead of being laid out by its parent.", "type": [ "number", "null" @@ -214,6 +214,43 @@ "type": "null" } ] + }, + "border_radius": { + "description": "(**default=`0.0`**) Radius of a rounded corner.", + "type": [ + "number", + "null" + ], + "format": "float" + }, + "border_width": { + "description": "(**default=`0.0`**) Border width.", + "type": [ + "number", + "null" + ], + "format": "float" + }, + "border_color_rgba": { + "description": "(**default=`\"#00000000\"`**) Border color in a `\"#RRGGBBAA\"` format.", + "anyOf": [ + { + "$ref": "#/definitions/RGBAColor" + }, + { + "type": "null" + } + ] + }, + "box_shadow": { + "description": "List of box shadows.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/BoxShadow" + } } }, "additionalProperties": false @@ -254,7 +291,7 @@ } }, "instance_id": { - "description": "Id of a web renderer instance. It identifies an instance registered using a [`register web renderer`](../routes.md#register-web-renderer-instance) request.\n\n:::warning\nYou can only refer to specific instances in one Component at a time.\n:::", + "description": "Id of a web renderer instance. It identifies an instance registered using a\n[`register web renderer`](../routes.md#register-web-renderer-instance) request.\n\n:::warning\nYou can only refer to specific instances in one Component at a time.\n:::", "allOf": [ { "$ref": "#/definitions/RendererId" @@ -308,7 +345,7 @@ ] }, "shader_param": { - "description": "Object that will be serialized into a `struct` and passed inside the shader as:\n\n```wgsl\n@group(1) @binding(0) var\n```\n:::note\nThis object's structure must match the structure defined in a shader source code. Currently, we do not handle memory layout automatically.\nTo achieve the correct memory alignment, you might need to pad your data with additional fields. See [WGSL documentation](https://www.w3.org/TR/WGSL/#alignment-and-size) for more details.\n:::", + "description": "Object that will be serialized into a `struct` and passed inside the shader as:\n\n```wgsl\n@group(1) @binding(0) var\n```\n:::note\nThis object's structure must match the structure defined in a shader source code.\nCurrently, we do not handle memory layout automatically. To achieve the correct memory\nalignment, you might need to pad your data with additional fields. See\n[WGSL documentation](https://www.w3.org/TR/WGSL/#alignment-and-size) for more details.\n:::", "anyOf": [ { "$ref": "#/definitions/ShaderParam" @@ -633,6 +670,13 @@ "type": "null" } ] + }, + "border_radius": { + "type": [ + "number", + "null" + ], + "format": "float" } }, "additionalProperties": false @@ -703,7 +747,7 @@ ] }, "width": { - "description": "Width of a component in pixels. Exact behavior might be different based on the parent\ncomponent:\n- If the parent component is a layout, check sections \"Absolute positioning\" and \"Static\npositioning\" of that component.\n- If the parent component is not a layout, then this field is required.", + "description": "Width of a component in pixels (without a border). Exact behavior might be different\nbased on the parent component:\n- If the parent component is a layout, check sections \"Absolute positioning\" and \"Static\npositioning\" of that component.\n- If the parent component is not a layout, then this field is required.", "type": [ "number", "null" @@ -711,7 +755,7 @@ "format": "float" }, "height": { - "description": "Height of a component in pixels. Exact behavior might be different based on the parent\ncomponent:\n- If the parent component is a layout, check sections \"Absolute positioning\" and \"Static\npositioning\" of that component.\n- If the parent component is not a layout, then this field is required.", + "description": "Height of a component in pixels (without a border). Exact behavior might be different\nbased on the parent component:\n- If the parent component is a layout, check sections \"Absolute positioning\" and \"Static\npositioning\" of that component.\n- If the parent component is not a layout, then this field is required.", "type": [ "number", "null" @@ -719,7 +763,7 @@ "format": "float" }, "top": { - "description": "Distance in pixels between this component's top edge and its parent's top edge.\nIf this field is defined, then the component will ignore a layout defined by its parent.", + "description": "Distance in pixels between this component's top edge and its parent's top edge (including a border).\nIf this field is defined, then the component will ignore a layout defined by its parent.", "type": [ "number", "null" @@ -727,7 +771,7 @@ "format": "float" }, "left": { - "description": "Distance in pixels between this component's left edge and its parent's left edge.\nIf this field is defined, this element will be absolutely positioned, instead of being\nlaid out by its parent.", + "description": "Distance in pixels between this component's left edge and its parent's left edge (including a border).\nIf this field is defined, this element will be absolutely positioned, instead of being\nlaid out by its parent.", "type": [ "number", "null" @@ -735,7 +779,7 @@ "format": "float" }, "bottom": { - "description": "Distance in pixels between this component's bottom edge and its parent's bottom edge.\nIf this field is defined, this element will be absolutely positioned, instead of being\nlaid out by its parent.", + "description": "Distance in pixels between the bottom edge of this component and the bottom edge of its\nparent (including a border). If this field is defined, this element will be absolutely\npositioned, instead of being laid out by its parent.", "type": [ "number", "null" @@ -768,6 +812,43 @@ "type": "null" } ] + }, + "border_radius": { + "description": "(**default=`0.0`**) Radius of a rounded corner.", + "type": [ + "number", + "null" + ], + "format": "float" + }, + "border_width": { + "description": "(**default=`0.0`**) Border width.", + "type": [ + "number", + "null" + ], + "format": "float" + }, + "border_color_rgba": { + "description": "(**default=`\"#00000000\"`**) Border color in a `\"#RRGGBBAA\"` format.", + "anyOf": [ + { + "$ref": "#/definitions/RGBAColor" + }, + { + "type": "null" + } + ] + }, + "box_shadow": { + "description": "List of box shadows.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/BoxShadow" + } } }, "additionalProperties": false @@ -907,6 +988,43 @@ "RGBAColor": { "type": "string" }, + "BoxShadow": { + "type": "object", + "properties": { + "offset_x": { + "type": [ + "number", + "null" + ], + "format": "float" + }, + "offset_y": { + "type": [ + "number", + "null" + ], + "format": "float" + }, + "color_rgba": { + "anyOf": [ + { + "$ref": "#/definitions/RGBAColor" + }, + { + "type": "null" + } + ] + }, + "blur_radius": { + "type": [ + "number", + "null" + ], + "format": "float" + } + }, + "additionalProperties": false + }, "RendererId": { "type": "string" }, diff --git a/snapshot_tests/rescaler/border_radius.scene.json b/snapshot_tests/rescaler/border_radius.scene.json new file mode 100644 index 000000000..d870231f7 --- /dev/null +++ b/snapshot_tests/rescaler/border_radius.scene.json @@ -0,0 +1,22 @@ +{ + "video": { + "root": { + "type": "view", + "background_color_rgba": "#FFFF00FF", + "children": [ + { + "type": "rescaler", + "top": 50, + "left": 50, + "width": 400, + "height": 200, + "border_radius": 50, + "child": { + "type": "view", + "background_color_rgba": "#FF0000FF" + } + } + ] + } + } +} diff --git a/snapshot_tests/rescaler/border_radius_border_box_shadow.scene.json b/snapshot_tests/rescaler/border_radius_border_box_shadow.scene.json new file mode 100644 index 000000000..669378200 --- /dev/null +++ b/snapshot_tests/rescaler/border_radius_border_box_shadow.scene.json @@ -0,0 +1,32 @@ +{ + "video": { + "root": { + "type": "view", + "background_color_rgba": "#FFFF00FF", + "children": [ + { + "type": "rescaler", + "top": 50, + "left": 50, + "width": 400, + "height": 200, + "border_radius": 50, + "border_width": 20, + "border_color_rgba": "#FFFFFFFF", + "box_shadow": [ + { + "offset_x": 60, + "offset_y": 30, + "blur_radius": 30, + "color_rgba": "#00FF00FF" + } + ], + "child": { + "type": "view", + "background_color_rgba": "#FF0000FF" + } + } + ] + } + } +} diff --git a/snapshot_tests/rescaler/border_radius_border_box_shadow_rescaled.scene.json b/snapshot_tests/rescaler/border_radius_border_box_shadow_rescaled.scene.json new file mode 100644 index 000000000..0f1084b6d --- /dev/null +++ b/snapshot_tests/rescaler/border_radius_border_box_shadow_rescaled.scene.json @@ -0,0 +1,37 @@ +{ + "video": { + "root": { + "type": "view", + "background_color_rgba": "#FFFF00FF", + "children": [ + { + "type": "rescaler", + "width": 600, + "height": 300, + "horizontal_align": "center", + "vertical_align": "center", + "child": { + "type": "rescaler", + "width": 200, + "height": 200, + "border_radius": 50, + "border_width": 20, + "border_color_rgba": "#FFFFFFFF", + "box_shadow": [ + { + "offset_x": 20, + "offset_y": 20, + "blur_radius": 5, + "color_rgba": "#00FF00FF" + } + ], + "child": { + "type": "input_stream", + "input_id": "input_1" + } + } + } + ] + } + } +} diff --git a/snapshot_tests/rescaler/border_radius_box_shadow.scene.json b/snapshot_tests/rescaler/border_radius_box_shadow.scene.json new file mode 100644 index 000000000..e226b108c --- /dev/null +++ b/snapshot_tests/rescaler/border_radius_box_shadow.scene.json @@ -0,0 +1,30 @@ +{ + "video": { + "root": { + "type": "view", + "background_color_rgba": "#FFFF00FF", + "children": [ + { + "type": "rescaler", + "top": 50, + "left": 50, + "width": 400, + "height": 200, + "border_radius": 50, + "box_shadow": [ + { + "offset_x": 60, + "offset_y": 30, + "blur_radius": 30, + "color_rgba": "#00FF00FF" + } + ], + "child": { + "type": "view", + "background_color_rgba": "#FF0000FF" + } + } + ] + } + } +} diff --git a/snapshot_tests/rescaler/border_radius_box_shadow_fill_input_stream.scene.json b/snapshot_tests/rescaler/border_radius_box_shadow_fill_input_stream.scene.json new file mode 100644 index 000000000..813b30e26 --- /dev/null +++ b/snapshot_tests/rescaler/border_radius_box_shadow_fill_input_stream.scene.json @@ -0,0 +1,33 @@ +{ + "video": { + "root": { + "type": "view", + "background_color_rgba": "#FFFF00FF", + "children": [ + { + "type": "rescaler", + "top": 50, + "left": 50, + "width": 400, + "height": 200, + "border_radius": 50, + "border_width": 20, + "border_color_rgba": "#FFFFFFFF", + "mode": "fill", + "box_shadow": [ + { + "offset_x": 60, + "offset_y": 30, + "blur_radius": 30, + "color_rgba": "#00FF00FF" + } + ], + "child": { + "type": "input_stream", + "input_id": "input_1" + } + } + ] + } + } +} diff --git a/snapshot_tests/rescaler/border_radius_box_shadow_fit_input_stream.scene.json b/snapshot_tests/rescaler/border_radius_box_shadow_fit_input_stream.scene.json new file mode 100644 index 000000000..ff9cc6e93 --- /dev/null +++ b/snapshot_tests/rescaler/border_radius_box_shadow_fit_input_stream.scene.json @@ -0,0 +1,33 @@ +{ + "video": { + "root": { + "type": "view", + "background_color_rgba": "#FFFF00FF", + "children": [ + { + "type": "rescaler", + "top": 50, + "left": 50, + "width": 400, + "height": 200, + "border_radius": 50, + "border_width": 20, + "border_color_rgba": "#FFFFFFFF", + "mode": "fit", + "box_shadow": [ + { + "offset_x": 60, + "offset_y": 30, + "blur_radius": 30, + "color_rgba": "#00FF00FF" + } + ], + "child": { + "type": "input_stream", + "input_id": "input_1" + } + } + ] + } + } +} diff --git a/snapshot_tests/rescaler/border_width.scene.json b/snapshot_tests/rescaler/border_width.scene.json new file mode 100644 index 000000000..8705ebd2c --- /dev/null +++ b/snapshot_tests/rescaler/border_width.scene.json @@ -0,0 +1,23 @@ +{ + "video": { + "root": { + "type": "view", + "background_color_rgba": "#FFFF00FF", + "children": [ + { + "type": "rescaler", + "top": 50, + "left": 50, + "width": 400, + "height": 200, + "border_width": 20, + "border_color_rgba": "#FFFFFFFF", + "child": { + "type": "view", + "background_color_rgba": "#FF0000FF" + } + } + ] + } + } +} diff --git a/snapshot_tests/rescaler/box_shadow.scene.json b/snapshot_tests/rescaler/box_shadow.scene.json new file mode 100644 index 000000000..dcfd76a9d --- /dev/null +++ b/snapshot_tests/rescaler/box_shadow.scene.json @@ -0,0 +1,29 @@ +{ + "video": { + "root": { + "type": "view", + "background_color_rgba": "#FFFF00FF", + "children": [ + { + "type": "rescaler", + "top": 50, + "left": 50, + "width": 400, + "height": 200, + "box_shadow": [ + { + "offset_x": 60, + "offset_y": 30, + "blur_radius": 30, + "color_rgba": "#00FF00FF" + } + ], + "child": { + "type": "view", + "background_color_rgba": "#FF0000FF" + } + } + ] + } + } +} diff --git a/snapshot_tests/rescaler/nested_border_width_radius.scene.json b/snapshot_tests/rescaler/nested_border_width_radius.scene.json new file mode 100644 index 000000000..342e39a0b --- /dev/null +++ b/snapshot_tests/rescaler/nested_border_width_radius.scene.json @@ -0,0 +1,37 @@ +{ + "video": { + "root": { + "type": "view", + "background_color_rgba": "#FFFF00FF", + "children": [ + { + "type": "rescaler", + "top": 50, + "left": 50, + "width": 400, + "height": 200, + "border_radius": 50, + "border_width": 20, + "border_color_rgba": "#FF0000FF", + "child": { + "type": "rescaler", + "border_radius": 50, + "border_width": 20, + "border_color_rgba": "#00FF00FF", + "child": { + "type": "rescaler", + "border_radius": 50, + "border_width": 20, + "border_color_rgba": "#0000FFFF", + "mode": "fill", + "child": { + "type": "input_stream", + "input_id": "input_1" + } + } + } + } + ] + } + } +} diff --git a/snapshot_tests/rescaler/nested_border_width_radius_aligned.scene.json b/snapshot_tests/rescaler/nested_border_width_radius_aligned.scene.json new file mode 100644 index 000000000..03b0e49c2 --- /dev/null +++ b/snapshot_tests/rescaler/nested_border_width_radius_aligned.scene.json @@ -0,0 +1,37 @@ +{ + "video": { + "root": { + "type": "view", + "background_color_rgba": "#FFFF00FF", + "children": [ + { + "type": "rescaler", + "top": 50, + "left": 50, + "width": 400, + "height": 200, + "border_radius": 80, + "border_width": 20, + "border_color_rgba": "#FF0000FF", + "child": { + "type": "rescaler", + "border_radius": 60, + "border_width": 20, + "border_color_rgba": "#00FF00FF", + "child": { + "type": "rescaler", + "border_radius": 40, + "border_width": 20, + "border_color_rgba": "#0000FFFF", + "mode": "fill", + "child": { + "type": "input_stream", + "input_id": "input_1" + } + } + } + } + ] + } + } +} diff --git a/snapshot_tests/snapshots b/snapshot_tests/snapshots index ac634ad38..fd3ef30ac 160000 --- a/snapshot_tests/snapshots +++ b/snapshot_tests/snapshots @@ -1 +1 @@ -Subproject commit ac634ad382cc6e6417167db3c768fe750e9b0240 +Subproject commit fd3ef30acb19aded98515cad20a16b0c5c89d7a9 diff --git a/snapshot_tests/view/border_radius.scene.json b/snapshot_tests/view/border_radius.scene.json new file mode 100644 index 000000000..c1ee5d386 --- /dev/null +++ b/snapshot_tests/view/border_radius.scene.json @@ -0,0 +1,19 @@ +{ + "video": { + "root": { + "type": "view", + "background_color_rgba": "#FFFF00FF", + "children": [ + { + "type": "view", + "background_color_rgba": "#FF0000FF", + "top": 50, + "left": 50, + "width": 400, + "height": 200, + "border_radius": 50 + } + ] + } + } +} diff --git a/snapshot_tests/view/border_radius_border_box_shadow.scene.json b/snapshot_tests/view/border_radius_border_box_shadow.scene.json new file mode 100644 index 000000000..93ba6bcd2 --- /dev/null +++ b/snapshot_tests/view/border_radius_border_box_shadow.scene.json @@ -0,0 +1,29 @@ +{ + "video": { + "root": { + "type": "view", + "background_color_rgba": "#FFFF00FF", + "children": [ + { + "type": "view", + "background_color_rgba": "#FF0000FF", + "top": 50, + "left": 50, + "width": 400, + "height": 200, + "border_radius": 50, + "border_width": 20, + "border_color_rgba": "#FFFFFFFF", + "box_shadow": [ + { + "offset_x": 60, + "offset_y": 30, + "blur_radius": 30, + "color_rgba": "#00FF00FF" + } + ] + } + ] + } + } +} diff --git a/snapshot_tests/view/border_radius_border_box_shadow_rescaled.scene.json b/snapshot_tests/view/border_radius_border_box_shadow_rescaled.scene.json new file mode 100644 index 000000000..4849370b2 --- /dev/null +++ b/snapshot_tests/view/border_radius_border_box_shadow_rescaled.scene.json @@ -0,0 +1,34 @@ +{ + "video": { + "root": { + "type": "view", + "background_color_rgba": "#FFFF00FF", + "children": [ + { + "type": "rescaler", + "width": 600, + "height": 300, + "horizontal_align": "center", + "vertical_align": "center", + "child": { + "type": "view", + "background_color_rgba": "#FF0000FF", + "width": 200, + "height": 200, + "border_radius": 50, + "border_width": 20, + "border_color_rgba": "#FFFFFFFF", + "box_shadow": [ + { + "offset_x": 20, + "offset_y": 20, + "blur_radius": 5, + "color_rgba": "#00FF00FF" + } + ] + } + } + ] + } + } +} diff --git a/snapshot_tests/view/border_radius_border_box_shadow_rescaled_and_hidden_by_parent.scene.json b/snapshot_tests/view/border_radius_border_box_shadow_rescaled_and_hidden_by_parent.scene.json new file mode 100644 index 000000000..45c66220f --- /dev/null +++ b/snapshot_tests/view/border_radius_border_box_shadow_rescaled_and_hidden_by_parent.scene.json @@ -0,0 +1,41 @@ +{ + "video": { + "root": { + "type": "view", + "background_color_rgba": "#FFFF00FF", + "children": [ + { + "type": "view", + "width": 460, + "height": 270, + "children": [ + { + "type": "rescaler", + "width": 600, + "height": 300, + "horizontal_align": "center", + "vertical_align": "center", + "child": { + "type": "view", + "background_color_rgba": "#FF0000FF", + "width": 200, + "height": 200, + "border_radius": 50, + "border_width": 20, + "border_color_rgba": "#FFFFFFFF", + "box_shadow": [ + { + "offset_x": 20, + "offset_y": 20, + "blur_radius": 5, + "color_rgba": "#00FF00FF" + } + ] + } + } + ] + } + ] + } + } +} diff --git a/snapshot_tests/view/border_radius_box_shadow.scene.json b/snapshot_tests/view/border_radius_box_shadow.scene.json new file mode 100644 index 000000000..7529d7fb7 --- /dev/null +++ b/snapshot_tests/view/border_radius_box_shadow.scene.json @@ -0,0 +1,27 @@ +{ + "video": { + "root": { + "type": "view", + "background_color_rgba": "#FFFF00FF", + "children": [ + { + "type": "view", + "background_color_rgba": "#FF0000FF", + "top": 50, + "left": 50, + "width": 400, + "height": 200, + "border_radius": 50, + "box_shadow": [ + { + "offset_x": 60, + "offset_y": 30, + "blur_radius": 30, + "color_rgba": "#00FF00FF" + } + ] + } + ] + } + } +} diff --git a/snapshot_tests/view/border_radius_box_shadow_overflow_fit.scene.json b/snapshot_tests/view/border_radius_box_shadow_overflow_fit.scene.json new file mode 100644 index 000000000..8cc107791 --- /dev/null +++ b/snapshot_tests/view/border_radius_box_shadow_overflow_fit.scene.json @@ -0,0 +1,36 @@ +{ + "video": { + "root": { + "type": "view", + "background_color_rgba": "#FFFF00FF", + "children": [ + { + "type": "view", + "background_color_rgba": "#FF0000FF", + "overflow": "fit", + "top": 50, + "left": 50, + "width": 400, + "height": 200, + "border_radius": 50, + "border_width": 20, + "border_color_rgba": "#FFFFFFFF", + "box_shadow": [ + { + "offset_x": 60, + "offset_y": 30, + "blur_radius": 30, + "color_rgba": "#00FF00FF" + } + ], + "children": [ + { + "type": "input_stream", + "input_id": "input_1" + } + ] + } + ] + } + } +} diff --git a/snapshot_tests/view/border_radius_box_shadow_overflow_hidden.scene.json b/snapshot_tests/view/border_radius_box_shadow_overflow_hidden.scene.json new file mode 100644 index 000000000..cc401d7a5 --- /dev/null +++ b/snapshot_tests/view/border_radius_box_shadow_overflow_hidden.scene.json @@ -0,0 +1,35 @@ +{ + "video": { + "root": { + "type": "view", + "background_color_rgba": "#FFFF00FF", + "children": [ + { + "type": "view", + "background_color_rgba": "#FF0000FF", + "top": 50, + "left": 50, + "width": 400, + "height": 200, + "border_radius": 50, + "border_width": 20, + "border_color_rgba": "#FFFFFFFF", + "box_shadow": [ + { + "offset_x": 60, + "offset_y": 30, + "blur_radius": 30, + "color_rgba": "#00FF00FF" + } + ], + "children": [ + { + "type": "input_stream", + "input_id": "input_1" + } + ] + } + ] + } + } +} diff --git a/snapshot_tests/view/border_radius_box_shadow_rescaler_input_stream.scene.json b/snapshot_tests/view/border_radius_box_shadow_rescaler_input_stream.scene.json new file mode 100644 index 000000000..3cb05a51a --- /dev/null +++ b/snapshot_tests/view/border_radius_box_shadow_rescaler_input_stream.scene.json @@ -0,0 +1,40 @@ +{ + "video": { + "root": { + "type": "view", + "background_color_rgba": "#FFFF00FF", + "children": [ + { + "type": "view", + "background_color_rgba": "#FF0000FF", + "top": 50, + "left": 50, + "width": 400, + "height": 200, + "border_radius": 50, + "border_width": 20, + "border_color_rgba": "#FFFFFFFF", + "box_shadow": [ + { + "offset_x": 60, + "offset_y": 30, + "blur_radius": 30, + "color_rgba": "#00FF00FF" + } + ], + "children": [ + { + "type": "rescaler", + "mode": "fill", + "vertical_align": "top", + "child": { + "type": "input_stream", + "input_id": "input_1" + } + } + ] + } + ] + } + } +} diff --git a/snapshot_tests/view/border_width.scene.json b/snapshot_tests/view/border_width.scene.json new file mode 100644 index 000000000..91410189a --- /dev/null +++ b/snapshot_tests/view/border_width.scene.json @@ -0,0 +1,20 @@ +{ + "video": { + "root": { + "type": "view", + "background_color_rgba": "#FFFF00FF", + "children": [ + { + "type": "view", + "background_color_rgba": "#FF0000FF", + "top": 50, + "left": 50, + "width": 400, + "height": 200, + "border_width": 20, + "border_color_rgba": "#FFFFFFFF" + } + ] + } + } +} diff --git a/snapshot_tests/view/box_shadow.scene.json b/snapshot_tests/view/box_shadow.scene.json new file mode 100644 index 000000000..00df6994b --- /dev/null +++ b/snapshot_tests/view/box_shadow.scene.json @@ -0,0 +1,26 @@ +{ + "video": { + "root": { + "type": "view", + "background_color_rgba": "#FFFF00FF", + "children": [ + { + "type": "view", + "background_color_rgba": "#FF0000FF", + "top": 50, + "left": 50, + "width": 400, + "height": 200, + "box_shadow": [ + { + "offset_x": 60, + "offset_y": 30, + "blur_radius": 30, + "color_rgba": "#00FF00FF" + } + ] + } + ] + } + } +} diff --git a/snapshot_tests/view/box_shadow_sibling.scene.json b/snapshot_tests/view/box_shadow_sibling.scene.json new file mode 100644 index 000000000..16e214f56 --- /dev/null +++ b/snapshot_tests/view/box_shadow_sibling.scene.json @@ -0,0 +1,52 @@ +{ + "video": { + "root": { + "type": "view", + "children": [ + { + "type": "view", + "background_color_rgba": "#FFFF00FF", + "overflow": "visible", + "top": 100, + "left": 100, + "width": 400, + "height": 200, + "children": [ + { + "type": "view", + "background_color_rgba": "#FF0000FF", + "box_shadow": [ + { + "offset_x": 0, + "offset_y": 60, + "blur_radius": 30, + "color_rgba": "#FF0000FF" + }, + { + "offset_x": -60, + "offset_y": -30, + "blur_radius": 30, + "color_rgba": "#0000FFFF" + } + ] + }, + { + "type": "view", + "background_color_rgba": "#FF0000FF", + "border_width": 20, + "border_color_rgba": "#FFFFFFFF", + "box_shadow": [ + { + "offset_x": 60, + "offset_y": 30, + "blur_radius": 30, + "color_rgba": "#0000FFFF" + } + ] + } + ] + } + ] + } + } +} diff --git a/snapshot_tests/view/nested_border_width_radius.scene.json b/snapshot_tests/view/nested_border_width_radius.scene.json new file mode 100644 index 000000000..d13427f84 --- /dev/null +++ b/snapshot_tests/view/nested_border_width_radius.scene.json @@ -0,0 +1,42 @@ +{ + "video": { + "root": { + "type": "view", + "background_color_rgba": "#FFFF00FF", + "children": [ + { + "type": "view", + "top": 50, + "left": 50, + "width": 400, + "height": 200, + "border_radius": 50, + "border_width": 20, + "border_color_rgba": "#FF0000FF", + "children": [ + { + "type": "view", + "border_radius": 50, + "border_width": 20, + "border_color_rgba": "#00FF00FF", + "children": [ + { + "type": "view", + "border_radius": 50, + "border_width": 20, + "border_color_rgba": "#0000FFFF", + "children": [ + { + "type": "input_stream", + "input_id": "input_1" + } + ] + } + ] + } + ] + } + ] + } + } +} diff --git a/snapshot_tests/view/nested_border_width_radius_aligned.scene.json b/snapshot_tests/view/nested_border_width_radius_aligned.scene.json new file mode 100644 index 000000000..ddf6b7cc2 --- /dev/null +++ b/snapshot_tests/view/nested_border_width_radius_aligned.scene.json @@ -0,0 +1,42 @@ +{ + "video": { + "root": { + "type": "view", + "background_color_rgba": "#FFFF00FF", + "children": [ + { + "type": "view", + "top": 50, + "left": 50, + "width": 400, + "height": 200, + "border_radius": 80, + "border_width": 20, + "border_color_rgba": "#FF0000FF", + "children": [ + { + "type": "view", + "border_radius": 60, + "border_width": 20, + "border_color_rgba": "#00FF00FF", + "children": [ + { + "type": "view", + "border_radius": 40, + "border_width": 20, + "border_color_rgba": "#0000FFFF", + "children": [ + { + "type": "input_stream", + "input_id": "input_1" + } + ] + } + ] + } + ] + } + ] + } + } +} diff --git a/snapshot_tests/view/nested_border_width_radius_multi_child.scene.json b/snapshot_tests/view/nested_border_width_radius_multi_child.scene.json new file mode 100644 index 000000000..c8a740c7f --- /dev/null +++ b/snapshot_tests/view/nested_border_width_radius_multi_child.scene.json @@ -0,0 +1,74 @@ +{ + "video": { + "root": { + "type": "view", + "background_color_rgba": "#FFFF00FF", + "children": [ + { + "type": "view", + "top": 50, + "left": 50, + "width": 400, + "height": 200, + "border_radius": 50, + "border_width": 10, + "border_color_rgba": "#FF0000FF", + "children": [ + { + "type": "view", + "border_radius": 40, + "border_width": 10, + "border_color_rgba": "#00FF00FF", + "children": [ + { + "type": "view", + "border_radius": 30, + "border_width": 10, + "border_color_rgba": "#0000FFFF", + "children": [ + { + "type": "input_stream", + "input_id": "input_1" + } + ] + } + ] + }, + { + "type": "view", + "border_radius": 40, + "border_width": 10, + "border_color_rgba": "#00FF00FF", + "children": [ + { + "type": "view", + "border_radius": 30, + "border_width": 10, + "border_color_rgba": "#0000FFFF", + "children": [ + { + "type": "input_stream", + "input_id": "input_1" + } + ] + }, + { + "type": "view", + "border_radius": 30, + "border_width": 10, + "border_color_rgba": "#0000FFFF", + "children": [ + { + "type": "input_stream", + "input_id": "input_1" + } + ] + } + ] + } + ] + } + ] + } + } +} diff --git a/snapshot_tests/view/root_border_radius_border_box_shadow.scene.json b/snapshot_tests/view/root_border_radius_border_box_shadow.scene.json new file mode 100644 index 000000000..3e424d513 --- /dev/null +++ b/snapshot_tests/view/root_border_radius_border_box_shadow.scene.json @@ -0,0 +1,19 @@ +{ + "video": { + "root": { + "type": "view", + "background_color_rgba": "#FF0000FF", + "border_radius": 50, + "border_width": 20, + "border_color_rgba": "#FFFFFFFF", + "box_shadow": [ + { + "offset_x": 60, + "offset_y": 30, + "blur_radius": 30, + "color_rgba": "#00FF00FF" + } + ] + } + } +} diff --git a/src/bin/update_snapshots/main.rs b/src/bin/update_snapshots/main.rs index 5be1e8ae1..7d47bd8d5 100644 --- a/src/bin/update_snapshots/main.rs +++ b/src/bin/update_snapshots/main.rs @@ -19,7 +19,8 @@ use crate::{ fn main() { println!("Updating snapshots:"); - tracing_subscriber::fmt().init(); + let log_filter = tracing_subscriber::EnvFilter::new("info,wgpu_core=warn,wgpu_hal=warn"); + tracing_subscriber::fmt().with_env_filter(log_filter).init(); let tests: Vec<_> = snapshot_tests(); let has_only_flag = tests.iter().any(|t| t.only); diff --git a/src/snapshot_tests.rs b/src/snapshot_tests.rs index 632ca90d3..6f5fb8639 100644 --- a/src/snapshot_tests.rs +++ b/src/snapshot_tests.rs @@ -20,7 +20,7 @@ fn test_snapshots() { check_test_names_uniqueness(&tests); for test in tests.iter() { - eprintln!("Test \"{}\"", test.case.name); + println!("Test \"{}\"", test.case.name); if let Err(err) = test.run() { handle_error(err); } diff --git a/src/snapshot_tests/tests.rs b/src/snapshot_tests/tests.rs index 3c0a6a6b5..714799c60 100644 --- a/src/snapshot_tests/tests.rs +++ b/src/snapshot_tests/tests.rs @@ -497,6 +497,97 @@ fn rescaler_snapshot_tests() -> Vec { inputs: vec![TestInput::new(1)], ..Default::default() }, + TestCase { + name: "rescaler/border_radius", + scene_updates: Updates::Scene( + include_str!("../../snapshot_tests/rescaler/border_radius.scene.json"), + DEFAULT_RESOLUTION, + ), + inputs: vec![TestInput::new(1)], + ..Default::default() + }, + TestCase { + name: "rescaler/border_width", + scene_updates: Updates::Scene( + include_str!("../../snapshot_tests/rescaler/border_width.scene.json"), + DEFAULT_RESOLUTION, + ), + inputs: vec![TestInput::new(1)], + ..Default::default() + }, + TestCase { + name: "rescaler/box_shadow", + scene_updates: Updates::Scene( + include_str!("../../snapshot_tests/rescaler/box_shadow.scene.json"), + DEFAULT_RESOLUTION, + ), + inputs: vec![TestInput::new(1)], + ..Default::default() + }, + TestCase { + name: "rescaler/border_radius_border_box_shadow", + scene_updates: Updates::Scene( + include_str!("../../snapshot_tests/rescaler/border_radius_border_box_shadow.scene.json"), + DEFAULT_RESOLUTION, + ), + inputs: vec![TestInput::new(1)], + ..Default::default() + }, + TestCase { + name: "rescaler/border_radius_box_shadow", + scene_updates: Updates::Scene( + include_str!("../../snapshot_tests/rescaler/border_radius_box_shadow.scene.json"), + DEFAULT_RESOLUTION, + ), + inputs: vec![TestInput::new(1)], + ..Default::default() + }, + TestCase { + name: "rescaler/border_radius_box_shadow_fit_input_stream", + scene_updates: Updates::Scene( + include_str!("../../snapshot_tests/rescaler/border_radius_box_shadow_fit_input_stream.scene.json"), + DEFAULT_RESOLUTION, + ), + inputs: vec![TestInput::new(1)], + ..Default::default() + }, + TestCase { + name: "rescaler/border_radius_box_shadow_fill_input_stream", + scene_updates: Updates::Scene( + include_str!("../../snapshot_tests/rescaler/border_radius_box_shadow_fill_input_stream.scene.json"), + DEFAULT_RESOLUTION, + ), + inputs: vec![TestInput::new(1)], + ..Default::default() + }, + TestCase { + name: "rescaler/nested_border_width_radius", + scene_updates: Updates::Scene( + include_str!("../../snapshot_tests/rescaler/nested_border_width_radius.scene.json"), + DEFAULT_RESOLUTION, + ), + inputs: vec![TestInput::new(1)], + ..Default::default() + }, + TestCase { + name: "rescaler/nested_border_width_radius_aligned", + scene_updates: Updates::Scene( + include_str!("../../snapshot_tests/rescaler/nested_border_width_radius_aligned.scene.json"), + DEFAULT_RESOLUTION, + ), + inputs: vec![TestInput::new(1)], + ..Default::default() + }, + TestCase { + // it is supposed to be cut off because of the rescaler that wraps it + name: "rescaler/border_radius_border_box_shadow_rescaled", + scene_updates: Updates::Scene( + include_str!("../../snapshot_tests/rescaler/border_radius_border_box_shadow_rescaled.scene.json"), + DEFAULT_RESOLUTION, + ), + inputs: vec![TestInput::new(1)], + ..Default::default() + } ]) } @@ -1565,7 +1656,144 @@ fn view_snapshot_tests() -> Vec { ), inputs: vec![TestInput::new(1)], ..Default::default() - } + }, + TestCase { + name: "view/border_radius", + scene_updates: Updates::Scene( + include_str!("../../snapshot_tests/view/border_radius.scene.json"), + DEFAULT_RESOLUTION, + ), + inputs: vec![TestInput::new(1)], + ..Default::default() + }, + TestCase { + name: "view/border_width", + scene_updates: Updates::Scene( + include_str!("../../snapshot_tests/view/border_width.scene.json"), + DEFAULT_RESOLUTION, + ), + inputs: vec![TestInput::new(1)], + ..Default::default() + }, + TestCase { + name: "view/box_shadow", + scene_updates: Updates::Scene( + include_str!("../../snapshot_tests/view/box_shadow.scene.json"), + DEFAULT_RESOLUTION, + ), + inputs: vec![TestInput::new(1)], + ..Default::default() + }, + TestCase { + name: "view/box_shadow_sibling", + scene_updates: Updates::Scene( + include_str!("../../snapshot_tests/view/box_shadow_sibling.scene.json"), + DEFAULT_RESOLUTION, + ), + inputs: vec![TestInput::new(1)], + ..Default::default() + }, + TestCase { + name: "view/border_radius_border_box_shadow", + scene_updates: Updates::Scene( + include_str!("../../snapshot_tests/view/border_radius_border_box_shadow.scene.json"), + DEFAULT_RESOLUTION, + ), + inputs: vec![TestInput::new(1)], + ..Default::default() + }, + TestCase { + name: "view/border_radius_box_shadow", + scene_updates: Updates::Scene( + include_str!("../../snapshot_tests/view/border_radius_box_shadow.scene.json"), + DEFAULT_RESOLUTION, + ), + inputs: vec![TestInput::new(1)], + ..Default::default() + }, + TestCase { + name: "view/border_radius_box_shadow_overflow_hidden", + scene_updates: Updates::Scene( + include_str!("../../snapshot_tests/view/border_radius_box_shadow_overflow_hidden.scene.json"), + DEFAULT_RESOLUTION, + ), + inputs: vec![TestInput::new(1)], + ..Default::default() + }, + TestCase { + name: "view/border_radius_box_shadow_overflow_fit", + only: true, + scene_updates: Updates::Scene( + include_str!("../../snapshot_tests/view/border_radius_box_shadow_overflow_fit.scene.json"), + DEFAULT_RESOLUTION, + ), + inputs: vec![TestInput::new(1)], + ..Default::default() + }, + TestCase { + name: "view/border_radius_box_shadow_rescaler_input_stream", + scene_updates: Updates::Scene( + include_str!("../../snapshot_tests/view/border_radius_box_shadow_rescaler_input_stream.scene.json"), + DEFAULT_RESOLUTION, + ), + inputs: vec![TestInput::new(1)], + ..Default::default() + }, + TestCase { + name: "view/nested_border_width_radius", + scene_updates: Updates::Scene( + include_str!("../../snapshot_tests/view/nested_border_width_radius.scene.json"), + DEFAULT_RESOLUTION, + ), + inputs: vec![TestInput::new(1)], + ..Default::default() + }, + TestCase { + name: "view/nested_border_width_radius_aligned", + scene_updates: Updates::Scene( + include_str!("../../snapshot_tests/view/nested_border_width_radius_aligned.scene.json"), + DEFAULT_RESOLUTION, + ), + inputs: vec![TestInput::new(1)], + ..Default::default() + }, + TestCase { + name: "view/nested_border_width_radius_multi_child", + scene_updates: Updates::Scene( + include_str!("../../snapshot_tests/view/nested_border_width_radius_multi_child.scene.json"), + DEFAULT_RESOLUTION, + ), + inputs: vec![TestInput::new(1)], + ..Default::default() + }, + TestCase { + // it is supposed to be cut off because of the rescaler that wraps it + name: "view/border_radius_border_box_shadow_rescaled", + scene_updates: Updates::Scene( + include_str!("../../snapshot_tests/view/border_radius_border_box_shadow_rescaled.scene.json"), + DEFAULT_RESOLUTION, + ), + inputs: vec![TestInput::new(1)], + ..Default::default() + }, + TestCase { + name: "view/root_border_radius_border_box_shadow", + scene_updates: Updates::Scene( + include_str!("../../snapshot_tests/view/root_border_radius_border_box_shadow.scene.json"), + DEFAULT_RESOLUTION, + ), + inputs: vec![TestInput::new(1)], + ..Default::default() + }, + TestCase { + name: "view/border_radius_border_box_shadow_rescaled_and_hidden_by_parent", + scene_updates: Updates::Scene( + include_str!("../../snapshot_tests/view/border_radius_border_box_shadow_rescaled_and_hidden_by_parent.scene.json"), + DEFAULT_RESOLUTION, + ), + inputs: vec![TestInput::new(1)], + ..Default::default() + }, ]) }