Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add PanelState to keep track if a component is open or closed. #43

Merged
merged 3 commits into from
Aug 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions core/src/main/scala/eu/joaocosta/interim/PanelState.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package eu.joaocosta.interim

/** State of a panel that can either be open or closed.
* Can also carry a value.
*/
final case class PanelState[T](isOpen: Boolean, value: T):
def isClosed: Boolean = !isOpen
def open: PanelState[T] = copy(isOpen = true)
def close: PanelState[T] = copy(isOpen = false)

object PanelState:
def open[T](value: T): PanelState[T] = PanelState(true, value)
def closed[T](value: T): PanelState[T] = PanelState(false, value)
47 changes: 30 additions & 17 deletions core/src/main/scala/eu/joaocosta/interim/api/Components.scala
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,7 @@ trait Components:
else (skin.renderButton(area, label, itemStatus))
value.get

/** Select box component. Returns the index value currently selected.
*
* The returned value is returned inside an Either.
* - Left means the select box is open
* - Right means the select box is closed
/** Select box component. Returns the index value currently selected inside a PanelState.
*
* @param labels text labels for each value
*/
Expand All @@ -85,22 +81,20 @@ trait Components:
area: Rect,
labels: Vector[String],
skin: SelectSkin = SelectSkin.default()
): ComponentWithValue[Either[Int, Int]] =
new ComponentWithValue[Either[Int, Int]]:
def applyRef(value: Ref[Either[Int, Int]]): Component[Either[Int, Int]] =
): ComponentWithValue[PanelState[Int]] =
new ComponentWithValue[PanelState[Int]]:
def applyRef(value: Ref[PanelState[Int]]): Component[PanelState[Int]] =
val selectBoxArea = skin.selectBoxArea(area)
val itemStatus = UiContext.registerItem(id, area)
val selectedValue = value.get.merge
if (itemStatus.selected) value := Left(selectedValue)
val isOpen = value.get.isLeft
skin.renderSelectBox(area, selectedValue, labels, itemStatus)
if (isOpen)
if (!itemStatus.selected) value := Right(selectedValue)
if (itemStatus.selected) value.modify(_.open)
skin.renderSelectBox(area, value.get.value, labels, itemStatus)
if (value.get.isOpen)
if (!itemStatus.selected) value.modify(_.close)
labels.zipWithIndex.foreach: (label, idx) =>
val selectOptionArea = skin.selectOptionArea(area, idx)
val optionStatus = UiContext.registerItem(id |> idx, selectOptionArea)
skin.renderSelectOption(area, idx, labels, optionStatus)
if (optionStatus.active) value := Right(idx)
if (optionStatus.active) value := PanelState.closed(idx)
value.get

