diff --git a/demo/src/main/scala/demo/ColourWindow.scala b/demo/src/main/scala/demo/ColourWindow.scala index 81a4043d..6be41512 100644 --- a/demo/src/main/scala/demo/ColourWindow.scala +++ b/demo/src/main/scala/demo/ColourWindow.scala @@ -138,7 +138,7 @@ object ColorPalette: // componentGroup = model.componentGroup.cascade(newBounds) // ) - def refresh(model: ColorPalette): ColorPalette = + def refresh(reference: Unit, model: ColorPalette): ColorPalette = model // .copy( // componentGroup = model.componentGroup.reflow diff --git a/demo/src/main/scala/demo/ComponentsWindow2.scala b/demo/src/main/scala/demo/ComponentsWindow2.scala index 5601c570..950a4729 100644 --- a/demo/src/main/scala/demo/ComponentsWindow2.scala +++ b/demo/src/main/scala/demo/ComponentsWindow2.scala @@ -9,68 +9,64 @@ object ComponentsWindow2: def window( charSheet: CharSheet - ): WindowModel[ComponentList[Int], Int] = + ): WindowModel[ComponentGroup[Int], Int] = WindowModel( windowId, charSheet, - ComponentList(Bounds(0, 0, 20, 3)) { (count: Int) => - Batch(Label[Int]("How many windows: ", Label.Theme(charSheet))) ++ - Batch.fill(count)(Label("x", Label.Theme(charSheet))) - } - .add((count: Int) => - Batch.fill(count)( - Button[Int]("Button", Button.Theme(charSheet)).onClick(Log("count: " + count)) - ) - :+ Button[Int]("test", Button.Theme(charSheet)).onClick(Log("test")) + ComponentGroup() + .withLayout(ComponentLayout.Vertical(Padding(0, 0, 1, 0))) + .add( + ComponentGroup() + .withLayout(ComponentLayout.Horizontal(Padding(0, 1, 0, 0))) + .add( + Label("label 1", Label.Theme(charSheet)), + Label("label 2", Label.Theme(charSheet)), + Label("label 3", Label.Theme(charSheet)) + ) + ) + // .add( + // ComponentGroup() + // .withLayout(ComponentLayout.Horizontal(Padding(0, 1, 0, 0))) + // .add( + // Batch( + // "History" -> Batch(), + // "Controls" -> Batch(), + // "Quit" -> Batch() + // ).map { case (label, clickEvents) => + // Button( + // label, + // Button.Theme(charSheet) + // ).onClick(clickEvents) + // } + // ) + // ) + // .add( + // ComponentList(Bounds(0, 0, 20, 3)) { (count: Int) => + // Batch(Label[Int]("How many windows: ", Label.Theme(charSheet))) ++ + // Batch.fill(count)(Label("x", Label.Theme(charSheet))) + // } + // .withLayout(ComponentLayout.Vertical()) + // ) + // .add( + // ComponentList(Bounds(0, 0, 20, 3)) { (count: Int) => + // Batch(Label[Int]("How many windows: ", Label.Theme(charSheet))) ++ + // Batch.fill(count)(Label("x", Label.Theme(charSheet))) + // } + // .add((count: Int) => + // Batch.fill(count)( + // Button[Int]("Button", Button.Theme(charSheet)).onClick(Log("count: " + count)) + // ) + // :+ Button[Int]("test", Button.Theme(charSheet)).onClick(Log("test")) + // ) + // .add((i: Int) => TextArea[Int]("abc.\nde,f\n0123456! " + i, TextArea.Theme(charSheet))) + // .withLayout(ComponentLayout.Vertical(Padding.zero)) + // ) + // .add( + // Input(20, Input.Theme(charSheet)) + // ) + .add( + TextArea("abc.\nde,f\n0123456!", TextArea.Theme(charSheet)) ) - .add((i: Int) => TextArea[Int]("abc.\nde,f\n0123456! " + i, TextArea.Theme(charSheet))) - .withLayout(ComponentLayout.Vertical(Padding.zero)) - // ComponentGroup() - // .withLayout(ComponentLayout.Vertical(Padding(0, 0, 1, 0))) - // .add( - // ComponentGroup() - // .withLayout(ComponentLayout.Horizontal(Padding(0, 1, 0, 0))) - // .add( - // Label("label 1", Label.Theme(charSheet)), - // Label("label 2", Label.Theme(charSheet)), - // Label("label 3", Label.Theme(charSheet)) - // ) - // ) - // .add( - // ComponentGroup() - // .withLayout(ComponentLayout.Horizontal(Padding(0, 1, 0, 0))) - // .add( - // Batch( - // "History" -> Batch(), - // "Controls" -> Batch(), - // "Quit" -> Batch() - // ).map { case (label, clickEvents) => - // Button( - // label, - // Button.Theme(charSheet) - // ).onClick(clickEvents) - // } - // ) - // ) - // .add( - // ComponentList(Bounds(0, 0, 20, 3)) { (count: Int) => - // Batch(Label[Int]("How many windows: ", Label.Theme(charSheet))) ++ - // Batch.fill(count)(Label("x", Label.Theme(charSheet))) - // } - // .withLayout(ComponentLayout.Vertical()) - // ) - // .add( - // ComponentList(Bounds(0, 0, 20, 3)) { (count: Int) => - // Batch.fill(count)(Button("y", Button.Theme(charSheet))) - // } - // .withLayout(ComponentLayout.Vertical()) - // ) - // .add( - // Input(20, Input.Theme(charSheet)) - // ) - // .add( - // TextArea("abc.\nde,f\n0123456!", TextArea.Theme(charSheet)) - // ) ) .withTitle("More component examples") .moveTo(2, 2) 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 387f5374..cca38d1e 100644 --- a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/component/Component.scala +++ b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/component/Component.scala @@ -30,7 +30,7 @@ trait Component[A, ReferenceData]: /** Used internally to instruct the component that the layout has changed in some way, and that it * should reflow it's contents - whatever that means in the context of this component type. */ - def reflow(model: A): A + def reflow(reference: ReferenceData, model: A): A /** Informs the Component that something about its parent has changed in case it needs to take * action. Currently the only cascaded change is the bounds. diff --git a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/component/StatelessComponent.scala b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/component/StatelessComponent.scala index 6c899ce6..c58a1189 100644 --- a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/component/StatelessComponent.scala +++ b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/component/StatelessComponent.scala @@ -20,7 +20,7 @@ trait StatelessComponent[A, ReferenceData] extends Component[A, ReferenceData]: model: A ): Outcome[ComponentFragment] - def reflow(model: A): A = model + def reflow(reference: ReferenceData, model: A): A = model def cascade(model: A, parentBounds: Bounds): A = model def updateModel(context: UiContext[ReferenceData], model: A): GlobalEvent => Outcome[A] = case e => Outcome(model) diff --git a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/Input.scala b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/Input.scala index 83c3b4ac..23f5de58 100644 --- a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/Input.scala +++ b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/Input.scala @@ -144,24 +144,6 @@ final case class Input( def withLoseFocusActions(actions: => Batch[GlobalEvent]): Input = this.copy(onLoseFocus = () => actions) - // Delegates, for convenience. - - def update[StartupData, ContextData]( - context: UiContext[?] - ): GlobalEvent => Outcome[Input] = - summon[Component[Input, ?]].updateModel(context, this) - - def present[StartupData, ContextData]( - context: UiContext[?] - ): Outcome[ComponentFragment] = - summon[Component[Input, ?]].present(context, this) - - def reflow: Input = - summon[Component[Input, ?]].reflow(this) - - def cascade(parentBounds: Bounds): Input = - summon[Component[Input, ?]].cascade(this, parentBounds) - object Input: /** Minimal input constructor with custom rendering function @@ -345,7 +327,7 @@ object Input: context.running ) - def reflow(model: Input): Input = + def reflow(reference: ReferenceData, model: Input): Input = model def cascade(model: Input, parentBounds: Bounds): Input = diff --git a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/group/ComponentGroup.scala b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/group/ComponentGroup.scala index dfb3b665..adbf8081 100644 --- a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/group/ComponentGroup.scala +++ b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/group/ComponentGroup.scala @@ -1,250 +1,241 @@ package roguelikestarterkit.ui.components.group -// import indigo.* -// import roguelikestarterkit.ui.component.* -// import roguelikestarterkit.ui.datatypes.* - -// import scala.annotation.tailrec -final case class ComponentGroup[A]() -// /** Describes a fixed arrangement of components, manages their layout, and propagates updates and -// * presention calls. -// */ -// final case class ComponentGroup[ReferenceData] private ( -// boundsType: BoundsType, -// layout: ComponentLayout, -// components: Batch[ComponentEntry[?, ReferenceData]], -// bounds: Bounds, -// referenceBounds: Batch[Bounds] -// ): - -// lazy val contentBounds: Bounds = -// val allBounds: Batch[Rectangle] = referenceBounds.map(_.unsafeToRectangle) - -// if allBounds.isEmpty then Bounds.zero -// else -// val h = allBounds.head -// val t = allBounds.tail - -// Bounds(t.foldLeft(h) { case (acc, r) => acc.expandToInclude(r) }) - -// private def addSingle[A](entry: A)(using -// c: Component[A, ReferenceData] -// ): ComponentGroup[ReferenceData] = -// this.copy( -// components = components :+ -// ComponentEntry(GroupFunctions.calculateNextOffset(bounds, layout)(components), entry, c), -// referenceBounds = referenceBounds :+ c.bounds(entry) -// ) - -// def add[A](entry: A)(using c: Component[A, ReferenceData]): ComponentGroup[ReferenceData] = -// addSingle(entry).reflow.cascade(bounds) - -// def add[A](entries: Batch[A])(using -// c: Component[A, ReferenceData] -// ): ComponentGroup[ReferenceData] = -// entries.foldLeft(this) { case (acc, next) => acc.addSingle(next) }.reflow.cascade(bounds) -// def add[A](entries: A*)(using c: Component[A, ReferenceData]): ComponentGroup[ReferenceData] = -// add(Batch.fromSeq(entries)) - -// def withBounds(value: Bounds): ComponentGroup[ReferenceData] = -// this.copy(bounds = value).reflow - -// def withBoundsType(value: BoundsType): ComponentGroup[ReferenceData] = -// value match -// case BoundsType.Fixed(bounds) => -// this.copy(boundsType = value, bounds = bounds).reflow - -// case _ => -// this.copy(boundsType = value).reflow - -// def defaultBounds: ComponentGroup[ReferenceData] = -// withBoundsType(BoundsType.default) -// def fixedBounds(value: Bounds): ComponentGroup[ReferenceData] = -// withBoundsType(BoundsType.Fixed(value)) -// def inheritBounds: ComponentGroup[ReferenceData] = -// withBoundsType(BoundsType.Inherit) -// 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[ReferenceData] = -// withBoundsType(BoundsType.RelativePosition(x, y)) -// def relativeSize(width: Double, height: Double): ComponentGroup[ReferenceData] = -// withBoundsType(BoundsType.RelativeSize(width, height)) -// def offset(amountPosition: Coords, amountSize: Dimensions): ComponentGroup[ReferenceData] = -// withBoundsType(BoundsType.Offset(amountPosition, amountSize)) -// def offset(x: Int, y: Int, width: Int, height: Int): ComponentGroup[ReferenceData] = -// offset(Coords(x, y), Dimensions(width, height)) -// def offsetPosition(amount: Coords): ComponentGroup[ReferenceData] = -// withBoundsType(BoundsType.OffsetPosition(amount)) -// def offsetPosition(x: Int, y: Int): ComponentGroup[ReferenceData] = -// offsetPosition(Coords(x, y)) -// def offsetSize(amount: Dimensions): ComponentGroup[ReferenceData] = -// withBoundsType(BoundsType.OffsetSize(amount)) -// def offsetSize(width: Int, height: Int): ComponentGroup[ReferenceData] = -// offsetSize(Dimensions(width, height)) -// def dynamicBounds(width: FitMode, height: FitMode): ComponentGroup[ReferenceData] = -// withBoundsType(BoundsType.Dynamic(width, height)) - -// def withLayout(value: ComponentLayout): ComponentGroup[ReferenceData] = -// this.copy(layout = value).reflow - -// def withPosition(value: Coords): ComponentGroup[ReferenceData] = -// withBounds(bounds.withPosition(value)) -// def moveTo(position: Coords): ComponentGroup[ReferenceData] = -// withPosition(position) -// def moveTo(x: Int, y: Int): ComponentGroup[ReferenceData] = -// moveTo(Coords(x, y)) -// def moveBy(amount: Coords): ComponentGroup[ReferenceData] = -// withPosition(bounds.coords + amount) -// def moveBy(x: Int, y: Int): ComponentGroup[ReferenceData] = -// moveBy(Coords(x, y)) - -// def withDimensions(value: Dimensions): ComponentGroup[ReferenceData] = -// withBounds(bounds.withDimensions(value)) -// def resizeTo(size: Dimensions): ComponentGroup[ReferenceData] = -// withDimensions(size) -// def resizeTo(x: Int, y: Int): ComponentGroup[ReferenceData] = -// resizeTo(Dimensions(x, y)) -// def resizeBy(amount: Dimensions): ComponentGroup[ReferenceData] = -// withDimensions(bounds.dimensions + amount) -// def resizeBy(x: Int, y: Int): ComponentGroup[ReferenceData] = -// resizeBy(Dimensions(x, y)) - -// // Delegates, for convenience. - -// def update[StartupData, ContextData]( -// context: UiContext[ReferenceData] -// ): GlobalEvent => Outcome[ComponentGroup[ReferenceData]] = -// summon[Component[ComponentGroup[ReferenceData], ReferenceData]].updateModel(context, this) - -// def present[StartupData, ContextData]( -// context: UiContext[ReferenceData] -// ): Outcome[ComponentFragment] = -// summon[Component[ComponentGroup[ReferenceData], ReferenceData]].present(context, this) - -// def reflow: ComponentGroup[ReferenceData] = -// summon[Component[ComponentGroup[ReferenceData], ReferenceData]].reflow(this) - -// def cascade(parentBounds: Bounds): ComponentGroup[ReferenceData] = -// summon[Component[ComponentGroup[ReferenceData], ReferenceData]].cascade(this, parentBounds) - -object ComponentGroup {} - // - -// def apply[ReferenceData](): ComponentGroup[ReferenceData] = -// ComponentGroup( -// BoundsType.default, -// ComponentLayout.None, -// Batch.empty, -// Bounds.zero, -// Batch.empty -// ) - -// def apply[ReferenceData](boundsType: BoundsType): ComponentGroup[ReferenceData] = -// ComponentGroup( -// boundsType, -// ComponentLayout.None, -// Batch.empty, -// Bounds.zero, -// Batch.empty -// ) - -// def apply[ReferenceData](bounds: Bounds): ComponentGroup[ReferenceData] = -// ComponentGroup( -// BoundsType.Fixed(bounds), -// ComponentLayout.None, -// Batch.empty, -// bounds, -// Batch.empty -// ) - -// given [ReferenceData]: Component[ComponentGroup[ReferenceData], ReferenceData] with - -// def bounds(model: ComponentGroup[ReferenceData]): Bounds = -// model.bounds - -// def updateModel( -// context: UiContext[ReferenceData], -// model: ComponentGroup[ReferenceData] -// ): GlobalEvent => Outcome[ComponentGroup[ReferenceData]] = -// case FrameTick => -// updateComponents(context, model)(FrameTick).map { updated => -// if updated.referenceBounds != model.referenceBounds then updated.reflow -// else updated -// } - -// case e => -// updateComponents(context, model)(e) - -// private def updateComponents[StartupData, ContextData]( -// context: UiContext[ReferenceData], -// model: ComponentGroup[ReferenceData] -// ): GlobalEvent => Outcome[ComponentGroup[ReferenceData]] = -// e => -// model.components -// .map { c => -// c.component -// .updateModel(context.copy(bounds = context.bounds.moveBy(c.offset)), c.model)(e) -// .map { updated => -// c.copy(model = updated) -// } -// } -// .sequence -// .map { updatedComponents => -// model.copy( -// components = updatedComponents, -// referenceBounds = updatedComponents.map { c => -// c.component.bounds(c.model) -// } -// ) -// } - -// def present( -// context: UiContext[ReferenceData], -// model: ComponentGroup[ReferenceData] -// ): Outcome[ComponentFragment] = -// GroupFunctions.present(context, model.components) - -// def reflow(model: ComponentGroup[ReferenceData]): ComponentGroup[ReferenceData] = -// val reflowed: Batch[ComponentEntry[?, ReferenceData]] = model.components.map { c => -// c.copy( -// model = c.component.reflow(c.model) -// ) -// } - -// val nextOffset = GroupFunctions.calculateNextOffset[ReferenceData](model.bounds, model.layout) - -// val newComponents = reflowed.foldLeft(Batch.empty[ComponentEntry[?, ReferenceData]]) { -// (acc, entry) => -// val reflowed = entry.copy( -// offset = nextOffset(acc), -// model = entry.component.reflow(entry.model) -// ) - -// acc :+ reflowed -// } - -// val newReferenceBounds = -// newComponents.map(c => c.component.bounds(c.model).withPosition(c.offset)) - -// model.copy( -// components = newComponents, -// referenceBounds = newReferenceBounds -// ) - -// def cascade( -// model: ComponentGroup[ReferenceData], -// parentBounds: Bounds -// ): ComponentGroup[ReferenceData] = -// val newBounds: Bounds = -// GroupFunctions.calculateCascadeBounds( -// model.bounds, -// model.contentBounds, -// parentBounds, -// model.boundsType -// ) - -// model -// .copy( -// bounds = newBounds, -// components = model.components.map(_.cascade(newBounds)) -// ) +import indigo.* +import roguelikestarterkit.tiles.Tile.r +import roguelikestarterkit.ui.component.* +import roguelikestarterkit.ui.datatypes.* + +import scala.annotation.tailrec + +/** Describes a fixed arrangement of components, manages their layout, and propagates updates and + * presention calls. + */ +final case class ComponentGroup[ReferenceData] private ( + boundsType: BoundsType, + layout: ComponentLayout, + components: Batch[ComponentGroupEntry[?, ReferenceData]], + bounds: Bounds, + contentBounds: Bounds, + dirty: Boolean +): + + private def addSingle[A](entry: A)(using + c: Component[A, ReferenceData] + ): ComponentGroup[ReferenceData] = + this.copy( + components = components :+ ComponentGroupEntry(Coords.zero, entry, c), + dirty = true + ) + + def add[A](entry: A)(using c: Component[A, ReferenceData]): ComponentGroup[ReferenceData] = + addSingle(entry) + + def add[A](entries: Batch[A])(using + c: Component[A, ReferenceData] + ): ComponentGroup[ReferenceData] = + entries.foldLeft(this.copy(dirty = true)) { case (acc, next) => acc.addSingle(next) } + def add[A](entries: A*)(using c: Component[A, ReferenceData]): ComponentGroup[ReferenceData] = + add(Batch.fromSeq(entries)) + + def withBounds(value: Bounds): ComponentGroup[ReferenceData] = + this.copy(bounds = value, dirty = true) + + def withBoundsType(value: BoundsType): ComponentGroup[ReferenceData] = + value match + case BoundsType.Fixed(bounds) => + this.copy(boundsType = value, bounds = bounds, dirty = true) + + case _ => + this.copy(boundsType = value, dirty = true) + + def defaultBounds: ComponentGroup[ReferenceData] = + withBoundsType(BoundsType.default) + def fixedBounds(value: Bounds): ComponentGroup[ReferenceData] = + withBoundsType(BoundsType.Fixed(value)) + def inheritBounds: ComponentGroup[ReferenceData] = + withBoundsType(BoundsType.Inherit) + 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[ReferenceData] = + withBoundsType(BoundsType.RelativePosition(x, y)) + def relativeSize(width: Double, height: Double): ComponentGroup[ReferenceData] = + withBoundsType(BoundsType.RelativeSize(width, height)) + def offset(amountPosition: Coords, amountSize: Dimensions): ComponentGroup[ReferenceData] = + withBoundsType(BoundsType.Offset(amountPosition, amountSize)) + def offset(x: Int, y: Int, width: Int, height: Int): ComponentGroup[ReferenceData] = + offset(Coords(x, y), Dimensions(width, height)) + def offsetPosition(amount: Coords): ComponentGroup[ReferenceData] = + withBoundsType(BoundsType.OffsetPosition(amount)) + def offsetPosition(x: Int, y: Int): ComponentGroup[ReferenceData] = + offsetPosition(Coords(x, y)) + def offsetSize(amount: Dimensions): ComponentGroup[ReferenceData] = + withBoundsType(BoundsType.OffsetSize(amount)) + def offsetSize(width: Int, height: Int): ComponentGroup[ReferenceData] = + offsetSize(Dimensions(width, height)) + def dynamicBounds(width: FitMode, height: FitMode): ComponentGroup[ReferenceData] = + withBoundsType(BoundsType.Dynamic(width, height)) + + def withLayout(value: ComponentLayout): ComponentGroup[ReferenceData] = + this.copy(layout = value, dirty = true) + + def withPosition(value: Coords): ComponentGroup[ReferenceData] = + withBounds(bounds.withPosition(value)) + def moveTo(position: Coords): ComponentGroup[ReferenceData] = + withPosition(position) + def moveTo(x: Int, y: Int): ComponentGroup[ReferenceData] = + moveTo(Coords(x, y)) + def moveBy(amount: Coords): ComponentGroup[ReferenceData] = + withPosition(bounds.coords + amount) + def moveBy(x: Int, y: Int): ComponentGroup[ReferenceData] = + moveBy(Coords(x, y)) + + def withDimensions(value: Dimensions): ComponentGroup[ReferenceData] = + withBounds(bounds.withDimensions(value)) + def resizeTo(size: Dimensions): ComponentGroup[ReferenceData] = + withDimensions(size) + def resizeTo(x: Int, y: Int): ComponentGroup[ReferenceData] = + resizeTo(Dimensions(x, y)) + def resizeBy(amount: Dimensions): ComponentGroup[ReferenceData] = + withDimensions(bounds.dimensions + amount) + def resizeBy(x: Int, y: Int): ComponentGroup[ReferenceData] = + resizeBy(Dimensions(x, y)) + +object ComponentGroup: + + def apply[ReferenceData](): ComponentGroup[ReferenceData] = + ComponentGroup( + BoundsType.default, + ComponentLayout.None, + Batch.empty, + Bounds.zero, + Bounds.zero, + dirty = true + ) + + def apply[ReferenceData](boundsType: BoundsType): ComponentGroup[ReferenceData] = + ComponentGroup( + boundsType, + ComponentLayout.None, + Batch.empty, + Bounds.zero, + Bounds.zero, + dirty = true + ) + + def apply[ReferenceData](bounds: Bounds): ComponentGroup[ReferenceData] = + ComponentGroup( + BoundsType.Fixed(bounds), + ComponentLayout.None, + Batch.empty, + bounds, + Bounds.zero, + dirty = true + ) + + given [ReferenceData]: Component[ComponentGroup[ReferenceData], ReferenceData] with + + def bounds(reference: ReferenceData, model: ComponentGroup[ReferenceData]): Bounds = + model.bounds + + def updateModel( + context: UiContext[ReferenceData], + model: ComponentGroup[ReferenceData] + ): GlobalEvent => Outcome[ComponentGroup[ReferenceData]] = + case FrameTick => + updateComponents(context, model)(FrameTick).map { updated => + if model.dirty then + val reflowed: ComponentGroup[ReferenceData] = reflow(context.reference, updated) + val cascaded: ComponentGroup[ReferenceData] = cascade(reflowed, context.bounds) + val contentBounds: Bounds = + calculateContentBounds(context.reference, cascaded.components) + + cascaded.copy( + dirty = false, + contentBounds = contentBounds + ) + else updated + } + + case e => + updateComponents(context, model)(e) + + private def updateComponents[StartupData, ContextData]( + context: UiContext[ReferenceData], + model: ComponentGroup[ReferenceData] + ): GlobalEvent => Outcome[ComponentGroup[ReferenceData]] = + e => + model.components + .map { c => + c.component + .updateModel(context.copy(bounds = context.bounds.moveBy(c.offset)), c.model)(e) + .map { updated => + c.copy(model = updated) + } + } + .sequence + .map { updatedComponents => + model.copy( + components = updatedComponents + ) + } + + private def calculateContentBounds[ReferenceData]( + reference: ReferenceData, + components: Batch[ComponentGroupEntry[?, ReferenceData]] + ): Bounds = + components.foldLeft(Bounds.zero) { (acc, c) => + val bounds = c.component.bounds(reference, c.model).moveTo(c.offset) + acc.expandToInclude(bounds) + } + + def present( + context: UiContext[ReferenceData], + model: ComponentGroup[ReferenceData] + ): Outcome[ComponentFragment] = + GroupFunctions.present(context, model.components) + + def reflow( + reference: ReferenceData, + model: ComponentGroup[ReferenceData] + ): ComponentGroup[ReferenceData] = + val reflowed: Batch[ComponentGroupEntry[?, ReferenceData]] = model.components.map { c => + c.copy( + model = c.component.reflow(reference, c.model) + ) + } + + val nextOffset = + GroupFunctions.calculateNextOffset[ReferenceData](reference, model.bounds, model.layout) + + val newComponents = reflowed.foldLeft(Batch.empty[ComponentGroupEntry[?, ReferenceData]]) { + (acc, entry) => + val reflowed = entry.copy( + offset = nextOffset(acc), + model = entry.component.reflow(reference, entry.model) + ) + + acc :+ reflowed + } + + model.copy( + components = newComponents + ) + + def cascade( + model: ComponentGroup[ReferenceData], + parentBounds: Bounds + ): ComponentGroup[ReferenceData] = + val newBounds: Bounds = + GroupFunctions.calculateCascadeBounds( + model.bounds, + model.contentBounds, + parentBounds, + model.boundsType + ) + + model + .copy( + bounds = newBounds, + components = model.components.map(_.cascade(newBounds)), + dirty = true + ) diff --git a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/group/ComponentEntry.scala b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/group/ComponentGroupEntry.scala similarity index 66% rename from roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/group/ComponentEntry.scala rename to roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/group/ComponentGroupEntry.scala index e0d9c6f6..10e90d97 100644 --- a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/group/ComponentEntry.scala +++ b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/group/ComponentGroupEntry.scala @@ -8,7 +8,11 @@ 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, ReferenceData](offset: Coords, model: A, component: Component[A, ReferenceData]): +final case class ComponentGroupEntry[A, ReferenceData]( + offset: Coords, + model: A, + component: Component[A, ReferenceData] +): - def cascade(parentBounds: Bounds): ComponentEntry[A, ReferenceData] = + def cascade(parentBounds: Bounds): ComponentGroupEntry[A, ReferenceData] = this.copy(model = component.cascade(model, parentBounds)) diff --git a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/group/GroupFunctions.scala b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/group/GroupFunctions.scala index 1b35da87..105729d5 100644 --- a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/group/GroupFunctions.scala +++ b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/group/GroupFunctions.scala @@ -15,8 +15,12 @@ object GroupFunctions: private def withPadding(p: Padding): Bounds = b.moveBy(p.left, p.top).resize(b.width + p.right, b.height + p.bottom) - def calculateNextOffset[ReferenceData](reference: ReferenceData, bounds: Bounds, layout: ComponentLayout)( - components: Batch[ComponentEntry[?, ReferenceData]] + def calculateNextOffset[ReferenceData]( + reference: ReferenceData, + bounds: Bounds, + layout: ComponentLayout + )( + components: Batch[ComponentGroupEntry[?, ReferenceData]] ): Coords = layout match case ComponentLayout.None => @@ -26,7 +30,9 @@ object GroupFunctions: components .takeRight(1) .headOption - .map(c => c.offset + Coords(c.component.bounds(reference, c.model).withPadding(padding).right, 0)) + .map(c => + c.offset + Coords(c.component.bounds(reference, c.model).withPadding(padding).right, 0) + ) .getOrElse(Coords(padding.left, padding.top)) case ComponentLayout.Horizontal(padding, Overflow.Wrap) => @@ -52,7 +58,9 @@ object GroupFunctions: components .takeRight(1) .headOption - .map(c => c.offset + Coords(0, c.component.bounds(reference, c.model).withPadding(padding).bottom)) + .map(c => + c.offset + Coords(0, c.component.bounds(reference, c.model).withPadding(padding).bottom) + ) .getOrElse(Coords(padding.left, padding.top)) def calculateCascadeBounds( @@ -132,7 +140,7 @@ object GroupFunctions: def present[ReferenceData]( context: UiContext[ReferenceData], - components: Batch[ComponentEntry[?, ReferenceData]] + components: Batch[ComponentGroupEntry[?, ReferenceData]] ): Outcome[ComponentFragment] = components .map { c => diff --git a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/list/ComponentList.scala b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/list/ComponentList.scala index 6b5a8ad2..eff7dd9c 100644 --- a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/list/ComponentList.scala +++ b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/list/ComponentList.scala @@ -126,10 +126,10 @@ object ComponentList: ): Outcome[ComponentFragment] = ListFunctions.present( context, - reflow(context.reference, model) + contentReflow(context.reference, model) ) - private def reflow( + private def contentReflow( reference: ReferenceData, model: ComponentList[ReferenceData] ): Batch[ComponentListEntry[?, ReferenceData]] = 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 2fd95896..b691c805 100644 --- a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/window/WindowContent.scala +++ b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/window/WindowContent.scala @@ -8,6 +8,11 @@ import roguelikestarterkit.ComponentGroup import roguelikestarterkit.ui.datatypes.Bounds import roguelikestarterkit.ui.datatypes.UiContext +// The only difference between this and Component is the bounds method. Can we merge them? Oh, and the fact that refresh is called reflow in Component. + +/** A typeclass that confirms that some type `A` can be used as a `WindowContent` provides the + * necessary operations for that type to act as a window content. + */ trait WindowContent[A, ReferenceData]: /** Update this content's model. @@ -31,10 +36,13 @@ trait WindowContent[A, ReferenceData]: /** Called when a window has been told to refresh its content, possibly by the content itself. */ - def refresh(model: A): A + def refresh(reference: ReferenceData, model: A): A +/** Companion object for `WindowContent` */ object WindowContent: + /** A `WindowContent` instance for any A with a `Component` instance. + */ given [A, ReferenceData](using comp: Component[A, ReferenceData] ): WindowContent[A, ReferenceData] with @@ -57,5 +65,5 @@ object WindowContent: ): A = comp.cascade(model, newBounds) - def refresh(model: A): A = - comp.reflow(model) + def refresh(reference: ReferenceData, model: A): A = + comp.reflow(reference, model) diff --git a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/window/WindowManager.scala b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/window/WindowManager.scala index a01c4f28..cb7a213f 100644 --- a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/window/WindowManager.scala +++ b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/window/WindowManager.scala @@ -148,7 +148,7 @@ object WindowManager: model: WindowManagerModel[ReferenceData] ): WindowEvent => Outcome[WindowManagerModel[ReferenceData]] = case WindowEvent.Refresh(id) => - model.refresh(id) + model.refresh(id, context.reference) case WindowEvent.GiveFocusAt(position) => Outcome(model.giveFocusAndSurfaceAt(position)) diff --git a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/window/WindowManagerModel.scala b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/window/WindowManagerModel.scala index d0f59903..5c3a2fb1 100644 --- a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/window/WindowManagerModel.scala +++ b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/window/WindowManagerModel.scala @@ -73,9 +73,9 @@ final case class WindowManagerModel[ReferenceData](windows: Batch[WindowModel[?, ) ) - def refresh(id: WindowId): Outcome[WindowManagerModel[ReferenceData]] = + def refresh(id: WindowId, reference: ReferenceData): Outcome[WindowManagerModel[ReferenceData]] = Outcome( - this.copy(windows = windows.map(w => if w.id == id then w.refresh else w)) + this.copy(windows = windows.map(w => if w.id == id then w.refresh(reference) else w)) ) def update( diff --git a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/window/WindowModel.scala b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/window/WindowModel.scala index 66034649..1fc0cf0a 100644 --- a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/window/WindowModel.scala +++ b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/window/WindowModel.scala @@ -131,8 +131,8 @@ final case class WindowModel[A, ReferenceData]( def isClosed: Boolean = state == WindowState.Closed - def refresh: WindowModel[A, ReferenceData] = - this.copy(contentModel = windowContent.refresh(contentModel)) + def refresh(reference: ReferenceData): WindowModel[A, ReferenceData] = + this.copy(contentModel = windowContent.refresh(reference, contentModel)) object WindowModel: diff --git a/roguelike-starterkit/src/test/scala/roguelikestarterkit/ui/components/group/ComponentGroupTests.scala b/roguelike-starterkit/src/test/scala/roguelikestarterkit/ui/components/group/ComponentGroupTests.scala index 20e192a2..98362b30 100644 --- a/roguelike-starterkit/src/test/scala/roguelikestarterkit/ui/components/group/ComponentGroupTests.scala +++ b/roguelike-starterkit/src/test/scala/roguelikestarterkit/ui/components/group/ComponentGroupTests.scala @@ -6,234 +6,239 @@ import roguelikestarterkit.Coords import roguelikestarterkit.UiContext import roguelikestarterkit.ui.component.* -class ComponentGroupTests extends munit.FunSuite { - - // given Component[String, Unit] with - // def bounds(model: String): Bounds = - // Bounds(0, 0, model.length, 1) - - // def updateModel( - // context: UiContext[Unit], - // model: String - // ): GlobalEvent => Outcome[String] = - // _ => Outcome(model) - - // def present( - // context: UiContext[Unit], - // model: String - // ): Outcome[ComponentFragment] = - // Outcome(ComponentFragment.empty) - - // def reflow(model: String): String = - // model - - // def cascade(model: String, parentBounds: Bounds): String = - // model - - // test("reflow should reapply the layout to all existing components") { - // val group: ComponentGroup[Unit] = - // ComponentGroup() - // .withLayout( - // ComponentLayout - // .Horizontal(Padding(5), Overflow.Wrap) - // ) - // .withBoundsType(BoundsType.Fixed(Bounds(0, 0, 100, 100))) - // .add("abc", "def") - - // val actual = group.reflow.components.toList.map(_.offset) - - // val expected = - // List( - // Coords(5, 5), - // Coords(18, 5) // It's like this: 5 |3| 5.5 |3| 5 - // ) - - // assertEquals(actual, expected) - // } - - // test("Calculate the next offset - vertical, padding 0") { - // val group = ComponentGroup(Bounds(0, 0, 10, 5)) - // .withLayout(ComponentLayout.Vertical(Padding(0))) - // .add("abc", "def") - - // val actual = - // group.components.map(_.offset).toList - - // val expected = - // List( - // Coords(0, 0), - // Coords(0, 1) - // ) - - // assertEquals(actual, expected) - // } - - // test("Calculate the next offset - vertical, padding 5") { - // val group = ComponentGroup(Bounds(0, 0, 10, 5)) - // .withLayout(ComponentLayout.Vertical(Padding(5))) - // .add("abc", "def") - - // val actual = - // group.components.map(_.offset).toList - - // val expected = - // List( - // Coords(5, 5), - // Coords(5, 5 + 1 + 5 + 5) - // ) - - // assertEquals(actual, expected) - // } - - // test("Calculate the next offset - vertical, padding top=5") { - // val group = ComponentGroup(Bounds(0, 0, 10, 5)) - // .withLayout( - // ComponentLayout.Vertical(Padding(5, 0, 0, 0)) - // ) - // .add("abc", "def") - - // val actual = - // group.components.map(_.offset).toList - - // val expected = - // List( - // Coords(0, 5), - // Coords(0, 5 + 1 + 5) - // ) - - // assertEquals(actual, expected) - // } - - // test("Calculate the next offset - horizontal, padding 0, hidden") { - // val group = ComponentGroup(Bounds(0, 0, 5, 5)) - // .withLayout( - // ComponentLayout - // .Horizontal(Padding(0), Overflow.Hidden) - // ) - // .add("abc", "def") - - // val actual = - // group.components.map(_.offset).toList - - // val expected = - // List( - // Coords(0, 0), - // Coords(3, 0) - // ) - - // assertEquals(actual, expected) - // } - - // test("Calculate the next offset - horizontal, padding 5, hidden") { - // val group = ComponentGroup(Bounds(0, 0, 5, 5)) - // .withLayout( - // ComponentLayout - // .Horizontal(Padding(5), Overflow.Hidden) - // ) - // .add("abc", "def") - - // val actual = - // group.components.map(_.offset).toList - - // val expected = - // List( - // Coords(5, 5), - // Coords(5 + 3 + 5 + 5, 5) - // ) - - // assertEquals(actual, expected) - // } - - // test("Calculate the next offset - horizontal, padding left=5, hidden") { - // val group = ComponentGroup(Bounds(0, 0, 5, 5)) - // .withLayout( - // ComponentLayout - // .Horizontal(Padding(0, 0, 0, 5), Overflow.Hidden) - // ) - // .add("abc", "def") - - // val actual = - // group.components.map(_.offset).toList - - // val expected = - // List( - // Coords(5, 0), - // Coords(5 + 3 + 5, 0) - // ) - - // assertEquals(actual, expected) - // } - - // test("Calculate the next offset - horizontal, padding 0, wrap") { - // val group = ComponentGroup(Bounds(0, 0, 5, 5)) - // .withLayout( - // ComponentLayout - // .Horizontal(Padding(0), Overflow.Wrap) - // ) - // .add("abc", "def") - - // val actual = - // group.components.map(_.offset).toList - - // val expected = - // List( - // Coords(0, 0), - // Coords(0, 1) - // ) - - // assertEquals(actual, expected) - // } - - // test("Calculate the next offset - horizontal, padding 5, wrap") { - // val group = ComponentGroup(Bounds(0, 0, 5, 5)) - // .withLayout( - // ComponentLayout - // .Horizontal(Padding(5), Overflow.Wrap) - // ) - // .add("abc", "def") - - // val actual = - // group.components.map(_.offset).toList - - // val expected = - // List( - // Coords(5, 5), - // Coords(5, 5 + 1 + 5) - // ) - - // assertEquals(actual, expected) - // } - - // test("Calculate the next offset - horizontal, padding left=5 top=2, wrap") { - // val group = ComponentGroup(Bounds(0, 0, 3, 5)) - // .withLayout( - // ComponentLayout - // .Horizontal(Padding(2, 0, 0, 5), Overflow.Wrap) - // ) - // .add("abc", "def") - - // val actual = - // group.components.map(_.offset).toList - - // val expected = - // List( - // Coords(5, 2), - // Coords(5, 2 + 1) - // ) - - // assertEquals(actual, expected) - // } - - // test("Cascade should snap to width of parent and height of contents by default.") { - // val group: ComponentGroup[Unit] = - // ComponentGroup() - // .withLayout( - // ComponentLayout.Vertical(Padding(0, 0, 1, 0)) - // ) - // .add("abc", "def") - // .cascade(Bounds(0, 0, 100, 100)) - - // assertEquals(group.contentBounds, Bounds(0, 0, 3, 3)) - // assertEquals(group.bounds, Bounds(0, 0, 100, 3)) - // } -} +class ComponentGroupTests extends munit.FunSuite: + + given Component[String, Unit] with + def bounds(reference: Unit, model: String): Bounds = + Bounds(0, 0, model.length, 1) + + def updateModel( + context: UiContext[Unit], + model: String + ): GlobalEvent => Outcome[String] = + _ => Outcome(model) + + def present( + context: UiContext[Unit], + model: String + ): Outcome[ComponentFragment] = + Outcome(ComponentFragment.empty) + + def reflow(reference: Unit, model: String): String = + model + + def cascade(model: String, parentBounds: Bounds): String = + model + + test("reflow should reapply the layout to all existing components") { + val group: ComponentGroup[Unit] = + ComponentGroup() + .withLayout( + ComponentLayout + .Horizontal(Padding(5), Overflow.Wrap) + ) + .withBoundsType(BoundsType.Fixed(Bounds(0, 0, 100, 100))) + .add("abc", "def") + + val actual = + summon[Component[ComponentGroup[Unit], Unit]] + .reflow((), group) + .components + .toList + .map(_.offset) + + val expected = + List( + Coords(5, 5), + Coords(18, 5) // It's like this: 5 |3| 5.5 |3| 5 + ) + + assertEquals(actual, expected) + } + + test("Calculate the next offset - vertical, padding 0") { + val group = ComponentGroup(Bounds(0, 0, 10, 5)) + .withLayout(ComponentLayout.Vertical(Padding(0))) + .add("abc", "def") + + val actual = + group.components.map(_.offset).toList + + val expected = + List( + Coords(0, 0), + Coords(0, 1) + ) + + assertEquals(actual, expected) + } + + test("Calculate the next offset - vertical, padding 5") { + val group = ComponentGroup(Bounds(0, 0, 10, 5)) + .withLayout(ComponentLayout.Vertical(Padding(5))) + .add("abc", "def") + + val actual = + group.components.map(_.offset).toList + + val expected = + List( + Coords(5, 5), + Coords(5, 5 + 1 + 5 + 5) + ) + + assertEquals(actual, expected) + } + + test("Calculate the next offset - vertical, padding top=5") { + val group = ComponentGroup(Bounds(0, 0, 10, 5)) + .withLayout( + ComponentLayout.Vertical(Padding(5, 0, 0, 0)) + ) + .add("abc", "def") + + val actual = + group.components.map(_.offset).toList + + val expected = + List( + Coords(0, 5), + Coords(0, 5 + 1 + 5) + ) + + assertEquals(actual, expected) + } + + test("Calculate the next offset - horizontal, padding 0, hidden") { + val group = ComponentGroup(Bounds(0, 0, 5, 5)) + .withLayout( + ComponentLayout + .Horizontal(Padding(0), Overflow.Hidden) + ) + .add("abc", "def") + + val actual = + group.components.map(_.offset).toList + + val expected = + List( + Coords(0, 0), + Coords(3, 0) + ) + + assertEquals(actual, expected) + } + + test("Calculate the next offset - horizontal, padding 5, hidden") { + val group = ComponentGroup(Bounds(0, 0, 5, 5)) + .withLayout( + ComponentLayout + .Horizontal(Padding(5), Overflow.Hidden) + ) + .add("abc", "def") + + val actual = + group.components.map(_.offset).toList + + val expected = + List( + Coords(5, 5), + Coords(5 + 3 + 5 + 5, 5) + ) + + assertEquals(actual, expected) + } + + test("Calculate the next offset - horizontal, padding left=5, hidden") { + val group = ComponentGroup(Bounds(0, 0, 5, 5)) + .withLayout( + ComponentLayout + .Horizontal(Padding(0, 0, 0, 5), Overflow.Hidden) + ) + .add("abc", "def") + + val actual = + group.components.map(_.offset).toList + + val expected = + List( + Coords(5, 0), + Coords(5 + 3 + 5, 0) + ) + + assertEquals(actual, expected) + } + + test("Calculate the next offset - horizontal, padding 0, wrap") { + val group = ComponentGroup(Bounds(0, 0, 5, 5)) + .withLayout( + ComponentLayout + .Horizontal(Padding(0), Overflow.Wrap) + ) + .add("abc", "def") + + val actual = + group.components.map(_.offset).toList + + val expected = + List( + Coords(0, 0), + Coords(0, 1) + ) + + assertEquals(actual, expected) + } + + test("Calculate the next offset - horizontal, padding 5, wrap") { + val group = ComponentGroup(Bounds(0, 0, 5, 5)) + .withLayout( + ComponentLayout + .Horizontal(Padding(5), Overflow.Wrap) + ) + .add("abc", "def") + + val actual = + group.components.map(_.offset).toList + + val expected = + List( + Coords(5, 5), + Coords(5, 5 + 1 + 5) + ) + + assertEquals(actual, expected) + } + + test("Calculate the next offset - horizontal, padding left=5 top=2, wrap") { + val group = ComponentGroup(Bounds(0, 0, 3, 5)) + .withLayout( + ComponentLayout + .Horizontal(Padding(2, 0, 0, 5), Overflow.Wrap) + ) + .add("abc", "def") + + val actual = + group.components.map(_.offset).toList + + val expected = + List( + Coords(5, 2), + Coords(5, 2 + 1) + ) + + assertEquals(actual, expected) + } + + test("Cascade should snap to width of parent and height of contents by default.") { + val group: ComponentGroup[Unit] = + summon[Component[ComponentGroup[Unit], Unit]] + .cascade( + ComponentGroup() + .withLayout(ComponentLayout.Vertical(Padding(0))) + .add("abc", "def"), + Bounds(0, 0, 100, 100) + ) + + assertEquals(group.contentBounds, Bounds(0, 0, 3, 3)) + assertEquals(group.bounds, Bounds(0, 0, 100, 3)) + } diff --git a/roguelike-starterkit/src/test/scala/roguelikestarterkit/ui/window/WindowModelTests.scala b/roguelike-starterkit/src/test/scala/roguelikestarterkit/ui/window/WindowModelTests.scala index feec6204..892a430e 100644 --- a/roguelike-starterkit/src/test/scala/roguelikestarterkit/ui/window/WindowModelTests.scala +++ b/roguelike-starterkit/src/test/scala/roguelikestarterkit/ui/window/WindowModelTests.scala @@ -9,248 +9,247 @@ import roguelikestarterkit.ui.components.group.Overflow import roguelikestarterkit.ui.components.group.Padding import roguelikestarterkit.ui.datatypes.* -class WindowModelTests extends munit.FunSuite {} - - // test("model cascades bounds changes") { - - // given WindowContent[Bounds, Unit] with - // def updateModel( - // context: UiContext[Unit], - // model: Bounds - // ): GlobalEvent => Outcome[Bounds] = - // _ => Outcome(model) - - // def present( - // context: UiContext[Unit], - // model: Bounds - // ): Outcome[Layer] = - // Outcome(Layer.empty) - - // def cascade(model: Bounds, newBounds: Bounds): Bounds = - // newBounds - - // def refresh(model: Bounds): Bounds = - // model - - // val charSheet = - // CharSheet( - // AssetName("test"), - // Size(10), - // Batch.empty, - // FontKey("test") - // ) - - // val bounds = - // Bounds(0, 0, 10, 10) - - // val model: WindowModel[Bounds, Unit] = - // WindowModel( - // WindowId("test"), - // charSheet, - // bounds - // ) - - // assertEquals(model.withBounds(Bounds(10, 10, 20, 20)).contentModel, Bounds(11, 11, 18, 18)) - // } - - // val charSheet = - // CharSheet( - // AssetName("test"), - // Size(10), - // Batch.empty, - // FontKey("test") - // ) - - // val group = - // ComponentGroup() - // .withLayout(ComponentLayout.Horizontal(Padding(5), Overflow.Wrap)) - // .withBoundsType(BoundsType.Fixed(Bounds(0, 0, 100, 100))) - // .add("abc") - - // test("model cascades bounds changes to component group - BoundsType.Fixed") { - // val model: WindowModel[ComponentGroup[Unit], Unit] = - // WindowModel( - // WindowId("test"), - // charSheet, - // group - // ).withBounds(Bounds(0, 0, 40, 40)) - - // assertEquals(model.contentModel.bounds, Bounds(0, 0, 100, 100)) - // } - - // test("model cascades bounds changes to component group - BoundsType.Inherit") { - // val model: WindowModel[ComponentGroup[Unit], Unit] = - // WindowModel( - // WindowId("test"), - // charSheet, - // group.inheritBounds - // ).withBounds(Bounds(0, 0, 40, 40)) - - // assertEquals(model.contentModel.bounds, Bounds(1, 1, 38, 38)) - // } - - // test("model cascades bounds changes to component group - BoundsType.Relative") { - // val model: WindowModel[ComponentGroup[Unit], Unit] = - // WindowModel( - // WindowId("test"), - // charSheet, - // group.relative(0.5, 0.5, 0.25, 0.25) - // ).withBounds(Bounds(0, 0, 40, 40)) - - // assertEquals(model.contentModel.bounds, Bounds(19, 19, 9, 9)) - // } - - // test("model cascades bounds changes to component group - BoundsType.Relative 100%") { - // val model: WindowModel[ComponentGroup[Unit], Unit] = - // WindowModel( - // WindowId("test"), - // charSheet, - // group.relative(0, 0, 1, 1) - // ).withBounds(Bounds(0, 0, 40, 40)) - - // assertEquals(model.contentModel.bounds, Bounds(0, 0, 38, 38)) - // } - - // test("model cascades bounds changes to component group - BoundsType.RelativePosition") { - // val model: WindowModel[ComponentGroup[Unit], Unit] = - // WindowModel( - // WindowId("test"), - // charSheet, - // group.relativePosition(0.5, 0.5) - // ).withBounds(Bounds(0, 0, 40, 40)) - - // assertEquals(model.contentModel.bounds, Bounds(19, 19, 100, 100)) - // } - - // test("model cascades bounds changes to component group - BoundsType.RelativeSize") { - // val model: WindowModel[ComponentGroup[Unit], Unit] = - // WindowModel( - // WindowId("test"), - // charSheet, - // group.relativeSize(0.5, 0.5) - // ).withBounds(Bounds(0, 0, 40, 40)) - - // assertEquals(model.contentModel.bounds, Bounds(0, 0, 19, 19)) - // } - - // test("model cascades bounds changes to component group - BoundsType.Offset") { - // val model: WindowModel[ComponentGroup[Unit], Unit] = - // WindowModel( - // WindowId("test"), - // charSheet, - // group.offset(5, 5, -5, -5) - // ).withBounds(Bounds(0, 0, 40, 40)) - - // assertEquals(model.contentModel.bounds, Bounds(6, 6, 33, 33)) - // } - - // test("model cascades bounds changes to component group - BoundsType.OffsetPosition") { - // val model: WindowModel[ComponentGroup[Unit], Unit] = - // WindowModel( - // WindowId("test"), - // charSheet, - // group.offsetPosition(5, 5) - // ).withBounds(Bounds(0, 0, 40, 40)) - - // assertEquals(model.contentModel.bounds, Bounds(6, 6, 100, 100)) - // } - - // test("model cascades bounds changes to component group - BoundsType.OffsetSize") { - // val model: WindowModel[ComponentGroup[Unit], Unit] = - // WindowModel( - // WindowId("test"), - // charSheet, - // group.offsetSize(-5, -5) - // ).withBounds(Bounds(0, 0, 40, 40)) - - // assertEquals(model.contentModel.bounds, Bounds(0, 0, 33, 33)) - // } - - // test("model cascades bounds changes to nested component groups") { - - // val group = - // ComponentGroup(Bounds(0, 0, 100, 100)) - // .withLayout(ComponentLayout.Horizontal(Padding(5), Overflow.Wrap)) - // .inheritBounds - // .add( - // ComponentGroup(Bounds(0, 0, 100, 100)) - // .withLayout(ComponentLayout.Horizontal(Padding(5), Overflow.Wrap)) - // .offset(2, 4, -2, -4) - // .add(Bounds(0, 0, 0, 0)) - // ) - - // val model: WindowModel[ComponentGroup[Unit], Unit] = - // WindowModel( - // WindowId("test"), - // charSheet, - // group.inheritBounds - // ).withBounds(Bounds(0, 0, 40, 40)) - - // // Window cascades to top level component group - // assertEquals(model.contentModel.bounds, Bounds(1, 1, 38, 38)) - - // model.contentModel.components.headOption match - // case None => - // fail("No sub components found") - - // case Some(value) => - // val cg = value.model.asInstanceOf[ComponentGroup[Unit]] - - // // top comp group cascades to next level. - // assertEquals(cg.bounds, Bounds(3, 5, 36, 34)) - - // cg.components.headOption match - // case None => - // fail("No sub components found 2") - - // case Some(value) => - // val b = value.model.asInstanceOf[Bounds] - - // // second comp group cascades to next level. - // assertEquals(b.asInstanceOf[Bounds], Bounds(3, 5, 36, 34)) - - // } - - // given Component[String, Unit] = new Component[String, Unit] { - // def bounds(model: String): Bounds = Bounds(0, 0, model.length, 1) - - // def updateModel( - // context: UiContext[Unit], - // model: String - // ): GlobalEvent => Outcome[String] = - // _ => Outcome(model) - - // def present( - // context: UiContext[Unit], - // model: String - // ): Outcome[ComponentFragment] = - // Outcome(ComponentFragment.empty) - - // def reflow(model: String): String = - // model - - // def cascade(model: String, parentBounds: Bounds): String = - // model - // } - - // given Component[Bounds, Unit] = new Component[Bounds, Unit] { - // def bounds(model: Bounds): Bounds = model - - // def updateModel( - // context: UiContext[Unit], - // model: Bounds - // ): GlobalEvent => Outcome[Bounds] = - // _ => Outcome(model) - - // def present( - // context: UiContext[Unit], - // model: Bounds - // ): Outcome[ComponentFragment] = - // Outcome(ComponentFragment.empty) - - // def reflow(model: Bounds): Bounds = - // model - - // def cascade(model: Bounds, parentBounds: Bounds): Bounds = - // parentBounds - // } +class WindowModelTests extends munit.FunSuite: + + test("model cascades bounds changes") { + + given WindowContent[Bounds, Unit] with + def updateModel( + context: UiContext[Unit], + model: Bounds + ): GlobalEvent => Outcome[Bounds] = + _ => Outcome(model) + + def present( + context: UiContext[Unit], + model: Bounds + ): Outcome[Layer] = + Outcome(Layer.empty) + + def cascade(model: Bounds, newBounds: Bounds): Bounds = + newBounds + + def refresh(reference: Unit, model: Bounds): Bounds = + model + + val charSheet = + CharSheet( + AssetName("test"), + Size(10), + Batch.empty, + FontKey("test") + ) + + val bounds = + Bounds(0, 0, 10, 10) + + val model: WindowModel[Bounds, Unit] = + WindowModel( + WindowId("test"), + charSheet, + bounds + ) + + assertEquals(model.withBounds(Bounds(10, 10, 20, 20)).contentModel, Bounds(11, 11, 18, 18)) + } + + val charSheet = + CharSheet( + AssetName("test"), + Size(10), + Batch.empty, + FontKey("test") + ) + + val group = + ComponentGroup() + .withLayout(ComponentLayout.Horizontal(Padding(5), Overflow.Wrap)) + .withBoundsType(BoundsType.Fixed(Bounds(0, 0, 100, 100))) + .add("abc") + + test("model cascades bounds changes to component group - BoundsType.Fixed") { + val model: WindowModel[ComponentGroup[Unit], Unit] = + WindowModel( + WindowId("test"), + charSheet, + group + ).withBounds(Bounds(0, 0, 40, 40)) + + assertEquals(model.contentModel.bounds, Bounds(0, 0, 100, 100)) + } + + test("model cascades bounds changes to component group - BoundsType.Inherit") { + val model: WindowModel[ComponentGroup[Unit], Unit] = + WindowModel( + WindowId("test"), + charSheet, + group.inheritBounds + ).withBounds(Bounds(0, 0, 40, 40)) + + assertEquals(model.contentModel.bounds, Bounds(1, 1, 38, 38)) + } + + test("model cascades bounds changes to component group - BoundsType.Relative") { + val model: WindowModel[ComponentGroup[Unit], Unit] = + WindowModel( + WindowId("test"), + charSheet, + group.relative(0.5, 0.5, 0.25, 0.25) + ).withBounds(Bounds(0, 0, 40, 40)) + + assertEquals(model.contentModel.bounds, Bounds(19, 19, 9, 9)) + } + + test("model cascades bounds changes to component group - BoundsType.Relative 100%") { + val model: WindowModel[ComponentGroup[Unit], Unit] = + WindowModel( + WindowId("test"), + charSheet, + group.relative(0, 0, 1, 1) + ).withBounds(Bounds(0, 0, 40, 40)) + + assertEquals(model.contentModel.bounds, Bounds(0, 0, 38, 38)) + } + + test("model cascades bounds changes to component group - BoundsType.RelativePosition") { + val model: WindowModel[ComponentGroup[Unit], Unit] = + WindowModel( + WindowId("test"), + charSheet, + group.relativePosition(0.5, 0.5) + ).withBounds(Bounds(0, 0, 40, 40)) + + assertEquals(model.contentModel.bounds, Bounds(19, 19, 100, 100)) + } + + test("model cascades bounds changes to component group - BoundsType.RelativeSize") { + val model: WindowModel[ComponentGroup[Unit], Unit] = + WindowModel( + WindowId("test"), + charSheet, + group.relativeSize(0.5, 0.5) + ).withBounds(Bounds(0, 0, 40, 40)) + + assertEquals(model.contentModel.bounds, Bounds(0, 0, 19, 19)) + } + + test("model cascades bounds changes to component group - BoundsType.Offset") { + val model: WindowModel[ComponentGroup[Unit], Unit] = + WindowModel( + WindowId("test"), + charSheet, + group.offset(5, 5, -5, -5) + ).withBounds(Bounds(0, 0, 40, 40)) + + assertEquals(model.contentModel.bounds, Bounds(6, 6, 33, 33)) + } + + test("model cascades bounds changes to component group - BoundsType.OffsetPosition") { + val model: WindowModel[ComponentGroup[Unit], Unit] = + WindowModel( + WindowId("test"), + charSheet, + group.offsetPosition(5, 5) + ).withBounds(Bounds(0, 0, 40, 40)) + + assertEquals(model.contentModel.bounds, Bounds(6, 6, 100, 100)) + } + + test("model cascades bounds changes to component group - BoundsType.OffsetSize") { + val model: WindowModel[ComponentGroup[Unit], Unit] = + WindowModel( + WindowId("test"), + charSheet, + group.offsetSize(-5, -5) + ).withBounds(Bounds(0, 0, 40, 40)) + + assertEquals(model.contentModel.bounds, Bounds(0, 0, 33, 33)) + } + + test("model cascades bounds changes to nested component groups") { + + val group = + ComponentGroup(Bounds(0, 0, 100, 100)) + .withLayout(ComponentLayout.Horizontal(Padding(5), Overflow.Wrap)) + .inheritBounds + .add( + ComponentGroup(Bounds(0, 0, 100, 100)) + .withLayout(ComponentLayout.Horizontal(Padding(5), Overflow.Wrap)) + .offset(2, 4, -2, -4) + .add(Bounds(0, 0, 0, 0)) + ) + + val model: WindowModel[ComponentGroup[Unit], Unit] = + WindowModel( + WindowId("test"), + charSheet, + group.inheritBounds + ).withBounds(Bounds(0, 0, 40, 40)) + + // Window cascades to top level component group + assertEquals(model.contentModel.bounds, Bounds(1, 1, 38, 38)) + + model.contentModel.components.headOption match + case None => + fail("No sub components found") + + case Some(value) => + val cg = value.model.asInstanceOf[ComponentGroup[Unit]] + + // top comp group cascades to next level. + assertEquals(cg.bounds, Bounds(3, 5, 36, 34)) + + cg.components.headOption match + case None => + fail("No sub components found 2") + + case Some(value) => + val b = value.model.asInstanceOf[Bounds] + + // second comp group cascades to next level. + assertEquals(b.asInstanceOf[Bounds], Bounds(3, 5, 36, 34)) + + } + + given Component[String, Unit] with + def bounds(reference: Unit, model: String): Bounds = + Bounds(0, 0, model.length, 1) + + def updateModel( + context: UiContext[Unit], + model: String + ): GlobalEvent => Outcome[String] = + _ => Outcome(model) + + def present( + context: UiContext[Unit], + model: String + ): Outcome[ComponentFragment] = + Outcome(ComponentFragment.empty) + + def reflow(reference: Unit, model: String): String = + model + + def cascade(model: String, parentBounds: Bounds): String = + model + + given Component[Bounds, Unit] with + def bounds(reference: Unit, model: Bounds): Bounds = model + + def updateModel( + context: UiContext[Unit], + model: Bounds + ): GlobalEvent => Outcome[Bounds] = + _ => Outcome(model) + + def present( + context: UiContext[Unit], + model: Bounds + ): Outcome[ComponentFragment] = + Outcome(ComponentFragment.empty) + + def reflow(reference: Unit, model: Bounds): Bounds = + model + + def cascade(model: Bounds, parentBounds: Bounds): Bounds = + parentBounds