mirror of
https://github.com/Kapendev/parin.git
synced 2025-04-27 05:29:53 +03:00
Fixed the map and ui modules and worked on examples.
This commit is contained in:
parent
25acd69a84
commit
ef058e59e1
15 changed files with 505 additions and 501 deletions
8
TOUR.md
8
TOUR.md
|
@ -187,7 +187,7 @@ Result!IStr loadTempText(IStr path);
|
|||
Fault saveText(IStr path, IStr text);
|
||||
```
|
||||
|
||||
Additional loading functions can be found in other modules, such as `parin.tilemap`.
|
||||
Additional loading functions can be found in other modules, such as `parin.map`.
|
||||
|
||||
### Managed Resources
|
||||
|
||||
|
@ -209,8 +209,4 @@ They don’t need to be freed manually.
|
|||
|
||||
Sprites and tile maps can be implemented in various ways.
|
||||
To avoid enforcing a specific approach, Parin provides optional modules for these features, allowing users to include or omit them as needed.
|
||||
Parin provides a sprite type inside the `parin.sprite` module and a tile map type inside the `parin.tilemap` module.
|
||||
|
||||
## Scenes
|
||||
|
||||
The `parin.scene` module provides a simple scene manager to organize game code by screens, such as the title screen or gameplay.
|
||||
Parin provides a sprite type inside the `parin.sprite` module and a tile map type inside the `parin.map` module.
|
||||
|
|
|
@ -6,7 +6,7 @@ This folder provides example projects to help you get started.
|
|||
> If an example uses textures,
|
||||
> be sure to download the [atlas.png](atlas.png) file and place it in the project's assets folder.
|
||||
|
||||
## Example Categories
|
||||
## Categories
|
||||
- [Intro](intro): Basic examples to get familiar with Parin.
|
||||
- [Games](games): Examples focused on making simple games with Parin.
|
||||
- [UI](ui): Examples demonstrating how to use the Parin UI toolkit.
|
||||
|
|
|
@ -7,44 +7,41 @@ auto map = TileMap();
|
|||
auto camera = Camera(0, 0, true);
|
||||
auto tile = Tile(16, 16, 145);
|
||||
auto tileSpeed = 120;
|
||||
auto tileLookDirection = -1;
|
||||
auto tileFlip = Flip.none;
|
||||
|
||||
void ready() {
|
||||
lockResolution(320, 180);
|
||||
setBackgroundColor(toRgb(0x0b0b0b));
|
||||
// Load the `atlas.png` file from the assets folder.
|
||||
atlas = loadTexture("atlas.png");
|
||||
// Parse the tile map CSV file.
|
||||
// Parse a CSV string representing a tile map, where each tile is 16x16 pixels in size.
|
||||
map.parse("-1,-1,-1\n21,22,23\n37,38,39\n53,54,55", 16, 16);
|
||||
}
|
||||
|
||||
bool update(float dt) {
|
||||
// Make the drawing options.
|
||||
// Create the drawing options for the map and tile.
|
||||
auto mapOptions = DrawOptions(Hook.center);
|
||||
mapOptions.scale = Vec2(2);
|
||||
auto tileOptions = mapOptions;
|
||||
tileOptions.flip = tileLookDirection > 0 ? Flip.x : Flip.none;
|
||||
// Move the tile and camera.
|
||||
tileOptions.flip = tileFlip;
|
||||
if (wasd.x > 0) tileFlip = Flip.x;
|
||||
else if (wasd.x < 0) tileFlip = Flip.none;
|
||||
|
||||
// Move the tile and the camera.
|
||||
tile.position += wasd * Vec2(tileSpeed * dt);
|
||||
camera.position = tile.position;
|
||||
if (wasd.x != 0) tileLookDirection = cast(int) wasd.normalize.round.x;
|
||||
// Check for collisions.
|
||||
auto collisionArea = Rect();
|
||||
foreach (gridPosition; map.gridPositions(camera.area, mapOptions)) {
|
||||
if (map[gridPosition] == -1) continue;
|
||||
auto gridTileArea = Rect(map.worldPosition(gridPosition, mapOptions), Vec2(16) * mapOptions.scale);
|
||||
while (gridTileArea.hasIntersection(Rect(tile.position, tile.size * mapOptions.scale).area(tileOptions.hook))) {
|
||||
// Check for collisions between the tile and the map and resolve the collision.
|
||||
foreach (position; map.gridPositions(camera.area, mapOptions)) {
|
||||
if (map[position] < 0) continue;
|
||||
auto area = Rect(map.worldPosition(position, mapOptions), map.tileSize * mapOptions.scale);
|
||||
while (area.hasIntersection(Rect(tile.position, tile.size * mapOptions.scale).area(mapOptions.hook))) {
|
||||
tile.position -= wasd * Vec2(dt);
|
||||
camera.position = tile.position;
|
||||
collisionArea = gridTileArea;
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the game.
|
||||
// Draw the tile and the map.
|
||||
camera.attach();
|
||||
drawTileMap(atlas, map, camera, mapOptions);
|
||||
drawTile(atlas, tile, tileOptions);
|
||||
drawRect(collisionArea, yellow.alpha(120));
|
||||
drawTileMap(atlas, map, camera, mapOptions);
|
||||
camera.detach();
|
||||
drawDebugText("Move with arrow keys.", Vec2(8));
|
||||
return false;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/// This example shows how to use the scene manager of Parin.
|
||||
|
||||
import parin;
|
||||
import parin.scene;
|
||||
|
||||
auto sceneManager = SceneManager();
|
||||
|
||||
|
|
|
@ -11,36 +11,24 @@ auto walkAnimation = SpriteAnimation(0, 2, 6);
|
|||
|
||||
void ready() {
|
||||
lockResolution(320, 180);
|
||||
setBackgroundColor(toRgb(0x0b0b0b));
|
||||
setIsCursorVisible(false);
|
||||
// Load the `atlas.png` file from the assets folder.
|
||||
atlas = loadTexture("atlas.png");
|
||||
}
|
||||
|
||||
bool update(float dt) {
|
||||
// The sprite should be updated every frame, regardless of whether it is running.
|
||||
sprite.update(dt);
|
||||
// Get some basic info about the mouse.
|
||||
auto mouseDistance = sprite.position.distanceTo(mouse);
|
||||
auto mouseDirection = sprite.position.directionTo(mouse);
|
||||
|
||||
// Move the sprite around in a smooth way.
|
||||
sprite.followPositionWithSlowdown(mouse, 0.2);
|
||||
|
||||
// Play the right animation.
|
||||
// Play the correct animation.
|
||||
auto isWaiting = mouseDistance < 0.2;
|
||||
if (isWaiting) {
|
||||
sprite.play(idleAnimation);
|
||||
} else {
|
||||
sprite.play(walkAnimation);
|
||||
}
|
||||
|
||||
if (isWaiting) sprite.play(idleAnimation);
|
||||
else sprite.play(walkAnimation);
|
||||
// Flip the sprite based on the mouse direction.
|
||||
if (mouseDirection.x > 0) {
|
||||
spriteFlip = Flip.x;
|
||||
} else if (mouseDirection.x < 0) {
|
||||
spriteFlip = Flip.none;
|
||||
}
|
||||
|
||||
if (mouseDirection.x > 0) spriteFlip = Flip.x;
|
||||
else if (mouseDirection.x < 0) spriteFlip = Flip.none;
|
||||
// Check if 1, 2, or 3 is pressed and change the character.
|
||||
foreach (i, digit; digitChars[1 .. 4]) {
|
||||
if (digit.isPressed) {
|
||||
|
@ -49,14 +37,11 @@ bool update(float dt) {
|
|||
}
|
||||
}
|
||||
|
||||
// Set the drawing options for the sprite.
|
||||
// Draw the sprite and some info.
|
||||
auto options = DrawOptions(Hook.center);
|
||||
options.flip = spriteFlip;
|
||||
options.scale = Vec2(2);
|
||||
|
||||
// Draw the sprite, the mouse position and some info.
|
||||
drawSprite(atlas, sprite, options);
|
||||
drawVec2(mouse, 8, isWaiting ? blank : white.alpha(150));
|
||||
drawDebugText("Press 1, 2 or 3 to change the character.", Vec2(8));
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ void ready() {
|
|||
}
|
||||
|
||||
bool update(float dt) {
|
||||
prepareUi();
|
||||
setUiStartPoint(Vec2(8));
|
||||
// Toggle the limit of the drag handle.
|
||||
if (uiButton(Vec2(80, 30), "Limit: {}".format(handleOptions.dragLimit))) {
|
||||
|
|
|
@ -9,6 +9,8 @@ void ready() {
|
|||
}
|
||||
|
||||
bool update(float dt) {
|
||||
// Prepare the UI for this frame. This call is required for the UI to function as expected.
|
||||
prepareUi();
|
||||
// Set the starting point for subsequent UI items.
|
||||
setUiStartPoint(Vec2(8));
|
||||
// Create a button and print if it is clicked.
|
||||
|
|
|
@ -9,6 +9,7 @@ void ready() {
|
|||
}
|
||||
|
||||
bool update(float dt) {
|
||||
prepareUi();
|
||||
// Set the margin between subsequent UI items.
|
||||
setUiMargin(2);
|
||||
setUiStartPoint(Vec2(8));
|
||||
|
|
|
@ -12,6 +12,7 @@ void ready() {
|
|||
}
|
||||
|
||||
bool update(float dt) {
|
||||
prepareUi();
|
||||
// Set the viewport state for subsequent UI items.
|
||||
setUiViewportState(viewportPosition, viewport.size, viewportScale);
|
||||
viewport.attach();
|
||||
|
|
|
@ -14,9 +14,6 @@ module parin.dialogue;
|
|||
|
||||
import joka.ascii;
|
||||
import parin.engine;
|
||||
public import joka.containers;
|
||||
public import joka.faults;
|
||||
public import joka.types;
|
||||
|
||||
@safe:
|
||||
|
||||
|
|
|
@ -6,9 +6,7 @@
|
|||
// Version: v0.0.29
|
||||
// ---
|
||||
|
||||
// TODO: Try to fix the ui item overlaping bug maybe.
|
||||
// TODO: Add way to get item point for some stuff. This is nice when making lists.
|
||||
// TODO: Test the ui code and think how to make it better while working on real stuff.
|
||||
// TODO: Add way to align text. I neeed this for the UI.
|
||||
// TODO: Test the resource loading code.
|
||||
// TODO: Think about the sound API.
|
||||
// TODO: Make sounds loop based on a variable and not on the file type.
|
||||
|
@ -20,9 +18,9 @@ module parin.engine;
|
|||
import stdc = joka.stdc;
|
||||
import rl = parin.rl;
|
||||
|
||||
import joka.unions;
|
||||
import joka.ascii;
|
||||
import joka.io;
|
||||
import joka.unions;
|
||||
import parin.timer;
|
||||
|
||||
public import joka.colors;
|
||||
|
@ -38,14 +36,6 @@ IStr[64] engineEnvArgsBuffer;
|
|||
Sz engineEnvArgsBufferLength;
|
||||
IStr[64] engineDroppedFilePathsBuffer;
|
||||
rl.FilePathList engineDroppedFilePathsDataBuffer;
|
||||
UiState uiPreviousState;
|
||||
UiState uiState;
|
||||
|
||||
enum defaultUiAlpha = 230;
|
||||
enum defaultUiDisabledColor = 0x202020.toRgb().alpha(defaultUiAlpha);
|
||||
enum defaultUiIdleColor = 0x414141.toRgb().alpha(defaultUiAlpha);
|
||||
enum defaultUiHotColor = 0x818181.toRgb().alpha(defaultUiAlpha);
|
||||
enum defaultUiActiveColor = 0xBABABA.toRgb().alpha(defaultUiAlpha);
|
||||
|
||||
/// A type representing layout orientations.
|
||||
enum Layout : ubyte {
|
||||
|
@ -53,6 +43,13 @@ enum Layout : ubyte {
|
|||
h, /// Horizontal layout.
|
||||
}
|
||||
|
||||
/// A type representing alignment orientations.
|
||||
enum Align : ubyte {
|
||||
left, /// Align to the left.
|
||||
center, /// Align to the center.
|
||||
right, /// Align to the right.
|
||||
}
|
||||
|
||||
/// A type representing flipping orientations.
|
||||
enum Flip : ubyte {
|
||||
none, /// No flipping.
|
||||
|
@ -839,68 +836,6 @@ struct Camera {
|
|||
}
|
||||
}
|
||||
|
||||
/// A type representing the constraints on drag movement.
|
||||
enum UiDragLimit: ubyte {
|
||||
none, /// No limits.
|
||||
viewport, /// Limited to the viewport.
|
||||
viewportAndX, /// Limited to the viewport and on the X-axis.
|
||||
viewportAndY, /// Limited to the viewport and on the Y-axis.
|
||||
custom, /// Limited to custom limits.
|
||||
customAndX, /// Limited to custom limits and on the X-axis.
|
||||
customAndY, /// Limited to custom limits and on the Y-axis.
|
||||
}
|
||||
|
||||
struct UiButtonOptions {
|
||||
Color disabledColor = defaultUiDisabledColor;
|
||||
Color idleColor = defaultUiIdleColor;
|
||||
Color hotColor = defaultUiHotColor;
|
||||
Color activeColor = defaultUiActiveColor;
|
||||
Font font;
|
||||
|
||||
bool isDisabled;
|
||||
UiDragLimit dragLimit;
|
||||
Vec2 dragLimitX = Vec2(-100000.0f, 100000.0f);
|
||||
Vec2 dragLimitY = Vec2(-100000.0f, 100000.0f);
|
||||
|
||||
@safe @nogc nothrow:
|
||||
|
||||
this(bool isDisabled) {
|
||||
this.isDisabled = isDisabled;
|
||||
}
|
||||
|
||||
this(UiDragLimit dragLimit) {
|
||||
this.dragLimit = dragLimit;
|
||||
}
|
||||
}
|
||||
|
||||
struct UiState {
|
||||
Mouse mouseClickAction = Mouse.left;
|
||||
Keyboard keyboardClickAction = Keyboard.space;
|
||||
Gamepad gamepadClickAction = Gamepad.a;
|
||||
bool isActOnPress;
|
||||
|
||||
Vec2 mousePressedPoint;
|
||||
Vec2 viewportPoint;
|
||||
Vec2 viewportSize;
|
||||
Vec2 viewportScale = Vec2(1);
|
||||
Vec2 startPoint;
|
||||
short margin;
|
||||
Layout layout;
|
||||
Vec2 layoutStartPoint;
|
||||
Vec2 layoutStartPointOffest;
|
||||
Vec2 layoutMaxItemSize;
|
||||
|
||||
Vec2 itemDragOffset;
|
||||
Vec2 itemPoint;
|
||||
Vec2 itemSize;
|
||||
short itemId;
|
||||
short hotItemId;
|
||||
short activeItemId;
|
||||
short clickedItemId;
|
||||
short draggedItemId;
|
||||
short focusedItemId;
|
||||
}
|
||||
|
||||
struct EngineFlags {
|
||||
bool isUpdating;
|
||||
bool isPixelSnapped;
|
||||
|
@ -1513,7 +1448,6 @@ void updateWindow(bool function(float dt) updateFunc) {
|
|||
}
|
||||
}
|
||||
|
||||
prepareUi();
|
||||
auto dt = deltaTime;
|
||||
auto result = _updateFunc(dt);
|
||||
engineState.tickCount = (engineState.tickCount + 1) % engineState.tickCount.max;
|
||||
|
@ -1613,7 +1547,6 @@ void updateWindow(bool function(float dt) updateFunc) {
|
|||
@trusted
|
||||
void closeWindow() {
|
||||
if (!rl.IsWindowReady) return;
|
||||
uiState = UiState();
|
||||
engineState.free();
|
||||
rl.CloseAudioDevice();
|
||||
rl.CloseWindow();
|
||||
|
@ -2413,357 +2346,3 @@ mixin template runGame(alias readyFunc, alias updateFunc, alias finishFunc, int
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
void prepareUi() {
|
||||
uiState.viewportPoint = Vec2();
|
||||
uiState.viewportSize = resolution;
|
||||
uiState.viewportScale = Vec2(1.0f);
|
||||
uiState.startPoint = Vec2();
|
||||
uiState.margin = 0;
|
||||
uiState.layout = Layout.v;
|
||||
uiState.layoutStartPoint = Vec2();
|
||||
uiState.layoutStartPointOffest = Vec2();
|
||||
uiState.layoutMaxItemSize = Vec2();
|
||||
uiState.itemPoint = Vec2();
|
||||
uiState.itemSize = Vec2();
|
||||
uiState.itemId = 0;
|
||||
uiState.hotItemId = 0;
|
||||
uiState.activeItemId = 0;
|
||||
uiState.clickedItemId = 0;
|
||||
|
||||
if (uiState.mouseClickAction.isPressed) {
|
||||
uiState.mousePressedPoint = uiMouse;
|
||||
}
|
||||
}
|
||||
|
||||
Vec2 uiMouse() {
|
||||
auto result = (mouse - uiState.viewportPoint) / uiState.viewportScale;
|
||||
if (result.x < 0) result.x = -100000.0f;
|
||||
else if (result.x > uiState.viewportSize.x) result.x = 100000.0f;
|
||||
if (result.y < 0) result.y = -100000.0f;
|
||||
else if (result.y > uiState.viewportSize.y) result.y = 100000.0f;
|
||||
return result;
|
||||
}
|
||||
|
||||
void setUiClickAction(Mouse value) {
|
||||
uiState.mouseClickAction = value;
|
||||
}
|
||||
|
||||
void setUiClickAction(Keyboard value) {
|
||||
uiState.keyboardClickAction = value;
|
||||
}
|
||||
|
||||
void setUiClickAction(Gamepad value) {
|
||||
uiState.gamepadClickAction = value;
|
||||
}
|
||||
|
||||
bool isUiActOnPress() {
|
||||
return uiState.isActOnPress;
|
||||
}
|
||||
|
||||
void setIsUiActOnPress(bool value) {
|
||||
uiState.isActOnPress = value;
|
||||
}
|
||||
|
||||
void setUiViewportState(Vec2 point, Vec2 size, Vec2 scale) {
|
||||
uiState.viewportPoint = point;
|
||||
uiState.viewportSize = size;
|
||||
uiState.viewportScale = scale;
|
||||
}
|
||||
|
||||
Vec2 uiStartPoint() {
|
||||
return uiState.startPoint;
|
||||
}
|
||||
|
||||
void setUiStartPoint(Vec2 value) {
|
||||
uiState.itemSize = Vec2();
|
||||
uiState.startPoint = value;
|
||||
uiState.layoutStartPoint = value;
|
||||
uiState.layoutStartPointOffest = Vec2();
|
||||
uiState.layoutMaxItemSize = Vec2();
|
||||
}
|
||||
|
||||
short uiMargin() {
|
||||
return uiState.margin;
|
||||
}
|
||||
|
||||
void setUiMargin(short value) {
|
||||
uiState.margin = value;
|
||||
}
|
||||
|
||||
void useUiLayout(Layout value) {
|
||||
if (uiState.layoutStartPointOffest) {
|
||||
final switch (value) {
|
||||
case Layout.v:
|
||||
if (uiState.layoutStartPointOffest.x > uiState.layoutMaxItemSize.x) {
|
||||
uiState.layoutStartPoint.x = uiState.layoutStartPoint.x + uiState.layoutStartPointOffest.x + uiState.margin;
|
||||
} else {
|
||||
uiState.layoutStartPoint.x += uiState.layoutMaxItemSize.x + uiState.margin;
|
||||
}
|
||||
uiState.layoutStartPointOffest = Vec2();
|
||||
uiState.layoutMaxItemSize.x = 0.0f;
|
||||
break;
|
||||
case Layout.h:
|
||||
uiState.layoutStartPoint.x = uiState.startPoint.x;
|
||||
if (uiState.layoutStartPointOffest.y > uiState.layoutMaxItemSize.y) {
|
||||
uiState.layoutStartPoint.y = uiState.layoutStartPoint.y + uiState.layoutStartPointOffest.y + uiState.margin;
|
||||
} else {
|
||||
uiState.layoutStartPoint.y += uiState.layoutMaxItemSize.y + uiState.margin;
|
||||
}
|
||||
uiState.layoutStartPointOffest = Vec2();
|
||||
uiState.layoutMaxItemSize.y = 0.0f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
uiState.layout = value;
|
||||
}
|
||||
|
||||
bool isUiItemHot() {
|
||||
return uiState.itemId == uiState.hotItemId;
|
||||
}
|
||||
|
||||
bool isUiHot() {
|
||||
return uiState.hotItemId > 0;
|
||||
}
|
||||
|
||||
bool isUiItemActive() {
|
||||
return uiState.itemId == uiState.activeItemId;
|
||||
}
|
||||
|
||||
bool isUiActive() {
|
||||
return uiState.activeItemId > 0;
|
||||
}
|
||||
|
||||
bool isUiItemClicked() {
|
||||
return uiState.itemId == uiState.clickedItemId;
|
||||
}
|
||||
|
||||
bool isUiClicked() {
|
||||
return uiState.clickedItemId > 0;
|
||||
}
|
||||
|
||||
bool isUiItemDragged() {
|
||||
return uiState.itemId == uiState.draggedItemId && deltaMouse;
|
||||
}
|
||||
|
||||
bool isUiDragged() {
|
||||
return uiState.draggedItemId > 0 && deltaMouse;
|
||||
}
|
||||
|
||||
Vec2 uiDragOffset() {
|
||||
return uiState.itemDragOffset;
|
||||
}
|
||||
|
||||
int uiFocus() {
|
||||
return uiState.focusedItemId;
|
||||
}
|
||||
|
||||
void setUiFocus(short id) {
|
||||
uiState.focusedItemId = id;
|
||||
}
|
||||
|
||||
void clampUiFocus(short step, Sz length) {
|
||||
auto min = cast(short) (uiState.itemId + 1);
|
||||
auto max = cast(short) (length - 1 + min);
|
||||
auto isOutside = uiState.focusedItemId < min || uiState.focusedItemId > max;
|
||||
if (step == 0) {
|
||||
uiState.focusedItemId = min;
|
||||
return;
|
||||
}
|
||||
if (isOutside) {
|
||||
if (step < 0) {
|
||||
uiState.focusedItemId = max;
|
||||
return;
|
||||
} else {
|
||||
uiState.focusedItemId = min;
|
||||
return;
|
||||
}
|
||||
}
|
||||
uiState.focusedItemId = clamp(cast(short) (uiState.focusedItemId + step), min, max);
|
||||
}
|
||||
|
||||
void wrapUiFocus(short step, Sz length) {
|
||||
auto min = cast(short) (uiState.itemId + 1);
|
||||
auto max = cast(short) (length - 1 + min);
|
||||
auto isOutside = uiState.focusedItemId < min || uiState.focusedItemId > max;
|
||||
if (step == 0) {
|
||||
uiState.focusedItemId = min;
|
||||
return;
|
||||
}
|
||||
if (isOutside) {
|
||||
if (step < 0) {
|
||||
uiState.focusedItemId = max;
|
||||
return;
|
||||
} else {
|
||||
uiState.focusedItemId = min;
|
||||
return;
|
||||
}
|
||||
}
|
||||
uiState.focusedItemId = wrap(cast(short) (uiState.focusedItemId + step), min, cast(short) (max + 1));
|
||||
}
|
||||
|
||||
void updateUiState(Vec2 itemPoint, Vec2 itemSize, bool isHot, bool isActive, bool isClicked) {
|
||||
uiPreviousState = uiState;
|
||||
uiState.itemPoint = itemPoint;
|
||||
uiState.itemSize = itemSize;
|
||||
uiState.itemId += 1;
|
||||
if (itemSize.x > uiState.layoutMaxItemSize.x) uiState.layoutMaxItemSize.x = itemSize.x;
|
||||
if (itemSize.y > uiState.layoutMaxItemSize.y) uiState.layoutMaxItemSize.y = itemSize.y;
|
||||
final switch (uiState.layout) {
|
||||
case Layout.v: uiState.layoutStartPointOffest.y += uiState.itemSize.y + uiState.margin; break;
|
||||
case Layout.h: uiState.layoutStartPointOffest.x += uiState.itemSize.x + uiState.margin; break;
|
||||
}
|
||||
if (isHot) uiState.hotItemId = uiState.itemId;
|
||||
if (isActive) {
|
||||
uiState.activeItemId = uiState.itemId;
|
||||
uiState.focusedItemId = uiState.itemId;
|
||||
}
|
||||
if (isClicked) uiState.clickedItemId = uiState.itemId;
|
||||
if (uiState.draggedItemId) {
|
||||
if (uiState.mouseClickAction.isReleased) uiState.draggedItemId = 0;
|
||||
} else if (uiState.mouseClickAction.isPressed && uiState.itemId == uiState.activeItemId) {
|
||||
auto m = uiMouse;
|
||||
uiState.itemDragOffset = uiState.itemPoint - m;
|
||||
uiState.draggedItemId = uiState.itemId;
|
||||
}
|
||||
}
|
||||
|
||||
bool updateUiButton(Vec2 size, IStr text, UiButtonOptions options = UiButtonOptions()) {
|
||||
if (options.font.isEmpty) options.font = engineFont;
|
||||
auto m = uiMouse;
|
||||
auto id = uiState.itemId + 1;
|
||||
auto area = Rect(uiState.layoutStartPoint + uiState.layoutStartPointOffest, size);
|
||||
// auto isHot = area.hasPoint(uiMouse)
|
||||
auto isHot = m.x >= area.position.x && m.x < area.position.x + area.size.x && m.y >= area.position.y && m.y < area.position.y + area.size.y;
|
||||
auto isActive = isHot && uiState.mouseClickAction.isDown;
|
||||
auto isClicked = isHot;
|
||||
if (uiState.isActOnPress) {
|
||||
isClicked = isClicked && uiState.mouseClickAction.isPressed;
|
||||
} else {
|
||||
auto isHotFromMousePressedPoint =
|
||||
uiState.mousePressedPoint.x >= area.position.x &&
|
||||
uiState.mousePressedPoint.x < area.position.x + area.size.x &&
|
||||
uiState.mousePressedPoint.y >= area.position.y &&
|
||||
uiState.mousePressedPoint.y < area.position.y + area.size.y;
|
||||
isClicked = isClicked && isHotFromMousePressedPoint && uiState.mouseClickAction.isReleased;
|
||||
}
|
||||
|
||||
if (options.isDisabled) {
|
||||
isHot = false;
|
||||
isActive = false;
|
||||
isClicked = false;
|
||||
} else if (id == uiState.focusedItemId) {
|
||||
isHot = true;
|
||||
if (uiState.keyboardClickAction.isDown || uiState.gamepadClickAction.isDown) isActive = true;
|
||||
if (uiState.isActOnPress) {
|
||||
if (uiState.keyboardClickAction.isPressed || uiState.gamepadClickAction.isPressed) isClicked = true;
|
||||
} else {
|
||||
if (uiState.keyboardClickAction.isReleased || uiState.gamepadClickAction.isReleased) isClicked = true;
|
||||
}
|
||||
}
|
||||
updateUiState(area.position, size, isHot, isActive, isClicked);
|
||||
return isClicked;
|
||||
}
|
||||
|
||||
void drawUiButton(Vec2 size, IStr text, UiButtonOptions options = UiButtonOptions()) {
|
||||
if (options.font.isEmpty) options.font = engineFont;
|
||||
auto area = Rect(uiState.itemPoint, size);
|
||||
if (options.isDisabled) {
|
||||
drawRect(area, options.disabledColor);
|
||||
} else if (isUiItemActive) {
|
||||
drawRect(area, options.activeColor);
|
||||
} else if (isUiItemHot) {
|
||||
drawRect(area, options.hotColor);
|
||||
} else {
|
||||
drawRect(area, options.idleColor);
|
||||
}
|
||||
if (options.isDisabled) {
|
||||
auto tempOptions = DrawOptions(Hook.center);
|
||||
tempOptions.color.a = defaultUiAlpha / 2;
|
||||
drawText(options.font, text, area.centerPoint, tempOptions);
|
||||
} else {
|
||||
drawText(options.font, text, area.centerPoint, DrawOptions(Hook.center));
|
||||
}
|
||||
}
|
||||
|
||||
bool uiButton(Vec2 size, IStr text, UiButtonOptions options = UiButtonOptions()) {
|
||||
auto result = updateUiButton(size, text, options);
|
||||
drawUiButton(size, text, options);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool uiDragHandle(Vec2 size, ref Vec2 point, UiButtonOptions options = UiButtonOptions()) {
|
||||
auto dragLimitX = Vec2(-100000.0f, 100000.0f);
|
||||
auto dragLimitY = Vec2(-100000.0f, 100000.0f);
|
||||
// NOTE: There is a potential bug here when size is bigger than the limit/viewport. I will ignore it for now.
|
||||
final switch (options.dragLimit) {
|
||||
case UiDragLimit.none: break;
|
||||
case UiDragLimit.viewport:
|
||||
dragLimitX = Vec2(0.0f, uiState.viewportSize.x);
|
||||
dragLimitY = Vec2(0.0f, uiState.viewportSize.y);
|
||||
break;
|
||||
case UiDragLimit.viewportAndX:
|
||||
point.y = clamp(point.y, 0.0f, uiState.viewportSize.y - size.y);
|
||||
dragLimitX = Vec2(0.0f, uiState.viewportSize.x);
|
||||
dragLimitY = Vec2(point.y, point.y + size.y);
|
||||
break;
|
||||
case UiDragLimit.viewportAndY:
|
||||
point.x = clamp(point.x, 0.0f, uiState.viewportSize.x - size.x);
|
||||
dragLimitX = Vec2(point.x, point.x + size.x);
|
||||
dragLimitY = Vec2(0.0f, uiState.viewportSize.y);
|
||||
break;
|
||||
case UiDragLimit.custom:
|
||||
dragLimitX = options.dragLimitX;
|
||||
dragLimitY = options.dragLimitY;
|
||||
break;
|
||||
case UiDragLimit.customAndX:
|
||||
point.y = clamp(point.y, 0.0f, options.dragLimitY.y - size.y);
|
||||
dragLimitX = options.dragLimitX;
|
||||
dragLimitY = Vec2(point.y, point.y + size.y);
|
||||
break;
|
||||
case UiDragLimit.customAndY:
|
||||
point.x = clamp(point.x, 0.0f, options.dragLimitX.y - size.x);
|
||||
dragLimitX = Vec2(point.x, point.x + size.x);
|
||||
dragLimitY = options.dragLimitY;
|
||||
break;
|
||||
}
|
||||
|
||||
size.x = clamp(size.x, 0.0f, dragLimitX.y - dragLimitX.x);
|
||||
size.y = clamp(size.y, 0.0f, dragLimitY.y - dragLimitY.x);
|
||||
point.x = clamp(point.x, dragLimitX.x, dragLimitX.y - size.x);
|
||||
point.y = clamp(point.y, dragLimitY.x, dragLimitY.y - size.y);
|
||||
setUiStartPoint(point);
|
||||
updateUiButton(size, "", options);
|
||||
if (isUiItemDragged) {
|
||||
auto m = (mouse - uiState.viewportPoint) / uiState.viewportScale; // NOTE: Maybe this should be a function?
|
||||
point.y = clamp(m.y + uiDragOffset.y, dragLimitY.x, dragLimitY.y - size.y);
|
||||
point.x = clamp(m.x + uiDragOffset.x, dragLimitX.x, dragLimitX.y - size.x);
|
||||
uiState = uiPreviousState;
|
||||
setUiStartPoint(point);
|
||||
updateUiButton(size, "", options);
|
||||
drawUiButton(size, "", options);
|
||||
return true;
|
||||
} else {
|
||||
drawUiButton(size, "", options);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void uiTexture(Texture texture, UiButtonOptions options = UiButtonOptions()) {
|
||||
auto point = uiState.layoutStartPoint + uiState.layoutStartPointOffest;
|
||||
drawRect(Rect(point, texture.size), black);
|
||||
drawTexture(texture, point);
|
||||
updateUiState(point, texture.size, false, false, false);
|
||||
}
|
||||
|
||||
void uiTexture(TextureId texture, UiButtonOptions options = UiButtonOptions()) {
|
||||
uiTexture(texture.get(), options);
|
||||
}
|
||||
|
||||
void uiText(IStr text, UiButtonOptions options = UiButtonOptions()) {
|
||||
if (options.font.isEmpty) options.font = engineFont;
|
||||
auto point = uiState.layoutStartPoint + uiState.layoutStartPointOffest;
|
||||
auto size = measureTextSize(options.font, text);
|
||||
drawText(options.font, text, point);
|
||||
updateUiState(point, size, false, false, false);
|
||||
}
|
||||
|
|
|
@ -9,15 +9,11 @@
|
|||
// TODO: Think about gaps in an atlas texture.
|
||||
// TODO: Update all the doc comments here.
|
||||
|
||||
/// The `tilemap` module provides a simple and fast tile map.
|
||||
module parin.tilemap;
|
||||
/// The `map` module provides a simple and fast tile map.
|
||||
module parin.map;
|
||||
|
||||
import joka.ascii;
|
||||
import parin.engine;
|
||||
public import joka.containers;
|
||||
public import joka.faults;
|
||||
public import joka.math;
|
||||
public import joka.types;
|
||||
|
||||
@safe @nogc nothrow:
|
||||
|
||||
|
@ -130,11 +126,11 @@ struct TileMap {
|
|||
}
|
||||
|
||||
Sz rowCount() {
|
||||
return data.length == 0 ? 0 : softMaxRowCount;
|
||||
return data.length ? softMaxRowCount : 0;
|
||||
}
|
||||
|
||||
Sz colCount() {
|
||||
return data.length == 0 ? 0 : softMaxColCount;
|
||||
return data.length ? softMaxColCount : 0;
|
||||
}
|
||||
|
||||
bool isEmpty() {
|
||||
|
@ -142,7 +138,7 @@ struct TileMap {
|
|||
}
|
||||
|
||||
bool has(Sz row, Sz col) {
|
||||
return row < softMaxRowCount && col < softMaxColCount;
|
||||
return row < rowCount && col < colCount;
|
||||
}
|
||||
|
||||
bool has(IVec2 position) {
|
||||
|
@ -160,11 +156,11 @@ struct TileMap {
|
|||
}
|
||||
|
||||
int width() {
|
||||
return cast(int) (softMaxColCount * tileWidth);
|
||||
return cast(int) (colCount * tileWidth);
|
||||
}
|
||||
|
||||
int height() {
|
||||
return cast(int) (softMaxRowCount * tileHeight);
|
||||
return cast(int) (rowCount * tileHeight);
|
||||
}
|
||||
|
||||
/// Returns the size of the tile map.
|
||||
|
@ -230,32 +226,35 @@ struct TileMap {
|
|||
}
|
||||
|
||||
IVec2 firstGridPosition(Vec2 topLeftWorldPosition, DrawOptions options = DrawOptions()) {
|
||||
if (rowCount == 0 || colCount == 0) return IVec2();
|
||||
auto result = IVec2();
|
||||
auto targetTileWidth = cast(int) (tileWidth * options.scale.x);
|
||||
auto targetTileHeight = cast(int) (tileHeight * options.scale.y);
|
||||
result.y = cast(int) floor(clamp((topLeftWorldPosition.y - position.y) / targetTileHeight, 0, softMaxRowCount));
|
||||
result.x = cast(int) floor(clamp((topLeftWorldPosition.x - position.x) / targetTileWidth, 0, softMaxColCount));
|
||||
result.y = cast(int) floor(clamp((topLeftWorldPosition.y - position.y) / targetTileHeight, 0, rowCount - 1));
|
||||
result.x = cast(int) floor(clamp((topLeftWorldPosition.x - position.x) / targetTileWidth, 0, colCount - 1));
|
||||
return result;
|
||||
}
|
||||
|
||||
IVec2 lastGridPosition(Vec2 bottomRightWorldPosition, DrawOptions options = DrawOptions()) {
|
||||
if (rowCount == 0 || colCount == 0) return IVec2();
|
||||
auto result = IVec2();
|
||||
auto targetTileWidth = cast(int) (tileWidth * options.scale.x);
|
||||
auto targetTileHeight = cast(int) (tileHeight * options.scale.y);
|
||||
auto extraTileCount = options.hook == Hook.topLeft ? 1 : 2;
|
||||
result.y = cast(int) floor(clamp((bottomRightWorldPosition.y - position.y) / targetTileHeight + extraTileCount, 0, softMaxRowCount));
|
||||
result.x = cast(int) floor(clamp((bottomRightWorldPosition.x - position.x) / targetTileWidth + extraTileCount, 0, softMaxColCount));
|
||||
result.y = cast(int) floor(clamp((bottomRightWorldPosition.y - position.y) / targetTileHeight + extraTileCount, 0, rowCount - 1));
|
||||
result.x = cast(int) floor(clamp((bottomRightWorldPosition.x - position.x) / targetTileWidth + extraTileCount, 0, colCount - 1));
|
||||
return result;
|
||||
}
|
||||
|
||||
auto gridPositions(Vec2 topLeftWorldPosition, Vec2 bottomRightWorldPosition, DrawOptions options = DrawOptions()) {
|
||||
static struct Range {
|
||||
Sz colCount;
|
||||
IVec2 first;
|
||||
IVec2 last;
|
||||
IVec2 position;
|
||||
|
||||
bool empty() {
|
||||
return position == last;
|
||||
return position.x > last.x || position.y > last.y;
|
||||
}
|
||||
|
||||
IVec2 front() {
|
||||
|
@ -264,7 +263,7 @@ struct TileMap {
|
|||
|
||||
void popFront() {
|
||||
position.x += 1;
|
||||
if (position.x >= maxColCount) {
|
||||
if (position.x >= colCount) {
|
||||
position.x = first.x;
|
||||
position.y += 1;
|
||||
}
|
||||
|
@ -272,6 +271,7 @@ struct TileMap {
|
|||
}
|
||||
|
||||
auto result = Range(
|
||||
colCount,
|
||||
firstGridPosition(topLeftWorldPosition, options),
|
||||
lastGridPosition(bottomRightWorldPosition, options),
|
||||
);
|
||||
|
@ -321,8 +321,8 @@ void drawTileMap(Texture texture, TileMap map, Camera camera, DrawOptions option
|
|||
if (colRow1.x == colRow2.x || colRow1.y == colRow2.y) return;
|
||||
|
||||
auto textureArea = Rect(map.tileWidth, map.tileHeight);
|
||||
foreach (row; colRow1.y .. colRow2.y) {
|
||||
foreach (col; colRow1.x .. colRow2.x) {
|
||||
foreach (row; colRow1.y .. colRow2.y + 1) {
|
||||
foreach (col; colRow1.x .. colRow2.x + 1) {
|
||||
auto id = map[row, col];
|
||||
if (id < 0) continue;
|
||||
textureArea.position.x = (id % textureColCount) * map.tileWidth;
|
|
@ -11,7 +11,7 @@ module parin;
|
|||
public import joka.ascii;
|
||||
public import joka.io;
|
||||
public import parin.engine;
|
||||
public import parin.scene;
|
||||
public import parin.map;
|
||||
public import parin.sprite;
|
||||
public import parin.tilemap;
|
||||
public import parin.timer;
|
||||
public import parin.ui;
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
// Version: v0.0.29
|
||||
// ---
|
||||
|
||||
// NOTE(Kapendev): I am not a fan of this module and I would remove it, but maybe someone is using it.
|
||||
// TODO: Update all the doc comments here.
|
||||
|
||||
/// The `scene` module provides a simple scene manager.
|
||||
|
|
443
source/parin/ui.d
Normal file
443
source/parin/ui.d
Normal file
|
@ -0,0 +1,443 @@
|
|||
// ---
|
||||
// Copyright 2024 Alexandros F. G. Kapretsos
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Email: alexandroskapretsos@gmail.com
|
||||
// Project: https://github.com/Kapendev/parin
|
||||
// Version: v0.0.29
|
||||
// ---
|
||||
|
||||
// TODO: Clean maybe the UiState struct and prepareUi func.
|
||||
// TODO: Add way to get item point for some stuff. This is nice when making lists.
|
||||
// TODO: Add focus style.
|
||||
// TODO: Add way to align text in buttons.
|
||||
// TODO: Look at the API again.
|
||||
// TODO: Test the ui code and think how to make it better while working on real stuff.
|
||||
|
||||
/// The `ui` module functions as a immediate mode UI toolkit.
|
||||
module parin.ui;
|
||||
|
||||
import parin.engine;
|
||||
|
||||
UiState uiState;
|
||||
UiState uiPreviousState;
|
||||
|
||||
enum defaultUiAlpha = 230;
|
||||
enum defaultUiDisabledColor = 0x202020.toRgb().alpha(defaultUiAlpha);
|
||||
enum defaultUiIdleColor = 0x414141.toRgb().alpha(defaultUiAlpha);
|
||||
enum defaultUiHotColor = 0x818181.toRgb().alpha(defaultUiAlpha);
|
||||
enum defaultUiActiveColor = 0xBABABA.toRgb().alpha(defaultUiAlpha);
|
||||
|
||||
/// A type representing the constraints on drag movement.
|
||||
enum UiDragLimit: ubyte {
|
||||
none, /// No limits.
|
||||
viewport, /// Limited to the viewport.
|
||||
viewportAndX, /// Limited to the viewport and on the X-axis.
|
||||
viewportAndY, /// Limited to the viewport and on the Y-axis.
|
||||
custom, /// Limited to custom limits.
|
||||
customAndX, /// Limited to custom limits and on the X-axis.
|
||||
customAndY, /// Limited to custom limits and on the Y-axis.
|
||||
}
|
||||
|
||||
struct UiButtonOptions {
|
||||
Color disabledColor = defaultUiDisabledColor;
|
||||
Color idleColor = defaultUiIdleColor;
|
||||
Color hotColor = defaultUiHotColor;
|
||||
Color activeColor = defaultUiActiveColor;
|
||||
Font font;
|
||||
|
||||
bool isDisabled;
|
||||
UiDragLimit dragLimit;
|
||||
Vec2 dragLimitX = Vec2(-100000.0f, 100000.0f);
|
||||
Vec2 dragLimitY = Vec2(-100000.0f, 100000.0f);
|
||||
|
||||
@safe @nogc nothrow:
|
||||
|
||||
this(bool isDisabled) {
|
||||
this.isDisabled = isDisabled;
|
||||
}
|
||||
|
||||
this(UiDragLimit dragLimit) {
|
||||
this.dragLimit = dragLimit;
|
||||
}
|
||||
}
|
||||
|
||||
struct UiState {
|
||||
Mouse mouseClickAction = Mouse.left;
|
||||
Keyboard keyboardClickAction = Keyboard.space;
|
||||
Gamepad gamepadClickAction = Gamepad.a;
|
||||
bool isActOnPress;
|
||||
|
||||
Vec2 mousePressedPoint;
|
||||
Vec2 viewportPoint;
|
||||
Vec2 viewportSize;
|
||||
Vec2 viewportScale = Vec2(1);
|
||||
Vec2 startPoint;
|
||||
short margin;
|
||||
Layout layout;
|
||||
Vec2 layoutStartPoint;
|
||||
Vec2 layoutStartPointOffest;
|
||||
Vec2 layoutMaxItemSize;
|
||||
|
||||
Vec2 itemDragOffset;
|
||||
Vec2 itemPoint;
|
||||
Vec2 itemSize;
|
||||
short itemId;
|
||||
short hotItemId;
|
||||
short activeItemId;
|
||||
short clickedItemId;
|
||||
short draggedItemId;
|
||||
short focusedItemId;
|
||||
}
|
||||
|
||||
void prepareUi() {
|
||||
setUiViewportState(Vec2(), resolution, Vec2(1.0f));
|
||||
uiState.startPoint = Vec2();
|
||||
uiState.margin = 0;
|
||||
uiState.layout = Layout.v;
|
||||
uiState.layoutStartPoint = Vec2();
|
||||
uiState.layoutStartPointOffest = Vec2();
|
||||
uiState.layoutMaxItemSize = Vec2();
|
||||
uiState.itemPoint = Vec2();
|
||||
uiState.itemSize = Vec2();
|
||||
uiState.itemId = 0;
|
||||
uiState.hotItemId = 0;
|
||||
uiState.activeItemId = 0;
|
||||
uiState.clickedItemId = 0;
|
||||
}
|
||||
|
||||
Vec2 uiMouse() {
|
||||
auto result = (mouse - uiState.viewportPoint) / uiState.viewportScale;
|
||||
if (result.x < 0) result.x = -100000.0f;
|
||||
else if (result.x > uiState.viewportSize.x) result.x = 100000.0f;
|
||||
if (result.y < 0) result.y = -100000.0f;
|
||||
else if (result.y > uiState.viewportSize.y) result.y = 100000.0f;
|
||||
return result;
|
||||
}
|
||||
|
||||
void setUiClickAction(Mouse value) {
|
||||
uiState.mouseClickAction = value;
|
||||
}
|
||||
|
||||
void setUiClickAction(Keyboard value) {
|
||||
uiState.keyboardClickAction = value;
|
||||
}
|
||||
|
||||
void setUiClickAction(Gamepad value) {
|
||||
uiState.gamepadClickAction = value;
|
||||
}
|
||||
|
||||
bool isUiActOnPress() {
|
||||
return uiState.isActOnPress;
|
||||
}
|
||||
|
||||
void setIsUiActOnPress(bool value) {
|
||||
uiState.isActOnPress = value;
|
||||
}
|
||||
|
||||
void setUiViewportState(Vec2 point, Vec2 size, Vec2 scale) {
|
||||
uiState.viewportPoint = point;
|
||||
uiState.viewportSize = size;
|
||||
uiState.viewportScale = scale;
|
||||
|
||||
if (uiState.mouseClickAction.isPressed) {
|
||||
uiState.mousePressedPoint = uiMouse;
|
||||
}
|
||||
}
|
||||
|
||||
Vec2 uiStartPoint() {
|
||||
return uiState.startPoint;
|
||||
}
|
||||
|
||||
void setUiStartPoint(Vec2 value) {
|
||||
uiState.itemSize = Vec2();
|
||||
uiState.startPoint = value;
|
||||
uiState.layoutStartPoint = value;
|
||||
uiState.layoutStartPointOffest = Vec2();
|
||||
uiState.layoutMaxItemSize = Vec2();
|
||||
}
|
||||
|
||||
short uiMargin() {
|
||||
return uiState.margin;
|
||||
}
|
||||
|
||||
void setUiMargin(short value) {
|
||||
uiState.margin = value;
|
||||
}
|
||||
|
||||
void useUiLayout(Layout value) {
|
||||
if (uiState.layoutStartPointOffest) {
|
||||
final switch (value) {
|
||||
case Layout.v:
|
||||
if (uiState.layoutStartPointOffest.x > uiState.layoutMaxItemSize.x) {
|
||||
uiState.layoutStartPoint.x = uiState.layoutStartPoint.x + uiState.layoutStartPointOffest.x + uiState.margin;
|
||||
} else {
|
||||
uiState.layoutStartPoint.x += uiState.layoutMaxItemSize.x + uiState.margin;
|
||||
}
|
||||
uiState.layoutStartPointOffest = Vec2();
|
||||
uiState.layoutMaxItemSize.x = 0.0f;
|
||||
break;
|
||||
case Layout.h:
|
||||
uiState.layoutStartPoint.x = uiState.startPoint.x;
|
||||
if (uiState.layoutStartPointOffest.y > uiState.layoutMaxItemSize.y) {
|
||||
uiState.layoutStartPoint.y = uiState.layoutStartPoint.y + uiState.layoutStartPointOffest.y + uiState.margin;
|
||||
} else {
|
||||
uiState.layoutStartPoint.y += uiState.layoutMaxItemSize.y + uiState.margin;
|
||||
}
|
||||
uiState.layoutStartPointOffest = Vec2();
|
||||
uiState.layoutMaxItemSize.y = 0.0f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
uiState.layout = value;
|
||||
}
|
||||
|
||||
bool isUiItemHot() {
|
||||
return uiState.itemId == uiState.hotItemId;
|
||||
}
|
||||
|
||||
bool isUiHot() {
|
||||
return uiState.hotItemId > 0;
|
||||
}
|
||||
|
||||
bool isUiItemActive() {
|
||||
return uiState.itemId == uiState.activeItemId;
|
||||
}
|
||||
|
||||
bool isUiActive() {
|
||||
return uiState.activeItemId > 0;
|
||||
}
|
||||
|
||||
bool isUiItemClicked() {
|
||||
return uiState.itemId == uiState.clickedItemId;
|
||||
}
|
||||
|
||||
bool isUiClicked() {
|
||||
return uiState.clickedItemId > 0;
|
||||
}
|
||||
|
||||
bool isUiItemDragged() {
|
||||
return uiState.itemId == uiState.draggedItemId && deltaMouse;
|
||||
}
|
||||
|
||||
bool isUiDragged() {
|
||||
return uiState.draggedItemId > 0 && deltaMouse;
|
||||
}
|
||||
|
||||
Vec2 uiDragOffset() {
|
||||
return uiState.itemDragOffset;
|
||||
}
|
||||
|
||||
int uiFocus() {
|
||||
return uiState.focusedItemId;
|
||||
}
|
||||
|
||||
void setUiFocus(short id) {
|
||||
uiState.focusedItemId = id;
|
||||
}
|
||||
|
||||
void clampUiFocus(short step, Sz length) {
|
||||
auto min = cast(short) (uiState.itemId + 1);
|
||||
auto max = cast(short) (length - 1 + min);
|
||||
auto isOutside = uiState.focusedItemId < min || uiState.focusedItemId > max;
|
||||
if (step == 0) {
|
||||
uiState.focusedItemId = min;
|
||||
return;
|
||||
}
|
||||
if (isOutside) {
|
||||
if (step < 0) {
|
||||
uiState.focusedItemId = max;
|
||||
return;
|
||||
} else {
|
||||
uiState.focusedItemId = min;
|
||||
return;
|
||||
}
|
||||
}
|
||||
uiState.focusedItemId = clamp(cast(short) (uiState.focusedItemId + step), min, max);
|
||||
}
|
||||
|
||||
void wrapUiFocus(short step, Sz length) {
|
||||
auto min = cast(short) (uiState.itemId + 1);
|
||||
auto max = cast(short) (length - 1 + min);
|
||||
auto isOutside = uiState.focusedItemId < min || uiState.focusedItemId > max;
|
||||
if (step == 0) {
|
||||
uiState.focusedItemId = min;
|
||||
return;
|
||||
}
|
||||
if (isOutside) {
|
||||
if (step < 0) {
|
||||
uiState.focusedItemId = max;
|
||||
return;
|
||||
} else {
|
||||
uiState.focusedItemId = min;
|
||||
return;
|
||||
}
|
||||
}
|
||||
uiState.focusedItemId = wrap(cast(short) (uiState.focusedItemId + step), min, cast(short) (max + 1));
|
||||
}
|
||||
|
||||
void updateUiState(Vec2 itemPoint, Vec2 itemSize, bool isHot, bool isActive, bool isClicked) {
|
||||
uiPreviousState = uiState;
|
||||
uiState.itemPoint = itemPoint;
|
||||
uiState.itemSize = itemSize;
|
||||
uiState.itemId += 1;
|
||||
if (itemSize.x > uiState.layoutMaxItemSize.x) uiState.layoutMaxItemSize.x = itemSize.x;
|
||||
if (itemSize.y > uiState.layoutMaxItemSize.y) uiState.layoutMaxItemSize.y = itemSize.y;
|
||||
final switch (uiState.layout) {
|
||||
case Layout.v: uiState.layoutStartPointOffest.y += uiState.itemSize.y + uiState.margin; break;
|
||||
case Layout.h: uiState.layoutStartPointOffest.x += uiState.itemSize.x + uiState.margin; break;
|
||||
}
|
||||
if (isHot) uiState.hotItemId = uiState.itemId;
|
||||
if (isActive) {
|
||||
uiState.activeItemId = uiState.itemId;
|
||||
uiState.focusedItemId = uiState.itemId;
|
||||
}
|
||||
if (isClicked) uiState.clickedItemId = uiState.itemId;
|
||||
if (uiState.mouseClickAction.isPressed && uiState.itemId == uiState.activeItemId) {
|
||||
auto m = uiMouse;
|
||||
uiState.itemDragOffset = uiState.itemPoint - m;
|
||||
uiState.draggedItemId = uiState.itemId;
|
||||
}
|
||||
if (uiState.draggedItemId) {
|
||||
if (uiState.mouseClickAction.isReleased) uiState.draggedItemId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool updateUiButton(Vec2 size, IStr text, UiButtonOptions options = UiButtonOptions()) {
|
||||
if (options.font.isEmpty) options.font = engineFont;
|
||||
auto m = uiMouse;
|
||||
auto id = uiState.itemId + 1;
|
||||
auto area = Rect(uiState.layoutStartPoint + uiState.layoutStartPointOffest, size);
|
||||
// auto isHot = area.hasPoint(uiMouse)
|
||||
auto isHot = m.x >= area.position.x && m.x < area.position.x + area.size.x && m.y >= area.position.y && m.y < area.position.y + area.size.y;
|
||||
auto isActive = isHot && uiState.mouseClickAction.isDown;
|
||||
auto isClicked = isHot;
|
||||
if (uiState.isActOnPress) {
|
||||
isClicked = isClicked && uiState.mouseClickAction.isPressed;
|
||||
} else {
|
||||
auto isHotFromMousePressedPoint =
|
||||
uiState.mousePressedPoint.x >= area.position.x &&
|
||||
uiState.mousePressedPoint.x < area.position.x + area.size.x &&
|
||||
uiState.mousePressedPoint.y >= area.position.y &&
|
||||
uiState.mousePressedPoint.y < area.position.y + area.size.y;
|
||||
isClicked = isClicked && isHotFromMousePressedPoint && uiState.mouseClickAction.isReleased;
|
||||
}
|
||||
|
||||
if (options.isDisabled) {
|
||||
isHot = false;
|
||||
isActive = false;
|
||||
isClicked = false;
|
||||
} else if (id == uiState.focusedItemId) {
|
||||
isHot = true;
|
||||
if (uiState.keyboardClickAction.isDown || uiState.gamepadClickAction.isDown) isActive = true;
|
||||
if (uiState.isActOnPress) {
|
||||
if (uiState.keyboardClickAction.isPressed || uiState.gamepadClickAction.isPressed) isClicked = true;
|
||||
} else {
|
||||
if (uiState.keyboardClickAction.isReleased || uiState.gamepadClickAction.isReleased) isClicked = true;
|
||||
}
|
||||
}
|
||||
updateUiState(area.position, size, isHot, isActive, isClicked);
|
||||
return isClicked;
|
||||
}
|
||||
|
||||
void drawUiButton(Vec2 size, IStr text, Vec2 point, bool isHot, bool isActive, UiButtonOptions options = UiButtonOptions()) {
|
||||
if (options.font.isEmpty) options.font = engineFont;
|
||||
auto area = Rect(point, size);
|
||||
if (options.isDisabled) {
|
||||
drawRect(area, options.disabledColor);
|
||||
} else if (isActive) {
|
||||
drawRect(area, options.activeColor);
|
||||
} else if (isHot) {
|
||||
drawRect(area, options.hotColor);
|
||||
} else {
|
||||
drawRect(area, options.idleColor);
|
||||
}
|
||||
if (options.isDisabled) {
|
||||
auto tempOptions = DrawOptions(Hook.center);
|
||||
tempOptions.color.a = defaultUiAlpha / 2;
|
||||
drawText(options.font, text, area.centerPoint, tempOptions);
|
||||
} else {
|
||||
drawText(options.font, text, area.centerPoint, DrawOptions(Hook.center));
|
||||
}
|
||||
}
|
||||
|
||||
bool uiButton(Vec2 size, IStr text, UiButtonOptions options = UiButtonOptions()) {
|
||||
auto result = updateUiButton(size, text, options);
|
||||
drawUiButton(size, text, uiState.itemPoint, isUiItemHot, isUiItemActive, options);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool uiDragHandle(Vec2 size, ref Vec2 point, UiButtonOptions options = UiButtonOptions()) {
|
||||
auto dragLimitX = Vec2(-100000.0f, 100000.0f);
|
||||
auto dragLimitY = Vec2(-100000.0f, 100000.0f);
|
||||
// NOTE: There is a potential bug here when size is bigger than the limit/viewport. I will ignore it for now.
|
||||
final switch (options.dragLimit) {
|
||||
case UiDragLimit.none: break;
|
||||
case UiDragLimit.viewport:
|
||||
dragLimitX = Vec2(0.0f, uiState.viewportSize.x);
|
||||
dragLimitY = Vec2(0.0f, uiState.viewportSize.y);
|
||||
break;
|
||||
case UiDragLimit.viewportAndX:
|
||||
point.y = clamp(point.y, 0.0f, uiState.viewportSize.y - size.y);
|
||||
dragLimitX = Vec2(0.0f, uiState.viewportSize.x);
|
||||
dragLimitY = Vec2(point.y, point.y + size.y);
|
||||
break;
|
||||
case UiDragLimit.viewportAndY:
|
||||
point.x = clamp(point.x, 0.0f, uiState.viewportSize.x - size.x);
|
||||
dragLimitX = Vec2(point.x, point.x + size.x);
|
||||
dragLimitY = Vec2(0.0f, uiState.viewportSize.y);
|
||||
break;
|
||||
case UiDragLimit.custom:
|
||||
dragLimitX = options.dragLimitX;
|
||||
dragLimitY = options.dragLimitY;
|
||||
break;
|
||||
case UiDragLimit.customAndX:
|
||||
point.y = clamp(point.y, 0.0f, options.dragLimitY.y - size.y);
|
||||
dragLimitX = options.dragLimitX;
|
||||
dragLimitY = Vec2(point.y, point.y + size.y);
|
||||
break;
|
||||
case UiDragLimit.customAndY:
|
||||
point.x = clamp(point.x, 0.0f, options.dragLimitX.y - size.x);
|
||||
dragLimitX = Vec2(point.x, point.x + size.x);
|
||||
dragLimitY = options.dragLimitY;
|
||||
break;
|
||||
}
|
||||
|
||||
size.x = clamp(size.x, 0.0f, dragLimitX.y - dragLimitX.x);
|
||||
size.y = clamp(size.y, 0.0f, dragLimitY.y - dragLimitY.x);
|
||||
point.x = clamp(point.x, dragLimitX.x, dragLimitX.y - size.x);
|
||||
point.y = clamp(point.y, dragLimitY.x, dragLimitY.y - size.y);
|
||||
setUiStartPoint(point);
|
||||
updateUiButton(size, "", options);
|
||||
if (isUiItemDragged) {
|
||||
auto m = (mouse - uiState.viewportPoint) / uiState.viewportScale; // NOTE: Maybe this should be a function?
|
||||
point.y = clamp(m.y + uiDragOffset.y, dragLimitY.x, dragLimitY.y - size.y);
|
||||
point.x = clamp(m.x + uiDragOffset.x, dragLimitX.x, dragLimitX.y - size.x);
|
||||
uiState = uiPreviousState;
|
||||
setUiStartPoint(point);
|
||||
updateUiButton(size, "", options);
|
||||
drawUiButton(size, "", uiState.itemPoint, isUiItemHot, isUiItemActive, options);
|
||||
return true;
|
||||
} else {
|
||||
drawUiButton(size, "", uiState.itemPoint, isUiItemHot, isUiItemActive, options);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void uiTexture(Texture texture, UiButtonOptions options = UiButtonOptions()) {
|
||||
auto point = uiState.layoutStartPoint + uiState.layoutStartPointOffest;
|
||||
drawRect(Rect(point, texture.size), black);
|
||||
drawTexture(texture, point);
|
||||
updateUiState(point, texture.size, false, false, false);
|
||||
}
|
||||
|
||||
void uiTexture(TextureId texture, UiButtonOptions options = UiButtonOptions()) {
|
||||
uiTexture(texture.get(), options);
|
||||
}
|
||||
|
||||
void uiText(IStr text, UiButtonOptions options = UiButtonOptions()) {
|
||||
if (options.font.isEmpty) options.font = engineFont;
|
||||
auto point = uiState.layoutStartPoint + uiState.layoutStartPointOffest;
|
||||
auto size = measureTextSize(options.font, text);
|
||||
drawText(options.font, text, point);
|
||||
updateUiState(point, size, false, false, false);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue