From d9ffdd145946932860d63b428d6eae931bf061a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Costa?= Date: Sat, 25 Nov 2023 16:04:40 +0100 Subject: [PATCH] Add ComponentWithBody abstraction --- .../eu/joaocosta/interim/api/Components.scala | 19 +++-- .../eu/joaocosta/interim/api/Panels.scala | 76 +++++++++---------- examples/snapshot/1-intro.md | 12 +-- examples/snapshot/2-layout.md | 14 ++-- examples/snapshot/3-windows.md | 4 +- examples/snapshot/4-refs.md | 23 +++--- examples/snapshot/5-colorpicker.md | 10 +-- 7 files changed, 80 insertions(+), 78 deletions(-) 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 c2d9e9b..3616b53 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 @@ -27,6 +27,13 @@ trait Components: case x: T => applyValue(x) case x: Ref[T] => applyRef(x) + trait ComponentWithBody[I, F[_]]: + def render[T](body: I => T): Component[F[T]] + + def apply[T](body: I => T): Component[F[T]] = render(body) + + def apply[T](body: => T)(using ev: I =:= Unit): Component[F[T]] = render(_ => body) + /** Button component. Returns true if it's being clicked, false otherwise. * * @param label text label to show on this button @@ -36,11 +43,13 @@ trait Components: area: Rect, label: String, skin: ButtonSkin = ButtonSkin.default() - ): Component[Boolean] = - val buttonArea = skin.buttonArea(area) - val itemStatus = UiContext.registerItem(id, buttonArea) - skin.renderButton(area, label, itemStatus) - itemStatus.clicked + ): ComponentWithBody[Unit, Option] = + new ComponentWithBody[Unit, Option]: + def render[T](body: Unit => T): Component[Option[T]] = + val buttonArea = skin.buttonArea(area) + val itemStatus = UiContext.registerItem(id, buttonArea) + skin.renderButton(area, label, itemStatus) + Option.when(itemStatus.clicked)(body(())) /** Checkbox component. Returns true if it's enabled, false otherwise. */ diff --git a/core/shared/src/main/scala/eu/joaocosta/interim/api/Panels.scala b/core/shared/src/main/scala/eu/joaocosta/interim/api/Panels.scala index 049150c..211feb8 100644 --- a/core/shared/src/main/scala/eu/joaocosta/interim/api/Panels.scala +++ b/core/shared/src/main/scala/eu/joaocosta/interim/api/Panels.scala @@ -29,7 +29,7 @@ trait Panels: * @param movable if true, the window will include a move handle in the title bar * @param resizable if true, the window will include a resize handle in the bottom corner */ - final def window[T]( + final def window( id: ItemId, area: Rect | PanelState[Rect] | Ref[PanelState[Rect]], title: String, @@ -38,40 +38,40 @@ trait Panels: resizable: Boolean = false, skin: WindowSkin = WindowSkin.default(), handleSkin: HandleSkin = HandleSkin.default() - )( - body: Rect => T - ): 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)) - if (panelStateRef.get.isOpen) - def 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 (resizable) - val newArea = Components - .resizeHandle( - id |> "internal_resize_handle", - skin.resizeArea(windowArea), - handleSkin - )(windowArea) - panelStateRef.modify(_.copy(value = skin.ensureMinimumArea(newArea))) - if (movable) - val newArea = Components - .moveHandle( - id |> "internal_move_handle", - skin.titleTextArea(windowArea), - handleSkin - )(windowArea) - panelStateRef.modify(_.copy(value = newArea)) - (Option.when(panelStateRef.get.isOpen)(res), panelStateRef.get) - else (None, panelStateRef.get) + ): Components.ComponentWithBody[Rect, [T] =>> (Option[T], PanelState[Rect])] = + new Components.ComponentWithBody[Rect, [T] =>> (Option[T], PanelState[Rect])]: + def render[T](body: Rect => T): 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)) + if (panelStateRef.get.isOpen) + def 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 (resizable) + val newArea = Components + .resizeHandle( + id |> "internal_resize_handle", + skin.resizeArea(windowArea), + handleSkin + )(windowArea) + panelStateRef.modify(_.copy(value = skin.ensureMinimumArea(newArea))) + if (movable) + val newArea = Components + .moveHandle( + id |> "internal_move_handle", + skin.titleTextArea(windowArea), + handleSkin + )(windowArea) + panelStateRef.modify(_.copy(value = newArea)) + (Option.when(panelStateRef.get.isOpen)(res), panelStateRef.get) + else (None, panelStateRef.get) diff --git a/examples/snapshot/1-intro.md b/examples/snapshot/1-intro.md index eda2636..aee0466 100644 --- a/examples/snapshot/1-intro.md +++ b/examples/snapshot/1-intro.md @@ -54,7 +54,7 @@ Now, let's write our interface. We are going to need the following components: def application(inputState: InputState) = import eu.joaocosta.interim.InterIm.* ui(inputState, uiContext): - if (button(id = "minus", area = Rect(x = 10, y = 10, w = 30, h = 30), label = "-")) + button(id = "minus", area = Rect(x = 10, y = 10, w = 30, h = 30), label = "-"): counter = counter - 1 text( area = Rect(x = 40, y = 10, w = 30, h = 30), @@ -64,7 +64,7 @@ def application(inputState: InputState) = horizontalAlignment = centerHorizontally, verticalAlignment = centerVertically ) - if (button(id = "plus", area = Rect(x = 70, y = 10, w = 30, h = 30), label = "+")) + button(id = "plus", area = Rect(x = 70, y = 10, w = 30, h = 30), label = "+"): counter = counter + 1 ``` @@ -128,7 +128,7 @@ def immutableApp(inputState: InputState, counter: Int): (List[RenderOp], Int) = import eu.joaocosta.interim.InterIm.* ui(inputState, uiContext): val (decrementCounter, _, incrementCounter) = ( - button(id = "minus", area = Rect(x = 10, y = 10, w = 30, h = 30), label = "-"), + button(id = "minus", area = Rect(x = 10, y = 10, w = 30, h = 30), label = "-")(true).getOrElse(false), text( area = Rect(x = 40, y = 10, w = 30, h = 30), color = Color(0, 0, 0), @@ -137,7 +137,7 @@ def immutableApp(inputState: InputState, counter: Int): (List[RenderOp], Int) = horizontalAlignment = centerHorizontally, verticalAlignment = centerVertically ), - button(id = "plus", area = Rect(x = 70, y = 10, w = 30, h = 30), label = "+") + button(id = "plus", area = Rect(x = 70, y = 10, w = 30, h = 30), label = "+")(true).getOrElse(false) ) if (decrementCounter && !incrementCounter) counter - 1 else if (!decrementCounter && incrementCounter) counter + 1 @@ -154,7 +154,7 @@ def localMutableApp(inputState: InputState, counter: Int): (List[RenderOp], Int) import eu.joaocosta.interim.InterIm.* var _counter = counter ui(inputState, uiContext): - if (button(id = "minus", area = Rect(x = 10, y = 10, w = 30, h = 30), label = "-")) + button(id = "minus", area = Rect(x = 10, y = 10, w = 30, h = 30), label = "-"): _counter = counter - 1 text( area = Rect(x = 40, y = 10, w = 30, h = 30), @@ -164,7 +164,7 @@ def localMutableApp(inputState: InputState, counter: Int): (List[RenderOp], Int) horizontalAlignment = centerHorizontally, verticalAlignment = centerVertically ) - if (button(id = "plus", area = Rect(x = 70, y = 10, w = 30, h = 30), label = "+")) + button(id = "plus", area = Rect(x = 70, y = 10, w = 30, h = 30), label = "+"): _counter = counter + 1 _counter ``` diff --git a/examples/snapshot/2-layout.md b/examples/snapshot/2-layout.md index 7b59560..fb8abda 100644 --- a/examples/snapshot/2-layout.md +++ b/examples/snapshot/2-layout.md @@ -50,8 +50,8 @@ var counter = 0 def application(inputState: InputState) = import eu.joaocosta.interim.InterIm.* ui(inputState, uiContext): - columns(area = Rect(x = 10, y = 10, w = 110, h = 30), numColumns = 3, padding = 10) { column => - if (button(id = "minus", area = column(0), label = "-")) + columns(area = Rect(x = 10, y = 10, w = 110, h = 30), numColumns = 3, padding = 10): column => + button(id = "minus", area = column(0), label = "-"): counter = counter - 1 text( area = column(1), @@ -61,9 +61,8 @@ def application(inputState: InputState) = horizontalAlignment = centerHorizontally, verticalAlignment = centerVertically ) - if (button(id = "plus", area = column(2), label = "+")) + button(id = "plus", area = column(2), label = "+"): counter = counter + 1 - } ``` Now let's run it: @@ -87,10 +86,10 @@ For example, this is how our application would look like with a dynamic layout: def dynamicApp(inputState: InputState) = import eu.joaocosta.interim.InterIm.* ui(inputState, uiContext): - dynamicColumns(area = Rect(x = 10, y = 10, w = 110, h = 30), padding = 10) { column => - if (button(id = "minus", area = column(30), label = "-")) // 30px from the left + dynamicColumns(area = Rect(x = 10, y = 10, w = 110, h = 30), padding = 10): column => + button(id = "minus", area = column(30), label = "-"): // 30px from the left counter = counter - 1 - if (button(id = "plus", area = column(-30), label = "+")) // 30px from the right + button(id = "plus", area = column(-30), label = "+"): // 30px from the right counter = counter + 1 text( area = column(maxSize), // Fill the remaining area @@ -100,5 +99,4 @@ def dynamicApp(inputState: InputState) = horizontalAlignment = centerHorizontally, verticalAlignment = centerVertically ) - } ``` diff --git a/examples/snapshot/3-windows.md b/examples/snapshot/3-windows.md index 6b97664..d1ee282 100644 --- a/examples/snapshot/3-windows.md +++ b/examples/snapshot/3-windows.md @@ -42,7 +42,7 @@ def application(inputState: InputState) = ui(inputState, uiContext): 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 = "-")) + button(id = "minus", area = column(0), label = "-"): counter = counter - 1 text( area = column(1), @@ -52,7 +52,7 @@ def application(inputState: InputState) = horizontalAlignment = centerHorizontally, verticalAlignment = centerVertically ) - if (button(id = "plus", area = column(2), label = "+")) + button(id = "plus", area = column(2), label = "+"): counter = counter + 1 } }._2 // We don't care about the value, just the rect diff --git a/examples/snapshot/4-refs.md b/examples/snapshot/4-refs.md index d4e479b..da379ae 100644 --- a/examples/snapshot/4-refs.md +++ b/examples/snapshot/4-refs.md @@ -39,9 +39,9 @@ def application(inputState: InputState) = import eu.joaocosta.interim.InterIm.* ui(inputState, uiContext): // window takes area as a ref, so will mutate the window area variable - window(id = "window", area = windowArea, title = "My Counter", movable = true) { area => - columns(area = area.shrink(5), numColumns = 3, padding = 10) { column => - if (button(id = "minus", area = column(0), label = "-")) + window(id = "window", area = windowArea, title = "My Counter", movable = true):area => + columns(area = area.shrink(5), numColumns = 3, padding = 10): column => + button(id = "minus", area = column(0), label = "-"): counter = counter - 1 text( area = column(1), @@ -51,10 +51,8 @@ def application(inputState: InputState) = horizontalAlignment = centerHorizontally, verticalAlignment = centerVertically ) - if (button(id = "plus", area = column(2), label = "+")) + button(id = "plus", area = column(2), label = "+"): counter = counter + 1 - } - } ``` Be aware that, while the code is more concise, coding with out parameters can lead to confusing code where it's hard @@ -82,10 +80,10 @@ val initialState = AppState() def applicationRef(inputState: InputState, appState: AppState) = import eu.joaocosta.interim.InterIm.* ui(inputState, uiContext): - appState.asRefs { (counter, windowArea) => - window(id = "window", area = windowArea, title = "My Counter", movable = true) { area => - columns(area = area.shrink(5), numColumns = 3, padding = 10) { column => - if (button(id = "minus", area = column(0), label = "-")) + appState.asRefs: (counter, windowArea) => + window(id = "window", area = windowArea, title = "My Counter", movable = true): area => + columns(area = area.shrink(5), numColumns = 3, padding = 10): column => + button(id = "minus", area = column(0), label = "-"): counter := counter.get - 1 // Counter is a Ref, so we need to use := text( area = column(1), @@ -95,11 +93,8 @@ def applicationRef(inputState: InputState, appState: AppState) = horizontalAlignment = centerHorizontally, verticalAlignment = centerVertically ) - if (button(id = "plus", area = column(2), label = "+")) + button(id = "plus", area = column(2), label = "+"): counter := counter.get + 1 // Counter is a Ref, so we need to use := - } - } - } ``` Then we can run our app: diff --git a/examples/snapshot/5-colorpicker.md b/examples/snapshot/5-colorpicker.md index be73515..2a14d59 100644 --- a/examples/snapshot/5-colorpicker.md +++ b/examples/snapshot/5-colorpicker.md @@ -111,11 +111,11 @@ def application(inputState: InputState, appState: AppState) = val clipArea = newColumn(maxSize) clip(area = clipArea): 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 - } + results.zip(rows).foreach: + case ((colorName, colorValue), row) => + button(s"$colorName button", row, colorName): + colorPickerArea.modify(_.open) + color := colorValue onBottom: window(id = "settings", area = PanelState.open(Rect(10, 430, 250, 40)), title = "Settings", movable = false): area =>