From a0fee0aa17bacbcff2ceb90d445909c03e043447 Mon Sep 17 00:00:00 2001 From: davesmith00000 Date: Thu, 2 Jan 2025 15:22:18 +0000 Subject: [PATCH] Added HitArea component --- .../scala/demo/scenes/WindowDemoScene.scala | 6 +- .../main/scala/demo/windows/DemoWindow.scala | 18 +- .../ui/components/HitArea.scala | 226 ++++++++++++++++++ 3 files changed, 242 insertions(+), 8 deletions(-) create mode 100644 ui/src/main/scala/roguelikestarterkit/ui/components/HitArea.scala diff --git a/demo/src/main/scala/demo/scenes/WindowDemoScene.scala b/demo/src/main/scala/demo/scenes/WindowDemoScene.scala index 7812c280..0945f666 100644 --- a/demo/src/main/scala/demo/scenes/WindowDemoScene.scala +++ b/demo/src/main/scala/demo/scenes/WindowDemoScene.scala @@ -27,11 +27,9 @@ object WindowDemoScene extends Scene[Size, Model, ViewModel]: val subSystems: Set[SubSystem[Model]] = Set( - WindowManager[Model, Unit]( + WindowManager[Model]( SubSystemId("demo window manager"), - RogueLikeGame.magnification, - Size(Model.defaultCharSheet.charSize), - _ => () + RogueLikeGame.magnification ) .withLayerKey(BindingKey("UI Layer")) .register( diff --git a/demo/src/main/scala/demo/windows/DemoWindow.scala b/demo/src/main/scala/demo/windows/DemoWindow.scala index f2facff4..8ea57311 100644 --- a/demo/src/main/scala/demo/windows/DemoWindow.scala +++ b/demo/src/main/scala/demo/windows/DemoWindow.scala @@ -4,19 +4,20 @@ import demo.Assets import indigo.* import roguelikestarterkit.* import roguelikestarterkit.syntax.* -import roguelikestarterkit.ui.component.Component import roguelikestarterkit.ui.components.ComponentGroup +import roguelikestarterkit.ui.components.HitArea +import roguelikestarterkit.ui.components.datatypes.Anchor object DemoWindow: val windowId: WindowId = WindowId("demo window") - def window: Window[Unit, Unit] = + def window: Window[ComponentGroup[Unit], Unit] = Window( windowId, Size(1), Dimensions(128, 64), - () + content ) .withBackground { ctx => Outcome( @@ -33,5 +34,14 @@ object DemoWindow: ) } - def content(charSheet: CharSheet): ComponentGroup[Unit] = + def content: ComponentGroup[Unit] = ComponentGroup() + .anchor( + HitArea(Bounds(0, 0, 16, 16)) + .onClick( + WindowEvent.Close(windowId) + ) + .withFill(RGBA.Green.withAlpha(0.5)) + .withStroke(Stroke(1, RGBA.Green.withAlpha(0.75))), + Anchor.TopRight + ) diff --git a/ui/src/main/scala/roguelikestarterkit/ui/components/HitArea.scala b/ui/src/main/scala/roguelikestarterkit/ui/components/HitArea.scala new file mode 100644 index 00000000..5b3064f8 --- /dev/null +++ b/ui/src/main/scala/roguelikestarterkit/ui/components/HitArea.scala @@ -0,0 +1,226 @@ +package roguelikestarterkit.ui.components + +import indigo.* +import indigo.syntax.* +import roguelikestarterkit.ui.component.Component +import roguelikestarterkit.ui.datatypes.Bounds +import roguelikestarterkit.ui.datatypes.Dimensions +import roguelikestarterkit.ui.datatypes.UIContext + +/** HitAreas `Component` s allow you to create invisible buttons for your UI. + * + * Functionally, a hit area is identical to a button that does not render anything. In fact a + * HitArea is isomorphic to a Button that renders nothing, and its component instance is mostly + * implemented by delegating to the button instance. + * + * All that said... for debug purposes, you can set a fill or stroke color to see the hit area. + */ +final case class HitArea[ReferenceData]( + bounds: Bounds, + state: ButtonState, + click: ReferenceData => Batch[GlobalEvent], + press: ReferenceData => Batch[GlobalEvent], + release: ReferenceData => Batch[GlobalEvent], + drag: (ReferenceData, DragData) => Batch[GlobalEvent], + boundsType: BoundsType[ReferenceData, Unit], + isDown: Boolean, + dragOptions: DragOptions, + dragStart: Option[DragData], + fill: Option[RGBA] = None, + stroke: Option[Stroke] = None +): + val isDragged: Boolean = dragStart.isDefined + + def onClick(events: ReferenceData => Batch[GlobalEvent]): HitArea[ReferenceData] = + this.copy(click = events) + def onClick(events: Batch[GlobalEvent]): HitArea[ReferenceData] = + onClick(_ => events) + def onClick(events: GlobalEvent*): HitArea[ReferenceData] = + onClick(Batch.fromSeq(events)) + + def onPress(events: ReferenceData => Batch[GlobalEvent]): HitArea[ReferenceData] = + this.copy(press = events) + def onPress(events: Batch[GlobalEvent]): HitArea[ReferenceData] = + onPress(_ => events) + def onPress(events: GlobalEvent*): HitArea[ReferenceData] = + onPress(Batch.fromSeq(events)) + + def onRelease(events: ReferenceData => Batch[GlobalEvent]): HitArea[ReferenceData] = + this.copy(release = events) + def onRelease(events: Batch[GlobalEvent]): HitArea[ReferenceData] = + onRelease(_ => events) + def onRelease(events: GlobalEvent*): HitArea[ReferenceData] = + onRelease(Batch.fromSeq(events)) + + def onDrag( + events: (ReferenceData, DragData) => Batch[GlobalEvent] + ): HitArea[ReferenceData] = + this.copy(drag = events) + def onDrag(events: Batch[GlobalEvent]): HitArea[ReferenceData] = + onDrag((_, _) => events) + def onDrag(events: GlobalEvent*): HitArea[ReferenceData] = + onDrag(Batch.fromSeq(events)) + + def withDragOptions(value: DragOptions): HitArea[ReferenceData] = + this.copy(dragOptions = value) + def makeDraggable: HitArea[ReferenceData] = + withDragOptions(dragOptions.withMode(DragMode.Drag)) + def reportDrag: HitArea[ReferenceData] = + withDragOptions(dragOptions.withMode(DragMode.ReportDrag)) + def notDraggable: HitArea[ReferenceData] = + withDragOptions(dragOptions.withMode(DragMode.None)) + + def withDragConstrain(value: DragConstrain): HitArea[ReferenceData] = + this.copy(dragOptions = dragOptions.withConstraints(value)) + def constrainDragTo(bounds: Bounds): HitArea[ReferenceData] = + withDragConstrain(DragConstrain.To(bounds)) + def constrainDragVertically: HitArea[ReferenceData] = + withDragConstrain(DragConstrain.Vertical) + def constrainDragVertically(from: Int, to: Int, x: Int): HitArea[ReferenceData] = + withDragConstrain(DragConstrain.vertical(from, to, x)) + def constrainDragHorizontally: HitArea[ReferenceData] = + withDragConstrain(DragConstrain.Horizontal) + def constrainDragHorizontally(from: Int, to: Int, y: Int): HitArea[ReferenceData] = + withDragConstrain(DragConstrain.horizontal(from, to, y)) + + def withDragArea(value: DragArea): HitArea[ReferenceData] = + this.copy(dragOptions = dragOptions.withArea(value)) + def noDragArea: HitArea[ReferenceData] = + withDragArea(DragArea.None) + def fixedDragArea(bounds: Bounds): HitArea[ReferenceData] = + withDragArea(DragArea.Fixed(bounds)) + def inheritDragArea: HitArea[ReferenceData] = + withDragArea(DragArea.Inherit) + + def withBoundsType(value: BoundsType[ReferenceData, Unit]): HitArea[ReferenceData] = + this.copy(boundsType = value) + + def withFill(value: RGBA): HitArea[ReferenceData] = + this.copy(fill = Option(value)) + def clearFill: HitArea[ReferenceData] = + this.copy(fill = None) + + def withStroke(value: Stroke): HitArea[ReferenceData] = + this.copy(stroke = Option(value)) + def clearStroke: HitArea[ReferenceData] = + this.copy(stroke = None) + + def toButton: Button[ReferenceData] = + Button( + bounds, + state, + (_, _, _) => Outcome(Layer.empty), + None, + None, + click, + press, + release, + drag, + boundsType, + isDown, + dragOptions, + dragStart + ) + +object HitArea: + + /** Minimal hitarea constructor with no events. + */ + def apply[ReferenceData](boundsType: BoundsType[ReferenceData, Unit]): HitArea[ReferenceData] = + HitArea( + Bounds.zero, + ButtonState.Up, + _ => Batch.empty, + _ => Batch.empty, + _ => Batch.empty, + (_, _) => Batch.empty, + boundsType, + isDown = false, + dragOptions = DragOptions.default, + dragStart = None, + fill = None, + stroke = None + ) + + /** Minimal hitarea constructor with no events. + */ + def apply[ReferenceData](bounds: Bounds): HitArea[ReferenceData] = + HitArea( + bounds, + ButtonState.Up, + _ => Batch.empty, + _ => Batch.empty, + _ => Batch.empty, + (_, _) => Batch.empty, + BoundsType.Fixed(bounds), + isDown = false, + dragOptions = DragOptions.default, + dragStart = None, + fill = None, + stroke = None + ) + + given [ReferenceData](using btn: Component[Button[ReferenceData], ReferenceData]): Component[ + HitArea[ReferenceData], + ReferenceData + ] with + def bounds(reference: ReferenceData, model: HitArea[ReferenceData]): Bounds = + btn.bounds(reference, model.toButton) + + def updateModel( + context: UIContext[ReferenceData], + model: HitArea[ReferenceData] + ): GlobalEvent => Outcome[HitArea[ReferenceData]] = + e => + val f = model.fill + val s = model.stroke + btn.updateModel(context, model.toButton)(e).map(_.toHitArea.copy(fill = f, stroke = s)) + + def present( + context: UIContext[ReferenceData], + model: HitArea[ReferenceData] + ): Outcome[Layer] = + (model.fill, model.stroke) match + case (Some(fill), Some(stroke)) => + Outcome( + Layer( + Shape.Box( + model.bounds.unsafeToRectangle.moveTo(context.bounds.coords.unsafeToPoint), + Fill.Color(fill), + stroke + ) + ) + ) + + case (Some(fill), None) => + Outcome( + Layer( + Shape.Box( + model.bounds.unsafeToRectangle.moveTo(context.bounds.coords.unsafeToPoint), + Fill.Color(fill) + ) + ) + ) + + case (None, Some(stroke)) => + Outcome( + Layer( + Shape.Box( + model.bounds.unsafeToRectangle.moveTo(context.bounds.coords.unsafeToPoint), + Fill.None, + stroke + ) + ) + ) + + case (None, None) => + Outcome(Layer.empty) + + def refresh( + reference: ReferenceData, + model: HitArea[ReferenceData], + parentDimensions: Dimensions + ): HitArea[ReferenceData] = + val f = model.fill + val s = model.stroke + btn.refresh(reference, model.toButton, parentDimensions).toHitArea.copy(fill = f, stroke = s)