/** Slider component. Returns the current position of the slider, between min and max.
Expand Down Expand Up @@ -160,9 +154,9 @@ trait Components:
final def moveHandle(id: ItemId, area: Rect, skin: HandleSkin = HandleSkin.default()): ComponentWithValue[Rect] =
new ComponentWithValue[Rect]:
def applyRef(value: Ref[Rect]): Component[Rect] =
val handleArea = skin.handleArea(area)
val handleArea = skin.moveHandleArea(area)
val itemStatus = UiContext.registerItem(id, handleArea)
skin.renderHandle(area, value.get, itemStatus)
skin.renderMoveHandle(area, value.get, itemStatus)
if (itemStatus.active)
val handleCenterX = handleArea.x + handleArea.w / 2
val handleCenterY = handleArea.y + handleArea.h / 2
Expand All @@ -172,3 +166,22 @@ trait Components:
val deltaY = mouseY - handleCenterY
value.modify(_.move(deltaX, deltaY))
value.get

/** Close handle. Closes the panel when clicked.
*
* Instead of using this component directly, it can be easier to use [[eu.joaocosta.interim.api.Panels.window]]
* with closable = true.
*/
final def closeHandle[T](
id: ItemId,
area: Rect,
skin: HandleSkin = HandleSkin.default()
): ComponentWithValue[PanelState[T]] =
new ComponentWithValue[PanelState[T]]:
def applyRef(value: Ref[PanelState[T]]): Component[PanelState[T]] =
val handleArea = skin.closeHandleArea(area)
val itemStatus = UiContext.registerItem(id, handleArea)
skin.renderCloseHandle(area, itemStatus)
if (itemStatus.active)
value.modify(_.close)
value.get
46 changes: 30 additions & 16 deletions core/src/main/scala/eu/joaocosta/interim/api/Panels.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,30 +26,44 @@ trait Panels:
/** Window with a title.
*
* @param title of this window
* @param closable if true, the window will include a closable handle in the title bar
* @param movable if true, the window will include a move handle in the title bar
*/
final def window[T](
id: ItemId,
area: Rect | Ref[Rect],
area: Rect | PanelState[Rect] | Ref[PanelState[Rect]],
title: String,
closable: Boolean = false,
movable: Boolean = false,
skin: WindowSkin = WindowSkin.default(),
handleSkin: HandleSkin = HandleSkin.default()
)(
body: Rect => T
): Components.Component[(T, Rect)] =
val areaRef = area match {
case ref: Ref[Rect] => ref
case v: Rect => Ref(v)
): Components.Component[(Option[T], PanelState[Rect])] =
val panelStateRef = area match {
case ref: Ref[PanelState[Rect]] => ref
case v: PanelState[Rect] => Ref(v)
case v: Rect => Ref(PanelState.open(v))
}
UiContext.registerItem(id, areaRef.get, passive = true)
skin.renderWindow(areaRef.get, title)
val res = body(skin.panelArea(areaRef.get))
if (movable)
Components
.moveHandle(
id |> "internal_move_handle",
skin.titleTextArea(areaRef.get),
handleSkin
)(areaRef)
(res, areaRef.get)
if (panelStateRef.get.isOpen)
val windowArea = panelStateRef.get.value
UiContext.registerItem(id, windowArea, passive = true)
skin.renderWindow(windowArea, title)
val res = body(skin.panelArea(windowArea))
if (closable)
Components
.closeHandle(
id |> "internal_close_handle",
skin.titleTextArea(windowArea),
handleSkin
)(panelStateRef)
if (movable)
val newArea = Components
.moveHandle(
id |> "internal_move_handle",
skin.titleTextArea(windowArea),
handleSkin
)(windowArea)
panelStateRef.modify(_.copy(value = newArea))
(Some(res), panelStateRef.get)
else (None, panelStateRef.get)
25 changes: 20 additions & 5 deletions core/src/main/scala/eu/joaocosta/interim/skins/HandleSkin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,23 @@ import eu.joaocosta.interim.*
import eu.joaocosta.interim.api.Primitives.*

trait HandleSkin:
def handleArea(area: Rect): Rect
def renderHandle(area: Rect, value: Rect, itemStatus: UiContext.ItemStatus)(using uiContext: UiContext): Unit
def moveHandleArea(area: Rect): Rect
def closeHandleArea(area: Rect): Rect
def renderMoveHandle(area: Rect, value: Rect, itemStatus: UiContext.ItemStatus)(using uiContext: UiContext): Unit
def renderCloseHandle(area: Rect, itemStatus: UiContext.ItemStatus)(using uiContext: UiContext): Unit

object HandleSkin extends DefaultSkin:
final case class Default(
inactiveColor: Color,
hotColor: Color,
activeColor: Color
) extends HandleSkin:
def handleArea(area: Rect): Rect =
def moveHandleArea(area: Rect): Rect =
val smallSide = math.min(area.w, area.h)
area.copy(w = smallSide, h = smallSide)
def renderHandle(area: Rect, value: Rect, itemStatus: UiContext.ItemStatus)(using uiContext: UiContext): Unit =
val handleArea = this.handleArea(area)

