Skip to content

Commit

Permalink
Button is stateful and has hold and release behaviour
Browse files Browse the repository at this point in the history
  • Loading branch information
davesmith00000 committed Jul 7, 2024
1 parent 4b9f28e commit aef03cd
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ object TerminalButton:
calculateBounds: ReferenceData => Bounds
): Button[ReferenceData] =
Button(
Bounds.zero,
ButtonState.Up,
presentButton(
label,
theme.up.foreground,
Expand All @@ -130,7 +132,10 @@ object TerminalButton:
)
),
_ => Batch.empty,
calculateBounds
_ => Batch.empty,
_ => Batch.empty,
calculateBounds,
isDown = false
)

/** Creates a button rendered using the RogueTerminalEmulator based on a `Button.Theme`, with
Expand Down
89 changes: 69 additions & 20 deletions ui/src/main/scala/roguelikestarterkit/ui/components/Button.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,16 @@ import roguelikestarterkit.ui.datatypes.UIContext
/** Buttons `Component`s allow you to create buttons for your UI.
*/
final case class Button[ReferenceData](
bounds: Bounds,
state: ButtonState,
up: (Coords, Bounds, ReferenceData) => Outcome[ComponentFragment],
over: Option[(Coords, Bounds, ReferenceData) => Outcome[ComponentFragment]],
down: Option[(Coords, Bounds, ReferenceData) => Outcome[ComponentFragment]],
click: ReferenceData => Batch[GlobalEvent],
calculateBounds: ReferenceData => Bounds
press: ReferenceData => Batch[GlobalEvent],
release: ReferenceData => Batch[GlobalEvent],
calculateBounds: ReferenceData => Bounds,
isDown: Boolean
):
def presentUp(
up: (Coords, Bounds, ReferenceData) => Outcome[ComponentFragment]
Expand All @@ -40,6 +45,20 @@ final case class Button[ReferenceData](
def onClick(events: GlobalEvent*): Button[ReferenceData] =
onClick(Batch.fromSeq(events))

def onPress(events: ReferenceData => Batch[GlobalEvent]): Button[ReferenceData] =
this.copy(press = events)
def onPress(events: Batch[GlobalEvent]): Button[ReferenceData] =
onPress(_ => events)
def onPress(events: GlobalEvent*): Button[ReferenceData] =
onPress(Batch.fromSeq(events))

def onRelease(events: ReferenceData => Batch[GlobalEvent]): Button[ReferenceData] =
this.copy(release = events)
def onRelease(events: Batch[GlobalEvent]): Button[ReferenceData] =
onRelease(_ => events)
def onRelease(events: GlobalEvent*): Button[ReferenceData] =
onRelease(Batch.fromSeq(events))

object Button:

/** Minimal button constructor with custom rendering function
Expand All @@ -48,11 +67,16 @@ object Button:
present: (Coords, Bounds, ReferenceData) => Outcome[ComponentFragment]
): Button[ReferenceData] =
Button(
bounds,
ButtonState.Up,
present,
None,
None,
_ => Batch.empty,
_ => bounds
_ => Batch.empty,
_ => Batch.empty,
_ => bounds,
isDown = false
)

/** Minimal button constructor with custom rendering function and dynamic sizing
Expand All @@ -61,11 +85,16 @@ object Button:
present: (Coords, Bounds, ReferenceData) => Outcome[ComponentFragment]
): Button[ReferenceData] =
Button(
Bounds.zero,
ButtonState.Up,
present,
None,
None,
_ => Batch.empty,
calculateBounds
_ => Batch.empty,
_ => Batch.empty,
calculateBounds,
isDown = false
)

given [ReferenceData]: Component[Button[ReferenceData], ReferenceData] with
Expand All @@ -76,41 +105,61 @@ object Button:
context: UIContext[ReferenceData],
model: Button[ReferenceData]
): GlobalEvent => Outcome[Button[ReferenceData]] =
_ => Outcome(model)
case FrameTick =>
val newBounds =
model.calculateBounds(context.reference)

Outcome(
model.copy(
state =
if model.isDown then ButtonState.Down
else if newBounds.moveBy(context.bounds.coords).contains(context.mouseCoords) then
if context.mouse.isLeftDown then ButtonState.Down
else ButtonState.Over
else ButtonState.Up,
bounds = newBounds
)
)

case _: MouseEvent.Click
if model.bounds.moveBy(context.bounds.coords).contains(context.mouseCoords) =>
Outcome(model.copy(state = ButtonState.Up, isDown = false))
.addGlobalEvents(model.click(context.reference))

case _: MouseEvent.MouseDown
if model.bounds.moveBy(context.bounds.coords).contains(context.mouseCoords) =>
Outcome(model.copy(state = ButtonState.Down, isDown = true))

case _: MouseEvent.MouseUp
if model.bounds.moveBy(context.bounds.coords).contains(context.mouseCoords) =>
Outcome(model.copy(state = ButtonState.Up, isDown = false))

case _: MouseEvent.MouseUp =>
// Released Outside.
Outcome(model.copy(state = ButtonState.Up, isDown = false))

case _ =>
Outcome(model)

def present(
context: UIContext[ReferenceData],
model: Button[ReferenceData]
): Outcome[ComponentFragment] =
val b = model.calculateBounds(context.reference)
val b = model.bounds
val mouseWithin = b.moveBy(context.bounds.coords).contains(context.mouseCoords)

val state =
if context.isActive && mouseWithin then
if context.mouse.isLeftDown then ButtonState.Down
else ButtonState.Over
else ButtonState.Up

val events =
if context.isActive && mouseWithin && context.mouse.mouseClicked then
model.click(context.reference)
else Batch.empty

state match
model.state match
case ButtonState.Up =>
model
.up(context.bounds.coords, b, context.reference)
.addGlobalEvents(events)

case ButtonState.Over =>
model.over
.getOrElse(model.up)(context.bounds.coords, b, context.reference)
.addGlobalEvents(events)

case ButtonState.Down =>
model.down
.getOrElse(model.up)(context.bounds.coords, b, context.reference)
.addGlobalEvents(events)

def refresh(
reference: ReferenceData,
Expand Down

0 comments on commit aef03cd

Please sign in to comment.