diff --git a/src/dlangide.d b/src/dlangide.d index cf9338d..f7af2d7 100644 --- a/src/dlangide.d +++ b/src/dlangide.d @@ -39,7 +39,7 @@ extern (C) int UIAppMain(string[] args) { // open home screen tab frame.showHomeScreen(); // for testing: load workspace at startup - frame.openFileOrWorkspace(appendPath(exePath, "../workspaces/sample1/sample1.dlangidews")); + //frame.openFileOrWorkspace(appendPath(exePath, "../workspaces/sample1/sample1.dlangidews")); // show window window.show(); diff --git a/src/dlangide/builders/builder.d b/src/dlangide/builders/builder.d index ae5f679..6f07359 100644 --- a/src/dlangide/builders/builder.d +++ b/src/dlangide/builders/builder.d @@ -63,9 +63,12 @@ class Builder : BackgroundOperationWatcher { params ~= "clean".dup; } else if (_buildOp == BuildOperation.Run) { params ~= "run".dup; + } else if (_buildOp == BuildOperation.Upgrade) { + params ~= "upgrade".dup; + params ~= "--force-remove".dup; } - if (_buildOp != BuildOperation.Clean) { + if (_buildOp != BuildOperation.Clean && _buildOp != BuildOperation.Upgrade) { switch (_buildConfig) { default: case BuildConfiguration.Debug: diff --git a/src/dlangide/ui/commands.d b/src/dlangide/ui/commands.d index c68952f..fbbec86 100644 --- a/src/dlangide/ui/commands.d +++ b/src/dlangide/ui/commands.d @@ -22,6 +22,8 @@ enum IDEActions : int { BuildProject, RebuildProject, CleanProject, + RefreshProject, + UpdateProjectDependencies, SetStartupProject, ProjectSettings, DebugStart, @@ -52,6 +54,8 @@ const Action ACTION_PROJECT_REBUILD = new Action(IDEActions.RebuildProject, "MEN const Action ACTION_PROJECT_CLEAN = new Action(IDEActions.CleanProject, "MENU_BUILD_PROJECT_CLEAN"c, null); const Action ACTION_PROJECT_SET_STARTUP = new Action(IDEActions.SetStartupProject, "MENU_PROJECT_SET_AS_STARTUP"c, null); const Action ACTION_PROJECT_SETTINGS = new Action(IDEActions.ProjectSettings, "MENU_PROJECT_SETTINGS"c, null); +const Action ACTION_PROJECT_REFRESH = new Action(IDEActions.RefreshProject, "MENU_PROJECT_REFRESH"c); +const Action ACTION_PROJECT_UPDATE_DEPENDENCIES = new Action(IDEActions.UpdateProjectDependencies, "MENU_PROJECT_UPDATE_DEPENDENCIES"c); const Action ACTION_DEBUG_START = new Action(IDEActions.DebugStart, "MENU_DEBUG_START_DEBUGGING"c, "debug-run"c, KeyCode.F5, 0); const Action ACTION_DEBUG_START_NO_DEBUG = new Action(IDEActions.DebugStartNoDebug, "MENU_DEBUG_START_NO_DEBUGGING"c, null, KeyCode.F5, KeyFlag.Control); const Action ACTION_DEBUG_CONTINUE = new Action(IDEActions.DebugContinue, "MENU_DEBUG_CONTINUE"c); diff --git a/src/dlangide/ui/frame.d b/src/dlangide/ui/frame.d index fc4eec0..cb498dd 100644 --- a/src/dlangide/ui/frame.d +++ b/src/dlangide/ui/frame.d @@ -298,7 +298,7 @@ class IDEFrame : AppFrame { editItem.add(new Action(20, "MENU_EDIT_PREFERENCES")); MenuItem projectItem = new MenuItem(new Action(21, "MENU_PROJECT")); - projectItem.add(ACTION_PROJECT_SET_STARTUP, ACTION_PROJECT_SETTINGS); + projectItem.add(ACTION_PROJECT_SET_STARTUP, ACTION_PROJECT_REFRESH, ACTION_PROJECT_UPDATE_DEPENDENCIES, ACTION_PROJECT_SETTINGS); MenuItem buildItem = new MenuItem(new Action(22, "MENU_BUILD")); buildItem.add(ACTION_WORKSPACE_BUILD, ACTION_WORKSPACE_REBUILD, ACTION_WORKSPACE_CLEAN, @@ -416,6 +416,14 @@ class IDEFrame : AppFrame { buildProject(BuildOperation.Run); //setBackgroundOperation(new BackgroundOperationWatcherTest(this)); return true; + case IDEActions.UpdateProjectDependencies: + buildProject(BuildOperation.Upgrade); + //setBackgroundOperation(new BackgroundOperationWatcherTest(this)); + return true; + case IDEActions.RefreshProject: + refreshWorkspace(); + //setBackgroundOperation(new BackgroundOperationWatcherTest(this)); + return true; case IDEActions.WindowCloseAllDocuments: askForUnsavedEdits(delegate() { closeAllDocuments(); @@ -454,15 +462,20 @@ class IDEFrame : AppFrame { return; } } else if (filename.isProjectFile) { + _logPanel.clear(); + _logPanel.logLine("Trying to open project from " ~ filename); Project project = new Project(); if (!project.load(filename)) { + _logPanel.logLine("Cannot read project file " ~ filename); window.showMessageBox(UIString("Cannot open project"d), UIString("Error occured while opening project"d)); return; } + _logPanel.logLine("Project file is opened ok"); string defWsFile = project.defWorkspaceFile; if (currentWorkspace) { Project existing = currentWorkspace.findProject(project.filename); if (existing) { + _logPanel.logLine("This project already exists in current workspace"); window.showMessageBox(UIString("Open project"d), UIString("Project is already in workspace"d)); return; } @@ -470,28 +483,43 @@ class IDEFrame : AppFrame { [ACTION_ADD_TO_CURRENT_WORKSPACE, ACTION_CREATE_NEW_WORKSPACE, ACTION_CANCEL], 0, delegate(const Action result) { if (result.id == IDEActions.CreateNewWorkspace) { // new ws - Workspace ws = new Workspace(); - ws.name = project.name; - ws.description = project.description; - ws.addProject(project); - ws.save(defWsFile); - setWorkspace(ws); + createNewWorkspaceForExistingProject(project); } else if (result.id == IDEActions.AddToCurrentWorkspace) { // add to current currentWorkspace.addProject(project); currentWorkspace.save(); - _wsPanel.reloadItems(); + refreshWorkspace(); } return true; }); } else { // new workspace file + createNewWorkspaceForExistingProject(project); } } else { + _logPanel.logLine("File is not recognized as DlangIDE project or workspace file"); window.showMessageBox(UIString("Invalid workspace file"d), UIString("This file is not a valid workspace or project file"d)); } } + void refreshWorkspace() { + _logPanel.logLine("Refreshing workspace"); + _wsPanel.reloadItems(); + } + + void createNewWorkspaceForExistingProject(Project project) { + string defWsFile = project.defWorkspaceFile; + _logPanel.logLine("Creating new workspace " ~ defWsFile); + // new ws + Workspace ws = new Workspace(); + ws.name = project.name; + ws.description = project.description; + ws.addProject(project); + ws.save(defWsFile); + setWorkspace(ws); + _logPanel.logLine("Done"); + } + //bool loadWorkspace(string path) { // // testing workspace loader // Workspace ws = new Workspace(); @@ -505,11 +533,17 @@ class IDEFrame : AppFrame { closeAllDocuments(); currentWorkspace = ws; _wsPanel.workspace = ws; + if (ws.startupProject && ws.startupProject.mainSourceFile) { + openSourceFile(ws.startupProject.mainSourceFile.filename); + _tabs.setFocus(); + } } void buildProject(BuildOperation buildOp) { - if (!currentWorkspace || !currentWorkspace.startupProject) + if (!currentWorkspace || !currentWorkspace.startupProject) { + _logPanel.logLine("No project is opened"); return; + } Builder op = new Builder(this, currentWorkspace.startupProject, _logPanel, currentWorkspace.buildConfiguration, buildOp, false); setBackgroundOperation(op); } diff --git a/src/dlangide/ui/outputpanel.d b/src/dlangide/ui/outputpanel.d index 26f26e5..c6937a4 100644 --- a/src/dlangide/ui/outputpanel.d +++ b/src/dlangide/ui/outputpanel.d @@ -4,6 +4,8 @@ import dlangui.all; import dlangide.workspace.workspace; import dlangide.workspace.project; +import std.utf; + class OutputPanel : DockWindow { protected LogWidget _logWidget; @@ -24,6 +26,22 @@ class OutputPanel : DockWindow { _logWidget.appendText(msg); } + void logLine(string category, dstring msg) { + appendText(category, msg ~ "\n"); + } + + void logLine(dstring msg) { + logLine(null, msg); + } + + void logLine(string category, string msg) { + appendText(category, toUTF32(msg ~ "\n")); + } + + void logLine(string msg) { + logLine(null, msg); + } + void clear(string category = null) { _logWidget.text = ""d; } diff --git a/src/dlangide/workspace/project.d b/src/dlangide/workspace/project.d index 1d7abd1..b5c22bc 100644 --- a/src/dlangide/workspace/project.d +++ b/src/dlangide/workspace/project.d @@ -209,6 +209,7 @@ class Project : WorkspaceItem { protected Workspace _workspace; protected bool _opened; protected ProjectFolder _items; + protected ProjectSourceFile _mainSourceFile; this(string fname = null) { super(fname); _items = new ProjectFolder(fname); @@ -220,6 +221,7 @@ class Project : WorkspaceItem { return buildNormalizedPath(_dir, path); } + @property ProjectSourceFile mainSourceFile() { return _mainSourceFile; } @property ProjectFolder items() { return _items; } @@ -244,6 +246,19 @@ class Project : WorkspaceItem { return folder; } + void findMainSourceFile() { + string n = toUTF8(name); + string[] mainnames = ["app.d", "main.d", n ~ ".d"]; + foreach(sname; mainnames) { + _mainSourceFile = findSourceFileItem(buildNormalizedPath(_dir, "src", sname)); + if (_mainSourceFile) + break; + _mainSourceFile = findSourceFileItem(buildNormalizedPath(_dir, "source", sname)); + if (_mainSourceFile) + break; + } + } + /// tries to find source file in project, returns found project source file item, or null if not found ProjectSourceFile findSourceFileItem(ProjectItem dir, string filename) { for (int i = 0; i < dir.childCount; i++) { @@ -266,6 +281,7 @@ class Project : WorkspaceItem { } override bool load(string fname = null) { + _mainSourceFile = null; if (fname.length > 0) filename = fname; if (!exists(filename) || !isFile(filename)) { @@ -282,6 +298,7 @@ class Project : WorkspaceItem { Log.d(" project description: ", _description); _items = findItems(); + findMainSourceFile(); } catch (JSONException e) { Log.e("Cannot parse json", e); return false; diff --git a/src/dlangide/workspace/workspace.d b/src/dlangide/workspace/workspace.d index 9e0924d..dbd5cba 100644 --- a/src/dlangide/workspace/workspace.d +++ b/src/dlangide/workspace/workspace.d @@ -12,7 +12,8 @@ enum BuildOperation { Build, Clean, Rebuild, - Run + Run, + Upgrade } enum BuildConfiguration { diff --git a/views/res/i18n/en.ini b/views/res/i18n/en.ini index fa69540..f49e621 100644 --- a/views/res/i18n/en.ini +++ b/views/res/i18n/en.ini @@ -28,6 +28,8 @@ MENU_BUILD_PROJECT_CLEAN=Clean Project MENU_PROJECT=&PROJECT MENU_PROJECT_SET_AS_STARTUP=Set as Startup Project MENU_PROJECT_SETTINGS=Project Settings +MENU_PROJECT_REFRESH=Refresh Workspace Items +MENU_PROJECT_UPDATE_DEPENDENCIES=Upgrade Dependencies MENU_DEBUG=&DEBUG MENU_DEBUG_START_DEBUGGING=Start Debugging MENU_DEBUG_START_NO_DEBUGGING=Start Without Debugging diff --git a/workspaces/tetris/dub.json b/workspaces/tetris/dub.json new file mode 100644 index 0000000..efbd2d8 --- /dev/null +++ b/workspaces/tetris/dub.json @@ -0,0 +1,17 @@ +{ + "name": "tetris", + "description": "dlangui library example: Tetris game", + "homepage": "https://github.com/buggins/dlangui", + "license": "Boost", + "authors": ["Vadim Lopatin"], + + "targetPath": "bin", + "targetType": "executable", + "targetName": "tetris", + + "stringImportPaths": ["views", "views/res", "views/res/i18n", "views/res/mdpi"], + + "dependencies": { + "dlangui:dlanguilib": "~master" + } +} diff --git a/workspaces/tetris/src/gui.d b/workspaces/tetris/src/gui.d new file mode 100644 index 0000000..61e19ba --- /dev/null +++ b/workspaces/tetris/src/gui.d @@ -0,0 +1,599 @@ +module gui; + +import model; + +import dlangui.all; + +/// game action codes +enum TetrisAction : int { + MoveLeft = 10000, + MoveRight, + RotateCCW, + FastDown, + Pause, + LevelUp, +} + +const Action ACTION_MOVE_LEFT = (new Action(TetrisAction.MoveLeft, KeyCode.LEFT)).addAccelerator(KeyCode.KEY_A).iconId("arrow-left"); +const Action ACTION_MOVE_RIGHT = (new Action(TetrisAction.MoveRight, KeyCode.RIGHT)).addAccelerator(KeyCode.KEY_D).iconId("arrow-right"); +const Action ACTION_ROTATE = (new Action(TetrisAction.RotateCCW, KeyCode.UP)).addAccelerator(KeyCode.KEY_W).iconId("rotate"); +const Action ACTION_FAST_DOWN = (new Action(TetrisAction.FastDown, KeyCode.SPACE)).addAccelerator(KeyCode.KEY_S).iconId("arrow-down"); +const Action ACTION_PAUSE = (new Action(TetrisAction.Pause, KeyCode.ESCAPE)).addAccelerator(KeyCode.PAUSE).iconId("pause"); +const Action ACTION_LEVEL_UP = (new Action(TetrisAction.LevelUp, KeyCode.ADD)).addAccelerator(KeyCode.INS).iconId("levelup"); + +const Action[] CUP_ACTIONS = [ACTION_PAUSE, ACTION_ROTATE, ACTION_LEVEL_UP, + ACTION_MOVE_LEFT, ACTION_FAST_DOWN, ACTION_MOVE_RIGHT]; + +/// about dialog +Widget createAboutWidget() +{ + LinearLayout res = new VerticalLayout(); + res.padding(Rect(10,10,10,10)); + res.addChild(new TextWidget(null, "DLangUI Tetris demo app"d)); + res.addChild(new TextWidget(null, "(C) Vadim Lopatin, 2014"d)); + res.addChild(new TextWidget(null, "http://github.com/buggins/dlangui"d)); + Button closeButton = new Button("close", "Close"d); + closeButton.onClickListener = delegate(Widget src) { + Log.i("Closing window"); + res.window.close(); + return true; + }; + res.addChild(closeButton); + return res; +} + +/// Cup States +enum CupState : int { + /// New figure appears + NewFigure, + /// Game is paused + Paused, + /// Figure is falling + FallingFigure, + /// Figure is hanging - pause between falling by one row + HangingFigure, + /// destroying complete rows + DestroyingRows, + /// falling after some rows were destroyed + FallingRows, + /// Game is over + GameOver, +} + +/// Cup widget +class CupWidget : Widget { + /// cup columns count + int _cols; + /// cup rows count + int _rows; + /// cup data + Cup _cup; + + + /// Level 1..10 + int _level; + /// Score + int _score; + /// Single cell movement duration for current level, in 1/10000000 of seconds + long _movementDuration; + /// When true, figure is falling down fast + bool _fastDownFlag; + /// animation helper for fade and movement in different states + AnimationHelper _animation; + /// GameOver popup + private PopupWidget _gameOverPopup; + /// Status widget + private StatusWidget _status; + /// Current state + protected CupState _state; + + protected int _totalRowsDestroyed; + + static const int[10] LEVEL_SPEED = [15000000, 10000000, 7000000, 6000000, 5000000, 4000000, 300000, 2000000, 1500000, 1000000]; + + static const int RESERVED_ROWS = 5; // reserved for next figure + + /// set difficulty level 1..10 + void setLevel(int level) { + if (level > 10) + return; + _level = level; + _movementDuration = LEVEL_SPEED[level - 1]; + _status.setLevel(_level); + } + + static const int MIN_FAST_FALLING_INTERVAL = 600000; + + static const int ROWS_FALLING_INTERVAL = 1200000; + + /// change game state, init state animation when necessary + void setCupState(CupState state) { + int animationIntervalPercent = 100; + switch (state) { + case CupState.FallingFigure: + animationIntervalPercent = _fastDownFlag ? 10 : 25; + break; + case CupState.HangingFigure: + animationIntervalPercent = 75; + break; + case CupState.NewFigure: + animationIntervalPercent = 100; + break; + case CupState.FallingRows: + animationIntervalPercent = 25; + break; + case CupState.DestroyingRows: + animationIntervalPercent = 50; + break; + default: + // no animation for other states + animationIntervalPercent = 0; + break; + } + _state = state; + if (animationIntervalPercent) { + long interval = _movementDuration * animationIntervalPercent / 100; + if (_fastDownFlag && falling && interval > MIN_FAST_FALLING_INTERVAL) + interval = MIN_FAST_FALLING_INTERVAL; + if (_state == CupState.FallingRows) + interval = ROWS_FALLING_INTERVAL; + _animation.start(interval, 255); + } + invalidate(); + } + + void addScore(int score) { + _score += score; + _status.setScore(_score); + } + + /// returns true if figure is in falling - movement state + @property bool falling() { + return _state == CupState.FallingFigure; + } + + /// Turn on / off fast falling down + bool handleFastDown(bool fast) { + if (fast == true) { + if (_fastDownFlag) + return false; + // handle turn on fast down + if (falling) { + _fastDownFlag = true; + // if already falling, just increase speed + _animation.interval = _movementDuration * 10 / 100; + if (_animation.interval > MIN_FAST_FALLING_INTERVAL) + _animation.interval = MIN_FAST_FALLING_INTERVAL; + return true; + } else if (_state == CupState.HangingFigure) { + _fastDownFlag = true; + setCupState(CupState.FallingFigure); + return true; + } else { + return false; + } + } + _fastDownFlag = fast; + return true; + } + + static const int[] NEXT_LEVEL_SCORE = [0, 20, 50, 100, 200, 350, 500, 750, 1000, 1500, 2000]; + + /// try start next figure + protected void nextFigure() { + if (!_cup.dropNextFigure()) { + // Game Over + setCupState(CupState.GameOver); + Widget popupWidget = new TextWidget("popup", "Game Over!"d); + popupWidget.padding(Rect(30, 30, 30, 30)).backgroundImageId("popup_background").alpha(0x40).fontWeight(800).fontSize(30); + _gameOverPopup = window.showPopup(popupWidget, this); + } else { + setCupState(CupState.NewFigure); + if (_level < 10 && _totalRowsDestroyed >= NEXT_LEVEL_SCORE[_level]) + setLevel(_level + 1); // level up + } + } + + protected void destroyFullRows() { + setCupState(CupState.DestroyingRows); + } + + protected void onAnimationFinished() { + switch (_state) { + case CupState.NewFigure: + _fastDownFlag = false; + _cup.genNextFigure(); + setCupState(CupState.HangingFigure); + break; + case CupState.FallingFigure: + if (_cup.isPositionFreeBelow()) { + _cup.move(0, -1, false); + if (_fastDownFlag) + setCupState(CupState.FallingFigure); + else + setCupState(CupState.HangingFigure); + } else { + // At bottom of cup + _cup.putFigure(); + _fastDownFlag = false; + if (_cup.hasFullRows) { + destroyFullRows(); + } else { + nextFigure(); + } + } + break; + case CupState.HangingFigure: + setCupState(CupState.FallingFigure); + break; + case CupState.DestroyingRows: + int rowsDestroyed = _cup.destroyFullRows(); + _totalRowsDestroyed += rowsDestroyed; + _status.setRowsDestroyed(_totalRowsDestroyed); + int scorePerRow = 0; + for (int i = 0; i < rowsDestroyed; i++) { + scorePerRow += 10; + addScore(scorePerRow); + } + if (_cup.markFallingCells()) { + setCupState(CupState.FallingRows); + } else { + nextFigure(); + } + break; + case CupState.FallingRows: + if (_cup.moveFallingCells()) { + // more cells to fall + setCupState(CupState.FallingRows); + } else { + // no more cells to fall, next figure + if (_cup.hasFullRows) { + // new full rows were constructed: destroy + destroyFullRows(); + } else { + // next figure + nextFigure(); + } + } + break; + default: + break; + } + } + + /// start new game + void newGame() { + setLevel(1); + init(_cols, _rows); + _cup.dropNextFigure(); + setCupState(CupState.NewFigure); + if (window && _gameOverPopup) { + window.removePopup(_gameOverPopup); + _gameOverPopup = null; + } + _score = 0; + _status.setScore(0); + _totalRowsDestroyed = 0; + _status.setRowsDestroyed(0); + } + + /// init cup + void init(int cols, int rows) { + _cup.init(cols, rows); + _cols = cols; + _rows = rows; + } + + protected Rect cellRect(Rect rc, int col, int row) { + int dx = rc.width / _cols; + int dy = rc.height / (_rows + RESERVED_ROWS); + int dd = dx; + if (dd > dy) + dd = dy; + int x0 = rc.left + (rc.width - dd * _cols) / 2 + dd * col; + int y0 = rc.bottom - (rc.height - dd * (_rows + RESERVED_ROWS)) / 2 - dd * row - dd; + return Rect(x0, y0, x0 + dd, y0 + dd); + } + + /// Handle keys + override bool onKeyEvent(KeyEvent event) { + if (event.action == KeyAction.KeyDown && _state == CupState.GameOver) { + // restart game + newGame(); + return true; + } + if (event.action == KeyAction.KeyDown && _state == CupState.NewFigure) { + // stop new figure fade in if key is pressed + onAnimationFinished(); + } + if (event.keyCode == KeyCode.DOWN) { + if (event.action == KeyAction.KeyDown) { + handleFastDown(true); + } else if (event.action == KeyAction.KeyUp) { + handleFastDown(false); + } + return true; + } + if ((event.action == KeyAction.KeyDown || event.action == KeyAction.KeyUp) && event.keyCode != KeyCode.SPACE) + handleFastDown(false); // don't stop fast down on Space key KeyUp + return super.onKeyEvent(event); + } + + /// draw cup cell + protected void drawCell(DrawBuf buf, Rect cellRc, uint color, int offset = 0) { + cellRc.top += offset; + cellRc.bottom += offset; + + cellRc.right--; + cellRc.bottom--; + + int w = cellRc.width / 6; + buf.drawFrame(cellRc, color, Rect(w,w,w,w)); + cellRc.shrink(w, w); + color = addAlpha(color, 0xC0); + buf.fillRect(cellRc, color); + } + + /// draw figure + protected void drawFigure(DrawBuf buf, Rect rc, FigurePosition figure, int dy, uint alpha = 0) { + uint color = addAlpha(_figureColors[figure.index - 1], alpha); + FigureShape shape = figure.shape; + foreach(cell; shape.cells) { + Rect cellRc = cellRect(rc, figure.x + cell.dx, figure.y + cell.dy); + cellRc.top += dy; + cellRc.bottom += dy; + drawCell(buf, cellRc, color); + } + } + + //================================================================================================= + // Overrides of Widget methods + + /// returns true is widget is being animated - need to call animate() and redraw + override @property bool animating() { + switch (_state) { + case CupState.NewFigure: + case CupState.FallingFigure: + case CupState.HangingFigure: + case CupState.DestroyingRows: + case CupState.FallingRows: + return true; + default: + return false; + } + } + + /// animates window; interval is time left from previous draw, in hnsecs (1/10000000 of second) + override void animate(long interval) { + _animation.animate(interval); + if (_animation.finished) { + onAnimationFinished(); + } + } + + /// Draw widget at its position to buffer + override void onDraw(DrawBuf buf) { + super.onDraw(buf); + Rect rc = _pos; + applyMargins(rc); + auto saver = ClipRectSaver(buf, rc, alpha); + applyPadding(rc); + + Rect topLeft = cellRect(rc, 0, _rows - 1); + Rect bottomRight = cellRect(rc, _cols - 1, 0); + Rect cupRc = Rect(topLeft.left, topLeft.top, bottomRight.right, bottomRight.bottom); + + int fw = 7; + int dw = 0; + uint fcl = 0xA0606090; + buf.fillRect(cupRc, 0xC0A0C0B0); + buf.fillRect(Rect(cupRc.left - dw - fw, cupRc.top, cupRc.left - dw, cupRc.bottom + dw), fcl); + buf.fillRect(Rect(cupRc.right + dw, cupRc.top, cupRc.right + dw + fw, cupRc.bottom + dw), fcl); + buf.fillRect(Rect(cupRc.left - dw - fw, cupRc.bottom + dw, cupRc.right + dw + fw, cupRc.bottom + dw + fw), fcl); + + int fallingCellOffset = 0; + if (_state == CupState.FallingRows) { + fallingCellOffset = _animation.getProgress(topLeft.height); + } + + for (int row = 0; row < _rows; row++) { + uint cellAlpha = 0; + if (_state == CupState.DestroyingRows && _cup.isRowFull(row)) + cellAlpha = _animation.progress; + for (int col = 0; col < _cols; col++) { + + int value = _cup[col, row]; + Rect cellRc = cellRect(rc, col, row); + + Point middle = cellRc.middle; + buf.fillRect(Rect(middle.x - 1, middle.y - 1, middle.x + 1, middle.y + 1), 0x80404040); + + if (value != EMPTY) { + uint cl = addAlpha(_figureColors[value - 1], cellAlpha); + int offset = fallingCellOffset > 0 && _cup.isCellFalling(col, row) ? fallingCellOffset : 0; + drawCell(buf, cellRc, cl, offset); + } + } + } + + // draw current figure falling + if (_state == CupState.FallingFigure || _state == CupState.HangingFigure) { + int dy = 0; + if (falling && _cup.isPositionFreeBelow()) + dy = _animation.getProgress(topLeft.height); + drawFigure(buf, rc, _cup.currentFigure, dy, 0); + } + + // draw next figure + if (_cup.hasNextFigure) { + //auto shape = _nextFigure.shape; + uint nextFigureAlpha = 0; + if (_state == CupState.NewFigure) { + nextFigureAlpha = _animation.progress; + drawFigure(buf, rc, _cup.currentFigure, 0, 255 - nextFigureAlpha); + } + if (_state != CupState.GameOver) { + drawFigure(buf, rc, _cup.nextFigure, 0, blendAlpha(0xA0, nextFigureAlpha)); + } + } + + } + + /// override to handle specific actions + override bool handleAction(const Action a) { + switch (a.id) { + case TetrisAction.MoveLeft: + _cup.move(-1, 0, falling); + return true; + case TetrisAction.MoveRight: + _cup.move(1, 0, falling); + return true; + case TetrisAction.RotateCCW: + _cup.rotate(1, falling); + return true; + case TetrisAction.FastDown: + handleFastDown(true); + return true; + case TetrisAction.Pause: + // TODO: implement pause + return true; + case TetrisAction.LevelUp: + setLevel(_level + 1); + return true; + default: + if (parent) // by default, pass to parent widget + return parent.handleAction(a); + return false; + } + } + + /// Measure widget according to desired width and height constraints. (Step 1 of two phase layout). + override void measure(int parentWidth, int parentHeight) { + measuredContent(parentWidth, parentHeight, parentWidth * 3 / 5, parentHeight); + } + + this(StatusWidget status) { + super("CUP"); + this._status = status; + layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT).layoutWeight(2).setState(State.Default).focusable(true).padding(Rect(20, 20, 20, 20)); + + _cols = 10; + _rows = 18; + newGame(); + + focusable = true; + + acceleratorMap.add(CUP_ACTIONS); + } +} + +/// Panel to show game status +class StatusWidget : VerticalLayout { + private TextWidget _level; + private TextWidget _rowsDestroyed; + private TextWidget _score; + private CupWidget _cup; + void setCup(CupWidget cup) { + _cup = cup; + } + TextWidget createTextWidget(dstring str, uint color) { + TextWidget res = new TextWidget(null, str); + res.layoutWidth(FILL_PARENT).alignment(Align.Center).fontSize(25).textColor(color); + return res; + } + + Widget createControls() { + TableLayout res = new TableLayout(); + res.colCount = 3; + foreach(const Action a; CUP_ACTIONS) { + ImageButton btn = new ImageButton(a); + btn.focusable = false; + res.addChild(btn); + } + res.layoutWidth(WRAP_CONTENT).layoutHeight(WRAP_CONTENT).margins(Rect(10, 10, 10, 10)).alignment(Align.Center); + return res; + } + + this() { + super("CUP_STATUS"); + + addChild(new VSpacer()); + + ImageWidget image = new ImageWidget(null, "tetris_logo_big"); + image.layoutWidth(FILL_PARENT).alignment(Align.Center).clickable(true); + image.onClickListener = delegate(Widget src) { + _cup.handleAction(ACTION_PAUSE); + // about dialog when clicking on image + Window wnd = Platform.instance.createWindow("About...", window, WindowFlag.Modal); + wnd.mainWidget = createAboutWidget(); + wnd.show(); + return true; + }; + addChild(image); + + addChild(new VSpacer()); + addChild(createTextWidget("Level:"d, 0x008000)); + addChild((_level = createTextWidget(""d, 0x008000))); + addChild(new VSpacer()); + addChild(createTextWidget("Rows:"d, 0x202080)); + addChild((_rowsDestroyed = createTextWidget(""d, 0x202080))); + addChild(new VSpacer()); + addChild(createTextWidget("Score:"d, 0x800000)); + addChild((_score = createTextWidget(""d, 0x800000))); + addChild(new VSpacer()); + addChild(createControls()); + addChild(new VSpacer()); + + layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT).layoutWeight(2).padding(Rect(10, 10, 10, 10)); + } + + void setLevel(int level) { + _level.text = toUTF32(to!string(level)); + } + + void setScore(int score) { + _score.text = toUTF32(to!string(score)); + } + + void setRowsDestroyed(int rows) { + _rowsDestroyed.text = toUTF32(to!string(rows)); + } + + override bool handleAction(const Action a) { + return _cup.handleAction(a); + } +} + +/// Cup page: cup widget + status widget +class CupPage : HorizontalLayout { + CupWidget _cup; + StatusWidget _status; + this() { + super("CUP_PAGE"); + layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT); + _status = new StatusWidget(); + _cup = new CupWidget(_status); + _status.setCup(_cup); + addChild(_cup); + addChild(_status); + } + /// Measure widget according to desired width and height constraints. (Step 1 of two phase layout). + override void measure(int parentWidth, int parentHeight) { + super.measure(parentWidth, parentHeight); + /// fixed size + measuredContent(parentWidth, parentHeight, 600, 550); + } +} + +// +class GameWidget : FrameLayout { + + CupPage _cupPage; + this() { + super("GAME"); + _cupPage = new CupPage(); + addChild(_cupPage); + //showChild(_cupPage.id, Visibility.Invisible, true); + backgroundImageId = "tx_fabric.tiled"; + } +} diff --git a/workspaces/tetris/src/model.d b/workspaces/tetris/src/model.d new file mode 100644 index 0000000..b7eee79 --- /dev/null +++ b/workspaces/tetris/src/model.d @@ -0,0 +1,444 @@ +module model; + +import std.random : uniform; + +/// Cell codes +enum : int { + WALL = -1, + EMPTY = 0, + FIGURE1, + FIGURE2, + FIGURE3, + FIGURE4, + FIGURE5, + FIGURE6, + FIGURE7, +} + +/// Orientations +enum : int { + ORIENTATION0, + ORIENTATION90, + ORIENTATION180, + ORIENTATION270 +} + + +/// Cell offset +struct FigureCell { + // horizontal offset + int dx; + // vertical offset + int dy; + this(int[2] v) { + dx = v[0]; + dy = v[1]; + } +} + +/// Single figure shape for some particular orientation - 4 cells +struct FigureShape { + /// by cell index 0..3 + FigureCell[4] cells; + /// lowest y coordinate - to show next figure above cup + int extent; + /// upper y coordinate - initial Y offset to place figure to cup + int y0; + /// Init cells (cell 0 is [0,0]) + this(int[2] c2, int[2] c3, int[2] c4) { + cells[0] = FigureCell([0, 0]); + cells[1] = FigureCell(c2); + cells[2] = FigureCell(c3); + cells[3] = FigureCell(c4); + extent = y0 = 0; + foreach (cell; cells) { + if (extent > cell.dy) + extent = cell.dy; + if (y0 < cell.dy) + y0 = cell.dy; + } + } +} + +/// Figure data - shapes for 4 orientations +struct Figure { + FigureShape[4] shapes; // by orientation + this(FigureShape[4] v) { + shapes = v; + } +} + +/// All shapes +const Figure[7] FIGURES = [ + // FIGURE1 =========================================== + // ## #### + // 00## 00## + // ## + Figure([FigureShape([1, 0], [ 1, 1], [0,-1]), + FigureShape([0, 1], [-1, 1], [1, 0]), + FigureShape([1, 0], [ 1, 1], [0,-1]), + FigureShape([0, 1], [-1, 1], [1, 0])]), + // FIGURE2 =========================================== + // ## #### + // 00## ##00 + // ## + Figure([FigureShape([1, 0], [0, 1], [ 1,-1]), + FigureShape([0, 1], [1, 1], [-1, 0]), + FigureShape([1, 0], [0, 1], [ 1,-1]), + FigureShape([0, 1], [1, 1], [-1, 0])]), + // FIGURE3 =========================================== + // ## ## #### + // ##00## 00 ##00## 00 + // ## #### ## + Figure([FigureShape([1, 0], [-1, 0], [-1,-1]), + FigureShape([0, 1], [ 0,-1], [ 1,-1]), + FigureShape([1, 0], [-1, 0], [ 1, 1]), + FigureShape([0, 1], [-1, 1], [ 0,-1])]), + // FIGURE4 =========================================== + // #### ## ## + // ##00## 00 ##00## 00 + // ## ## #### + Figure([FigureShape([1, 0], [-1, 0], [ 1,-1]), + FigureShape([0, 1], [ 0,-1], [ 1, 1]), + FigureShape([1, 0], [-1, 0], [-1, 1]), + FigureShape([0, 1], [-1,-1], [ 0,-1])]), + // FIGURE5 =========================================== + // #### + // 00## + // + Figure([FigureShape([1, 0], [0, 1], [ 1, 1]), + FigureShape([1, 0], [0, 1], [ 1, 1]), + FigureShape([1, 0], [0, 1], [ 1, 1]), + FigureShape([1, 0], [0, 1], [ 1, 1])]), + // FIGURE6 =========================================== + // ## + // ## + // 00 ##00#### + // ## + Figure([FigureShape([0, 1], [0, 2], [ 0,-1]), + FigureShape([1, 0], [2, 0], [-1, 0]), + FigureShape([0, 1], [0, 2], [ 0,-1]), + FigureShape([1, 0], [2, 0], [-1, 0])]), + // FIGURE7 =========================================== + // ## ## ## + // ##00## 00## ##00## ##00 + // ## ## ## + Figure([FigureShape([1, 0], [-1,0], [ 0,-1]), + FigureShape([0, 1], [0,-1], [ 1, 0]), + FigureShape([1, 0], [-1,0], [ 0, 1]), + FigureShape([0, 1], [0,-1], [-1, 0])]), +]; + +/// colors for different figure types +const uint[7] _figureColors = [0xC00000, 0x80A000, 0xA00080, 0x0000C0, 0x800020, 0x408000, 0x204000]; + +/// Figure type, orientation and position container +struct FigurePosition { + int index; + int orientation; + int x; + int y; + this(int index, int orientation, int x, int y) { + this.index = index; + this.orientation = orientation; + this.x = x; + this.y = y; + } + /// return rotated position CCW for angle=1, CW for angle=-1 + FigurePosition rotate(int angle) { + int newOrientation = (orientation + 4 + angle) & 3; + return FigurePosition(index, newOrientation, x, y); + } + /// return moved position + FigurePosition move(int dx, int dy = 0) { + return FigurePosition(index, orientation, x + dx, y + dy); + } + /// return shape for figure orientation + @property FigureShape shape() const { + return FIGURES[index - 1].shapes[orientation]; + } + /// return color for figure + @property uint color() const { + return _figureColors[index - 1]; + } + /// return true if figure index is not initialized + @property empty() const { + return index == 0; + } + /// clears content + void reset() { + index = 0; + } +} + +/** +Cup content + +Coordinates are relative to bottom left corner. +*/ +struct Cup { + private int[] _cup; + private int _cols; + private int _rows; + private bool[] _destroyedFullRows; + private int[] _cellGroups; + + private FigurePosition _currentFigure; + /// current figure index, orientation, position + @property ref FigurePosition currentFigure() { return _currentFigure; } + + private FigurePosition _nextFigure; + /// next figure + @property ref FigurePosition nextFigure() { return _nextFigure; } + + /// returns number of columns + @property int cols() { + return _cols; + } + /// returns number of columns + @property int rows() { + return _rows; + } + /// inits empty cup of specified size + void init(int cols, int rows) { + _cols = cols; + _rows = rows; + _cup = new int[_cols * _rows]; + _destroyedFullRows = new bool[_rows]; + _cellGroups = new int[_cols * _rows]; + } + /// returns cell content at specified position + int opIndex(int col, int row) { + if (col < 0 || row < 0 || col >= _cols || row >= _rows) + return WALL; + return _cup[row * _cols + col]; + } + /// set cell value + void opIndexAssign(int value, int col, int row) { + if (col < 0 || row < 0 || col >= _cols || row >= _rows) + return; // ignore modification of cells outside cup + _cup[row * _cols + col] = value; + } + /// put current figure into cup at current position and orientation + void putFigure() { + FigureShape shape = _currentFigure.shape; + foreach(cell; shape.cells) { + this[_currentFigure.x + cell.dx, _currentFigure.y + cell.dy] = _currentFigure.index; + } + } + + /// check if all cells where specified figure is located are free + bool isPositionFree(in FigurePosition pos) { + FigureShape shape = pos.shape; + foreach(cell; shape.cells) { + int value = this[pos.x + cell.dx, pos.y + cell.dy]; + if (value != 0) // occupied + return false; + } + return true; + } + /// returns true if specified row is full + bool isRowFull(int row) { + for (int i = 0; i < _cols; i++) + if (this[i, row] == EMPTY) + return false; + return true; + } + /// returns true if at least one row is full + @property bool hasFullRows() { + for (int i = 0; i < _rows; i++) + if (isRowFull(i)) + return true; + return false; + } + /// destroy all full rows, saving flags for destroyed rows; returns count of destroyed rows, 0 if no rows destroyed + int destroyFullRows() { + int res = 0; + for (int i = 0; i < _rows; i++) { + if (isRowFull(i)) { + _destroyedFullRows[i] = true; + res++; + for (int col = 0; col < _cols; col++) + this[col, i] = EMPTY; + } else { + _destroyedFullRows[i] = false; + } + } + return res; + } + + /// check if all cells where current figire is located are free + bool isPositionFree() { + return isPositionFree(_currentFigure); + } + + /// check if all cells where current figire is located are free + bool isPositionFreeBelow() { + return isPositionFree(_currentFigure.move(0, -1)); + } + + /// try to rotate current figure, returns true if figure rotated + bool rotate(int angle, bool falling) { + FigurePosition newpos = _currentFigure.rotate(angle); + if (isPositionFree(newpos)) { + if (falling) { + // special handling for fall animation + if (!isPositionFree(newpos.move(0, -1))) { + if (isPositionFreeBelow()) + return false; + } + } + _currentFigure = newpos; + return true; + } else if (isPositionFree(newpos.move(0, -1))) { + _currentFigure = newpos.move(0, -1); + return true; + } + return false; + } + + /// try to move current figure, returns true if figure rotated + bool move(int deltaX, int deltaY, bool falling) { + FigurePosition newpos = _currentFigure.move(deltaX, deltaY); + if (isPositionFree(newpos)) { + if (falling && !isPositionFree(newpos.move(0, -1))) { + if (isPositionFreeBelow()) + return false; + } + _currentFigure = newpos; + return true; + } + return false; + } + + /// random next figure + void genNextFigure() { + _nextFigure.index = uniform(FIGURE1, FIGURE7 + 1); + _nextFigure.orientation = ORIENTATION0; + _nextFigure.x = _cols / 2; + _nextFigure.y = _rows - _nextFigure.shape.extent + 1; + } + + /// New figure: put it on top of cup + bool dropNextFigure() { + if (_nextFigure.empty) + genNextFigure(); + _currentFigure = _nextFigure; + _currentFigure.x = _cols / 2; + _currentFigure.y = _rows - 1 - _currentFigure.shape.y0; + return isPositionFree(); + } + + /// get cell group / falling cell value + private int cellGroup(int col, int row) { + if (col < 0 || row < 0 || col >= _cols || row >= _rows) + return 0; + return _cellGroups[col + row * _cols]; + } + + /// set cell group / falling cells value + private void setCellGroup(int value, int col, int row) { + _cellGroups[col + row * _cols] = value; + } + + /// recursive fill occupied area of cells with group id + private void fillCellGroup(int x, int y, int value) { + if (x < 0 || y < 0 || x >= _cols || y >= _rows) + return; + if (this[x, y] != EMPTY && cellGroup(x, y) == 0) { + setCellGroup(value, x, y); + fillCellGroup(x + 1, y, value); + fillCellGroup(x - 1, y, value); + fillCellGroup(x, y + 1, value); + fillCellGroup(x, y - 1, value); + } + } + + /// 1 == next cell below is occupied, 2 == one empty cell + private int distanceToOccupiedCellBelow(int col, int row) { + for (int y = row - 1; y >= -1; y--) { + if (this[col, y] != EMPTY) + return row - y; + } + return 1; + } + + /// mark cells in _cellGroups[] matrix which can fall down (value > 0 is distance to fall) + bool markFallingCells() { + _cellGroups = new int[_cols * _rows]; + int groupId = 1; + for (int y = 0; y < _rows; y++) { + for (int x = 0; x < _cols; x++) { + if (this[x, y] != EMPTY && cellGroup(x, y) == 0) { + fillCellGroup(x, y, groupId); + groupId++; + } + } + } + // check space below each group - can it fall down? + int[] spaceBelowGroup = new int[groupId]; + for (int y = 0; y < _rows; y++) { + for (int x = 0; x < _cols; x++) { + int group = cellGroup(x, y); + if (group > 0) { + if (y == 0) + spaceBelowGroup[group] = 1; + else if (this[x, y - 1] != EMPTY && cellGroup(x, y - 1) != group) + spaceBelowGroup[group] = 1; + else if (this[x, y - 1] == EMPTY) { + int dist = distanceToOccupiedCellBelow(x, y); + if (spaceBelowGroup[group] == 0 || spaceBelowGroup[group] > dist) + spaceBelowGroup[group] = dist; + } + } + } + } + // replace group IDs with distance to fall (0 == cell cannot fall) + for (int y = 0; y < _rows; y++) { + for (int x = 0; x < _cols; x++) { + int group = cellGroup(x, y); + if (group > 0) { + // distance to fall + setCellGroup(spaceBelowGroup[group] - 1, x, y); + } + } + } + bool canFall = false; + for (int i = 1; i < groupId; i++) + if (spaceBelowGroup[i] > 1) + canFall = true; + return canFall; + } + + /// moves all falling cells one cell down + /// returns true if there are more cells to fall + bool moveFallingCells() { + bool res = false; + for (int y = 0; y < _rows - 1; y++) { + for (int x = 0; x < _cols; x++) { + int dist = cellGroup(x, y + 1); + if (dist > 0) { + // move cell down, decreasing distance + setCellGroup(dist - 1, x, y); + this[x, y] = this[x, y + 1]; + setCellGroup(0, x, y + 1); + this[x, y + 1] = EMPTY; + if (dist > 1) + res = true; + } + } + } + return res; + } + + /// return true if cell is currently falling + bool isCellFalling(int col, int row) { + return cellGroup(col, row) > 0; + } + + /// returns true if next figure is generated + @property bool hasNextFigure() { + return !_nextFigure.empty; + } +} + diff --git a/workspaces/tetris/src/tetris.d b/workspaces/tetris/src/tetris.d new file mode 100644 index 0000000..564fc27 --- /dev/null +++ b/workspaces/tetris/src/tetris.d @@ -0,0 +1,50 @@ +// Written in the D programming language. + +/** +This app is a Tetris demo for DlangUI library. + +Synopsis: + +---- + dub run dlangui:tetris +---- + +Copyright: Vadim Lopatin, 2014 +License: Boost License 1.0 +Authors: Vadim Lopatin, coolreader.org@gmail.com + */ +module main; + +import dlangui.all; +import model; +import gui; + +/// Required for Windows platform: DMD cannot find WinMain if it's in library +mixin APP_ENTRY_POINT; + +/// entry point for dlangui based application +extern (C) int UIAppMain(string[] args) { + + //auto power2 = delegate(int X) { return X * X; }; + auto power2 = (int X) => X * X; + + // embed resources listed in views/resources.list into executable + embeddedResourceList.addResources(embedResourcesFromList!("resources.list")()); + + // select translation file - for english language + Platform.instance.uiLanguage = "en"; + // load theme from file "theme_default.xml" + Platform.instance.uiTheme = "theme_default"; + + // create window + Window window = Platform.instance.createWindow("DLangUI: Tetris game example"d, null, WindowFlag.Modal); + + window.mainWidget = new GameWidget(); + + window.windowIcon = drawableCache.getImage("dtetris-logo1"); + + window.show(); + + // run message loop + return Platform.instance.enterMessageLoop(); +} diff --git a/workspaces/tetris/views/res/arrow-down.png b/workspaces/tetris/views/res/arrow-down.png new file mode 100644 index 0000000..88a570d Binary files /dev/null and b/workspaces/tetris/views/res/arrow-down.png differ diff --git a/workspaces/tetris/views/res/arrow-left.png b/workspaces/tetris/views/res/arrow-left.png new file mode 100644 index 0000000..520be1f Binary files /dev/null and b/workspaces/tetris/views/res/arrow-left.png differ diff --git a/workspaces/tetris/views/res/arrow-right.png b/workspaces/tetris/views/res/arrow-right.png new file mode 100644 index 0000000..accf5ff Binary files /dev/null and b/workspaces/tetris/views/res/arrow-right.png differ diff --git a/workspaces/tetris/views/res/dtetris-logo1.png b/workspaces/tetris/views/res/dtetris-logo1.png new file mode 100644 index 0000000..bd4ce45 Binary files /dev/null and b/workspaces/tetris/views/res/dtetris-logo1.png differ diff --git a/workspaces/tetris/views/res/levelup.png b/workspaces/tetris/views/res/levelup.png new file mode 100644 index 0000000..51881ea Binary files /dev/null and b/workspaces/tetris/views/res/levelup.png differ diff --git a/workspaces/tetris/views/res/pause.png b/workspaces/tetris/views/res/pause.png new file mode 100644 index 0000000..5c7d128 Binary files /dev/null and b/workspaces/tetris/views/res/pause.png differ diff --git a/workspaces/tetris/views/res/popup_background.9.png b/workspaces/tetris/views/res/popup_background.9.png new file mode 100644 index 0000000..3c32265 Binary files /dev/null and b/workspaces/tetris/views/res/popup_background.9.png differ diff --git a/workspaces/tetris/views/res/rotate.png b/workspaces/tetris/views/res/rotate.png new file mode 100644 index 0000000..57fad56 Binary files /dev/null and b/workspaces/tetris/views/res/rotate.png differ diff --git a/workspaces/tetris/views/res/tetris_logo_big.png b/workspaces/tetris/views/res/tetris_logo_big.png new file mode 100644 index 0000000..54175c0 Binary files /dev/null and b/workspaces/tetris/views/res/tetris_logo_big.png differ diff --git a/workspaces/tetris/views/res/tx_fabric.jpg b/workspaces/tetris/views/res/tx_fabric.jpg new file mode 100644 index 0000000..6c33894 Binary files /dev/null and b/workspaces/tetris/views/res/tx_fabric.jpg differ diff --git a/workspaces/tetris/views/resources.list b/workspaces/tetris/views/resources.list new file mode 100644 index 0000000..03c36c3 --- /dev/null +++ b/workspaces/tetris/views/resources.list @@ -0,0 +1,10 @@ +res/arrow-down.png +res/arrow-left.png +res/arrow-right.png +res/dtetris-logo1.png +res/levelup.png +res/pause.png +res/popup_background.9.png +res/rotate.png +res/tetris_logo_big.png +res/tx_fabric.jpg