def renderMoveHandle(area: Rect, value: Rect, itemStatus: UiContext.ItemStatus)(using uiContext: UiContext): Unit =
val handleArea = this.moveHandleArea(area)
val color = itemStatus match
case UiContext.ItemStatus(false, false, _) => inactiveColor
case UiContext.ItemStatus(true, false, _) => hotColor
Expand All @@ -26,6 +29,18 @@ object HandleSkin extends DefaultSkin:
rectangle(handleArea.copy(h = lineHeight), color)
rectangle(handleArea.copy(y = handleArea.y + 2 * lineHeight, h = lineHeight), color)

def closeHandleArea(area: Rect): Rect =
val smallSide = math.min(area.w, area.h)
area.copy(x = area.x + area.w - smallSide, w = smallSide, h = smallSide)

def renderCloseHandle(area: Rect, itemStatus: UiContext.ItemStatus)(using uiContext: UiContext): Unit =
val handleArea = this.closeHandleArea(area)
val color = itemStatus match
case UiContext.ItemStatus(false, false, _) => inactiveColor
case UiContext.ItemStatus(true, false, _) => hotColor
case UiContext.ItemStatus(_, true, _) => activeColor
rectangle(handleArea, color)

val lightDefault: Default = Default(
inactiveColor = ColorScheme.black,
hotColor = ColorScheme.lightPrimary,
Expand Down
8 changes: 4 additions & 4 deletions examples/snapshot/3-windows.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ For this, it's helpful to have a floating window abstraction, and that's exactly

A window is a special component that:
- Passes an area to a function, which is the window drawable region
- Returns it's value and a `Rect`. That `Rect` is the one that needs to be passed as the window area.
- Returns it's value (optional) and a `PanelState[Rect]`. That `PanelState[Rect]` is the one that needs to be passed as the window area.

This might sound a little convoluted, but this is what allows windows to be dragged.
This might sound a little convoluted, but this is what allows windows to be dragged and closed.

## Using window in the counter application

Expand All @@ -34,13 +34,13 @@ import eu.joaocosta.interim.*

val uiContext = new UiContext()

var windowArea = Rect(x = 10, y = 10, w = 110, h = 50)
var windowArea = PanelState.open(Rect(x = 10, y = 10, w = 110, h = 50))
var counter = 0

def application(inputState: InputState) =
import eu.joaocosta.interim.InterIm._
ui(inputState, uiContext):
windowArea = window(id = "window", area = windowArea, title = "My Counter", movable = true) { area =>
windowArea = window(id = "window", area = windowArea, title = "My Counter", movable = true, closable = false) { area =>
columns(area = area.shrink(5), numColumns = 3, padding = 10) { column =>
if (button(id = "minus", area = column(0), label = "-"))
counter = counter - 1
Expand Down
4 changes: 2 additions & 2 deletions examples/snapshot/4-refs.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import eu.joaocosta.interim.api.Ref

val uiContext = new UiContext()

val windowArea = Ref(Rect(x = 10, y = 10, w = 110, h = 50)) // Now a val instead of a var
val windowArea = Ref(PanelState.open(Rect(x = 10, y = 10, w = 110, h = 50))) // Now a val instead of a var
var counter = 0

def application(inputState: InputState) =
Expand Down Expand Up @@ -78,7 +78,7 @@ import eu.joaocosta.interim.api.Ref

val uiContext = new UiContext()

case class AppState(counter: Int = 0, windowArea: Rect = Rect(x = 10, y = 10, w = 110, h = 50))
case class AppState(counter: Int = 0, windowArea: PanelState[Rect] = PanelState.open(Rect(x = 10, y = 10, w = 110, h = 50)))
val initialState = AppState()

def applicationRef(inputState: InputState, appState: AppState) =
Expand Down
33 changes: 22 additions & 11 deletions examples/snapshot/5-colorpicker.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ import eu.joaocosta.interim.api.Ref.asRefs
val uiContext = new UiContext()

case class AppState(
colorPickerArea: Rect = Rect(x = 10, y = 10, w = 190, h = 180),
colorSearchArea: Rect = Rect(x = 300, y = 10, w = 210, h = 210),
colorPickerArea: PanelState[Rect] = PanelState.open(Rect(x = 10, y = 10, w = 190, h = 180)),
colorSearchArea: PanelState[Rect] = PanelState.open(Rect(x = 300, y = 10, w = 210, h = 210)),
colorRange: PanelState[Int] = PanelState(false, 0),
resultDelta: Int = 0,
color: Color = Color(0, 0, 0),
query: String = ""
Expand Down Expand Up @@ -76,18 +77,27 @@ def application(inputState: InputState, appState: AppState) =
import eu.joaocosta.interim.InterIm.*

ui(inputState, uiContext):
appState.asRefs: (colorPickerArea, colorSearchArea, resultDelta, color, query) =>
appState.asRefs: (colorPickerArea, colorSearchArea, colorRange, resultDelta, color, query) =>
onTop:
window(id = "color picker", area = colorPickerArea, title = "Color Picker", movable = true): area =>
rows(area = area.shrink(5), numRows = 5, padding = 10): row =>
window(id = "color picker", area = colorPickerArea, title = "Color Picker", closable = true, movable = true): area =>
rows(area = area.shrink(5), numRows = 6, padding = 10): row =>
rectangle(row(0), color.get)
text(row(1), textColor, color.get.toString, Font.default, alignLeft, centerVertically)
val r = slider("red slider", row(2), min = 0, max = 255)(color.get.r)
val g = slider("green slider", row(3), min = 0, max = 255)(color.get.g)
val b = slider("blue slider", row(4), min = 0, max = 255)(color.get.b)
select(id = "range", row(1), Vector("0-255","0-100", "0x00-0xff"))(colorRange).value match
case 0 =>
val colorStr = f"R:${color.get.r}%03d G:${color.get.g}%03d B:${color.get.b}%03d"
text(row(2), textColor, colorStr, Font.default, alignLeft, centerVertically)
case 1 =>
val colorStr = f"R:${color.get.r * 100 / 255}%03d G:${color.get.g * 100 / 255}%03d B:${color.get.b * 100 / 255}%03d"
text(row(2), textColor, colorStr, Font.default, alignLeft, centerVertically)
case 2 =>
val colorStr = f"R:0x${color.get.r}%02x G:0x${color.get.g}%02x B:0x${color.get.b}%02x"
text(row(2), textColor, colorStr, Font.default, alignLeft, centerVertically)
val r = slider("red slider", row(3), min = 0, max = 255)(color.get.r)
val g = slider("green slider", row(4), min = 0, max = 255)(color.get.g)
val b = slider("blue slider", row(5), min = 0, max = 255)(color.get.b)
color := Color(r, g, b)

window(id = "color search", area = colorSearchArea, title = "Color Search", movable = true): area =>
window(id = "color search", area = colorSearchArea, title = "Color Search", closable = false, movable = true): area =>
dynamicRows(area = area.shrink(5), padding = 10): newRow =>
val oldQuery = query.get
textInput("query", newRow(16))(query)
Expand All @@ -104,11 +114,12 @@ def application(inputState: InputState, appState: AppState) =
rows(area = clipArea.copy(y = clipArea.y - resultDelta.get, h = resultsHeight), numRows = results.size, padding = 10): rows =>
results.zip(rows).foreach { case ((colorName, colorValue), row) =>
if (button(s"$colorName button", row, colorName))
colorPickerArea.modify(_.open)
color := colorValue
}

onBottom:
window(id = "settings", area = Rect(10, 430, 250, 40), title = "Settings", movable = false): area =>
window(id = "settings", area = PanelState.open(Rect(10, 430, 250, 40)), title = "Settings", movable = false): area =>
dynamicColumns(area = area.shrink(5), padding = 10): newColumn =>
if (checkbox(id = "dark mode", newColumn(-16))(skins.ColorScheme.darkModeEnabled()))
skins.ColorScheme.useDarkMode()
Expand Down
Loading