diff --git a/demo/src/main/scala/demo/ComponentsWindow2.scala b/demo/src/main/scala/demo/ComponentsWindow2.scala index a218b766..a73af76e 100644 --- a/demo/src/main/scala/demo/ComponentsWindow2.scala +++ b/demo/src/main/scala/demo/ComponentsWindow2.scala @@ -25,7 +25,12 @@ object ComponentsWindow2: // ) // .withBoundsType(BoundsType.Fixed(Bounds(0, 0, 20, 2))) ) - .withBoundsType(BoundsType.Fixed(Bounds(0, 0, 20, 2))) + // .add( + // Label("label 1", Label.Theme(charSheet)), + // Label("label 2", Label.Theme(charSheet)), + // Label("label 3", Label.Theme(charSheet)) + // ) + // .withBoundsType(BoundsType.Fixed(Bounds(0, 0, 20, 2))) // .add( // ComponentGroup() // .withLayout(ComponentLayout.Horizontal(Padding(0, 1, 0, 0))) @@ -49,26 +54,26 @@ object ComponentsWindow2: // } // .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)) + // ComponentList(Bounds(0, 0, 20, 8)) { (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)) + // ) ) .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 cca38d1e..e83a7b4f 100644 --- a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/component/Component.scala +++ b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/component/Component.scala @@ -28,11 +28,7 @@ trait Component[A, ReferenceData]: ): Outcome[ComponentFragment] /** 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. + * should reflow/refresh it's contents - whatever that means in the context of this component + * type. */ - 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. - */ - def cascade(model: A, parentBounds: Bounds): A + def refresh(reference: ReferenceData, model: A, parentBounds: Bounds): A 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 c58a1189..14ebdb7a 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,6 @@ trait StatelessComponent[A, ReferenceData] extends Component[A, ReferenceData]: model: A ): Outcome[ComponentFragment] - def reflow(reference: ReferenceData, model: A): A = model - def cascade(model: A, parentBounds: Bounds): A = model + def refresh(reference: ReferenceData, 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 23f5de58..998631e5 100644 --- a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/Input.scala +++ b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/Input.scala @@ -327,10 +327,7 @@ object Input: context.running ) - def reflow(reference: ReferenceData, model: Input): Input = - model - - def cascade(model: Input, parentBounds: Bounds): Input = + def refresh(reference: ReferenceData, model: Input, parentBounds: Bounds): Input = model final case class Theme( diff --git a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/common/ComponentEntry.scala b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/common/ComponentEntry.scala index 6593e687..21c96d73 100644 --- a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/common/ComponentEntry.scala +++ b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/common/ComponentEntry.scala @@ -12,7 +12,4 @@ final case class ComponentEntry[A, ReferenceData]( offset: Coords, model: A, component: Component[A, ReferenceData] -): - - def cascade(parentBounds: Bounds): ComponentEntry[A, ReferenceData] = - this.copy(model = component.cascade(model, parentBounds)) +) diff --git a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/group/BoundsType.scala b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/group/BoundsType.scala index d39a8114..c2a0d65e 100644 --- a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/group/BoundsType.scala +++ b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/group/BoundsType.scala @@ -6,49 +6,33 @@ import roguelikestarterkit.ui.datatypes.Dimensions /** Describes how a ComponentGroup responds to changes in its parents bounds. */ -enum BoundsType: - - /** The component group ignores parent bounds changes - */ - case Fixed(bounds: Bounds) - - /** The component group uses its parents bounds - */ - case Inherit - - /** The component group positions and resizes itself within / based on the parents bounds - * according to percentages expressed as a value from 0.0 to 1.0, e.g. 50% is 0.5 - */ - case Relative(x: Double, y: Double, width: Double, height: Double) - - /** The component group positions itself within the parents bounds according to percentages - * expressed as a value from 0.0 to 1.0, e.g. 50% is 0.5 - */ - case RelativePosition(x: Double, y: Double) - - /** The component group resizes itself based on the parents bounds according to percentages - * expressed as a value from 0.0 to 1.0, e.g. 50% is 0.5 - */ - case RelativeSize(width: Double, height: Double) - - /** The component group positions and resizes itself within / based on the parents bounds, offset - * by the amounts given. - */ - case Offset(coords: Coords, dimensions: Dimensions) - - /** The component group positions itself within the parents bounds, offset by the amount given. - */ - case OffsetPosition(coords: Coords) - - /** The component group resizes itself based on the parents bounds, offset by the amount given. - */ - case OffsetSize(dimensions: Dimensions) - - /** The component group bases its size on some aspect of its contents or the available space - */ - case Dynamic(width: FitMode, height: FitMode) +final case class BoundsType(width: FitMode, height: FitMode) object BoundsType: val default: BoundsType = - BoundsType.Dynamic(FitMode.Available, FitMode.Content) + BoundsType(FitMode.Available, FitMode.Content) + + def fixed(dimensions: Dimensions): BoundsType = + BoundsType( + FitMode.Fixed(dimensions.width), + FitMode.Fixed(dimensions.height) + ) + + def inherit: BoundsType = + BoundsType(FitMode.Available, FitMode.Available) + + def fit: BoundsType = + BoundsType(FitMode.Content, FitMode.Content) + + def halfHorizontal: BoundsType = + BoundsType( + FitMode.Relative(0.5), + FitMode.Available + ) + + def halfVertical: BoundsType = + BoundsType( + FitMode.Available, + FitMode.Relative(0.5) + ) 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 d0dacd7a..755cdf14 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 @@ -19,9 +19,10 @@ final case class ComponentGroup[ReferenceData] private[group] ( boundsType: BoundsType, layout: ComponentLayout, components: Batch[ComponentEntry[?, ReferenceData]], - bounds: Bounds, - contentBounds: Bounds, - dirty: Boolean + // Internal + bounds: Bounds, // The actual cached bounds of the group + contentBounds: Bounds, // The calculated and cached bounds of the content + dirty: Boolean // Whether the groups content needs to be refreshed, and it's bounds recalculated ): private def addSingle[A](entry: A)(using @@ -46,65 +47,11 @@ final case class ComponentGroup[ReferenceData] private[group] ( 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)) + this.copy(boundsType = value, dirty = true) 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] = @@ -127,12 +74,12 @@ object ComponentGroup: dirty = true ) - def apply[ReferenceData](bounds: Bounds): ComponentGroup[ReferenceData] = + def apply[ReferenceData](dimensions: Dimensions): ComponentGroup[ReferenceData] = ComponentGroup( - BoundsType.Fixed(bounds), + BoundsType.fixed(dimensions), ComponentLayout.Horizontal(Padding.zero, Overflow.Wrap), Batch.empty, - bounds, + Bounds(dimensions), Bounds.zero, dirty = true ) @@ -147,45 +94,15 @@ object ComponentGroup: model: ComponentGroup[ReferenceData] ): GlobalEvent => Outcome[ComponentGroup[ReferenceData]] = case FrameTick => + // Sub-groups will naturally refresh themselves as needed updateComponents(context, model)(FrameTick).map { updated => - if model.dirty then - refreshLayout(context.reference, context.bounds, updated) + if model.dirty then refresh(context.reference, updated, context.bounds) else updated } case e => updateComponents(context, model)(e) - private[group] def refreshLayout( - reference: ReferenceData, - parentBounds: Bounds, - model: ComponentGroup[ReferenceData] - ) = - val reflowed: ComponentGroup[ReferenceData] = reflow(reference, model) - val cascaded: ComponentGroup[ReferenceData] = cascade(reflowed, parentBounds) - val contentBounds: Bounds = - calculateContentBounds(reference, cascaded.components) - - val updatedBounds = - cascaded.boundsType match - case BoundsType.Dynamic(FitMode.Content, FitMode.Content) => - cascaded.bounds.withDimensions(contentBounds.dimensions) - - case BoundsType.Dynamic(FitMode.Content, _) => - cascaded.bounds.withWidth(contentBounds.width) - - case BoundsType.Dynamic(_, FitMode.Content) => - cascaded.bounds.withHeight(contentBounds.height) - - case _ => - cascaded.bounds - - cascaded.copy( - dirty = false, - contentBounds = contentBounds, - bounds = updatedBounds - ) - private def updateComponents[StartupData, ContextData]( context: UiContext[ReferenceData], model: ComponentGroup[ReferenceData] @@ -212,132 +129,111 @@ object ComponentGroup: ): Outcome[ComponentFragment] = ContainerLikeFunctions.present(context, model.components) - def reflow( + def refresh( reference: ReferenceData, - model: ComponentGroup[ReferenceData] + model: ComponentGroup[ReferenceData], + parentBounds: Bounds ): ComponentGroup[ReferenceData] = - val reflowed: Batch[ComponentEntry[?, ReferenceData]] = model.components.map { c => - c.copy( - model = c.component.reflow(reference, c.model) - ) - } - val nextOffset = - ContainerLikeFunctions.calculateNextOffset[ReferenceData](model.bounds, model.layout) + // First, calculate the bounds without content + val boundsWithoutContent = + model.boundsType match + case BoundsType(FitMode.Available, FitMode.Available) => + parentBounds + + case BoundsType(FitMode.Available, FitMode.Content) => + parentBounds.withHeight(0) + + case BoundsType(FitMode.Available, FitMode.Fixed(height)) => + parentBounds.withHeight(height) + + case BoundsType(FitMode.Available, FitMode.Relative(amountH)) => + parentBounds.withHeight((parentBounds.height * amountH).toInt) + + case BoundsType(FitMode.Content, FitMode.Available) => + Bounds.zero.withHeight(parentBounds.height) + + case BoundsType(FitMode.Content, FitMode.Content) => + Bounds.zero + + case BoundsType(FitMode.Content, FitMode.Fixed(height)) => + Bounds.zero.withHeight(height) + + case BoundsType(FitMode.Content, FitMode.Relative(amountH)) => + Bounds.zero.withHeight((parentBounds.height * amountH).toInt) + + case BoundsType(FitMode.Fixed(width), FitMode.Available) => + Bounds.zero.withDimensions(width, parentBounds.height) - val newComponents = reflowed.foldLeft(Batch.empty[ComponentEntry[?, ReferenceData]]) { - (acc, entry) => - val reflowed = entry.copy( - offset = nextOffset(reference, acc), - model = entry.component.reflow(reference, entry.model) - ) + case BoundsType(FitMode.Fixed(width), FitMode.Content) => + Bounds.zero.withWidth(width) - acc :+ reflowed - } + case BoundsType(FitMode.Fixed(width), FitMode.Fixed(height)) => + Bounds.zero.withDimensions(width, height) + case BoundsType(FitMode.Fixed(width), FitMode.Relative(amountH)) => + Bounds.zero.withDimensions(width, (parentBounds.height * amountH).toInt) + + case BoundsType(FitMode.Relative(amountW), FitMode.Available) => + Bounds.zero.withDimensions((parentBounds.width * amountW).toInt, parentBounds.height) + + case BoundsType(FitMode.Relative(amountW), FitMode.Content) => + Bounds.zero.withWidth((parentBounds.width * amountW).toInt) + + case BoundsType(FitMode.Relative(amountW), FitMode.Fixed(height)) => + Bounds.zero.withDimensions((parentBounds.width * amountW).toInt, height) + + case BoundsType(FitMode.Relative(amountW), FitMode.Relative(amountH)) => + Bounds.zero.withDimensions( + (parentBounds.width * amountW).toInt, + (parentBounds.height * amountH).toInt + ) + + // Next, loop over all the children, calling refresh on each one, and supplying the best guess for the bounds + val updatedComponents = + model.components.map { c => + val refreshed = c.component.refresh(reference, c.model, boundsWithoutContent) + c.copy(model = refreshed) + } + + // Now we need to set the offset of each child, based on the layout + val withOffsets = + updatedComponents.foldLeft(Batch.empty[ComponentEntry[?, ReferenceData]]) { (acc, next) => + val nextOffset = + ContainerLikeFunctions.calculateNextOffset[ReferenceData]( + boundsWithoutContent, + model.layout + )(reference, acc) + + acc :+ next.copy(offset = nextOffset) + } + + // Now we can calculate the content bounds + val contentBounds: Bounds = + withOffsets.foldLeft(Bounds.zero) { (acc, c) => + val bounds = c.component.bounds(reference, c.model).moveTo(c.offset) + acc.expandToInclude(bounds) + } + + // And finally, we can calculate the boundsWithoutContent updating in the FitMode.Content cases and leaving as-is in others + val updatedBounds = + model.boundsType match + case BoundsType(FitMode.Content, FitMode.Content) => + boundsWithoutContent.withDimensions(contentBounds.dimensions) + + case BoundsType(FitMode.Content, _) => + boundsWithoutContent.withWidth(contentBounds.width) + + case BoundsType(_, FitMode.Content) => + boundsWithoutContent.withHeight(contentBounds.height) + + case _ => + boundsWithoutContent + + // Return the updated model with the new bounds and content bounds and dirty flag reset model.copy( - components = newComponents + dirty = false, + contentBounds = contentBounds, + bounds = updatedBounds, + components = withOffsets ) - - def cascade( - model: ComponentGroup[ReferenceData], - parentBounds: Bounds - ): ComponentGroup[ReferenceData] = - val newBounds: Bounds = - calculateCascadeBounds( - model.bounds, - model.contentBounds, - parentBounds, - model.boundsType - ) - - model - .copy( - bounds = newBounds, - components = model.components.map(_.cascade(newBounds)), - dirty = true - ) - - def calculateCascadeBounds( - currentBounds: Bounds, - contentBounds: Bounds, - parentBounds: Bounds, - boundsType: BoundsType - ): Bounds = - boundsType match - case BoundsType.Fixed(b) => - b - - case BoundsType.Inherit => - parentBounds - - case BoundsType.Relative(x, y, width, height) => - Bounds( - (parentBounds.width.toDouble * x).toInt, - (parentBounds.height.toDouble * y).toInt, - (parentBounds.width.toDouble * width).toInt, - (parentBounds.height.toDouble * height).toInt - ) - - case BoundsType.RelativePosition(x, y) => - currentBounds.withPosition( - (parentBounds.width.toDouble * x).toInt, - (parentBounds.height.toDouble * y).toInt - ) - - case BoundsType.RelativeSize(width, height) => - currentBounds.withDimensions( - (parentBounds.width.toDouble * width).toInt, - (parentBounds.height.toDouble * height).toInt - ) - - case BoundsType.Offset(amountPosition, amountSize) => - Bounds(parentBounds.coords + amountPosition, parentBounds.dimensions + amountSize) - - case BoundsType.OffsetPosition(amount) => - currentBounds.withPosition(parentBounds.coords + amount) - - case BoundsType.OffsetSize(amount) => - currentBounds.withDimensions(parentBounds.dimensions + amount) - - case BoundsType.Dynamic(FitMode.Available, FitMode.Available) => - parentBounds - - case BoundsType.Dynamic(FitMode.Available, FitMode.Content) => - parentBounds.withDimensions( - parentBounds.dimensions.width, - contentBounds.dimensions.height - ) - - case BoundsType.Dynamic(FitMode.Available, FitMode.Fixed(units)) => - parentBounds.withDimensions(parentBounds.dimensions.width, units) - - case BoundsType.Dynamic(FitMode.Content, FitMode.Available) => - parentBounds.withDimensions( - contentBounds.dimensions.height, - parentBounds.dimensions.width - ) - - case BoundsType.Dynamic(FitMode.Content, FitMode.Content) => - contentBounds - - case BoundsType.Dynamic(FitMode.Content, FitMode.Fixed(units)) => - contentBounds.withDimensions(contentBounds.dimensions.width, units) - - case BoundsType.Dynamic(FitMode.Fixed(units), FitMode.Available) => - parentBounds.withDimensions(units, parentBounds.dimensions.height) - - case BoundsType.Dynamic(FitMode.Fixed(units), FitMode.Content) => - contentBounds.withDimensions(units, contentBounds.dimensions.height) - - case BoundsType.Dynamic(FitMode.Fixed(unitsW), FitMode.Fixed(unitsH)) => - currentBounds.withDimensions(unitsW, unitsH) - - def calculateContentBounds[ReferenceData]( - reference: ReferenceData, - components: Batch[ComponentEntry[?, ReferenceData]] - ): Bounds = - components.foldLeft(Bounds.zero) { (acc, c) => - val bounds = c.component.bounds(reference, c.model).moveTo(c.offset) - acc.expandToInclude(bounds) - } diff --git a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/group/FitMode.scala b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/group/FitMode.scala index 335a8f87..bb1bd62b 100644 --- a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/group/FitMode.scala +++ b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/components/group/FitMode.scala @@ -18,3 +18,8 @@ enum FitMode: /** Fixes the size in one dimension. */ case Fixed(units: Int) + + /** Fills the available space in one dimension, but only up to a certain percentage of the + * available space. + */ + case Relative(amount: Double) 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 b691c805..30f2fa68 100644 --- a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/window/WindowContent.scala +++ b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/window/WindowContent.scala @@ -29,14 +29,9 @@ trait WindowContent[A, ReferenceData]: model: A ): Outcome[Layer] - /** Called when the window's content area bounds changes, gives the model an opportunity to - * respond to the new content area. - */ - def cascade(model: A, newBounds: Bounds): A - /** Called when a window has been told to refresh its content, possibly by the content itself. */ - def refresh(reference: ReferenceData, model: A): A + def refresh(reference: ReferenceData, model: A, parentBounds: Bounds): A /** Companion object for `WindowContent` */ object WindowContent: @@ -59,11 +54,5 @@ object WindowContent: ): Outcome[Layer] = comp.present(context, model).map(_.toLayer) - def cascade( - model: A, - newBounds: Bounds - ): A = - comp.cascade(model, newBounds) - - def refresh(reference: ReferenceData, model: A): A = - comp.reflow(reference, model) + def refresh(reference: ReferenceData, model: A, parentBounds: Bounds): A = + comp.refresh(reference, model, parentBounds) 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 1fc0cf0a..f89c785b 100644 --- a/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/window/WindowModel.scala +++ b/roguelike-starterkit/src/main/scala/roguelikestarterkit/ui/window/WindowModel.scala @@ -32,14 +32,7 @@ final case class WindowModel[A, ReferenceData]( this.copy(id = value) def withBounds(value: Bounds): WindowModel[A, ReferenceData] = - val withNewBounds = this.copy(bounds = value) - - withNewBounds.copy( - contentModel = windowContent.cascade( - contentModel, - Window.calculateContentRectangle(value, withNewBounds) - ) - ) + this.copy(bounds = value) def withPosition(value: Coords): WindowModel[A, ReferenceData] = withBounds(bounds.moveTo(value)) @@ -132,7 +125,9 @@ final case class WindowModel[A, ReferenceData]( state == WindowState.Closed def refresh(reference: ReferenceData): WindowModel[A, ReferenceData] = - this.copy(contentModel = windowContent.refresh(reference, contentModel)) + this.copy(contentModel = + windowContent.refresh(reference, contentModel, Window.calculateContentRectangle(bounds, this)) + ) object WindowModel: diff --git a/roguelike-starterkit/src/test/scala/roguelikestarterkit/ui/components/common/ContainerLikeFunctionsTests.scala b/roguelike-starterkit/src/test/scala/roguelikestarterkit/ui/components/common/ContainerLikeFunctionsTests.scala new file mode 100644 index 00000000..36e712a7 --- /dev/null +++ b/roguelike-starterkit/src/test/scala/roguelikestarterkit/ui/components/common/ContainerLikeFunctionsTests.scala @@ -0,0 +1,90 @@ +package roguelikestarterkit.ui.components.common + +import indigo.* +import roguelikestarterkit.ui.component.* +import roguelikestarterkit.ui.components.* +import roguelikestarterkit.ui.components.group.* +import roguelikestarterkit.ui.datatypes.* + +class ContainerLikeFunctionsTests extends munit.FunSuite: + + val charSheet = + CharSheet( + AssetName("test"), + Size(10), + Batch.empty, + FontKey("test") + ) + + // test("calculateNextOffset labels") { + + // val c: Component[Label[Unit], Unit] = + // summon[Component[Label[Unit], Unit]] + + // val group: ComponentGroup[Unit] = + // ComponentGroup() + // .withLayout( + // ComponentLayout.Vertical(Padding.zero) + // ) + // .add( + // Label("label 1", Label.Theme(charSheet)), + // Label("label 2", Label.Theme(charSheet)), + // Label("label 3", Label.Theme(charSheet)) + // ) + + // val updated: ComponentGroup[Unit] = + // summon[Component[ComponentGroup[Unit], Unit]].refreshLayout((), Bounds(0, 0, 100, 100), group) + + // val actual = + // ContainerLikeFunctions.calculateNextOffset[Unit]( + // Bounds(0, 0, 20, 20), + // updated.layout + // )((), updated.components) + + // val expected = + // Coords(0, 3) + + // assertEquals(actual, expected) + // } + + // test("calculateNextOffset group of labels".only) { + + // val group: ComponentGroup[Unit] = + // ComponentGroup() + // .withLayout( + // ComponentLayout.Horizontal() + // ) + // .add( + // ComponentGroup() + // .withLayout( + // ComponentLayout.Vertical(Padding.zero) + // ) + // .add( + // Label("label 1", Label.Theme(charSheet)), + // Label("label 2", Label.Theme(charSheet)), + // Label("label 3", Label.Theme(charSheet)) + // ) + // ) + + // val parentBounds = Bounds(0, 0, 100, 100) + + // val updated: ComponentGroup[Unit] = + // summon[Component[ComponentGroup[Unit], Unit]].refreshLayout((), parentBounds, group) + + // assertEquals(updated.contentBounds, Bounds(0, 0, 100, 3)) + // assertEquals(updated.bounds, Bounds(0, 0, 100, 3)) + + // val c: Component[ComponentGroup[Unit], Unit] = + // summon[Component[ComponentGroup[Unit], Unit]] + + // val actual = + // ContainerLikeFunctions.calculateNextOffset[Unit]( + // Bounds(0, 0, 20, 20), + // updated.layout + // )((), updated.components) + + // val expected = + // Coords(100, 100) + + // assertEquals(actual, expected) + // } 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 2206c83f..d714a0ff 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 @@ -25,104 +25,104 @@ class ComponentGroupTests extends munit.FunSuite: ): Outcome[ComponentFragment] = Outcome(ComponentFragment.empty) - def reflow(reference: Unit, model: String): String = + def refresh(reference: Unit, model: String, parentBounds: Bounds): String = model def cascade(model: String, parentBounds: Bounds): String = model - test("ComponentGroup.calculateContentBounds should return the correct bounds (Vertical)") { - val group: ComponentGroup[Unit] = - ComponentGroup() - .withLayout( - ComponentLayout.Vertical(Padding.zero.withBottom(2)) - ) - .withBoundsType(BoundsType.Fixed(Bounds(0, 0, 100, 100))) - .add("abc", "def") - - val instance = - summon[Component[ComponentGroup[Unit], Unit]] - - // This normally happens as part of the update process - val processed = instance.cascade(instance.reflow((), group), Bounds(0, 0, 100, 100)) - - val actual = - ComponentGroup.calculateContentBounds((), processed.components) - - val expected = - Bounds(0, 0, 3, 4) - - assertEquals(actual, expected) - } - - test("ComponentGroup.calculateContentBounds should return the correct bounds (Horizontal)") { - val group: ComponentGroup[Unit] = - ComponentGroup() - .withLayout( - ComponentLayout.Horizontal(Padding.zero.withRight(2)) - ) - .withBoundsType(BoundsType.Fixed(Bounds(0, 0, 100, 100))) - .add("abc", "def") - - val instance = - summon[Component[ComponentGroup[Unit], Unit]] - - // This normally happens as part of the update process - val processed = instance.cascade(instance.reflow((), group), Bounds(0, 0, 100, 100)) - - val actual = - ComponentGroup.calculateContentBounds((), processed.components) - - val expected = - Bounds(0, 0, 8, 1) - - assertEquals(actual, expected) - } - - // Write a test for ComponentGroup.calculateCascadeBounds - test("ComponentGroup.calculateCascadeBounds should return the correct bounds") { - val group: ComponentGroup[Unit] = - ComponentGroup() - .withLayout( - ComponentLayout.Vertical(Padding.zero.withBottom(2)) - ) - .withBoundsType(BoundsType.Fixed(Bounds(0, 0, 100, 100))) - .add("abc", "def") - - val instance = - summon[Component[ComponentGroup[Unit], Unit]] - - // This normally happens as part of the update process - val processed = - instance.cascade(instance.reflow((), group), Bounds(0, 0, 100, 100)) - val updated = - processed.copy( - contentBounds = ComponentGroup.calculateContentBounds((), processed.components), - dirty = false - ) - - val actualFixed = - ComponentGroup.calculateCascadeBounds( - updated.bounds, - updated.contentBounds, - Bounds(0, 0, 100, 100), - updated.boundsType - ) - - assertEquals(actualFixed, Bounds(0, 0, 100, 100)) - - val actualDefault = - ComponentGroup.calculateCascadeBounds( - updated.bounds, - updated.contentBounds, - Bounds(0, 0, 100, 100), - BoundsType.default - ) - - assertEquals(actualDefault, Bounds(0, 0, 100, 4)) - } - - test("reflow should reapply the layout to all existing components") { + // test("ComponentGroup.calculateContentBounds should return the correct bounds (Vertical)") { + // val group: ComponentGroup[Unit] = + // ComponentGroup() + // .withLayout( + // ComponentLayout.Vertical(Padding.zero.withBottom(2)) + // ) + // .withBoundsType(BoundsType.Fixed(Bounds(0, 0, 100, 100))) + // .add("abc", "def") + + // val instance = + // summon[Component[ComponentGroup[Unit], Unit]] + + // // This normally happens as part of the update process + // val processed = instance.refresh((), group, Bounds(0, 0, 100, 100)) + + // val actual = + // ComponentGroup.calculateContentBounds((), processed.components) + + // val expected = + // Bounds(0, 0, 3, 4) + + // assertEquals(actual, expected) + // } + + // test("ComponentGroup.calculateContentBounds should return the correct bounds (Horizontal)") { + // val group: ComponentGroup[Unit] = + // ComponentGroup() + // .withLayout( + // ComponentLayout.Horizontal(Padding.zero.withRight(2)) + // ) + // .withBoundsType(BoundsType.Fixed(Bounds(0, 0, 100, 100))) + // .add("abc", "def") + + // val instance = + // summon[Component[ComponentGroup[Unit], Unit]] + + // // This normally happens as part of the update process + // val processed = instance.refresh((), group, Bounds(0, 0, 100, 100)) + + // val actual = + // ComponentGroup.calculateContentBounds((), processed.components) + + // val expected = + // Bounds(0, 0, 8, 1) + + // assertEquals(actual, expected) + // } + + // // Write a test for ComponentGroup.calculateCascadeBounds + // test("ComponentGroup.calculateCascadeBounds should return the correct bounds") { + // val group: ComponentGroup[Unit] = + // ComponentGroup() + // .withLayout( + // ComponentLayout.Vertical(Padding.zero.withBottom(2)) + // ) + // .withBoundsType(BoundsType.Fixed(Bounds(0, 0, 100, 100))) + // .add("abc", "def") + + // val instance = + // summon[Component[ComponentGroup[Unit], Unit]] + + // // This normally happens as part of the update process + // val processed = + // instance.refresh((), group, Bounds(0, 0, 100, 100)) + // val updated = + // processed.copy( + // contentBounds = ComponentGroup.calculateContentBounds((), processed.components), + // dirty = false + // ) + + // val actualFixed = + // ComponentGroup.calculateCascadeBounds( + // updated.bounds, + // updated.contentBounds, + // Bounds(0, 0, 100, 100), + // updated.boundsType + // ) + + // assertEquals(actualFixed, Bounds(0, 0, 100, 100)) + + // val actualDefault = + // ComponentGroup.calculateCascadeBounds( + // updated.bounds, + // updated.contentBounds, + // Bounds(0, 0, 100, 100), + // BoundsType.default + // ) + + // assertEquals(actualDefault, Bounds(0, 0, 100, 4)) + // } + + test("refresh should re-apply the layout to all existing components") { val group: ComponentGroup[Unit] = ComponentGroup() .withLayout( @@ -133,7 +133,7 @@ class ComponentGroupTests extends munit.FunSuite: val actual = summon[Component[ComponentGroup[Unit], Unit]] - .reflow((), group) + .refresh((), group, Bounds(0, 0, 3, 5)) .components .toList .map(_.offset) @@ -154,7 +154,7 @@ class ComponentGroupTests extends munit.FunSuite: val actual = summon[Component[ComponentGroup[Unit], Unit]] - .reflow((), group) + .refresh((), group, Bounds(0, 0, 3, 5)) .components .toList .map(_.offset) @@ -175,7 +175,7 @@ class ComponentGroupTests extends munit.FunSuite: val actual = summon[Component[ComponentGroup[Unit], Unit]] - .reflow((), group) + .refresh((), group, Bounds(0, 0, 3, 5)) .components .toList .map(_.offset) @@ -198,7 +198,7 @@ class ComponentGroupTests extends munit.FunSuite: val actual = summon[Component[ComponentGroup[Unit], Unit]] - .reflow((), group) + .refresh((), group, Bounds(0, 0, 3, 5)) .components .toList .map(_.offset) @@ -221,7 +221,7 @@ class ComponentGroupTests extends munit.FunSuite: val actual = summon[Component[ComponentGroup[Unit], Unit]] - .reflow((), group) + .refresh((), group, Bounds(0, 0, 3, 5)) .components .toList .map(_.offset) @@ -244,7 +244,7 @@ class ComponentGroupTests extends munit.FunSuite: val actual = summon[Component[ComponentGroup[Unit], Unit]] - .reflow((), group) + .refresh((), group, Bounds(0, 0, 3, 5)) .components .toList .map(_.offset) @@ -267,7 +267,7 @@ class ComponentGroupTests extends munit.FunSuite: val actual = summon[Component[ComponentGroup[Unit], Unit]] - .reflow((), group) + .refresh((), group, Bounds(0, 0, 3, 5)) .components .toList .map(_.offset) @@ -290,7 +290,7 @@ class ComponentGroupTests extends munit.FunSuite: val actual = summon[Component[ComponentGroup[Unit], Unit]] - .reflow((), group) + .refresh((), group, Bounds(0, 0, 3, 5)) .components .toList .map(_.offset) @@ -313,7 +313,7 @@ class ComponentGroupTests extends munit.FunSuite: val actual = summon[Component[ComponentGroup[Unit], Unit]] - .reflow((), group) + .refresh((), group, Bounds(0, 0, 3, 5)) .components .toList .map(_.offset) @@ -336,7 +336,7 @@ class ComponentGroupTests extends munit.FunSuite: val actual = summon[Component[ComponentGroup[Unit], Unit]] - .reflow((), group) + .refresh((), group, Bounds(0, 0, 3, 5)) .components .toList .map(_.offset) @@ -350,7 +350,7 @@ class ComponentGroupTests extends munit.FunSuite: assertEquals(actual, expected) } - test("Cascade should snap to width of parent and height of contents by default.") { + test("Refresh should snap to width of parent and height of contents by default.") { val c = summon[Component[ComponentGroup[Unit], Unit]] @@ -362,7 +362,7 @@ class ComponentGroupTests extends munit.FunSuite: .add("abc", "def") val updated: ComponentGroup[Unit] = - c.refreshLayout((), Bounds(0, 0, 100, 100), group) + c.refresh((), group, Bounds(0, 0, 100, 100)) assertEquals(updated.contentBounds, Bounds(0, 0, 3, 12)) assertEquals(updated.bounds, Bounds(0, 0, 100, 12)) 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 354c26b5..d7221043 100644 --- a/roguelike-starterkit/src/test/scala/roguelikestarterkit/ui/window/WindowModelTests.scala +++ b/roguelike-starterkit/src/test/scala/roguelikestarterkit/ui/window/WindowModelTests.scala @@ -26,10 +26,7 @@ class WindowModelTests extends munit.FunSuite: ): Outcome[Layer] = Outcome(Layer.empty) - def cascade(model: Bounds, newBounds: Bounds): Bounds = - newBounds - - def refresh(reference: Unit, model: Bounds): Bounds = + def refresh(reference: Unit, model: Bounds, parentBounds: Bounds): Bounds = model val charSheet = @@ -227,14 +224,12 @@ class WindowModelTests extends munit.FunSuite: ): Outcome[ComponentFragment] = Outcome(ComponentFragment.empty) - def reflow(reference: Unit, model: String): String = - model - - def cascade(model: String, parentBounds: Bounds): String = + def refresh(reference: Unit, model: String, parentBounds: Bounds): String = model given Component[Bounds, Unit] with - def bounds(reference: Unit, model: Bounds): Bounds = model + def bounds(reference: Unit, model: Bounds): Bounds = + model def updateModel( context: UiContext[Unit], @@ -248,8 +243,5 @@ class WindowModelTests extends munit.FunSuite: ): Outcome[ComponentFragment] = Outcome(ComponentFragment.empty) - def reflow(reference: Unit, model: Bounds): Bounds = + def refresh(reference: Unit, model: Bounds, parentBounds: Bounds): Bounds = model - - def cascade(model: Bounds, parentBounds: Bounds): Bounds = - parentBounds