Skip to content

Commit

Permalink
Support allocators on components
Browse files Browse the repository at this point in the history
  • Loading branch information
JD557 committed May 1, 2024
1 parent 7696142 commit 2e82cfb
Show file tree
Hide file tree
Showing 13 changed files with 133 additions and 105 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,17 @@ object TextLayout:
else
val (nextFullLine, remainingLines) = str.span(_ != '\n')
// If the line fits, simply return the line
if (textSize(nextFullLine) < lineSize) (nextFullLine, remainingLines.drop(1))
if (textSize(nextFullLine) <= lineSize) (nextFullLine, remainingLines.drop(1))
else
val words = nextFullLine.split(" ")
val firstWord = words.headOption.getOrElse("")
// If the first word is too big, it needs to be broken
if (textSize(firstWord) > lineSize)
val (firstPart, secondPart) = cumulativeSum(firstWord)(charWidth).span(_._2 < lineSize)
val (firstPart, secondPart) = cumulativeSum(firstWord)(charWidth).span(_._2 <= lineSize)
(firstPart.map(_._1).mkString(""), secondPart.map(_._1).mkString("") ++ remainingLines)
else // Otherwise, pick as many words as fit
val (selectedWords, remainingWords) = cumulativeSum(words)(charWidth(' ') + textSize(_)).span(_._2 < lineSize)
val (selectedWords, remainingWords) =
cumulativeSum(words)(charWidth(' ') + textSize(_)).span(_._2 <= lineSize)
(selectedWords.map(_._1).mkString(" "), remainingWords.map(_._1).mkString(" ") ++ remainingLines)

