diff --git a/build.gradle.kts b/build.gradle.kts index 16f3b26..3d29e56 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,6 +12,10 @@ val slf4j = "2.0.18" val lombokVersion = "1.18.46" val avajeInjectVersion = "12.5" +application { + mainClass = "be.seeseemelk.diceos.Bootloader" +} + repositories { mavenCentral() } diff --git a/src/main/java/be/seeseemelk/diceos/system/DiceOS.java b/src/main/java/be/seeseemelk/diceos/system/DiceOS.java index ad21173..b2220db 100644 --- a/src/main/java/be/seeseemelk/diceos/system/DiceOS.java +++ b/src/main/java/be/seeseemelk/diceos/system/DiceOS.java @@ -10,6 +10,7 @@ public class DiceOS { private static DiceOS INSTANCE; private final ResourceLoader resourceLoader; private final WindowService windowService; + private final InputService inputService; @PostConstruct void register() { @@ -33,4 +34,8 @@ public class DiceOS { public static WindowService getWindowService() { return INSTANCE.windowService; } + + public static InputService getInputService() { + return INSTANCE.inputService; + } } diff --git a/src/main/java/be/seeseemelk/diceos/system/DiceOSAdapter.java b/src/main/java/be/seeseemelk/diceos/system/DiceOSAdapter.java index 7927548..8bf9777 100644 --- a/src/main/java/be/seeseemelk/diceos/system/DiceOSAdapter.java +++ b/src/main/java/be/seeseemelk/diceos/system/DiceOSAdapter.java @@ -5,7 +5,6 @@ import com.badlogic.gdx.ApplicationAdapter; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.Texture; -import com.badlogic.gdx.graphics.g2d.BitmapFont; import com.badlogic.gdx.graphics.g2d.TextureRegion; import com.badlogic.gdx.utils.ScreenUtils; import com.badlogic.gdx.utils.viewport.ScreenViewport; @@ -24,6 +23,7 @@ public class DiceOSAdapter extends ApplicationAdapter { private final DisplayService display; private final CursorService cursorService; private final WindowService windowService; + private final InputService inputService; private final List startupTasks; private Texture clouds; private Texture border; @@ -62,6 +62,8 @@ public class DiceOSAdapter extends ApplicationAdapter { offsetY = (height - (displayHeight * scaling)) / 2; cursorService.setScale(scaling); + inputService.setScale(scaling); + inputService.setOffset(offsetX, offsetY); gc = new GraphicsContext(screenViewport.getCamera(), display.getBatch(), displayHeight, displayWidth, DiceOS.getResourceLoader().getDefaultFont().getBitmapFont()); diff --git a/src/main/java/be/seeseemelk/diceos/system/InputService.java b/src/main/java/be/seeseemelk/diceos/system/InputService.java index dfc20ef..89aae1a 100644 --- a/src/main/java/be/seeseemelk/diceos/system/InputService.java +++ b/src/main/java/be/seeseemelk/diceos/system/InputService.java @@ -8,13 +8,14 @@ import io.avaje.inject.Component; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import java.awt.event.KeyEvent; - @Slf4j @Component @RequiredArgsConstructor public class InputService implements OnStartup { private final DisplayService displayService; + private int scale = 1; + private int offsetX = 0; + private int offsetY = 0; @Override public void onStartup() { @@ -49,4 +50,36 @@ public class InputService implements OnStartup { return Gdx.input.isKeyPressed(com.badlogic.gdx.Input.Keys.ALT_LEFT) || Gdx.input.isKeyPressed(com.badlogic.gdx.Input.Keys.ALT_RIGHT); } + + public int getMouseX() { + return (Gdx.input.getX() - offsetX) / scale; + } + + public int getMouseY() { + return (Gdx.input.getY() - offsetY) / scale; + } + + void setScale(int scale) { + this.scale = scale; + } + + void setOffset(int x, int y) { + this.offsetX = x; + this.offsetY = y; + } + + /** + * Checks if the mouse is currently within the specified rectangle. + * @param x The x-coordinate of the rectangle. + * @param y The y-coordinate of the rectangle. + * @param width The width of the rectangle. + * @param height The height of the rectangle. + * @return True if the mouse is within the rectangle, false otherwise. + */ + public boolean mouseInBounds(int x, int y, int width, int height) { + int mx = getMouseX(); + int my = getMouseY(); + return mx >= x && mx < x + width && + my >= y && my < y + height; + } } diff --git a/src/main/java/be/seeseemelk/diceos/system/gfx/GraphicsContext.java b/src/main/java/be/seeseemelk/diceos/system/gfx/GraphicsContext.java index ff523fc..67c7b4a 100644 --- a/src/main/java/be/seeseemelk/diceos/system/gfx/GraphicsContext.java +++ b/src/main/java/be/seeseemelk/diceos/system/gfx/GraphicsContext.java @@ -6,11 +6,14 @@ import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.graphics.g2d.BitmapFont; import com.badlogic.gdx.graphics.g2d.NinePatch; +import com.badlogic.gdx.math.Matrix4; import com.badlogic.gdx.math.Rectangle; import com.badlogic.gdx.scenes.scene2d.utils.ScissorStack; import lombok.Getter; import lombok.RequiredArgsConstructor; +import java.util.Stack; + /** * Graphics context and rendering utilities. */ @@ -23,13 +26,13 @@ public class GraphicsContext { private final int width; private final BitmapFont font; - private final java.util.Stack stateStack = new java.util.Stack<>(); + private final Stack stateStack = new java.util.Stack<>(); private static class GraphicsState { - private final com.badlogic.gdx.math.Matrix4 transform; - private final boolean scissorEnabled; + private final Matrix4 transform; + private boolean scissorEnabled; - public GraphicsState(com.badlogic.gdx.math.Matrix4 transform, boolean scissorEnabled) { + public GraphicsState(Matrix4 transform, boolean scissorEnabled) { this.transform = transform.cpy(); this.scissorEnabled = scissorEnabled; } @@ -114,6 +117,19 @@ public class GraphicsContext { batch.setTransformMatrix(batch.getTransformMatrix().trn(x, -y, 0)); } + /** + * Gets the total amount of X-translation applied by this graphics context. + * In other words, a draw command with `x=0` would actually write to this screen coordinate. + * @return The total amount of X-translation. + */ + public int getXTranslation() { + return (int) batch.getTransformMatrix().val[com.badlogic.gdx.math.Matrix4.M03]; + } + + public int getYTranslation() { + return (int) batch.getTransformMatrix().val[com.badlogic.gdx.math.Matrix4.M13]; + } + /** * Sets a clipping rectangle for subsequent draw operations. * @@ -125,6 +141,15 @@ public class GraphicsContext { ScissorStack.calculateScissors(camera, batch.getTransformMatrix(), scissors, scissors); batch.flush(); ScissorStack.pushScissors(scissors); + getCurrentState().scissorEnabled = true; + } + + /** + * Returns the current graphics state from the top of the stack. + * @return The current state. + */ + private GraphicsState getCurrentState() { + return stateStack.peek(); } /** @@ -146,6 +171,9 @@ public class GraphicsContext { batch.flush(); GraphicsState state = stateStack.pop(); batch.setTransformMatrix(state.transform); + if (state.scissorEnabled) { + ScissorStack.popScissors(); + } } /** diff --git a/src/main/java/be/seeseemelk/diceos/system/toolkit/menu/Menu.java b/src/main/java/be/seeseemelk/diceos/system/toolkit/menu/Menu.java index cacbc80..7902238 100644 --- a/src/main/java/be/seeseemelk/diceos/system/toolkit/menu/Menu.java +++ b/src/main/java/be/seeseemelk/diceos/system/toolkit/menu/Menu.java @@ -5,12 +5,14 @@ import be.seeseemelk.diceos.system.font.Font; import be.seeseemelk.diceos.system.gfx.GraphicsContext; import lombok.Getter; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.List; @RequiredArgsConstructor @Getter +@Slf4j public class Menu implements MenuItem { private final String title; private final List items = new ArrayList<>(); @@ -33,7 +35,7 @@ public class Menu implements MenuItem { @Override public int getHeight() { - return items.stream().mapToInt(MenuItem::getHeight).sum(); + return 13; } /** @@ -49,7 +51,15 @@ public class Menu implements MenuItem { @Override public void paint(GraphicsContext gc) { + if (DiceOS.getInputService().mouseInBounds(gc.getXTranslation(), gc.getYTranslation(), getWidth(), getHeight())) { + log.info("Mouse in bounds"); + gc.setColour(0, 0, 255); + } + + gc.setColour(255, 255, 255); + gc.fillRect(0, 1, getWidth() + 3, getHeight() - 1); + // Renders the menu as a simple menu button - gc.draw(title, 0, 5); + gc.draw(title, 2, 5); } } diff --git a/src/main/java/be/seeseemelk/diceos/system/utils/Utils.java b/src/main/java/be/seeseemelk/diceos/system/utils/Utils.java index 592e7db..1d70e5d 100644 --- a/src/main/java/be/seeseemelk/diceos/system/utils/Utils.java +++ b/src/main/java/be/seeseemelk/diceos/system/utils/Utils.java @@ -12,4 +12,18 @@ public class Utils { public static boolean isPowerOfTwo(int value) { return (value & (value - 1)) == 0 && value != 0; } + + /** + * Determines if the point at (x, y) is inside the bounds of (x1, y1, width, height). + * @param x The x-coordinate of the point. + * @param y The y-coordinate of the point. + * @param x1 The x-coordinate of the bounds. + * @param y1 The y-coordinate of the bounds. + * @param width The width of the bounds. + * @param height The height of the bounds. + * @return True if the point is inside the bounds, false otherwise. + */ + public static boolean isInBounds(int x, int y, int x1, int y1, int width, int height) { + return x >= x1 && x < x1 + width && y >= y1 && y < y1 + height; + } } diff --git a/src/test/java/be/seeseemelk/diceos/system/InputServiceTest.java b/src/test/java/be/seeseemelk/diceos/system/InputServiceTest.java new file mode 100644 index 0000000..8de4bc7 --- /dev/null +++ b/src/test/java/be/seeseemelk/diceos/system/InputServiceTest.java @@ -0,0 +1,39 @@ +package be.seeseemelk.diceos.system; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.Input; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import static org.junit.jupiter.api.Assertions.*; + +class InputServiceTest { + private DisplayService displayService; + private InputService inputService; + + @BeforeEach + void setUp() { + displayService = Mockito.mock(DisplayService.class); + inputService = new InputService(displayService); + Gdx.input = Mockito.mock(Input.class); + } + + @Test + void testMouseMethods() { + Mockito.when(Gdx.input.getX()).thenReturn(100); + Mockito.when(Gdx.input.getY()).thenReturn(200); + + assertEquals(100, inputService.getMouseX()); + assertEquals(200, inputService.getMouseY()); + } + + @Test + void testMouseInBounds() { + Mockito.when(Gdx.input.getX()).thenReturn(100); + Mockito.when(Gdx.input.getY()).thenReturn(200); + + assertTrue(inputService.mouseInBounds(90, 190, 20, 20)); + assertFalse(inputService.mouseInBounds(0, 0, 50, 50)); + } +} diff --git a/src/test/java/be/seeseemelk/diceos/system/gfx/GraphicsContextTest.java b/src/test/java/be/seeseemelk/diceos/system/gfx/GraphicsContextTest.java new file mode 100644 index 0000000..3e66505 --- /dev/null +++ b/src/test/java/be/seeseemelk/diceos/system/gfx/GraphicsContextTest.java @@ -0,0 +1,71 @@ +package be.seeseemelk.diceos.system.gfx; + +import com.badlogic.gdx.graphics.Camera; +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.g2d.Batch; +import com.badlogic.gdx.graphics.g2d.BitmapFont; +import com.badlogic.gdx.graphics.g2d.NinePatch; +import com.badlogic.gdx.math.Matrix4; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +class GraphicsContextTest { + private Camera camera; + private Batch batch; + private BitmapFont font; + private GraphicsContext gc; + + @BeforeEach + void setUp() { + camera = mock(Camera.class); + batch = mock(Batch.class); + font = mock(BitmapFont.class); + gc = new GraphicsContext(camera, batch, 600, 800, font); + when(batch.getTransformMatrix()).thenReturn(new Matrix4()); + } + + @Test + void testDrawNinePatch() { + NinePatch np = mock(NinePatch.class); + gc.draw(np, 10, 20, 100, 200); + verify(np).draw(eq(batch), eq(10f), eq(380f), eq(100f), eq(200f)); + } + + @Test + void testDrawTexture() { + Texture tex = mock(Texture.class); + when(tex.getHeight()).thenReturn(50); + when(tex.getWidth()).thenReturn(50); + gc.draw(tex, 10, 20); + verify(batch).draw(eq(tex), eq(10f), anyFloat(), anyFloat(), anyFloat(), anyInt(), anyInt(), anyInt(), anyInt(), anyBoolean(), anyBoolean()); + } + + @Test + void testDrawText() { + gc.draw("test", 10, 20); + verify(font).draw(batch, "test", 10f, 600f - (20f + font.getLineHeight())); + } + + @Test + void testSetColour() { + gc.setColour(255, 0, 0); + verify(batch).setColor(1f, 0f, 0f, 1f); + } + + @Test + void testTranslate() { + gc.translate(10, 20); + verify(batch).setTransformMatrix(any(Matrix4.class)); + } + + @Test + void testSaveRestore() { + gc.save(); + assertDoesNotThrow(() -> gc.restore()); + assertThrows(IllegalStateException.class, () -> gc.restore()); + } +}