diff --git a/core/shared/src/main/scala/eu/joaocosta/interim/LayoutAllocator.scala b/core/shared/src/main/scala/eu/joaocosta/interim/LayoutAllocator.scala index 62430e5..de3aadf 100644 --- a/core/shared/src/main/scala/eu/joaocosta/interim/LayoutAllocator.scala +++ b/core/shared/src/main/scala/eu/joaocosta/interim/LayoutAllocator.scala @@ -1,24 +1,40 @@ package eu.joaocosta.interim -import eu.joaocosta.interim.{Font, HorizontalAlignment, Rect, TextLayout, VerticalAlignment} +import eu.joaocosta.interim._ -/** A layout allocator is a side-effectful function that, given a size (width or height) tries to allocate - * a new area. +/** A layout allocator is a side-effectful function that tries to allocate some space inside of an area. * * Note that calls to this function are side effectful, as each call reserves an area. */ -trait LayoutAllocator: +sealed trait LayoutAllocator: def area: Rect - def allocate(width: Int, height: Int): Rect - def allocate(text: String, font: Font, paddingW: Int = 0, paddingH: Int = 0): Rect = - val textArea = - TextLayout.computeArea(area.resize(-2 * paddingW, -2 * paddingH), text, font) - allocate(textArea.w + 2 * paddingW, textArea.h + 2 * paddingH) +object LayoutAllocator: + /** Allocator that allows one to allocate space based on a required area. + */ + trait AreaAllocator extends LayoutAllocator: + def allocate(width: Int, height: Int): Rect + def fill(): Rect = allocate(Int.MaxValue, Int.MaxValue) + def allocate(text: String, font: Font, paddingW: Int = 0, paddingH: Int = 0): Rect = + val textArea = + TextLayout.computeArea(area.resize(-2 * paddingW, -2 * paddingH), text, font) + allocate(textArea.w + 2 * paddingW, textArea.h + 2 * paddingH) - def fill(): Rect = allocate(Int.MaxValue, Int.MaxValue) + /** Allocator that allows one to request new cells. + * + * The preallocated cells can also be accessed as an `IndexedSeq` + */ + trait CellAllocator extends LayoutAllocator with IndexedSeq[Rect]: + lazy val cells: IndexedSeq[Rect] + protected lazy val cellsIterator = cells.iterator + + def apply(i: Int): Rect = cells(i) + val length = cells.length + + def nextCell(): Rect = + if (!cellsIterator.hasNext) area.copy(w = 0, h = 0) + else cellsIterator.next() -object LayoutAllocator: trait RowAllocator extends LayoutAllocator: def nextRow(height: Int): Rect def allocate(width: Int, height: Int): Rect = nextRow(height) diff --git a/core/shared/src/main/scala/eu/joaocosta/interim/api/Components.scala b/core/shared/src/main/scala/eu/joaocosta/interim/api/Components.scala index 9ab3afc..a16c889 100644 --- a/core/shared/src/main/scala/eu/joaocosta/interim/api/Components.scala +++ b/core/shared/src/main/scala/eu/joaocosta/interim/api/Components.scala @@ -14,11 +14,11 @@ trait Components: type Component[+T] = (inputState: InputState.Historical, uiContext: UiContext) ?=> T trait ComponentWithValue[T]: - def allocateArea(using allocator: LayoutAllocator): Rect + def allocateArea(using allocator: LayoutAllocator.AreaAllocator): Rect def render(area: Rect, value: Ref[T]): Component[Unit] - def render(value: Ref[T])(using allocator: LayoutAllocator): Component[Unit] = + def render(value: Ref[T])(using allocator: LayoutAllocator.AreaAllocator): Component[Unit] = render(allocateArea, value) def applyRef(area: Rect, value: Ref[T]): Component[T] = @@ -32,23 +32,25 @@ trait Components: case x: T => applyValue(area, x) case x: Ref[T] => applyRef(area, x) - inline def apply(value: T | Ref[T])(using allocator: LayoutAllocator): Component[T] = apply(allocateArea, value) + inline def apply(value: T | Ref[T])(using allocator: LayoutAllocator.AreaAllocator): Component[T] = + apply(allocateArea, value) trait ComponentWithBody[I, F[_]]: - def allocateArea(using allocator: LayoutAllocator): Rect + def allocateArea(using allocator: LayoutAllocator.AreaAllocator): Rect def render[T](area: Rect, body: I => T): Component[F[T]] - def render[T](body: I => T)(using allocator: LayoutAllocator): Component[Unit] = + def render[T](body: I => T)(using allocator: LayoutAllocator.AreaAllocator): Component[Unit] = render(allocateArea, body) def apply[T](area: Rect)(body: I => T): Component[F[T]] = render(area, body) def apply[T](area: Rect)(body: => T)(using ev: I =:= Unit): Component[F[T]] = render(area, _ => body) - def apply[T](body: I => T)(using allocator: LayoutAllocator): Component[F[T]] = render(allocateArea, body) + def apply[T](body: I => T)(using allocator: LayoutAllocator.AreaAllocator): Component[F[T]] = + render(allocateArea, body) - def apply[T](body: => T)(using allocator: LayoutAllocator, ev: I =:= Unit): Component[F[T]] = + def apply[T](body: => T)(using allocator: LayoutAllocator.AreaAllocator, ev: I =:= Unit): Component[F[T]] = render(allocateArea, _ => body) /** Button component. Returns true if it's being clicked, false otherwise. @@ -61,7 +63,7 @@ trait Components: skin: ButtonSkin = ButtonSkin.default() ): ComponentWithBody[Unit, Option] = new ComponentWithBody[Unit, Option]: - def allocateArea(using allocator: LayoutAllocator): Rect = + def allocateArea(using allocator: LayoutAllocator.AreaAllocator): Rect = skin.allocateArea(allocator, label) def render[T](area: Rect, body: Unit => T): Component[Option[T]] = @@ -77,7 +79,7 @@ trait Components: skin: CheckboxSkin = CheckboxSkin.default() ): ComponentWithValue[Boolean] = new ComponentWithValue[Boolean]: - def allocateArea(using allocator: LayoutAllocator): Rect = + def allocateArea(using allocator: LayoutAllocator.AreaAllocator): Rect = skin.allocateArea(allocator) def render(area: Rect, value: Ref[Boolean]): Component[Unit] = @@ -99,7 +101,7 @@ trait Components: skin: ButtonSkin = ButtonSkin.default() ): ComponentWithValue[T] = new ComponentWithValue[T]: - def allocateArea(using allocator: LayoutAllocator): Rect = + def allocateArea(using allocator: LayoutAllocator.AreaAllocator): Rect = skin.allocateArea(allocator, label) def render(area: Rect, value: Ref[T]): Component[Unit] = @@ -121,7 +123,7 @@ trait Components: skin: SelectSkin = SelectSkin.default() ): ComponentWithValue[PanelState[Int]] = new ComponentWithValue[PanelState[Int]]: - def allocateArea(using allocator: LayoutAllocator): Rect = + def allocateArea(using allocator: LayoutAllocator.AreaAllocator): Rect = skin.allocateArea(allocator, labels) def render(area: Rect, value: Ref[PanelState[Int]]): Component[Unit] = @@ -152,7 +154,7 @@ trait Components: skin: SliderSkin = SliderSkin.default() ): ComponentWithValue[Int] = new ComponentWithValue[Int]: - def allocateArea(using allocator: LayoutAllocator): Rect = + def allocateArea(using allocator: LayoutAllocator.AreaAllocator): Rect = skin.allocateArea(allocator) def render(area: Rect, value: Ref[Int]): Component[Unit] = @@ -175,7 +177,7 @@ trait Components: skin: TextInputSkin = TextInputSkin.default() ): ComponentWithValue[String] = new ComponentWithValue[String]: - def allocateArea(using allocator: LayoutAllocator): Rect = + def allocateArea(using allocator: LayoutAllocator.AreaAllocator): Rect = skin.allocateArea(allocator) def render(area: Rect, value: Ref[String]): Component[Unit] = @@ -191,7 +193,7 @@ trait Components: */ final def moveHandle(id: ItemId, skin: HandleSkin = HandleSkin.default()): ComponentWithValue[Rect] = new ComponentWithValue[Rect]: - def allocateArea(using allocator: LayoutAllocator): Rect = + def allocateArea(using allocator: LayoutAllocator.AreaAllocator): Rect = skin.allocateArea(allocator) def render(area: Rect, value: Ref[Rect]): Component[Unit] = @@ -209,7 +211,7 @@ trait Components: */ final def resizeHandle(id: ItemId, skin: HandleSkin = HandleSkin.default()): ComponentWithValue[Rect] = new ComponentWithValue[Rect]: - def allocateArea(using allocator: LayoutAllocator): Rect = + def allocateArea(using allocator: LayoutAllocator.AreaAllocator): Rect = skin.allocateArea(allocator) def render(area: Rect, value: Ref[Rect]): Component[Unit] = @@ -230,7 +232,7 @@ trait Components: skin: HandleSkin = HandleSkin.default() ): ComponentWithValue[PanelState[T]] = new ComponentWithValue[PanelState[T]]: - def allocateArea(using allocator: LayoutAllocator): Rect = + def allocateArea(using allocator: LayoutAllocator.AreaAllocator): Rect = skin.allocateArea(allocator) def render(area: Rect, value: Ref[PanelState[T]]): Component[Unit] = diff --git a/core/shared/src/main/scala/eu/joaocosta/interim/api/Primitives.scala b/core/shared/src/main/scala/eu/joaocosta/interim/api/Primitives.scala index c5c3ad2..edff3fc 100644 --- a/core/shared/src/main/scala/eu/joaocosta/interim/api/Primitives.scala +++ b/core/shared/src/main/scala/eu/joaocosta/interim/api/Primitives.scala @@ -12,16 +12,26 @@ trait Primitives: /** Draws a rectangle filling the specified area with a color. */ - final def rectangle(area: Rect, color: Color)(using uiContext: UiContext): Unit = - uiContext.pushRenderOp(RenderOp.DrawRect(area, color)) + final def rectangle(area: Rect | LayoutAllocator.CellAllocator, color: Color)(using uiContext: UiContext): Unit = + val reservedArea = area match { + case rect: Rect => rect + case alloc: LayoutAllocator.CellAllocator => alloc.nextCell() + } + uiContext.pushRenderOp(RenderOp.DrawRect(reservedArea, color)) /** Draws the outline a rectangle inside the specified area with a color. */ - final def rectangleOutline(area: Rect, color: Color, strokeSize: Int)(using uiContext: UiContext): Unit = - val top = area.copy(h = strokeSize) - val bottom = top.move(dx = 0, dy = area.h - strokeSize) - val left = area.copy(w = strokeSize) - val right = left.move(dx = area.w - strokeSize, dy = 0) + final def rectangleOutline(area: Rect | LayoutAllocator.CellAllocator, color: Color, strokeSize: Int)(using + uiContext: UiContext + ): Unit = + val reservedArea = area match { + case rect: Rect => rect + case alloc: LayoutAllocator.CellAllocator => alloc.nextCell() + } + val top = reservedArea.copy(h = strokeSize) + val bottom = top.move(dx = 0, dy = reservedArea.h - strokeSize) + val left = reservedArea.copy(w = strokeSize) + val right = left.move(dx = reservedArea.w - strokeSize, dy = 0) rectangle(top, color) rectangle(bottom, color) rectangle(left, color) @@ -35,7 +45,7 @@ trait Primitives: * @param verticalAlignment how the text should be aligned vertically */ final def text( - area: Rect | LayoutAllocator, + area: Rect | LayoutAllocator.AreaAllocator, color: Color, message: String, font: Font = Font.default, @@ -46,8 +56,8 @@ trait Primitives: ): Unit = if (message.nonEmpty) val reservedArea = area match { - case rect: Rect => rect - case alloc: LayoutAllocator => alloc.allocate(message, font) + case rect: Rect => rect + case alloc: LayoutAllocator.AreaAllocator => alloc.allocate(message, font) } uiContext.pushRenderOp( RenderOp.DrawText(reservedArea, color, message, font, reservedArea, horizontalAlignment, verticalAlignment) diff --git a/core/shared/src/main/scala/eu/joaocosta/interim/layouts/DynamicColumnAllocator.scala b/core/shared/src/main/scala/eu/joaocosta/interim/layouts/DynamicColumnAllocator.scala index 6bf8cd5..67201e0 100644 --- a/core/shared/src/main/scala/eu/joaocosta/interim/layouts/DynamicColumnAllocator.scala +++ b/core/shared/src/main/scala/eu/joaocosta/interim/layouts/DynamicColumnAllocator.scala @@ -7,6 +7,7 @@ final class DynamicColumnAllocator( padding: Int, alignment: HorizontalAlignment.Left.type | HorizontalAlignment.Right.type ) extends LayoutAllocator.ColumnAllocator + with LayoutAllocator.AreaAllocator with (Int => Rect): private var currentX = area.x private var currentW = area.w diff --git a/core/shared/src/main/scala/eu/joaocosta/interim/layouts/DynamicRowAllocator.scala b/core/shared/src/main/scala/eu/joaocosta/interim/layouts/DynamicRowAllocator.scala index 08cae8f..bf242b3 100644 --- a/core/shared/src/main/scala/eu/joaocosta/interim/layouts/DynamicRowAllocator.scala +++ b/core/shared/src/main/scala/eu/joaocosta/interim/layouts/DynamicRowAllocator.scala @@ -7,6 +7,7 @@ final class DynamicRowAllocator( padding: Int, alignment: VerticalAlignment.Top.type | VerticalAlignment.Bottom.type ) extends LayoutAllocator.RowAllocator + with LayoutAllocator.AreaAllocator with (Int => Rect): private var currentY = area.y private var currentH = area.h diff --git a/core/shared/src/main/scala/eu/joaocosta/interim/layouts/StaticColumnAllocator.scala b/core/shared/src/main/scala/eu/joaocosta/interim/layouts/StaticColumnAllocator.scala index a1ed5f1..4afdeed 100644 --- a/core/shared/src/main/scala/eu/joaocosta/interim/layouts/StaticColumnAllocator.scala +++ b/core/shared/src/main/scala/eu/joaocosta/interim/layouts/StaticColumnAllocator.scala @@ -8,8 +8,9 @@ final class StaticColumnAllocator( numColumns: Int, alignment: HorizontalAlignment.Left.type | HorizontalAlignment.Right.type ) extends LayoutAllocator.ColumnAllocator - with IndexedSeq[Rect]: - val cells: IndexedSeq[Rect] = + with LayoutAllocator.AreaAllocator + with LayoutAllocator.CellAllocator: + lazy val cells: IndexedSeq[Rect] = if (numColumns == 0) Vector.empty else val columnSize = (area.w - (numColumns - 1) * padding) / numColumns.toDouble @@ -21,14 +22,7 @@ final class StaticColumnAllocator( if (alignment == HorizontalAlignment.Left) baseCells else baseCells.reverse - def apply(i: Int): Rect = cells(i) - val length = cells.length - - private val cellsIterator = cells.iterator - - def nextColumn(): Rect = - if (!cellsIterator.hasNext) area.copy(w = 0, h = 0) - else cellsIterator.next() + def nextColumn(): Rect = nextCell() def nextColumn(width: Int): Rect = if (!cellsIterator.hasNext) area.copy(w = 0, h = 0) diff --git a/core/shared/src/main/scala/eu/joaocosta/interim/layouts/StaticRowAllocator.scala b/core/shared/src/main/scala/eu/joaocosta/interim/layouts/StaticRowAllocator.scala index e27a52b..aef6e9c 100644 --- a/core/shared/src/main/scala/eu/joaocosta/interim/layouts/StaticRowAllocator.scala +++ b/core/shared/src/main/scala/eu/joaocosta/interim/layouts/StaticRowAllocator.scala @@ -8,8 +8,9 @@ final class StaticRowAllocator( numRows: Int, alignment: VerticalAlignment.Top.type | VerticalAlignment.Bottom.type ) extends LayoutAllocator.RowAllocator - with IndexedSeq[Rect]: - val cells: IndexedSeq[Rect] = + with LayoutAllocator.AreaAllocator + with LayoutAllocator.CellAllocator: + lazy val cells: IndexedSeq[Rect] = if (numRows == 0) Vector.empty else val rowSize = (area.h - (numRows - 1) * padding) / numRows.toDouble @@ -21,14 +22,7 @@ final class StaticRowAllocator( if (alignment == VerticalAlignment.Top) baseCells else baseCells.reverse - def apply(i: Int): Rect = cells(i) - val length = cells.length - - private val cellsIterator = cells.iterator - - def nextRow(): Rect = - if (!cellsIterator.hasNext) area.copy(w = 0, h = 0) - else cellsIterator.next() + def nextRow(): Rect = nextCell() def nextRow(height: Int): Rect = if (!cellsIterator.hasNext) area.copy(w = 0, h = 0) diff --git a/core/shared/src/main/scala/eu/joaocosta/interim/skins/ButtonSkin.scala b/core/shared/src/main/scala/eu/joaocosta/interim/skins/ButtonSkin.scala index ec15e12..9cff2ee 100644 --- a/core/shared/src/main/scala/eu/joaocosta/interim/skins/ButtonSkin.scala +++ b/core/shared/src/main/scala/eu/joaocosta/interim/skins/ButtonSkin.scala @@ -5,7 +5,7 @@ import eu.joaocosta.interim._ import eu.joaocosta.interim.api.Primitives._ trait ButtonSkin: - def allocateArea(allocator: LayoutAllocator, label: String): Rect + def allocateArea(allocator: LayoutAllocator.AreaAllocator, label: String): Rect def buttonArea(area: Rect): Rect def renderButton(area: Rect, label: String, itemStatus: UiContext.ItemStatus)(using uiContext: UiContext @@ -19,7 +19,7 @@ object ButtonSkin extends DefaultSkin: colorScheme: ColorScheme ) extends ButtonSkin: - def allocateArea(allocator: LayoutAllocator, label: String): Rect = + def allocateArea(allocator: LayoutAllocator.AreaAllocator, label: String): Rect = allocator.allocate(label, font, paddingH = buttonHeight / 2) def buttonArea(area: Rect): Rect = diff --git a/core/shared/src/main/scala/eu/joaocosta/interim/skins/CheckboxSkin.scala b/core/shared/src/main/scala/eu/joaocosta/interim/skins/CheckboxSkin.scala index 84a8fac..89a2305 100644 --- a/core/shared/src/main/scala/eu/joaocosta/interim/skins/CheckboxSkin.scala +++ b/core/shared/src/main/scala/eu/joaocosta/interim/skins/CheckboxSkin.scala @@ -4,7 +4,7 @@ import eu.joaocosta.interim._ import eu.joaocosta.interim.api.Primitives._ trait CheckboxSkin: - def allocateArea(allocator: LayoutAllocator): Rect + def allocateArea(allocator: LayoutAllocator.AreaAllocator): Rect def checkboxArea(area: Rect): Rect def renderCheckbox(area: Rect, value: Boolean, itemStatus: UiContext.ItemStatus)(using uiContext: UiContext): Unit @@ -15,7 +15,7 @@ object CheckboxSkin extends DefaultSkin: colorScheme: ColorScheme ) extends CheckboxSkin: - def allocateArea(allocator: LayoutAllocator): Rect = + def allocateArea(allocator: LayoutAllocator.AreaAllocator): Rect = allocator.allocate(Font.default.fontSize, Font.default.fontSize) def checkboxArea(area: Rect): Rect = diff --git a/core/shared/src/main/scala/eu/joaocosta/interim/skins/HandleSkin.scala b/core/shared/src/main/scala/eu/joaocosta/interim/skins/HandleSkin.scala index ed9dd0f..31c2b7b 100644 --- a/core/shared/src/main/scala/eu/joaocosta/interim/skins/HandleSkin.scala +++ b/core/shared/src/main/scala/eu/joaocosta/interim/skins/HandleSkin.scala @@ -4,7 +4,7 @@ import eu.joaocosta.interim._ import eu.joaocosta.interim.api.Primitives._ trait HandleSkin: - def allocateArea(allocator: LayoutAllocator): Rect + def allocateArea(allocator: LayoutAllocator.AreaAllocator): Rect def moveHandleArea(area: Rect): Rect def closeHandleArea(area: Rect): Rect @@ -18,7 +18,7 @@ object HandleSkin extends DefaultSkin: final case class Default(colorScheme: ColorScheme) extends HandleSkin: - def allocateArea(allocator: LayoutAllocator): Rect = + def allocateArea(allocator: LayoutAllocator.AreaAllocator): Rect = allocator.allocate(Font.default.fontSize, Font.default.fontSize) def moveHandleArea(area: Rect): Rect = diff --git a/core/shared/src/main/scala/eu/joaocosta/interim/skins/SelectSkin.scala b/core/shared/src/main/scala/eu/joaocosta/interim/skins/SelectSkin.scala index 3be2d4c..d151c28 100644 --- a/core/shared/src/main/scala/eu/joaocosta/interim/skins/SelectSkin.scala +++ b/core/shared/src/main/scala/eu/joaocosta/interim/skins/SelectSkin.scala @@ -4,7 +4,7 @@ import eu.joaocosta.interim._ import eu.joaocosta.interim.api.Primitives._ trait SelectSkin: - def allocateArea(allocator: LayoutAllocator, labels: Vector[String]): Rect + def allocateArea(allocator: LayoutAllocator.AreaAllocator, labels: Vector[String]): Rect def selectBoxArea(area: Rect): Rect def renderSelectBox(area: Rect, value: Int, labels: Vector[String], itemStatus: UiContext.ItemStatus)(using @@ -24,7 +24,7 @@ object SelectSkin extends DefaultSkin: colorScheme: ColorScheme ) extends SelectSkin: - def allocateArea(allocator: LayoutAllocator, labels: Vector[String]): Rect = + def allocateArea(allocator: LayoutAllocator.AreaAllocator, labels: Vector[String]): Rect = val largestLabel = labels.maxByOption(_.size).getOrElse("") allocator.allocate(largestLabel, font, paddingW = padding, paddingH = padding) diff --git a/core/shared/src/main/scala/eu/joaocosta/interim/skins/SliderSkin.scala b/core/shared/src/main/scala/eu/joaocosta/interim/skins/SliderSkin.scala index 285b913..d6cef9d 100644 --- a/core/shared/src/main/scala/eu/joaocosta/interim/skins/SliderSkin.scala +++ b/core/shared/src/main/scala/eu/joaocosta/interim/skins/SliderSkin.scala @@ -4,7 +4,7 @@ import eu.joaocosta.interim._ import eu.joaocosta.interim.api.Primitives._ trait SliderSkin: - def allocateArea(allocator: LayoutAllocator): Rect + def allocateArea(allocator: LayoutAllocator.AreaAllocator): Rect def sliderArea(area: Rect): Rect def renderSlider(area: Rect, min: Int, value: Int, max: Int, itemStatus: UiContext.ItemStatus)(using uiContext: UiContext @@ -18,7 +18,7 @@ object SliderSkin extends DefaultSkin: colorScheme: ColorScheme ) extends SliderSkin: - def allocateArea(allocator: LayoutAllocator): Rect = + def allocateArea(allocator: LayoutAllocator.AreaAllocator): Rect = allocator.allocate(Font.default.fontSize + 2 * padding, Font.default.fontSize + 2 * padding) def sliderArea(area: Rect): Rect = area.shrink(padding) diff --git a/core/shared/src/main/scala/eu/joaocosta/interim/skins/TextInputSkin.scala b/core/shared/src/main/scala/eu/joaocosta/interim/skins/TextInputSkin.scala index c8f4d24..9829cb2 100644 --- a/core/shared/src/main/scala/eu/joaocosta/interim/skins/TextInputSkin.scala +++ b/core/shared/src/main/scala/eu/joaocosta/interim/skins/TextInputSkin.scala @@ -4,7 +4,7 @@ import eu.joaocosta.interim._ import eu.joaocosta.interim.api.Primitives._ trait TextInputSkin: - def allocateArea(allocator: LayoutAllocator): Rect + def allocateArea(allocator: LayoutAllocator.AreaAllocator): Rect def textInputArea(area: Rect): Rect def renderTextInput(area: Rect, value: String, itemStatus: UiContext.ItemStatus)(using uiContext: UiContext): Unit @@ -17,7 +17,7 @@ object TextInputSkin extends DefaultSkin: colorScheme: ColorScheme ) extends TextInputSkin: - def allocateArea(allocator: LayoutAllocator): Rect = + def allocateArea(allocator: LayoutAllocator.AreaAllocator): Rect = val maxBorder = math.max(border, activeBorder) + 2 allocator.allocate(2 * maxBorder + 8 * font.fontSize, 2 * maxBorder + font.fontSize)