diff --git a/demo/src/main/scala/demo/ColourWindow.scala b/demo/src/main/scala/demo/ColourWindow.scala index 0efbd10c..55d0721f 100644 --- a/demo/src/main/scala/demo/ColourWindow.scala +++ b/demo/src/main/scala/demo/ColourWindow.scala @@ -110,7 +110,7 @@ object ColourWindow: ) ) -final case class ColorPalette(components: ComponentGroup) +final case class ColorPalette(components: ComponentGroup[Unit]) object ColorPalette: given WindowContent[ColorPalette, Unit] with diff --git a/demo/src/main/scala/demo/ComponentsWindow.scala b/demo/src/main/scala/demo/ComponentsWindow.scala index 4999f1a5..67e6f4b9 100644 --- a/demo/src/main/scala/demo/ComponentsWindow.scala +++ b/demo/src/main/scala/demo/ComponentsWindow.scala @@ -11,7 +11,7 @@ object ComponentsWindow: def window( charSheet: CharSheet - ): WindowModel[ComponentGroup, Unit] = + ): WindowModel[ComponentGroup[Int], Int] = WindowModel( windowId, charSheet, @@ -76,7 +76,8 @@ object ComponentsWindow: ) .add( Label("Terminal rendered label", Label.Theme(charSheet, RGBA.Magenta, RGBA.Cyan)), - Label("Default theme", Label.Theme(charSheet)) + Label("Default theme", Label.Theme(charSheet)), + Label((count: Int) => "Mouse over windows: " + count, Label.Theme(charSheet)) ) ) .withTitle("Components example") diff --git a/demo/src/main/scala/demo/UIScene.scala b/demo/src/main/scala/demo/UIScene.scala index fcff5bee..7a6bd131 100644 --- a/demo/src/main/scala/demo/UIScene.scala +++ b/demo/src/main/scala/demo/UIScene.scala @@ -23,11 +23,11 @@ object UIScene extends Scene[Size, Model, ViewModel]: val subSystems: Set[SubSystem[Model]] = Set( - WindowManager[Model, Unit]( + WindowManager[Model, Int]( SubSystemId("window manager 2"), RogueLikeGame.magnification, Model.defaultCharSheet, - _ => () + _.mouseOverWindows.length ) .register( ComponentsWindow.window( @@ -41,6 +41,18 @@ object UIScene extends Scene[Size, Model, ViewModel]: context: SceneContext[Size], model: Model ): GlobalEvent => Outcome[Model] = + case WindowEvent.MouseOver(id) => + println("Mouse over window: " + id) + val ids = id :: model.mouseOverWindows.filterNot(_ == id) + + Outcome(model.copy(mouseOverWindows = ids)) + + case WindowEvent.MouseOut(id) => + println("Mouse out window: " + id) + val ids = model.mouseOverWindows.filterNot(_ == id) + + Outcome(model.copy(mouseOverWindows = ids)) + case _ => Outcome(model) diff --git a/roguelike-starterkit/src/main/scala/roguelikestarterkit/package.scala b/roguelike-starterkit/src/main/scala/roguelikestarterkit/package.scala index 5c8b454f..75613006 100644 --- a/roguelike-starterkit/src/main/scala/roguelikestarterkit/package.scala +++ b/roguelike-starterkit/src/main/scala/roguelikestarterkit/package.scala @@ -106,9 +106,9 @@ val WindowContent: ui.window.WindowContent.type = ui.window.WindowContent // UI Components -type Component[A] = ui.component.Component[A] +type Component[A, ReferenceData] = ui.component.Component[A, ReferenceData] -type ComponentGroup = ui.component.ComponentGroup +type ComponentGroup[ReferenceData] = ui.component.ComponentGroup[ReferenceData] val ComponentGroup: ui.component.ComponentGroup.type = ui.component.ComponentGroup type ComponentFragment = ui.component.ComponentFragment @@ -131,5 +131,5 @@ val BoundsType: ui.component.BoundsType.type = ui.component.BoundsType type Button = ui.components.Button val Button: ui.components.Button.type = ui.components.Button -type Label = ui.components.Label +type Label[ReferenceData] = ui.components.Label[ReferenceData] val Label: ui.components.Label.type = ui.components.Label diff --git a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/component/Component.scala b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/component/Component.scala index aeaebef7..65e2c4a5 100644 --- a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/component/Component.scala +++ b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/component/Component.scala @@ -7,7 +7,7 @@ import roguelikestarterkit.ui.datatypes.UiContext /** A typeclass that confirms that some type `A` can be used as a `Component` provides the necessary * operations for that type to act as a component. */ -trait Component[A]: +trait Component[A, ReferenceData]: /** The position and size of the component */ @@ -15,14 +15,14 @@ trait Component[A]: /** Update this componenets model. */ - def updateModel[ReferenceData]( + def updateModel( context: UiContext[ReferenceData], model: A ): GlobalEvent => Outcome[A] /** Produce a renderable output for this component, based on the component's model. */ - def present[ReferenceData]( + def present( context: UiContext[ReferenceData], model: A ): Outcome[ComponentFragment] diff --git a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/component/ComponentEntry.scala b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/component/ComponentEntry.scala index f9d8762d..e1cd08b5 100644 --- a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/component/ComponentEntry.scala +++ b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/component/ComponentEntry.scala @@ -7,7 +7,7 @@ import roguelikestarterkit.ui.datatypes.Coords /** `ComponentEntry`s record a components model, position, and relevant component typeclass instance * for use inside a `ComponentGroup`. */ -final case class ComponentEntry[A](offset: Coords, model: A, component: Component[A]): +final case class ComponentEntry[A, ReferenceData](offset: Coords, model: A, component: Component[A, ReferenceData]): - def cascade(parentBounds: Bounds): ComponentEntry[A] = + def cascade(parentBounds: Bounds): ComponentEntry[A, ReferenceData] = this.copy(model = component.cascade(model, parentBounds)) diff --git a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/component/ComponentGroup.scala b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/component/ComponentGroup.scala index be91578e..077dd6ce 100644 --- a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/component/ComponentGroup.scala +++ b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/component/ComponentGroup.scala @@ -11,18 +11,18 @@ import scala.annotation.tailrec /** Encapsulates a collection of components and describes and manages their layout, as well as * propagating update and presention calls. */ -final case class ComponentGroup( +final case class ComponentGroup[ReferenceData]( bounds: Bounds, boundsType: BoundsType, layout: ComponentLayout, - components: Batch[ComponentEntry[?]] + components: Batch[ComponentEntry[?, ReferenceData]] ): extension (b: Bounds) def withPadding(p: Padding): Bounds = b.moveBy(p.left, p.top).resize(b.width + p.right, b.height + p.bottom) - def nextOffset(components: Batch[ComponentEntry[?]]): Coords = + def nextOffset(components: Batch[ComponentEntry[?, ReferenceData]]): Coords = layout match case ComponentLayout.None => Coords.zero @@ -60,19 +60,20 @@ final case class ComponentGroup( .map(c => c.offset + Coords(0, c.component.bounds(c.model).withPadding(padding).bottom)) .getOrElse(Coords(padding.left, padding.top)) - def reflow: ComponentGroup = - val newComponents = components.foldLeft(Batch.empty[ComponentEntry[?]]) { (acc, entry) => - val reflowed = entry.copy( - offset = nextOffset(acc), - model = entry.component.reflow(entry.model) - ) + def reflow: ComponentGroup[ReferenceData] = + val newComponents = components.foldLeft(Batch.empty[ComponentEntry[?, ReferenceData]]) { + (acc, entry) => + val reflowed = entry.copy( + offset = nextOffset(acc), + model = entry.component.reflow(entry.model) + ) - acc :+ reflowed + acc :+ reflowed } this.copy(components = newComponents) - def cascade(parentBounds: Bounds): ComponentGroup = + def cascade(parentBounds: Bounds): ComponentGroup[ReferenceData] = val newBounds = boundsType match case BoundsType.Fixed => @@ -116,17 +117,19 @@ final case class ComponentGroup( ) .reflow - def add[A](entry: A)(using c: Component[A]): ComponentGroup = + def add[A](entry: A)(using c: Component[A, ReferenceData]): ComponentGroup[ReferenceData] = this.copy(components = components :+ ComponentEntry(nextOffset(components), entry, c)) - def add[A](entries: Batch[A])(using c: Component[A]): ComponentGroup = + def add[A](entries: Batch[A])(using + c: Component[A, ReferenceData] + ): ComponentGroup[ReferenceData] = entries.foldLeft(this) { case (acc, next) => acc.add(next) } - def add[A](entries: A*)(using c: Component[A]): ComponentGroup = + def add[A](entries: A*)(using c: Component[A, ReferenceData]): ComponentGroup[ReferenceData] = add(Batch.fromSeq(entries)) - def update[StartupData, ContextData, ReferenceData]( + def update[StartupData, ContextData]( context: UiContext[ReferenceData] - ): GlobalEvent => Outcome[ComponentGroup] = + ): GlobalEvent => Outcome[ComponentGroup[ReferenceData]] = e => components .map { c => @@ -143,7 +146,7 @@ final case class ComponentGroup( ) } - def present[StartupData, ContextData, ReferenceData]( + def present[StartupData, ContextData]( context: UiContext[ReferenceData] ): Outcome[ComponentFragment] = components @@ -153,88 +156,91 @@ final case class ComponentGroup( .sequence .map(_.foldLeft(ComponentFragment.empty)(_ |+| _)) - def withBounds(value: Bounds): ComponentGroup = + def withBounds(value: Bounds): ComponentGroup[ReferenceData] = this.copy(bounds = value).reflow - def withBoundsType(value: BoundsType): ComponentGroup = + def withBoundsType(value: BoundsType): ComponentGroup[ReferenceData] = this.copy(boundsType = value).reflow - def fixedBounds: ComponentGroup = + def fixedBounds: ComponentGroup[ReferenceData] = withBoundsType(BoundsType.Fixed) - def inheritBounds: ComponentGroup = + def inheritBounds: ComponentGroup[ReferenceData] = withBoundsType(BoundsType.Inherit) - def relative(x: Double, y: Double, width: Double, height: Double): ComponentGroup = + def relative(x: Double, y: Double, width: Double, height: Double): ComponentGroup[ReferenceData] = withBoundsType(BoundsType.Relative(x, y, width, height)) - def relativePosition(x: Double, y: Double): ComponentGroup = + def relativePosition(x: Double, y: Double): ComponentGroup[ReferenceData] = withBoundsType(BoundsType.RelativePosition(x, y)) - def relativeSize(width: Double, height: Double): ComponentGroup = + def relativeSize(width: Double, height: Double): ComponentGroup[ReferenceData] = withBoundsType(BoundsType.RelativeSize(width, height)) - def offset(amountPosition: Coords, amountSize: Dimensions): ComponentGroup = + def offset(amountPosition: Coords, amountSize: Dimensions): ComponentGroup[ReferenceData] = withBoundsType(BoundsType.Offset(amountPosition, amountSize)) - def offset(x: Int, y: Int, width: Int, height: Int): ComponentGroup = + def offset(x: Int, y: Int, width: Int, height: Int): ComponentGroup[ReferenceData] = offset(Coords(x, y), Dimensions(width, height)) - def offsetPosition(amount: Coords): ComponentGroup = + def offsetPosition(amount: Coords): ComponentGroup[ReferenceData] = withBoundsType(BoundsType.OffsetPosition(amount)) - def offsetPosition(x: Int, y: Int): ComponentGroup = + def offsetPosition(x: Int, y: Int): ComponentGroup[ReferenceData] = offsetPosition(Coords(x, y)) - def offsetSize(amount: Dimensions): ComponentGroup = + def offsetSize(amount: Dimensions): ComponentGroup[ReferenceData] = withBoundsType(BoundsType.OffsetSize(amount)) - def offsetSize(width: Int, height: Int): ComponentGroup = + def offsetSize(width: Int, height: Int): ComponentGroup[ReferenceData] = offsetSize(Dimensions(width, height)) - def withLayout(value: ComponentLayout): ComponentGroup = + def withLayout(value: ComponentLayout): ComponentGroup[ReferenceData] = this.copy(layout = value).reflow - def withPosition(value: Coords): ComponentGroup = + def withPosition(value: Coords): ComponentGroup[ReferenceData] = withBounds(bounds.withPosition(value)) - def moveTo(position: Coords): ComponentGroup = + def moveTo(position: Coords): ComponentGroup[ReferenceData] = withPosition(position) - def moveTo(x: Int, y: Int): ComponentGroup = + def moveTo(x: Int, y: Int): ComponentGroup[ReferenceData] = moveTo(Coords(x, y)) - def moveBy(amount: Coords): ComponentGroup = + def moveBy(amount: Coords): ComponentGroup[ReferenceData] = withPosition(bounds.coords + amount) - def moveBy(x: Int, y: Int): ComponentGroup = + def moveBy(x: Int, y: Int): ComponentGroup[ReferenceData] = moveBy(Coords(x, y)) - def withDimensions(value: Dimensions): ComponentGroup = + def withDimensions(value: Dimensions): ComponentGroup[ReferenceData] = withBounds(bounds.withDimensions(value)) - def resizeTo(size: Dimensions): ComponentGroup = + def resizeTo(size: Dimensions): ComponentGroup[ReferenceData] = withDimensions(size) - def resizeTo(x: Int, y: Int): ComponentGroup = + def resizeTo(x: Int, y: Int): ComponentGroup[ReferenceData] = resizeTo(Dimensions(x, y)) - def resizeBy(amount: Dimensions): ComponentGroup = + def resizeBy(amount: Dimensions): ComponentGroup[ReferenceData] = withDimensions(bounds.dimensions + amount) - def resizeBy(x: Int, y: Int): ComponentGroup = + def resizeBy(x: Int, y: Int): ComponentGroup[ReferenceData] = resizeBy(Dimensions(x, y)) object ComponentGroup: - def apply(bounds: Bounds): ComponentGroup = + def apply[ReferenceData](bounds: Bounds): ComponentGroup[ReferenceData] = ComponentGroup(bounds, BoundsType.Fixed, ComponentLayout.None, Batch.empty) - given Component[ComponentGroup] with + given [ReferenceData]: Component[ComponentGroup[ReferenceData], ReferenceData] with - def bounds(model: ComponentGroup): Bounds = + def bounds(model: ComponentGroup[ReferenceData]): Bounds = model.bounds - def updateModel[ReferenceData]( + def updateModel( context: UiContext[ReferenceData], - model: ComponentGroup - ): GlobalEvent => Outcome[ComponentGroup] = + model: ComponentGroup[ReferenceData] + ): GlobalEvent => Outcome[ComponentGroup[ReferenceData]] = case e => model.update(context)(e) - def present[ReferenceData]( + def present( context: UiContext[ReferenceData], - model: ComponentGroup + model: ComponentGroup[ReferenceData] ): Outcome[ComponentFragment] = model.present(context) - def reflow(model: ComponentGroup): ComponentGroup = - val reflowed: Batch[ComponentEntry[?]] = model.components.map { c => + def reflow(model: ComponentGroup[ReferenceData]): ComponentGroup[ReferenceData] = + val reflowed: Batch[ComponentEntry[?, ReferenceData]] = model.components.map { c => c.copy( model = c.component.reflow(c.model) ) } model.reflow.copy(components = reflowed) - def cascade(model: ComponentGroup, parentBounds: Bounds): ComponentGroup = + def cascade( + model: ComponentGroup[ReferenceData], + parentBounds: Bounds + ): ComponentGroup[ReferenceData] = model.cascade(parentBounds) diff --git a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/Button.scala b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/Button.scala index 82b730df..a57536a3 100644 --- a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/Button.scala +++ b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/Button.scala @@ -172,11 +172,11 @@ object Button: if theme.hasBorder then Bounds(0, 0, label.length + 2, 3) else Bounds(0, 0, label.length, 1) ) - given Component[Button] with + given [ReferenceData]: Component[Button, ReferenceData] with def bounds(model: Button): Bounds = model.bounds - def updateModel[ReferenceData]( + def updateModel( context: UiContext[ReferenceData], model: Button ): GlobalEvent => Outcome[Button] = @@ -197,7 +197,7 @@ object Button: case _ => Outcome(model) - def present[ReferenceData]( + def present( context: UiContext[ReferenceData], model: Button ): Outcome[ComponentFragment] = diff --git a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/Label.scala b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/Label.scala index 4e1af29a..6bcb7a6b 100644 --- a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/Label.scala +++ b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/Label.scala @@ -18,17 +18,36 @@ import scala.annotation.targetName /** Labels are a simple `Component` that render text. */ -final case class Label(text: String, render: (Coords, String) => Outcome[ComponentFragment]): - def withText(value: String): Label = - this.copy(text = value) +final case class Label[ReferenceData]( + text: ReferenceData => String, + bounds: Bounds, + render: (Coords, String) => Outcome[ComponentFragment] +): + def withText(value: String): Label[ReferenceData] = + this.copy(text = _ => value) + def withText(f: ReferenceData => String): Label[ReferenceData] = + this.copy(text = f) + + def withBounds(value: Bounds): Label[ReferenceData] = + this.copy(bounds = value) object Label: + private def findBounds(text: String): Bounds = + Bounds(0, 0, text.length, 1) + /** Minimal label constructor with custom rendering function */ + def apply[ReferenceData](text: String)( + present: (Coords, String) => Outcome[ComponentFragment] + ): Label[ReferenceData] = + Label(_ => text, findBounds(text), present) + @targetName("Label_apply_curried") - def apply(text: String)(present: (Coords, String) => Outcome[ComponentFragment]): Label = - Label(text, present) + def apply[ReferenceData](text: ReferenceData => String)( + present: (Coords, String) => Outcome[ComponentFragment] + ): Label[ReferenceData] = + Label(text, Bounds.zero, present) private val graphic = Graphic(0, 0, TerminalMaterial(AssetName(""), RGBA.White, RGBA.Black)) @@ -57,30 +76,49 @@ object Label: /** Creates a Label rendered using the RogueTerminalEmulator based on a `Label.Theme`, with custom * bounds */ - def apply(text: String, theme: Theme): Label = - Label(text, presentLabel(theme.charSheet, theme.colors.foreground, theme.colors.background)) + def apply[ReferenceData](text: String, theme: Theme): Label[ReferenceData] = + Label( + _ => text, + findBounds(text), + presentLabel(theme.charSheet, theme.colors.foreground, theme.colors.background) + ) - given Component[Label] with - def bounds(model: Label): Bounds = - Bounds(0, 0, model.text.length, 1) - - def updateModel[ReferenceData]( + /** Creates a Label rendered using the RogueTerminalEmulator based on a `Label.Theme`, with custom + * bounds + */ + def apply[ReferenceData](text: ReferenceData => String, theme: Theme): Label[ReferenceData] = + Label( + text, + Bounds.zero, + presentLabel(theme.charSheet, theme.colors.foreground, theme.colors.background) + ) + + given [ReferenceData]: Component[Label[ReferenceData], ReferenceData] with + def bounds(model: Label[ReferenceData]): Bounds = + model.bounds + + def updateModel( context: UiContext[ReferenceData], - model: Label - ): GlobalEvent => Outcome[Label] = + model: Label[ReferenceData] + ): GlobalEvent => Outcome[Label[ReferenceData]] = + case FrameTick => + Outcome( + model.withBounds(findBounds(model.text(context.reference))) + ) + case _ => Outcome(model) - def present[ReferenceData]( + def present( context: UiContext[ReferenceData], - model: Label + model: Label[ReferenceData] ): Outcome[ComponentFragment] = - model.render(context.bounds.coords, model.text) + model.render(context.bounds.coords, model.text(context.reference)) - def reflow(model: Label): Label = + def reflow(model: Label[ReferenceData]): Label[ReferenceData] = model - def cascade(model: Label, parentBounds: Bounds): Label = + def cascade(model: Label[ReferenceData], parentBounds: Bounds): Label[ReferenceData] = model final case class Theme( diff --git a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/window/WindowContent.scala b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/window/WindowContent.scala index 5a7cfb01..7b2c2af8 100644 --- a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/window/WindowContent.scala +++ b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/window/WindowContent.scala @@ -30,19 +30,19 @@ trait WindowContent[A, ReferenceData]: object WindowContent: - given [ReferenceData]: WindowContent[ComponentGroup, ReferenceData] with + given [ReferenceData]: WindowContent[ComponentGroup[ReferenceData], ReferenceData] with def updateModel( context: UiContext[ReferenceData], - model: ComponentGroup - ): GlobalEvent => Outcome[ComponentGroup] = + model: ComponentGroup[ReferenceData] + ): GlobalEvent => Outcome[ComponentGroup[ReferenceData]] = e => model.update(context)(e) def present( context: UiContext[ReferenceData], - model: ComponentGroup + model: ComponentGroup[ReferenceData] ): Outcome[Layer] = model.present(context).map(_.toLayer) - def cascade(model: ComponentGroup, newBounds: Bounds): ComponentGroup = + def cascade(model: ComponentGroup[ReferenceData], newBounds: Bounds): ComponentGroup[ReferenceData] = model.cascade(newBounds)