tirbofish/dropbear · commit
2f038d7e62aeb33bdc2766943bd0729ba3f0ed39
feature: rows and columns + scripting
feature: scripting for rectangles
fix: dpi implemented
Signature present but could not be verified.
Unverified
@@ -1,5 +1,5 @@ -use bytemuck::{Pod, Zeroable}; -use glam::{Mat4, Vec2}; + use bytemuck::{Pod, Zeroable}; +use glam::{Mat4, Vec2, Vec3}; use crate::math::Rect; pub struct Camera2D { @@ -19,15 +19,24 @@ impl Default for Camera2D { impl Camera2D { /// Returns the orthographic view-projection matrix for the current camera state pub fn view_proj(&self, screen_size: Vec2) -> Mat4 { - let width = screen_size.x / self.zoom; - let height = screen_size.y / self.zoom; + let (width, height) = (screen_size.x, screen_size.y); - let left = self.position.x; - let right = self.position.x + width; - let top = self.position.y; - let bottom = self.position.y + height; + let view = Mat4::look_at_rh( + self.position.extend(1.0), + self.position.extend(0.0), + Vec3::Y, + ); - Mat4::orthographic_lh(left, right, bottom, top, -1.0, 1.0) + let proj = Mat4::orthographic_rh( + 0.0, + width, + height, + 0.0, + -1.0, + 1.0, + ); + + proj * view } /// Set the camera's position (top-left corner of view) @@ -0,0 +1,218 @@ +// In widgets/layout.rs + +use std::any::Any; +use glam::{vec2, Vec2}; +use crate::{KinoState, UiNode, UiInstructionType, ContaineredWidgetType, WidgetId}; +use crate::widgets::{Anchor, ContaineredWidget}; + +fn calculate_node_size(node: &UiNode) -> Vec2 { + match &node.instruction { + UiInstructionType::Widget(widget) => widget.size(), + UiInstructionType::Containered(ContaineredWidgetType::Start { widget, .. }) => { + if let Some(row) = widget.as_any().downcast_ref::<Row>() { + calculate_row_size(&node.children, row.spacing) + } else if let Some(column) = widget.as_any().downcast_ref::<Column>() { + calculate_column_size(&node.children, column.spacing) + } else { + Vec2::ZERO + } + } + _ => Vec2::ZERO, + } +} + +fn calculate_row_size(children: &[UiNode], spacing: f32) -> Vec2 { + if children.is_empty() { + return Vec2::ZERO; + } + + let mut total_width = 0.0; + let mut max_height: f32 = 0.0; + + for (i, child) in children.iter().enumerate() { + let child_size = calculate_node_size(child); + total_width += child_size.x; + max_height = max_height.max(child_size.y); + + if i < children.len() - 1 { + total_width += spacing; + } + } + + vec2(total_width, max_height) +} + +fn calculate_column_size(children: &[UiNode], spacing: f32) -> Vec2 { + if children.is_empty() { + return Vec2::ZERO; + } + + let mut total_height = 0.0; + let mut max_width: f32 = 0.0; + + for (i, child) in children.iter().enumerate() { + let child_size = calculate_node_size(child); + total_height += child_size.y; + max_width = max_width.max(child_size.x); + + if i < children.len() - 1 { + total_height += spacing; + } + } + + vec2(max_width, total_height) +} + +pub struct Row { + pub id: WidgetId, + pub anchor: Anchor, + pub position: Vec2, + pub spacing: f32, +} + +impl Row { + pub fn new(id: impl Into<WidgetId>) -> Self { + Self { + id: id.into(), + anchor: Anchor::TopLeft, + position: Vec2::ZERO, + spacing: 8.0, + } + } + + pub fn at(mut self, position: impl Into<Vec2>) -> Self { + self.position = position.into(); + self + } + + pub fn spacing(mut self, spacing: f32) -> Self { + self.spacing = spacing; + self + } + + pub fn anchor(mut self, anchor: Anchor) -> Self { + self.anchor = anchor; + self + } + + pub fn build(self) -> Box<Self> { + Box::new(self) + } +} + +impl ContaineredWidget for Row { + fn render(self: Box<Self>, children: Vec<UiNode>, state: &mut KinoState) { + let total_size = calculate_row_size(&children, self.spacing); + + let offset = match self.anchor { + Anchor::TopLeft => Vec2::ZERO, + Anchor::Center => vec2(-total_size.x / 2.0, 0.0), + }; + + let start_pos = self.position + offset; + let mut x_offset = 0.0; + + for child in children { + let child_size = calculate_node_size(&child); + + state.push_container(crate::math::Rect::new( + start_pos + vec2(x_offset, 0.0), + vec2(child_size.x, total_size.y) + )); + + state.render_tree(vec![child]); + state.pop_container(); + + x_offset += child_size.x + self.spacing; + } + } + + fn size(&self, children: &[UiNode]) -> Vec2 { + calculate_row_size(children, self.spacing) + } + + fn id(&self) -> WidgetId { + self.id + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +pub struct Column { + pub id: WidgetId, + pub anchor: Anchor, + pub position: Vec2, + pub spacing: f32, +} + +impl Column { + pub fn new(id: impl Into<WidgetId>) -> Self { + Self { + id: id.into(), + anchor: Anchor::TopLeft, + position: Vec2::ZERO, + spacing: 8.0, + } + } + + pub fn at(mut self, position: impl Into<Vec2>) -> Self { + self.position = position.into(); + self + } + + pub fn spacing(mut self, spacing: f32) -> Self { + self.spacing = spacing; + self + } + + pub fn anchor(mut self, anchor: Anchor) -> Self { + self.anchor = anchor; + self + } + + pub fn build(self) -> Box<Self> { + Box::new(self) + } +} + +impl ContaineredWidget for Column { + fn render(self: Box<Self>, children: Vec<UiNode>, state: &mut KinoState) { + let total_size = calculate_column_size(&children, self.spacing); + + let offset = match self.anchor { + Anchor::TopLeft => Vec2::ZERO, + Anchor::Center => vec2(0.0, -total_size.y / 2.0), + }; + + let start_pos = self.position + offset; + let mut y_offset = 0.0; + + for child in children { + let child_size = calculate_node_size(&child); + + state.push_container(crate::math::Rect::new( + start_pos + vec2(0.0, y_offset), + vec2(total_size.x, child_size.y) + )); + + state.render_tree(vec![child]); + state.pop_container(); + + y_offset += child_size.y + self.spacing; + } + } + + fn size(&self, children: &[UiNode]) -> Vec2 { + calculate_column_size(children, self.spacing) + } + + fn id(&self) -> WidgetId { + self.id + } + + fn as_any(&self) -> &dyn Any { + self + } +} @@ -1,8 +1,10 @@ pub mod rect; pub mod shorthand; pub mod text; +pub mod layout; use std::any::Any; +use glam::Vec2; use crate::{KinoState, UiNode, WidgetId}; /// Determines how the object is anchored. @@ -21,12 +23,14 @@ pub trait NativeWidget: Send + Sync { /// The state is provided for you to manipulate, such as adding a new response based on the /// [`WidgetId`]. fn render(self: Box<Self>, state: &mut KinoState); + fn size(&self) -> Vec2; fn id(&self) -> WidgetId; fn as_any(&self) -> &dyn Any; } pub trait ContaineredWidget: Send + Sync { fn render(self: Box<Self>, children: Vec<UiNode>, state: &mut KinoState); + fn size(&self, children: &[UiNode]) -> Vec2; fn id(&self) -> WidgetId; fn as_any(&self) -> &dyn Any; } @@ -65,7 +65,7 @@ impl Rectangle { id: id.into(), anchor: Anchor::TopLeft, position: Vec2::ZERO, - size: vec2(64.0, 64.0), + size: vec2(64.0, 128.0), rotation: 0.0, uvs: [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]], fill: Fill::default(), @@ -220,6 +220,10 @@ impl NativeWidget for Rectangle { self.render_body(state, &rect, rot); } + fn size(&self) -> Vec2 { + self.size + } + fn id(&self) -> WidgetId { self.id } @@ -238,6 +242,10 @@ impl ContaineredWidget for Rectangle { state.pop_container(); } + fn size(&self, _children: &[UiNode]) -> Vec2 { + self.size + } + fn id(&self) -> WidgetId { self.id } @@ -1,4 +1,5 @@ use crate::{KinoState, WidgetId}; +use crate::widgets::layout::{Column, Row}; use crate::widgets::rect::Rectangle; use crate::widgets::text::Text; @@ -42,4 +43,38 @@ where let mut text = Text::new(text); configure(&mut text); kino.add_widget(Box::new(text)) +} + +/// Shorthand for [`Row`], used for displaying items that are displayed horizontally. +pub fn row<C>( + kino: &mut KinoState, + row: Row, + contents: C, +) -> WidgetId +where + C: FnOnce(&mut KinoState), +{ + let id = row.id; + + kino.add_container(Box::new(row)); + contents(kino); + kino.end_container(id); + id +} + +/// Shorthand for [`Column`], used for displaying items that are displayed vertically. +pub fn column<C>( + kino: &mut KinoState, + column: Column, + contents: C, +) -> WidgetId +where + C: FnOnce(&mut KinoState), +{ + let id = column.id; + + kino.add_container(Box::new(column)); + contents(kino); + kino.end_container(id); + id } @@ -126,6 +126,10 @@ impl NativeWidget for Text { }); } + fn size(&self) -> Vec2 { + self.size + } + fn id(&self) -> WidgetId { self.id } @@ -2,6 +2,7 @@ use std::sync::Arc; use glam::Vec2; use winit::event::{ElementState, MouseButton, WindowEvent}; use winit::window::Window; +use crate::KinoState; pub struct KinoWinitWindowing { // mouse @@ -9,29 +10,46 @@ pub struct KinoWinitWindowing { pub mouse_button: MouseButton, pub mouse_press_state: ElementState, - // keyboard - // windowing - _window: Arc<Window>, + window: Arc<Window>, /// The top-left most pixel pub viewport_offset: Vec2, /// Scale from screen-space to viewport texture space pub viewport_scale: Vec2, + + pub scale_factor: f32, + pub auto_scale: bool, } impl KinoWinitWindowing { /// Creates a new instance of [KinoWinitWindowing] with a specified viewport texture offset. - pub fn new(window: Arc<Window>) -> Self { + pub fn new(window: Arc<Window>, scale_factor: Option<f32>) -> Self { + let auto_scale = scale_factor.is_none(); + let scale_factor = scale_factor.unwrap_or(window.scale_factor() as f32); Self { mouse_position: Default::default(), mouse_button: MouseButton::Left, mouse_press_state: ElementState::Released, - _window: window, + window, viewport_offset: Default::default(), viewport_scale: Vec2::ONE, + scale_factor, + auto_scale, } } + /// Get the physical size of the window in pixels + pub fn physical_size(&self) -> (u32, u32) { + let size = self.window.inner_size(); + (size.width, size.height) + } + + /// Get the logical size of the window (physical size / scale_factor) + pub fn logical_size(&self) -> Vec2 { + let (w, h) = self.physical_size(); + Vec2::new(w as f32, h as f32) / self.scale_factor + } + pub(crate) fn handle_event(&mut self, event: &WindowEvent) { match event { WindowEvent::CursorMoved { position, .. } => { @@ -43,11 +61,50 @@ impl KinoWinitWindowing { self.mouse_button = *button; self.mouse_press_state = *state; } + WindowEvent::ScaleFactorChanged { scale_factor, .. } => { + if self.auto_scale { return; } + self.scale_factor = *scale_factor as f32; + } WindowEvent::Resized(_) => {} WindowEvent::KeyboardInput { .. } => {} WindowEvent::MouseWheel { .. } => {} - WindowEvent::ScaleFactorChanged { .. } => {} _ => {} } } +} + +impl KinoState { + /// Get the current DPI scale factor + pub fn scale_factor(&self) -> f32 { + self.windowing.scale_factor + } + + /// Scale a logical size to physical pixels + pub fn to_physical(&self, logical: f32) -> f32 { + logical * self.windowing.scale_factor + } + + /// Scale a logical Vec2 to physical pixels + pub fn to_physical_vec(&self, logical: Vec2) -> Vec2 { + logical * self.windowing.scale_factor + } + + /// Scale physical pixels to logical size + pub fn to_logical(&self, physical: f32) -> f32 { + physical / self.windowing.scale_factor + } + + /// Scale physical Vec2 to logical size + pub fn to_logical_vec(&self, physical: Vec2) -> Vec2 { + physical / self.windowing.scale_factor + } + + pub fn set_scale_factor(&mut self, factor: Option<f32>) { + if let Some(factor) = factor { + self.windowing.scale_factor = factor; + self.windowing.auto_scale = false; + } else { + self.windowing.auto_scale = true; + } + } } @@ -1,9 +1,9 @@ package com.dropbear -import com.dropbear.asset.AssetHandle +import com.dropbear.asset.AssetType +import com.dropbear.asset.Handle import com.dropbear.ffi.NativeEngine import com.dropbear.input.InputState -import com.dropbear.logging.Logger import com.dropbear.scene.SceneManager import com.dropbear.ui.UIInstruction import com.dropbear.ui.UIInstructionSet @@ -32,17 +32,6 @@ class DropbearEngine(val native: NativeEngine) { fun getLastErrMsg(): String? { return lastErrorMessage } - - /** - * Globally sets whether exceptions should be thrown when an error occurs. - * - * This can be run in your update loop without consequences. - */ - @Deprecated("Currently not supported anymore, automatically throws exception on error. " + - "Better to catch the exception instead", level = DeprecationLevel.HIDDEN) - fun callExceptionOnError(toggle: Boolean) { - exceptionOnError = toggle - } } /** @@ -61,19 +50,10 @@ class DropbearEngine(val native: NativeEngine) { * ## Warning * The eucalyptus asset URI (or `euca://`) is case-sensitive. */ - fun getAsset(eucaURI: String): AssetHandle? { + fun <T : AssetType> getAsset(eucaURI: String): Handle<T>? { val id = com.dropbear.getAsset(eucaURI) - return if (id != null) AssetHandle(id) else null - } - - /** - * Globally sets whether exceptions should be thrown when an error occurs. - * - * This can be run in your update loop without consequences. - */ - @Deprecated("Currently not supported anymore, automatically throws exception on error. " + - "Better to catch the exception instead", level = DeprecationLevel.HIDDEN) - fun callExceptionOnError(toggle: Boolean) { + if (id == null || id <= 0L) return null + return Handle(id) } /** @@ -41,6 +41,8 @@ class AnimationComponent(parentEntity: EntityId) : Component(parentEntity, "Anim fun reset() { time = 0f } + + fun setAnimation(index: Int, speed: Float = 1f) = setActiveAnimationIndex(index).let { setSpeed(speed) } } expect fun AnimationComponent.getActiveAnimationIndex(): Int? @@ -1,4 +1,3 @@ package com.dropbear.asset -interface AssetType { -} +open class AssetType internal constructor(open val id: Long) @@ -6,7 +6,7 @@ import com.dropbear.asset.model.Skin import com.dropbear.asset.model.Animation import com.dropbear.asset.model.Node -class Model(val id: Long): AssetType { +class Model(override val id: Long): AssetType(id) { val label: String get() = getLabel() @@ -1,6 +1,6 @@ package com.dropbear.asset -class Texture(val id: Long): AssetType { +class Texture(override val id: Long): AssetType(id) { var label: String? get() = getLabel() set(value) = setLabel(value) @@ -14,12 +14,34 @@ data class AnimationChannel( val times: DoubleArray, val values: ChannelValues, val interpolation: AnimationInterpolation -) +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as AnimationChannel + + if (targetNode != other.targetNode) return false + if (!times.contentEquals(other.times)) return false + if (values != other.values) return false + if (interpolation != other.interpolation) return false + + return true + } + + override fun hashCode(): Int { + var result = targetNode + result = 31 * result + times.contentHashCode() + result = 31 * result + values.hashCode() + result = 31 * result + interpolation.hashCode() + return result + } +} enum class AnimationInterpolation { LINEAR, STEP, - CUBICSPLINE + CUBIC_SPLINE } sealed class ChannelValues { @@ -0,0 +1,14 @@ +package com.dropbear.ui.styling + +enum class Anchor { + /** + * A center anchor is when the position is based on the center of the object (such as the + * center of a circle) + */ + Center, + + /** + * A top left anchor is when the position is based on the top left corner of the object. + */ + TopLeft, +} @@ -0,0 +1,5 @@ +package com.dropbear.ui.styling + +import com.dropbear.utils.Colour + +data class Fill(var colour: Colour) @@ -0,0 +1,70 @@ +package com.dropbear.ui.widgets + +import com.dropbear.math.Vector2d +import com.dropbear.ui.UIBuilder +import com.dropbear.ui.UIInstruction +import com.dropbear.ui.Widget +import com.dropbear.ui.WidgetId +import com.dropbear.ui.styling.Anchor + +class Column( + override var id: WidgetId, + var anchor: Anchor = Anchor.TopLeft, + var position: Vector2d = Vector2d.zero(), + var spacing: Double = 8.0, +): Widget() { + sealed class ColumnInstruction: UIInstruction { + data class StartColumnBlock(val id: WidgetId, val column: com.dropbear.ui.widgets.Column) : ColumnInstruction() + data class EndColumnBlock(val id: WidgetId) : ColumnInstruction() + } + + override fun toInstruction(): List<UIInstruction> { + return listOf(ColumnInstruction.StartColumnBlock(id, this), ColumnInstruction.EndColumnBlock(id)) + } + + fun toContaineredInstructions(children: List<UIInstruction>): List<UIInstruction> { + return listOf(ColumnInstruction.StartColumnBlock(id, this)) + + children + + ColumnInstruction.EndColumnBlock(id) + } + + fun startInstruction(): UIInstruction = ColumnInstruction.StartColumnBlock(id, this) + + fun endInstruction(): UIInstruction = ColumnInstruction.EndColumnBlock(id) + + fun at(position: Vector2d): Column { + this.position = position + return this + } + + fun spacing(spacing: Double): Column { + this.spacing = spacing + return this + } + + fun withAnchor(anchor: Anchor): Column { + this.anchor = anchor + return this + } +} + +fun UIBuilder.column(id: WidgetId = WidgetId(generateId().toLong()), block: Column.() -> Unit = {}): Column { + val column = Column(id = id).apply(block) + column.toInstruction().forEach { instructions.add(it) } + return column +} + +fun UIBuilder.column(id: WidgetId, content: UIBuilder.(Column) -> Unit) { + column(id, columnBlock = {}, content = content) +} + +fun UIBuilder.column( + id: WidgetId, + columnBlock: Column.() -> Unit = {}, + content: UIBuilder.(Column) -> Unit +) { + val column = Column(id = id).apply(columnBlock) + instructions.add(column.startInstruction()) + this.content(column) + instructions.add(column.endInstruction()) +} @@ -0,0 +1,118 @@ +package com.dropbear.ui.widgets + +import com.dropbear.asset.Handle +import com.dropbear.asset.Texture +import com.dropbear.math.Angle +import com.dropbear.math.Vector2d +import com.dropbear.ui.UIBuilder +import com.dropbear.ui.UIInstruction +import com.dropbear.ui.Widget +import com.dropbear.ui.WidgetId +import com.dropbear.ui.styling.Anchor +import com.dropbear.ui.styling.Border +import com.dropbear.ui.styling.Fill +import com.dropbear.utils.Colour + +class Rectangle( + override var id: WidgetId, + var anchor: Anchor = Anchor.TopLeft, + var position: Vector2d = Vector2d.zero(), + var size: Vector2d = Vector2d(64.0, 128.0), + var texture: Handle<Texture>? = null, + var rotation: Angle = Angle.ZERO, + var uv: List<Vector2d> = listOf(Vector2d.zero(), Vector2d(1.0, 0.0), Vector2d.one(), Vector2d(0.0, 1.0)), + var fill: Fill = Fill(Colour.WHITE), + var border: Border? = null +): Widget() { + init { + require(uv.size == 4) { "Rectangle uv must have 4 coordinates." } + } + + sealed class RectangleInstruction: UIInstruction { + data class Rectangle(val id: WidgetId, val rect: com.dropbear.ui.widgets.Rectangle) : RectangleInstruction() + data class StartRectangleBlock(val id: WidgetId, val rect: com.dropbear.ui.widgets.Rectangle) : RectangleInstruction() + data class EndRectangleBlock(val id: WidgetId) : RectangleInstruction() + } + + override fun toInstruction(): List<UIInstruction> { + return listOf(RectangleInstruction.Rectangle(id, this)) + } + + fun toContaineredInstructions(children: List<UIInstruction>): List<UIInstruction> { + return listOf(RectangleInstruction.StartRectangleBlock(id, this)) + + children + + RectangleInstruction.EndRectangleBlock(id) + } + + fun startInstruction(): UIInstruction = RectangleInstruction.StartRectangleBlock(id, this) + + fun endInstruction(): UIInstruction = RectangleInstruction.EndRectangleBlock(id) + + fun with(position: Vector2d, size: Vector2d): Rectangle { + this.position = position + this.size = size + return this + } + + fun withAnchor(anchor: Anchor): Rectangle { + this.anchor = anchor + return this + } + + fun at(position: Vector2d): Rectangle { + this.position = position + return this + } + + fun size(size: Vector2d): Rectangle { + this.size = size + return this + } + + fun fill(fill: Fill): Rectangle { + this.fill = fill + return this + } + + fun border(border: Border?): Rectangle { + this.border = border + return this + } + + fun rotate(angle: Angle): Rectangle { + this.rotation = angle + return this + } + + fun texture(texture: Handle<Texture>?): Rectangle { + this.texture = texture + return this + } + + fun uv(coords: List<Vector2d>): Rectangle { + require(coords.size == 4) { "Rectangle uv must have 4 coordinates." } + this.uv = coords + return this + } +} + +fun UIBuilder.rectangle(id: WidgetId = WidgetId(generateId().toLong()), block: Rectangle.() -> Unit = {}): Rectangle { + val rect = Rectangle(id = id).apply(block) + rect.toInstruction().forEach { instructions.add(it) } + return rect +} + +fun UIBuilder.container(id: WidgetId, block: UIBuilder.(Rectangle) -> Unit) { + container(id, rectBlock = {}, content = block) +} + +fun UIBuilder.container( + id: WidgetId, + rectBlock: Rectangle.() -> Unit = {}, + content: UIBuilder.(Rectangle) -> Unit +) { + val rect = Rectangle(id = id).apply(rectBlock) + instructions.add(rect.startInstruction()) + this.content(rect) + instructions.add(rect.endInstruction()) +} @@ -0,0 +1,70 @@ +package com.dropbear.ui.widgets + +import com.dropbear.math.Vector2d +import com.dropbear.ui.UIBuilder +import com.dropbear.ui.UIInstruction +import com.dropbear.ui.Widget +import com.dropbear.ui.WidgetId +import com.dropbear.ui.styling.Anchor + +class Row( + override var id: WidgetId, + var anchor: Anchor = Anchor.TopLeft, + var position: Vector2d = Vector2d.zero(), + var spacing: Double = 8.0, +): Widget() { + sealed class RowInstruction: UIInstruction { + data class StartRowBlock(val id: WidgetId, val row: com.dropbear.ui.widgets.Row) : RowInstruction() + data class EndRowBlock(val id: WidgetId) : RowInstruction() + } + + override fun toInstruction(): List<UIInstruction> { + return listOf(RowInstruction.StartRowBlock(id, this), RowInstruction.EndRowBlock(id)) + } + + fun toContaineredInstructions(children: List<UIInstruction>): List<UIInstruction> { + return listOf(RowInstruction.StartRowBlock(id, this)) + + children + + RowInstruction.EndRowBlock(id) + } + + fun startInstruction(): UIInstruction = RowInstruction.StartRowBlock(id, this) + + fun endInstruction(): UIInstruction = RowInstruction.EndRowBlock(id) + + fun at(position: Vector2d): Row { + this.position = position + return this + } + + fun spacing(spacing: Double): Row { + this.spacing = spacing + return this + } + + fun withAnchor(anchor: Anchor): Row { + this.anchor = anchor + return this + } +} + +fun UIBuilder.row(id: WidgetId = WidgetId(generateId().toLong()), block: Row.() -> Unit = {}): Row { + val row = Row(id = id).apply(block) + row.toInstruction().forEach { instructions.add(it) } + return row +} + +fun UIBuilder.row(id: WidgetId, content: UIBuilder.(Row) -> Unit) { + row(id, rowBlock = {}, content = content) +} + +fun UIBuilder.row( + id: WidgetId, + rowBlock: Row.() -> Unit = {}, + content: UIBuilder.(Row) -> Unit +) { + val row = Row(id = id).apply(rowBlock) + instructions.add(row.startInstruction()) + this.content(row) + instructions.add(row.endInstruction()) +}