From 13dbd13374e1354f290e24b7d150908be8ebc4ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Costa?= Date: Sun, 13 Aug 2023 12:37:43 +0200 Subject: [PATCH 1/3] Add PanelStatus object --- .../eu/joaocosta/interim/PanelState.scala | 13 ++++++++++ .../eu/joaocosta/interim/api/Components.scala | 24 +++++++------------ 2 files changed, 22 insertions(+), 15 deletions(-) create mode 100644 core/src/main/scala/eu/joaocosta/interim/PanelState.scala diff --git a/core/src/main/scala/eu/joaocosta/interim/PanelState.scala b/core/src/main/scala/eu/joaocosta/interim/PanelState.scala new file mode 100644 index 0000000..074d3c1 --- /dev/null +++ b/core/src/main/scala/eu/joaocosta/interim/PanelState.scala @@ -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) diff --git a/core/src/main/scala/eu/joaocosta/interim/api/Components.scala b/core/src/main/scala/eu/joaocosta/interim/api/Components.scala index 20b5a66..983bd45 100644 --- a/core/src/main/scala/eu/joaocosta/interim/api/Components.scala +++ b/core/src/main/scala/eu/joaocosta/interim/api/Components.scala @@ -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 */ @@ -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. From 45e43c3b753999f55e9a4343407c76c2973fd8f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Costa?= Date: Sun, 13 Aug 2023 12:37:56 +0200 Subject: [PATCH 2/3] Add select to the color picker example --- examples/snapshot/5-colorpicker.md | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/examples/snapshot/5-colorpicker.md b/examples/snapshot/5-colorpicker.md index 062f406..3496b0c 100644 --- a/examples/snapshot/5-colorpicker.md +++ b/examples/snapshot/5-colorpicker.md @@ -43,6 +43,7 @@ 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), + colorRange: PanelState[Int] = PanelState(false, 0), resultDelta: Int = 0, color: Color = Color(0, 0, 0), query: String = "" @@ -76,15 +77,24 @@ 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 => + 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 => From 48429d935ca06f82056926d0f4e2f7b1804e0021 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Costa?= Date: Sun, 13 Aug 2023 14:40:32 +0200 Subject: [PATCH 3/3] Allow windows to be closed --- .../eu/joaocosta/interim/api/Components.scala | 23 +++++++++- .../eu/joaocosta/interim/api/Panels.scala | 46 ++++++++++++------- .../joaocosta/interim/skins/HandleSkin.scala | 25 ++++++++-- examples/snapshot/3-windows.md | 8 ++-- examples/snapshot/4-refs.md | 4 +- examples/snapshot/5-colorpicker.md | 11 +++-- 6 files changed, 83 insertions(+), 34 deletions(-) diff --git a/core/src/main/scala/eu/joaocosta/interim/api/Components.scala b/core/src/main/scala/eu/joaocosta/interim/api/Components.scala index 983bd45..d6adc59 100644 --- a/core/src/main/scala/eu/joaocosta/interim/api/Components.scala +++ b/core/src/main/scala/eu/joaocosta/interim/api/Components.scala @@ -154,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 @@ -166,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 diff --git a/core/src/main/scala/eu/joaocosta/interim/api/Panels.scala b/core/src/main/scala/eu/joaocosta/interim/api/Panels.scala index b609319..e1ef8e1 100644 --- a/core/src/main/scala/eu/joaocosta/interim/api/Panels.scala +++ b/core/src/main/scala/eu/joaocosta/interim/api/Panels.scala @@ -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) diff --git a/core/src/main/scala/eu/joaocosta/interim/skins/HandleSkin.scala b/core/src/main/scala/eu/joaocosta/interim/skins/HandleSkin.scala index cc47d4b..64cbdc1 100644 --- a/core/src/main/scala/eu/joaocosta/interim/skins/HandleSkin.scala +++ b/core/src/main/scala/eu/joaocosta/interim/skins/HandleSkin.scala @@ -4,8 +4,10 @@ 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( @@ -13,11 +15,12 @@ object HandleSkin extends DefaultSkin: 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 @@ -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, diff --git a/examples/snapshot/3-windows.md b/examples/snapshot/3-windows.md index 42c8175..fb3c306 100644 --- a/examples/snapshot/3-windows.md +++ b/examples/snapshot/3-windows.md @@ -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 @@ -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 diff --git a/examples/snapshot/4-refs.md b/examples/snapshot/4-refs.md index dc1e161..31ef115 100644 --- a/examples/snapshot/4-refs.md +++ b/examples/snapshot/4-refs.md @@ -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) = @@ -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) = diff --git a/examples/snapshot/5-colorpicker.md b/examples/snapshot/5-colorpicker.md index 3496b0c..c100690 100644 --- a/examples/snapshot/5-colorpicker.md +++ b/examples/snapshot/5-colorpicker.md @@ -41,8 +41,8 @@ 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), @@ -79,7 +79,7 @@ def application(inputState: InputState, appState: AppState) = ui(inputState, uiContext): appState.asRefs: (colorPickerArea, colorSearchArea, colorRange, resultDelta, color, query) => onTop: - window(id = "color picker", area = colorPickerArea, title = "Color Picker", movable = true): area => + 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) select(id = "range", row(1), Vector("0-255","0-100", "0x00-0xff"))(colorRange).value match @@ -97,7 +97,7 @@ def application(inputState: InputState, appState: AppState) = 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) @@ -114,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()