module dialogs.dialogbox; import raylib; import std.string; import std.stdio; import std.conv; import std.uni; import std.typecons; import std.algorithm; import variables; // Dialog state variables int currentPage = 0; float textDisplayProgress = 0.0f; bool textFullyDisplayed = false; // Texture variables float circleRotationAngle = 0.0f; // Border sizes for 9-slice scaling const int DIALOG_BORDER = 32; // Border that won't be stretched const int CHOICE_BORDER = 32; // Choice window border // Draws a texture with 9-slice scaling void draw9SliceTexture(Texture2D tex, Rectangle dest, int borderSize, Color tint) { Rectangle src = Rectangle(0, 0, tex.width, tex.height); // Prevent invalid border sizes borderSize = max(1, min(borderSize, tex.width/3, tex.height/3)); Rectangle innerSrc = Rectangle( borderSize, borderSize, src.width - borderSize*2, src.height - borderSize*2 ); Rectangle innerDest = Rectangle( dest.x + borderSize, dest.y + borderSize, dest.width - borderSize*2, dest.height - borderSize*2 ); // Draw all 9 parts void drawPart(Rectangle s, Rectangle d) { DrawTexturePro(tex, s, d, Vector2(0, 0), 0, tint); } // Corners drawPart(Rectangle(src.x, src.y, borderSize, borderSize), Rectangle(dest.x, dest.y, borderSize, borderSize)); drawPart(Rectangle(src.x + src.width - borderSize, src.y, borderSize, borderSize), Rectangle(dest.x + dest.width - borderSize, dest.y, borderSize, borderSize)); drawPart(Rectangle(src.x, src.y + src.height - borderSize, borderSize, borderSize), Rectangle(dest.x, dest.y + dest.height - borderSize, borderSize, borderSize)); drawPart(Rectangle(src.x + src.width - borderSize, src.y + src.height - borderSize, borderSize, borderSize), Rectangle(dest.x + dest.width - borderSize, dest.y + dest.height - borderSize, borderSize, borderSize)); // Edges drawPart(Rectangle(src.x + borderSize, src.y, innerSrc.width, borderSize), Rectangle(dest.x + borderSize, dest.y, innerDest.width, borderSize)); drawPart(Rectangle(src.x, src.y + borderSize, borderSize, innerSrc.height), Rectangle(dest.x, dest.y + borderSize, borderSize, innerDest.height)); drawPart(Rectangle(src.x + src.width - borderSize, src.y + borderSize, borderSize, innerSrc.height), Rectangle(dest.x + dest.width - borderSize, dest.y + borderSize, borderSize, innerDest.height)); drawPart(Rectangle(src.x + borderSize, src.y + src.height - borderSize, innerSrc.width, borderSize), Rectangle(dest.x + borderSize, dest.y + dest.height - borderSize, innerDest.width, borderSize)); // Center drawPart(innerSrc, innerDest); } // Main dialog display function void displayDialog(string[] pages, string[] choices, ref int selectedChoice, int choicePage, Font dialogFont, bool* showDialog, ref float textSpeed, Texture2D circle, Texture2D dialogBackgroundTex, Texture2D choiceWindowTex) { const screenWidth = GetScreenWidth(); const screenHeight = GetScreenHeight(); const screenPadding = 10; // Dialog background rectangle with padding Rectangle dialogRect = Rectangle( screenPadding, screenHeight - screenHeight/3 - screenPadding, screenWidth - 2*screenPadding, screenHeight/3 ); // Draw dialog background draw9SliceTexture(dialogBackgroundTex, dialogRect, DIALOG_BORDER, Colors.WHITE); // Text layout parameters const float textLeftMargin = 33.0f; const float textTopMargin = 20.0f; const float textRightMargin = 33.0f; const float textWidth = dialogRect.width - textLeftMargin - textRightMargin; const float fontSize = 40.0f; const float spacing = 1.0f; string name; // Text display logic string currentText = pages[currentPage]; string displayText = currentText; if (currentText[0] == '[') { size_t start = currentText.indexOf('[') + 1; size_t end = currentText.lastIndexOf(']'); name = currentText[start..end]; size_t nameStart = currentText.indexOf('['); size_t nameEnd = currentText.lastIndexOf(']') + 1; displayText = currentText[0..nameStart] ~ currentText[nameEnd..$]; Rectangle nameRect = Rectangle( screenPadding, dialogRect.y - screenHeight/12, screenWidth / 12 + MeasureTextEx(dialogFont, name.toStringz(), fontSize, spacing).x, screenHeight / 12 ); draw9SliceTexture(choiceWindowTex, nameRect, DIALOG_BORDER, Colors.WHITE); Vector2 nameSize = MeasureTextEx(dialogFont, name.toStringz(), fontSize, spacing); Vector2 namePos = Vector2( nameRect.x + (nameRect.width - nameSize.x) / 2, nameRect.y + (nameRect.height - nameSize.y) / 2 ); DrawTextEx(dialogFont, name.toStringz(), namePos + Vector2(2, 2), fontSize, spacing, Colors.BLACK); DrawTextEx(dialogFont, name.toStringz(), namePos, fontSize, spacing, Colors.WHITE); } if (IsKeyPressed(KeyboardKey.KEY_ENTER)) { if (!textFullyDisplayed) { textDisplayProgress = displayText.length; textFullyDisplayed = true; } else { currentPage++; textDisplayProgress = 0.0f; textFullyDisplayed = false; } } else if (!textFullyDisplayed) { textDisplayProgress = min(textDisplayProgress + textSpeed, cast(float)displayText.length); textFullyDisplayed = textDisplayProgress >= displayText.length; } // Split text into lines that fit the dialog width string[] lines; string remaining = displayText[0..min(cast(int)textDisplayProgress, displayText.length)]; while (!remaining.empty) { int fitChars = 0; float lineWidth = 0.0f; while (fitChars < remaining.length) { int wordEnd = fitChars; while (wordEnd < remaining.length && !remaining[wordEnd].isWhite) wordEnd++; string word = remaining[fitChars..wordEnd]; float wordWidth = MeasureTextEx(dialogFont, word.toStringz(), fontSize, spacing).x; if (lineWidth > 0 && lineWidth + wordWidth > textWidth) break; lineWidth += wordWidth; fitChars = wordEnd; // Add whitespace while (fitChars < remaining.length && remaining[fitChars].isWhite) { lineWidth += MeasureTextEx(dialogFont, " ".toStringz(), fontSize, spacing).x; fitChars++; } } if (fitChars == 0) fitChars = 1; lines ~= remaining[0..fitChars]; remaining = remaining[fitChars..$]; } // Draw text lines with shadow effect const float lineHeight = MeasureTextEx(dialogFont, "A", fontSize, spacing).y * 1.4; foreach(i, line; lines) { Vector2 pos = Vector2(dialogRect.x + textLeftMargin, dialogRect.y + textTopMargin + i*lineHeight); DrawTextEx(dialogFont, line.toStringz(), pos + Vector2(3, 3), fontSize, spacing, Colors.BLACK); DrawTextEx(dialogFont, line.toStringz(), pos, fontSize, spacing, Colors.WHITE); } // Draw continue indicator when text is fully displayed if (textFullyDisplayed && !lines.empty) { circleRotationAngle = (circleRotationAngle + 45*GetFrameTime()) % 360; float lastLineWidth = MeasureTextEx(dialogFont, lines[$-1].toStringz(), fontSize, spacing).x; Vector2 circlePos = Vector2( dialogRect.x + textLeftMargin + lastLineWidth + 20, dialogRect.y + textTopMargin + (lines.length-1)*lineHeight ); DrawTexturePro( circle, Rectangle(0, 0, circle.width, circle.height), Rectangle(circlePos.x, circlePos.y+20, 30, 30), Vector2(15, 15), circleRotationAngle, Colors.WHITE ); } // Choice selection logic if (currentPage == choicePage) { const choiceHeight = 50; const choiceSpacing = 10; const verticalPadding = 30; Rectangle choiceWindow = Rectangle( (screenWidth - screenWidth/2)/2, (screenHeight - (verticalPadding*2 + choices.length*(choiceHeight + choiceSpacing)))/2, screenWidth/2, verticalPadding*2 + choices.length*(choiceHeight + choiceSpacing) ); draw9SliceTexture(choiceWindowTex, choiceWindow, CHOICE_BORDER, Colors.WHITE); Vector2 mousePos = GetMousePosition(); bool mouseClicked = IsMouseButtonPressed(MouseButton.MOUSE_BUTTON_LEFT); foreach(i, choice; choices) { Rectangle choiceRect = Rectangle( choiceWindow.x + 40, choiceWindow.y + verticalPadding + i*(choiceHeight + choiceSpacing), choiceWindow.width - 80, choiceHeight ); bool hovered = CheckCollisionPointRec(mousePos, choiceRect); if (hovered) selectedChoice = cast(int)i; if (hovered || i == selectedChoice) { DrawRectangleRec(choiceRect, Color(60, 60, 60, 200)); } Vector2 textSize = MeasureTextEx(dialogFont, choice.toStringz(), 39, spacing); Vector2 textPos = Vector2( choiceRect.x + (choiceRect.width - textSize.x)/2, choiceRect.y + (choiceRect.height - textSize.y)/2 ); DrawTextEx(dialogFont, choice.toStringz(), textPos + Vector2(3, 3), 39, spacing, Colors.BLACK); DrawTextEx(dialogFont, choice.toStringz(), textPos, 39, spacing, (i == selectedChoice) ? Colors.YELLOW : (hovered ? Colors.LIGHTGRAY : Colors.WHITE)); if (hovered && mouseClicked) { currentPage++; textDisplayProgress = 0.0f; textFullyDisplayed = false; } } // Keyboard navigation if (IsKeyPressed(KeyboardKey.KEY_DOWN)) selectedChoice = cast(int)((selectedChoice + 1) % choices.length); if (IsKeyPressed(KeyboardKey.KEY_UP)) selectedChoice = cast(int)((selectedChoice - 1 + choices.length) % choices.length); } // Fast-forward with Ctrl if ((IsKeyDown(KeyboardKey.KEY_LEFT_CONTROL) || IsKeyDown(KeyboardKey.KEY_RIGHT_CONTROL)) && currentPage != choicePage && currentPage < pages.length) { currentPage++; } // Close dialog when all pages are shown if (currentPage >= pages.length) { currentPage = 0; textDisplayProgress = 0.0f; textFullyDisplayed = false; *showDialog = false; } }