private def alignH(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,25 +40,37 @@ trait Components:
*/
final def button(
id: ItemId,
area: Rect,
area: Rect | LayoutAllocator,
label: String,
skin: ButtonSkin = ButtonSkin.default()
): ComponentWithBody[Unit, Option] =
new ComponentWithBody[Unit, Option]:
def render[T](body: Unit => T): Component[Option[T]] =
val buttonArea = skin.buttonArea(area)
val reservedArea = area match {
case rect: Rect => rect
case alloc: LayoutAllocator => skin.allocateArea(alloc, label)
}
val buttonArea = skin.buttonArea(reservedArea)
val itemStatus = UiContext.registerItem(id, buttonArea)
skin.renderButton(area, label, itemStatus)
skin.renderButton(reservedArea, label, itemStatus)
Option.when(itemStatus.clicked)(body(()))

/** Checkbox component. Returns true if it's enabled, false otherwise.
*/
final def checkbox(id: ItemId, area: Rect, skin: CheckboxSkin = CheckboxSkin.default()): ComponentWithValue[Boolean] =
final def checkbox(
id: ItemId,
area: Rect | LayoutAllocator,
skin: CheckboxSkin = CheckboxSkin.default()
): ComponentWithValue[Boolean] =
new ComponentWithValue[Boolean]:
def render(value: Ref[Boolean]): Component[Unit] =
val checkboxArea = skin.checkboxArea(area)
val reservedArea = area match {
case rect: Rect => rect
case alloc: LayoutAllocator => skin.allocateArea(alloc)
}
val checkboxArea = skin.checkboxArea(reservedArea)
val itemStatus = UiContext.registerItem(id, checkboxArea)
skin.renderCheckbox(area, value.get, itemStatus)
skin.renderCheckbox(reservedArea, value.get, itemStatus)
value.modifyIf(itemStatus.clicked)(!_)

/** Radio button component. Returns value currently selected.
Expand All @@ -68,18 +80,22 @@ trait Components:
*/
final def radioButton[T](
id: ItemId,
area: Rect,
area: Rect | LayoutAllocator,
buttonValue: T,
label: String,
skin: ButtonSkin = ButtonSkin.default()
): ComponentWithValue[T] =
new ComponentWithValue[T]:
def render(value: Ref[T]): Component[Unit] =
val buttonArea = skin.buttonArea(area)
val reservedArea = area match {
case rect: Rect => rect
case alloc: LayoutAllocator => skin.allocateArea(alloc, label)
}
val buttonArea = skin.buttonArea(reservedArea)
val itemStatus = UiContext.registerItem(id, buttonArea)
if (itemStatus.clicked) value := buttonValue
if (value.get == buttonValue) skin.renderButton(area, label, itemStatus.copy(hot = true, active = true))
else skin.renderButton(area, label, itemStatus)
if (value.get == buttonValue) skin.renderButton(reservedArea, label, itemStatus.copy(hot = true, active = true))
else skin.renderButton(reservedArea, label, itemStatus)

/** Select box component. Returns the index value currently selected inside a PanelState.
*
Expand All @@ -88,26 +104,30 @@ trait Components:
*/
final def select(
id: ItemId,
area: Rect,
area: Rect | LayoutAllocator,
labels: Vector[String],
undefinedFirstValue: Boolean = false,
skin: SelectSkin = SelectSkin.default()
): ComponentWithValue[PanelState[Int]] =
new ComponentWithValue[PanelState[Int]]:
def render(value: Ref[PanelState[Int]]): Component[Unit] =
val selectBoxArea = skin.selectBoxArea(area)
val itemStatus = UiContext.registerItem(id, area)
val reservedArea = area match {
case rect: Rect => rect
case alloc: LayoutAllocator => skin.allocateArea(alloc, labels)
}
val selectBoxArea = skin.selectBoxArea(reservedArea)
val itemStatus = UiContext.registerItem(id, reservedArea)
value.modifyIf(itemStatus.selected)(_.open)
skin.renderSelectBox(area, value.get.value, labels, itemStatus)
skin.renderSelectBox(reservedArea, value.get.value, labels, itemStatus)
if (value.get.isOpen)
value.modifyIf(!itemStatus.selected)(_.close)
val selectableLabels = labels.drop(if (undefinedFirstValue) 1 else 0)
Primitives.onTop:
selectableLabels.zipWithIndex
.foreach: (label, idx) =>
val selectOptionArea = skin.selectOptionArea(area, idx)
val selectOptionArea = skin.selectOptionArea(reservedArea, idx)
val optionStatus = UiContext.registerItem(id |> idx, selectOptionArea)
skin.renderSelectOption(area, idx, selectableLabels, optionStatus)
skin.renderSelectOption(reservedArea, idx, selectableLabels, optionStatus)
if (optionStatus.active) value := PanelState.closed(if (undefinedFirstValue) idx + 1 else idx)

/** Slider component. Returns the current position of the slider, between min and max.
Expand All @@ -117,37 +137,45 @@ trait Components:
*/
final def slider(
id: ItemId,
area: Rect,
area: Rect | LayoutAllocator,
min: Int,
max: Int,
skin: SliderSkin = SliderSkin.default()
): ComponentWithValue[Int] =
new ComponentWithValue[Int]:
def render(value: Ref[Int]): Component[Unit] =
val sliderArea = skin.sliderArea(area)
val reservedArea = area match {
case rect: Rect => rect
case alloc: LayoutAllocator => skin.allocateArea(alloc)
}
val sliderArea = skin.sliderArea(reservedArea)
val steps = max - min + 1
val itemStatus = UiContext.registerItem(id, sliderArea)
val clampedValue = math.max(min, math.min(value.get, max))
skin.renderSlider(area, min, clampedValue, max, itemStatus)
skin.renderSlider(reservedArea, min, clampedValue, max, itemStatus)
if (itemStatus.active)
summon[InputState].mouseInput.position.foreach: (mouseX, mouseY) =>
val intPosition =
if (area.w > area.h) steps * (mouseX - sliderArea.x) / sliderArea.w
if (reservedArea.w > reservedArea.h) steps * (mouseX - sliderArea.x) / sliderArea.w
else steps * (mouseY - sliderArea.y) / sliderArea.h
value := math.max(min, math.min(min + intPosition, max))

/** Text input component. Returns the current string inputed.
*/
final def textInput(
id: ItemId,
area: Rect,
area: Rect | LayoutAllocator,
skin: TextInputSkin = TextInputSkin.default()
): ComponentWithValue[String] =
new ComponentWithValue[String]:
def render(value: Ref[String]): Component[Unit] =
val textInputArea = skin.textInputArea(area)
val reservedArea = area match {
case rect: Rect => rect
case alloc: LayoutAllocator => skin.allocateArea(alloc)
}
val textInputArea = skin.textInputArea(reservedArea)
val itemStatus = UiContext.registerItem(id, textInputArea)
skin.renderTextInput(area, value.get, itemStatus)
skin.renderTextInput(reservedArea, value.get, itemStatus)
value.modifyIf(itemStatus.selected)(summon[InputState].appendKeyboardInput)

/** Draggable handle. Returns the moved area.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ trait LayoutAllocator:
def area: Rect

def allocate(width: Int, height: Int): Rect
def allocate(text: String, font: Font): Rect =
val textArea = TextLayout.computeArea(area, text, font, (font.fontSize * 1.3).toInt)
allocate(textArea.w, textArea.h)
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, (font.fontSize * 1.3).toInt)
allocate(textArea.w + 2 * paddingW, textArea.h + 2 * paddingH)

def fill(): Rect = allocate(Int.MaxValue, Int.MaxValue)

Expand Down Expand Up @@ -68,6 +69,10 @@ object LayoutAllocator:

private val cellsIterator = cells.iterator

def nextRow(): Rect =
if (!cellsIterator.hasNext) area.copy(w = 0, h = 0)
else cellsIterator.next()

def nextRow(height: Int): Rect =
if (!cellsIterator.hasNext) area.copy(w = 0, h = 0)
else
Expand Down Expand Up @@ -120,6 +125,10 @@ object LayoutAllocator:

private val cellsIterator = cells.iterator

def nextColumn(): Rect =
if (!cellsIterator.hasNext) area.copy(w = 0, h = 0)
else cellsIterator.next()

def nextColumn(width: Int): Rect =
if (!cellsIterator.hasNext) area.copy(w = 0, h = 0)
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,6 @@ trait Primitives:
final def rectangle(area: Rect, color: Color)(using uiContext: UiContext): Unit =
uiContext.pushRenderOp(RenderOp.DrawRect(area, color))

/** Draws a rectangle filling the specified area with a color.
*/
final def rectangle(color: Color)(using uiContext: UiContext, allocator: LayoutAllocator): Unit =
rectangle(allocator.fill(), 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 =
Expand All @@ -33,14 +28,6 @@ trait Primitives:
rectangle(left, color)
rectangle(right, color)

/** Draws the outline a rectangle inside the specified area with a color.
*/
final def rectangleOutline(color: Color, strokeSize: Int)(using
uiContext: UiContext,
allocator: LayoutAllocator
): Unit =
rectangleOutline(allocator.fill(), color, strokeSize)

/** Draws a block of text in the specified area with a color.
*
* @param text text to write
Expand All @@ -49,7 +36,7 @@ trait Primitives:
* @param verticalAlignment how the text should be aligned vertically
*/
final def text(
area: Rect,
area: Rect | LayoutAllocator,
color: Color,
message: String,
font: Font = Font.default,
Expand All @@ -59,29 +46,14 @@ trait Primitives:
uiContext: UiContext
): Unit =
if (message.nonEmpty)
val reservedArea = area match {
case rect: Rect => rect
case alloc: LayoutAllocator => alloc.allocate(message, font)
}
uiContext.pushRenderOp(
RenderOp.DrawText(area, color, message, font, area, horizontalAlignment, verticalAlignment)
RenderOp.DrawText(reservedArea, color, message, font, reservedArea, horizontalAlignment, verticalAlignment)
)

/** Draws a block of text in the specified area with a color.
*
* @param text text to write
* @param font font definition
* @param horizontalAlignment how the text should be aligned horizontally
* @param verticalAlignment how the text should be aligned vertically
*/
final def text(
color: Color,
message: String,
font: Font,
horizontalAlignment: HorizontalAlignment,
verticalAlignment: VerticalAlignment
)(using
uiContext: UiContext,
allocator: LayoutAllocator
): Unit =
text(allocator.allocate(message, font), color, message, font, horizontalAlignment, verticalAlignment)

/** Advanced operation to add a custom primitive to the list of render operations.
*
* Supports an arbitrary data value. It's up to the backend to interpret it as it sees fit.
Expand All @@ -92,16 +64,6 @@ trait Primitives:
final def custom[T](area: Rect, color: Color, data: T)(using uiContext: UiContext): Unit =
uiContext.pushRenderOp(RenderOp.Custom(area, color, data))

/** Advanced operation to add a custom primitive to the list of render operations.
*
* Supports an arbitrary data value. It's up to the backend to interpret it as it sees fit.
* If the backend does not know how to interpret it, it can just render a colored rect.
*
* @param data custom value to be interpreted by the backend.
*/
final def custom[T](color: Color, data: T)(using uiContext: UiContext, allocator: LayoutAllocator): Unit =
uiContext.pushRenderOp(RenderOp.Custom(allocator.fill(), color, data))

/** Applies the operations in a code block at the next z-index. */
def onTop[T](body: (UiContext) ?=> T)(using uiContext: UiContext): T =
UiContext.withZIndex(uiContext.currentZ + 1)(body)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package eu.joaocosta.interim.skins

import eu.joaocosta.interim.TextLayout._
import eu.joaocosta.interim._
import eu.joaocosta.interim.api.LayoutAllocator
import eu.joaocosta.interim.api.Primitives._

trait ButtonSkin:
def allocateArea(allocator: LayoutAllocator, label: String): Rect
def buttonArea(area: Rect): Rect
def renderButton(area: Rect, label: String, itemStatus: UiContext.ItemStatus)(using
uiContext: UiContext
Expand All @@ -18,6 +20,9 @@ object ButtonSkin extends DefaultSkin:
colorScheme: ColorScheme
) extends ButtonSkin:

def allocateArea(allocator: LayoutAllocator, label: String): Rect =
allocator.allocate(label, font, paddingH = buttonHeight / 2)

def buttonArea(area: Rect): Rect =
area.copy(w = area.w, h = area.h - buttonHeight)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package eu.joaocosta.interim.skins

import eu.joaocosta.interim._
import eu.joaocosta.interim.api.LayoutAllocator
import eu.joaocosta.interim.api.Primitives._

trait CheckboxSkin:
def allocateArea(allocator: LayoutAllocator): Rect
def checkboxArea(area: Rect): Rect
def renderCheckbox(area: Rect, value: Boolean, itemStatus: UiContext.ItemStatus)(using uiContext: UiContext): Unit

Expand All @@ -14,6 +16,9 @@ object CheckboxSkin extends DefaultSkin:
colorScheme: ColorScheme
) extends CheckboxSkin:

def allocateArea(allocator: LayoutAllocator): Rect =
allocator.allocate(Font.default.fontSize, Font.default.fontSize)

def checkboxArea(area: Rect): Rect =
val smallSide = math.min(area.w, area.h)
area.copy(w = smallSide, h = smallSide)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package eu.joaocosta.interim.skins

import eu.joaocosta.interim._
import eu.joaocosta.interim.api.LayoutAllocator
import eu.joaocosta.interim.api.Primitives._

trait SelectSkin:
def allocateArea(allocator: LayoutAllocator, labels: Vector[String]): Rect

def selectBoxArea(area: Rect): Rect
def renderSelectBox(area: Rect, value: Int, labels: Vector[String], itemStatus: UiContext.ItemStatus)(using
uiContext: UiContext
Expand All @@ -22,6 +25,10 @@ object SelectSkin extends DefaultSkin:
colorScheme: ColorScheme
) extends SelectSkin:

def allocateArea(allocator: LayoutAllocator, labels: Vector[String]): Rect =
val largestLabel = labels.maxByOption(_.size).getOrElse("")
allocator.allocate(largestLabel, font, paddingW = padding, paddingH = padding)

// Select box
def selectBoxArea(area: Rect): Rect =
area
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package eu.joaocosta.interim.skins

import eu.joaocosta.interim._
import eu.joaocosta.interim.api.LayoutAllocator
import eu.joaocosta.interim.api.Primitives._

trait SliderSkin:
def allocateArea(allocator: LayoutAllocator): Rect
def sliderArea(area: Rect): Rect
def renderSlider(area: Rect, min: Int, value: Int, max: Int, itemStatus: UiContext.ItemStatus)(using
uiContext: UiContext
Expand All @@ -17,6 +19,9 @@ object SliderSkin extends DefaultSkin:
colorScheme: ColorScheme
) extends SliderSkin:

def allocateArea(allocator: LayoutAllocator): Rect =
allocator.allocate(Font.default.fontSize, Font.default.fontSize)

def sliderArea(area: Rect): Rect = area.shrink(padding)

def renderSlider(area: Rect, min: Int, value: Int, max: Int, itemStatus: UiContext.ItemStatus)(using
Expand Down
Loading

0 comments on commit 2e82cfb

Please sign in to comment.