From 7b7ea343069f598da5df4127c9f1633060c06c6b Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Tue, 24 Oct 2023 20:11:52 +0100 Subject: [PATCH] Organised a WindowManager --- .../src/main/scala/roguepaint/PaintGame.scala | 2 +- .../main/scala/roguepaint/PaintScene.scala | 49 +++++-- .../scala/roguepaint/components/Window.scala | 127 ++++++++++++++---- .../roguepaint/components/WindowManager.scala | 90 +++++++++++++ 4 files changed, 230 insertions(+), 38 deletions(-) create mode 100644 paint/src/main/scala/roguepaint/components/WindowManager.scala diff --git a/paint/src/main/scala/roguepaint/PaintGame.scala b/paint/src/main/scala/roguepaint/PaintGame.scala index 2188196a..ec08754f 100644 --- a/paint/src/main/scala/roguepaint/PaintGame.scala +++ b/paint/src/main/scala/roguepaint/PaintGame.scala @@ -24,7 +24,7 @@ object PaintGame extends IndigoGame[Unit, Unit, Model, ViewModel]: BootResult .noData( Config.config - .withMagnification(2) + .withMagnification(1) ) .withFonts(RoguelikeTiles.Size10x10.Fonts.fontInfo) .withAssets(Assets.assets.assetSet) diff --git a/paint/src/main/scala/roguepaint/PaintScene.scala b/paint/src/main/scala/roguepaint/PaintScene.scala index ffe4bfa0..a5c33437 100644 --- a/paint/src/main/scala/roguepaint/PaintScene.scala +++ b/paint/src/main/scala/roguepaint/PaintScene.scala @@ -3,9 +3,11 @@ package roguepaint import indigo.* import indigo.scenes.* import io.indigoengine.roguelike.starterkit.* -import roguepaint.components.Window +import roguepaint.components.WindowId +import roguepaint.components.WindowManager +import roguepaint.components.WindowManagerModel +import roguepaint.components.WindowManagerViewModel import roguepaint.components.WindowModel -import roguepaint.components.WindowViewModel object PaintScene extends Scene[Unit, Model, ViewModel]: @@ -38,9 +40,9 @@ object PaintScene extends Scene[Unit, Model, ViewModel]: model: PaintModel ): GlobalEvent => Outcome[PaintModel] = case e => - val updated = model.window.update(context.frameContext, e) + val updated = model.windowManager.update(context.frameContext, e) - updated.map(w => model.copy(window = w)) + updated.map(w => model.copy(windowManager = w)) def updateViewModel( context: SceneContext[Unit], @@ -48,30 +50,55 @@ object PaintScene extends Scene[Unit, Model, ViewModel]: viewModel: PaintViewModel ): GlobalEvent => Outcome[PaintViewModel] = case e => - val updated = viewModel.window.update(context.frameContext, model.window, e) + val updated = viewModel.windowManager.update(context.frameContext, model.windowManager, e) - updated.map(w => viewModel.copy(window = w)) + updated.map(w => viewModel.copy(windowManager = w)) def present( context: SceneContext[Unit], model: PaintModel, viewModel: PaintViewModel ): Outcome[SceneUpdateFragment] = - val tiles = Window.present(context.frameContext, model.window, viewModel.window) + val tiles = + WindowManager.present(context.frameContext, model.windowManager, viewModel.windowManager) Outcome( SceneUpdateFragment(tiles.clones) .addCloneBlanks(tiles.blanks) ) -final case class PaintModel(window: WindowModel) +final case class PaintModel(windowManager: WindowManagerModel) object PaintModel: val initial: PaintModel = - PaintModel(WindowModel("test window")) + PaintModel( + WindowManagerModel.initial + .add( + WindowModel(WindowId("fixed")) + .moveTo(15, 40) + .resizeTo(15, 10) + ) + .add( + WindowModel(WindowId("controls")) + .isCloseable + .isResizable + .moveTo(175, 60) + .resizeTo(20, 20) + ) + .add( + WindowModel(WindowId("title bar")) + .isCloseable + .isResizable + .isDraggable + .withTitle("Controls") + .moveTo(30, 200) + .resizeTo(10, 30) + ) -final case class PaintViewModel(window: WindowViewModel) + ) + +final case class PaintViewModel(windowManager: WindowManagerViewModel) object PaintViewModel: val initial: PaintViewModel = PaintViewModel( - WindowViewModel.initial + WindowManagerViewModel.initial ) diff --git a/paint/src/main/scala/roguepaint/components/Window.scala b/paint/src/main/scala/roguepaint/components/Window.scala index 82c6a404..667febd2 100644 --- a/paint/src/main/scala/roguepaint/components/Window.scala +++ b/paint/src/main/scala/roguepaint/components/Window.scala @@ -7,6 +7,9 @@ import roguepaint.Assets object Window: + private val graphic10x10: Graphic[TerminalText] = + Graphic(10, 10, TerminalText(Assets.assets.AnikkiSquare10x10, RGBA.White, RGBA.Black)) + def updateModel( frameContext: FrameContext[Unit], model: WindowModel @@ -19,43 +22,109 @@ object Window: viewModel: WindowViewModel ): GlobalEvent => Outcome[WindowViewModel] = case FrameTick if model.bounds.size != viewModel.terminal.screenSize => - Outcome(viewModel.resize(model)) + val vm = viewModel.resize(model) + val clones = + vm.terminal.toCloneTiles(model.bounds.position, RoguelikeTiles.Size10x10.charCrops) { + case (fg, bg) => + graphic10x10.withMaterial(TerminalText(Assets.assets.AnikkiSquare10x10, fg, bg)) + } + + Outcome( + vm.copy(terminalClones = clones) + ) case _ => Outcome(viewModel) - val graphic10x10: Graphic[TerminalText] = - Graphic(10, 10, TerminalText(Assets.assets.AnikkiSquare10x10, RGBA.White, RGBA.Black)) + def present(viewModel: WindowViewModel): TerminalClones = + viewModel.terminalClones - def present( - frameContext: FrameContext[Unit], - model: WindowModel, - viewModel: WindowViewModel - ): TerminalClones = - viewModel.terminal.toCloneTiles(model.bounds.position, RoguelikeTiles.Size10x10.charCrops) { - case (fg, bg) => - graphic10x10.withMaterial(TerminalText(Assets.assets.AnikkiSquare10x10, fg, bg)) - } +opaque type WindowId = String +object WindowId: + def apply(id: String): WindowId = id + extension (id: WindowId) def toString: String = id final case class WindowModel( - id: String, + id: WindowId, bounds: Rectangle, - title: Option[String] + title: Option[String], + draggable: Boolean, + resizable: Boolean, + closeable: Boolean ): + def withId(value: WindowId): WindowModel = + this.copy(id = value) + + def withBounds(value: Rectangle): WindowModel = + this.copy(bounds = value) + + def withPosition(value: Point): WindowModel = + withBounds(bounds.withPosition(value)) + def moveTo(position: Point): WindowModel = + withPosition(position) + def moveTo(x: Int, y: Int): WindowModel = + moveTo(Point(x, y)) + def moveBy(amount: Point): WindowModel = + withPosition(bounds.position + amount) + def moveBy(x: Int, y: Int): WindowModel = + moveBy(Point(x, y)) + + def withSize(value: Size): WindowModel = + withBounds(bounds.withSize(value)) + def resizeTo(size: Size): WindowModel = + withSize(size) + def resizeTo(x: Int, y: Int): WindowModel = + resizeTo(Size(x, y)) + def resizeBy(amount: Size): WindowModel = + withSize(bounds.size + amount) + def resizeBy(x: Int, y: Int): WindowModel = + resizeBy(Size(x, y)) + + def withTitle(value: String): WindowModel = + this.copy(title = Option(value)) + + def withDraggable(value: Boolean): WindowModel = + this.copy(draggable = value) + def isDraggable: WindowModel = + withDraggable(true) + def notDraggable: WindowModel = + withDraggable(false) + + def withResizable(value: Boolean): WindowModel = + this.copy(resizable = value) + def isResizable: WindowModel = + withResizable(true) + def notResizable: WindowModel = + withResizable(false) + + def withCloseable(value: Boolean): WindowModel = + this.copy(closeable = value) + def isCloseable: WindowModel = + withCloseable(true) + def notCloseable: WindowModel = + withCloseable(false) + def update(frameContext: FrameContext[Unit], event: GlobalEvent): Outcome[WindowModel] = Window.updateModel(frameContext, this)(event) object WindowModel: - def apply(id: String): WindowModel = + def apply(id: WindowId): WindowModel = WindowModel( id, - Rectangle(Point(15, 15), Size(15, 10)), - Some("Inventory") + Rectangle(Point.zero, Size.zero), + None, + false, + false, + false ) -final case class WindowViewModel(terminal: TerminalEmulator): +final case class WindowViewModel( + id: WindowId, + terminal: TerminalEmulator, + terminalClones: TerminalClones +): def update( frameContext: FrameContext[Unit], @@ -69,9 +138,11 @@ final case class WindowViewModel(terminal: TerminalEmulator): object WindowViewModel: - def initial: WindowViewModel = + def initial(id: WindowId): WindowViewModel = WindowViewModel( - TerminalEmulator(Size.zero) + id, + TerminalEmulator(Size.zero), + TerminalClones.empty ) def makeWindowTerminal(model: WindowModel, current: TerminalEmulator): TerminalEmulator = @@ -80,8 +151,8 @@ object WindowViewModel: if validSize == current.screenSize then current else val tiles: Batch[(Point, MapTile)] = - val grey = RGBA.White.mix(RGBA.Black, 0.5) - val title = model.title.getOrElse("").toCharArray() + val grey = RGBA.White.mix(RGBA.Black, 0.3) + val title = model.title.getOrElse("").take(model.bounds.size.width - 2).toCharArray() (0 to validSize.height).toBatch.flatMap { _y => (0 to validSize.width).toBatch.map { _x => @@ -132,6 +203,10 @@ object WindowViewModel: // top left coords -> MapTile(Tile.`┌`, RGBA.White, RGBA.Zero) + case Point(x, 0) if model.closeable && x == maxX => + // top right closable + coords -> MapTile(Tile.`x`, RGBA.Black, RGBA.White) + case Point(x, 0) if x == maxX => // top right coords -> MapTile(Tile.`┐`, RGBA.White, RGBA.Zero) @@ -144,6 +219,10 @@ object WindowViewModel: // bottom left coords -> MapTile(Tile.`└`, RGBA.White, RGBA.Zero) + case Point(x, y) if model.resizable && x == maxX && y == maxY => + // bottom right with resize + coords -> MapTile(Tile.`▼`, RGBA.White, RGBA.Zero) + case Point(x, y) if x == maxX && y == maxY => // bottom right coords -> MapTile(Tile.`┘`, RGBA.White, RGBA.Zero) @@ -160,10 +239,6 @@ object WindowViewModel: // Middle right coords -> MapTile(Tile.`│`, RGBA.White, RGBA.Zero) - case Point(x, y) if x == maxX - 1 && y == maxY - 1 => - // Resize corner - coords -> MapTile(Tile.`/`, RGBA.White, RGBA.Zero) - case Point(x, y) => // Window background coords -> MapTile(Tile.`░`, grey, RGBA.Zero) diff --git a/paint/src/main/scala/roguepaint/components/WindowManager.scala b/paint/src/main/scala/roguepaint/components/WindowManager.scala new file mode 100644 index 00000000..5d7fc347 --- /dev/null +++ b/paint/src/main/scala/roguepaint/components/WindowManager.scala @@ -0,0 +1,90 @@ +package roguepaint.components + +import indigo.* +import indigo.syntax.* +import io.indigoengine.roguelike.starterkit.* + +object WindowManager: + + def updateModel( + frameContext: FrameContext[Unit], + model: WindowManagerModel + ): GlobalEvent => Outcome[WindowManagerModel] = + case e => + model.windows.map(_.update(frameContext, e)).sequence.map(m => model.copy(windows = m)) + + def updateViewModel( + frameContext: FrameContext[Unit], + model: WindowManagerModel, + viewModel: WindowManagerViewModel + ): GlobalEvent => Outcome[WindowManagerViewModel] = + case e => + val updated = + model.windows.flatMap { m => + viewModel.prune(model).windows.find(_.id == m.id) match + case None => + Batch(Outcome(WindowViewModel.initial(m.id))) + + case Some(vm) => + Batch(vm.update(frameContext, m, e)) + } + + updated.sequence.map(vm => viewModel.copy(windows = vm)) + + def present( + frameContext: FrameContext[Unit], + model: WindowManagerModel, + viewModel: WindowManagerViewModel + ): TerminalClones = + model.windows + .flatMap { m => + viewModel.windows.find(_.id == m.id) match + case None => + // Shouldn't get here. + Batch.empty + + case Some(vm) => + Batch(Window.present(vm)) + } + .foldLeft(TerminalClones.empty)(_ |+| _) + + extension (tc: TerminalClones) + def |+|(other: TerminalClones): TerminalClones = + TerminalClones(tc.blanks ++ other.blanks, tc.clones ++ other.clones) + +final case class WindowManagerModel(windows: Batch[WindowModel]): + def add(model: WindowModel): WindowManagerModel = + this.copy(windows = windows :+ model) + + def remove(id: WindowId): WindowManagerModel = + this.copy(windows = windows.filterNot(_.id == id)) + + def bringToTop(id: WindowId): WindowManagerModel = + windows.find(_.id == id) match + case None => + this + + case Some(w) => + this.copy(windows = windows.filterNot(_.id == id) :+ w) + + def update(frameContext: FrameContext[Unit], event: GlobalEvent): Outcome[WindowManagerModel] = + WindowManager.updateModel(frameContext, this)(event) + +object WindowManagerModel: + val initial: WindowManagerModel = + WindowManagerModel(Batch.empty) + +final case class WindowManagerViewModel(windows: Batch[WindowViewModel]): + def prune(model: WindowManagerModel): WindowManagerViewModel = + this.copy(windows = windows.filter(w => model.windows.exists(_.id == w.id))) + + def update( + frameContext: FrameContext[Unit], + model: WindowManagerModel, + event: GlobalEvent + ): Outcome[WindowManagerViewModel] = + WindowManager.updateViewModel(frameContext, model, this)(event) + +object WindowManagerViewModel: + val initial: WindowManagerViewModel = + WindowManagerViewModel(Batch.empty)