dialogbox.d 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. module dialogs.dialogbox;
  2. import raylib;
  3. import std.string;
  4. import std.stdio;
  5. import std.conv;
  6. import std.uni;
  7. import std.typecons;
  8. import std.algorithm;
  9. import variables;
  10. // Dialog state variables
  11. int currentPage = 0;
  12. float textDisplayProgress = 0.0f;
  13. bool textFullyDisplayed = false;
  14. // Texture variables
  15. float circleRotationAngle = 0.0f;
  16. // Border sizes for 9-slice scaling
  17. const int DIALOG_BORDER = 32; // Border that won't be stretched
  18. const int CHOICE_BORDER = 32; // Choice window border
  19. // Draws a texture with 9-slice scaling
  20. void draw9SliceTexture(Texture2D tex, Rectangle dest, int borderSize, Color tint) {
  21. Rectangle src = Rectangle(0, 0, tex.width, tex.height);
  22. // Prevent invalid border sizes
  23. borderSize = max(1, min(borderSize, tex.width/3, tex.height/3));
  24. Rectangle innerSrc = Rectangle(
  25. borderSize, borderSize,
  26. src.width - borderSize*2, src.height - borderSize*2
  27. );
  28. Rectangle innerDest = Rectangle(
  29. dest.x + borderSize, dest.y + borderSize,
  30. dest.width - borderSize*2, dest.height - borderSize*2
  31. );
  32. // Draw all 9 parts
  33. void drawPart(Rectangle s, Rectangle d) {
  34. DrawTexturePro(tex, s, d, Vector2(0, 0), 0, tint);
  35. }
  36. // Corners
  37. drawPart(Rectangle(src.x, src.y, borderSize, borderSize),
  38. Rectangle(dest.x, dest.y, borderSize, borderSize));
  39. drawPart(Rectangle(src.x + src.width - borderSize, src.y, borderSize, borderSize),
  40. Rectangle(dest.x + dest.width - borderSize, dest.y, borderSize, borderSize));
  41. drawPart(Rectangle(src.x, src.y + src.height - borderSize, borderSize, borderSize),
  42. Rectangle(dest.x, dest.y + dest.height - borderSize, borderSize, borderSize));
  43. drawPart(Rectangle(src.x + src.width - borderSize, src.y + src.height - borderSize, borderSize, borderSize),
  44. Rectangle(dest.x + dest.width - borderSize, dest.y + dest.height - borderSize, borderSize, borderSize));
  45. // Edges
  46. drawPart(Rectangle(src.x + borderSize, src.y, innerSrc.width, borderSize),
  47. Rectangle(dest.x + borderSize, dest.y, innerDest.width, borderSize));
  48. drawPart(Rectangle(src.x, src.y + borderSize, borderSize, innerSrc.height),
  49. Rectangle(dest.x, dest.y + borderSize, borderSize, innerDest.height));
  50. drawPart(Rectangle(src.x + src.width - borderSize, src.y + borderSize, borderSize, innerSrc.height),
  51. Rectangle(dest.x + dest.width - borderSize, dest.y + borderSize, borderSize, innerDest.height));
  52. drawPart(Rectangle(src.x + borderSize, src.y + src.height - borderSize, innerSrc.width, borderSize),
  53. Rectangle(dest.x + borderSize, dest.y + dest.height - borderSize, innerDest.width, borderSize));
  54. // Center
  55. drawPart(innerSrc, innerDest);
  56. }
  57. // Main dialog display function
  58. void displayDialog(string[] pages, string[] choices, ref int selectedChoice,
  59. int choicePage, Font dialogFont, bool* showDialog,
  60. ref float textSpeed, Texture2D circle,
  61. Texture2D dialogBackgroundTex, Texture2D choiceWindowTex) {
  62. const screenWidth = GetScreenWidth();
  63. const screenHeight = GetScreenHeight();
  64. const screenPadding = 10;
  65. // Dialog background rectangle with padding
  66. Rectangle dialogRect = Rectangle(
  67. screenPadding,
  68. screenHeight - screenHeight/3 - screenPadding,
  69. screenWidth - 2*screenPadding,
  70. screenHeight/3
  71. );
  72. // Draw dialog background
  73. draw9SliceTexture(dialogBackgroundTex, dialogRect, DIALOG_BORDER, Colors.WHITE);
  74. // Text layout parameters
  75. const float textLeftMargin = 33.0f;
  76. const float textTopMargin = 20.0f;
  77. const float textRightMargin = 33.0f;
  78. const float textWidth = dialogRect.width - textLeftMargin - textRightMargin;
  79. const float fontSize = 40.0f;
  80. const float spacing = 1.0f;
  81. string name;
  82. // Text display logic
  83. string currentText = pages[currentPage];
  84. string displayText = currentText;
  85. if (currentText[0] == '[') {
  86. size_t start = currentText.indexOf('[') + 1;
  87. size_t end = currentText.lastIndexOf(']');
  88. name = currentText[start..end];
  89. size_t nameStart = currentText.indexOf('[');
  90. size_t nameEnd = currentText.lastIndexOf(']') + 1;
  91. displayText = currentText[0..nameStart] ~ currentText[nameEnd..$];
  92. Rectangle nameRect = Rectangle(
  93. screenPadding,
  94. dialogRect.y - screenHeight/12,
  95. screenWidth / 12 + MeasureTextEx(dialogFont, name.toStringz(), fontSize, spacing).x,
  96. screenHeight / 12
  97. );
  98. draw9SliceTexture(choiceWindowTex, nameRect, DIALOG_BORDER, Colors.WHITE);
  99. Vector2 nameSize = MeasureTextEx(dialogFont, name.toStringz(), fontSize, spacing);
  100. Vector2 namePos = Vector2(
  101. nameRect.x + (nameRect.width - nameSize.x) / 2,
  102. nameRect.y + (nameRect.height - nameSize.y) / 2
  103. );
  104. DrawTextEx(dialogFont, name.toStringz(), namePos + Vector2(2, 2), fontSize, spacing, Colors.BLACK);
  105. DrawTextEx(dialogFont, name.toStringz(), namePos, fontSize, spacing, Colors.WHITE);
  106. }
  107. if (IsKeyPressed(KeyboardKey.KEY_ENTER)) {
  108. if (!textFullyDisplayed) {
  109. textDisplayProgress = displayText.length;
  110. textFullyDisplayed = true;
  111. } else {
  112. currentPage++;
  113. textDisplayProgress = 0.0f;
  114. textFullyDisplayed = false;
  115. }
  116. } else if (!textFullyDisplayed) {
  117. textDisplayProgress = min(textDisplayProgress + textSpeed, cast(float)displayText.length);
  118. textFullyDisplayed = textDisplayProgress >= displayText.length;
  119. }
  120. // Split text into lines that fit the dialog width
  121. string[] lines;
  122. string remaining = displayText[0..min(cast(int)textDisplayProgress, displayText.length)];
  123. while (!remaining.empty) {
  124. int fitChars = 0;
  125. float lineWidth = 0.0f;
  126. while (fitChars < remaining.length) {
  127. int wordEnd = fitChars;
  128. while (wordEnd < remaining.length && !remaining[wordEnd].isWhite) wordEnd++;
  129. string word = remaining[fitChars..wordEnd];
  130. float wordWidth = MeasureTextEx(dialogFont, word.toStringz(), fontSize, spacing).x;
  131. if (lineWidth > 0 && lineWidth + wordWidth > textWidth) break;
  132. lineWidth += wordWidth;
  133. fitChars = wordEnd;
  134. // Add whitespace
  135. while (fitChars < remaining.length && remaining[fitChars].isWhite) {
  136. lineWidth += MeasureTextEx(dialogFont, " ".toStringz(), fontSize, spacing).x;
  137. fitChars++;
  138. }
  139. }
  140. if (fitChars == 0) fitChars = 1;
  141. lines ~= remaining[0..fitChars];
  142. remaining = remaining[fitChars..$];
  143. }
  144. // Draw text lines with shadow effect
  145. const float lineHeight = MeasureTextEx(dialogFont, "A", fontSize, spacing).y * 1.4;
  146. foreach(i, line; lines) {
  147. Vector2 pos = Vector2(dialogRect.x + textLeftMargin, dialogRect.y + textTopMargin + i*lineHeight);
  148. DrawTextEx(dialogFont, line.toStringz(), pos + Vector2(3, 3), fontSize, spacing, Colors.BLACK);
  149. DrawTextEx(dialogFont, line.toStringz(), pos, fontSize, spacing, Colors.WHITE);
  150. }
  151. // Draw continue indicator when text is fully displayed
  152. if (textFullyDisplayed && !lines.empty) {
  153. circleRotationAngle = (circleRotationAngle + 45*GetFrameTime()) % 360;
  154. float lastLineWidth = MeasureTextEx(dialogFont, lines[$-1].toStringz(), fontSize, spacing).x;
  155. Vector2 circlePos = Vector2(
  156. dialogRect.x + textLeftMargin + lastLineWidth + 20,
  157. dialogRect.y + textTopMargin + (lines.length-1)*lineHeight
  158. );
  159. DrawTexturePro(
  160. circle,
  161. Rectangle(0, 0, circle.width, circle.height),
  162. Rectangle(circlePos.x, circlePos.y+20, 30, 30),
  163. Vector2(15, 15),
  164. circleRotationAngle,
  165. Colors.WHITE
  166. );
  167. }
  168. // Choice selection logic
  169. if (currentPage == choicePage) {
  170. const choiceHeight = 50;
  171. const choiceSpacing = 10;
  172. const verticalPadding = 30;
  173. Rectangle choiceWindow = Rectangle(
  174. (screenWidth - screenWidth/2)/2,
  175. (screenHeight - (verticalPadding*2 + choices.length*(choiceHeight + choiceSpacing)))/2,
  176. screenWidth/2,
  177. verticalPadding*2 + choices.length*(choiceHeight + choiceSpacing)
  178. );
  179. draw9SliceTexture(choiceWindowTex, choiceWindow, CHOICE_BORDER, Colors.WHITE);
  180. Vector2 mousePos = GetMousePosition();
  181. bool mouseClicked = IsMouseButtonPressed(MouseButton.MOUSE_BUTTON_LEFT);
  182. foreach(i, choice; choices) {
  183. Rectangle choiceRect = Rectangle(
  184. choiceWindow.x + 40,
  185. choiceWindow.y + verticalPadding + i*(choiceHeight + choiceSpacing),
  186. choiceWindow.width - 80,
  187. choiceHeight
  188. );
  189. bool hovered = CheckCollisionPointRec(mousePos, choiceRect);
  190. if (hovered) selectedChoice = cast(int)i;
  191. if (hovered || i == selectedChoice) {
  192. DrawRectangleRec(choiceRect, Color(60, 60, 60, 200));
  193. }
  194. Vector2 textSize = MeasureTextEx(dialogFont, choice.toStringz(), 39, spacing);
  195. Vector2 textPos = Vector2(
  196. choiceRect.x + (choiceRect.width - textSize.x)/2,
  197. choiceRect.y + (choiceRect.height - textSize.y)/2
  198. );
  199. DrawTextEx(dialogFont, choice.toStringz(), textPos + Vector2(3, 3), 39, spacing, Colors.BLACK);
  200. DrawTextEx(dialogFont, choice.toStringz(), textPos, 39, spacing,
  201. (i == selectedChoice) ? Colors.YELLOW : (hovered ? Colors.LIGHTGRAY : Colors.WHITE));
  202. if (hovered && mouseClicked) {
  203. currentPage++;
  204. textDisplayProgress = 0.0f;
  205. textFullyDisplayed = false;
  206. }
  207. }
  208. // Keyboard navigation
  209. if (IsKeyPressed(KeyboardKey.KEY_DOWN)) selectedChoice = cast(int)((selectedChoice + 1) % choices.length);
  210. if (IsKeyPressed(KeyboardKey.KEY_UP)) selectedChoice = cast(int)((selectedChoice - 1 + choices.length) % choices.length);
  211. }
  212. // Fast-forward with Ctrl
  213. if ((IsKeyDown(KeyboardKey.KEY_LEFT_CONTROL) || IsKeyDown(KeyboardKey.KEY_RIGHT_CONTROL))
  214. && currentPage != choicePage && currentPage < pages.length) {
  215. currentPage++;
  216. }
  217. // Close dialog when all pages are shown
  218. if (currentPage >= pages.length) {
  219. currentPage = 0;
  220. textDisplayProgress = 0.0f;
  221. textFullyDisplayed = false;
  222. *showDialog = false;
  223. }
  224. }