Seeking Dear ImGui and Cairo codebase for text editing functionality

I am currently in need of a codebase or example project that demonstrates the implementation of text editing functionality using the Dear ImGui and Cairo libraries. Specifically, I am looking for a solution that utilizes these libraries to allow users to input, edit, and manipulate text within a graphical user interface. The codebase should showcase how to integrate Dear ImGui for creating the interface elements and Cairo for rendering text and graphical elements. I have already searched on GitHub but haven’t found a suitable repository that meets my requirements. Therefore, I am reaching out to the community for assistance in locating or providing such a codebase. Any help or guidance in this matter would be greatly appreciated. Thank you.

I implemented functions to draw text frames, handle keyboard input for text editing, and display text using Cairo. I expected the text editing functionalities to work smoothly, allowing users to insert, delete, and move the cursor within the text frame. However, I encountered issues with cursor positioning and text selection, leading to unexpected behaviour during editing have attached my code file as follows

#include "text_display.hpp"

// Function to create a texture for cairo surface
ImTextureID TextDisplay::createTextureForFillingTheShape(unsigned char *imageData, int width, int height)
{
   GLuint texture;
   glGenTextures(1, &texture);
   glBindTexture(GL_TEXTURE_2D, texture);

   // Set texture parameters
   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

   // Upload the image data to the GPU
   glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, imageData);
   // Return the texture ID that ImGui can use
   return reinterpret_cast<ImTextureID>(texture);
}

// Function to load a custom font face using FreeType and create a cairo font face
cairo_font_face_t *load_custom_font_face(cairo_t *cr, const char *font_path)
{
   FT_Library library;
   FT_Face face;
   FT_Error error = FT_Init_FreeType(&library);
   if (error)
   {
      std::cerr << "An error occurred during FreeType library initialization." << 'n';
      return nullptr;
   }

   error = FT_New_Face(library, font_path, 0, &face);
   if (error)
   {
      std::cerr << "Failed to load font: " << font_path << 'n';
      FT_Done_FreeType(library);
      return nullptr;
   }

   cairo_font_face_t *cairo_font_face = cairo_ft_font_face_create_for_ft_face(face, 0);
   if (!cairo_font_face)
   {
      std::cerr << "Failed to create cairo font face from FreeType face." << 'n';
      FT_Done_Face(face);
      FT_Done_FreeType(library);
      return nullptr;
   }

   // FreeType face can be destroyed after creating the cairo_font_face
   FT_Done_Face(face);
   FT_Done_FreeType(library);

   return cairo_font_face;
}

// Function to print the details of a string (text and hex values)
void printStringDetails(const std::string &str)
{
   std::cout << "Text: " << str << "n";
   std::cout << "Hex: ";
   for (unsigned char c : str)
   {
      std::cout << std::hex << std::setw(2) << std::setfill('0') << (int)c << " ";
   }
   std::cout << std::dec << "n"; // Switch back to decimal for other outputs
}

// Global and static variable to keep track of the cursor blink state
static bool cursorVisible = true;
std::string editableText;           // The text currently being edited
size_t cursorPosition = 0;          // The position of the cursor in the editable text
static std::string textBuffer;      // Buffer to hold the text for ImGui input
static char editedTextBuffer[1024]; // Buffer to hold the edited text for ImGui Input

// Function to draw the text frame using cairo
void drawTextFrame(cairo_t *cr, int frameLeft, int frameTop, int frameRight, int frameBottom, int frameWidth, int frameHeight)
{
   // Draw the dashed rectangle to frame the text
   const double dash_pattern[] = {4.0, 4.0};
   int dash_pattern_length = sizeof(dash_pattern) / sizeof(dash_pattern[0]);

   // Set the dash pattern for the stroke
   cairo_set_dash(cr, dash_pattern, dash_pattern_length, 0);

   // Set the color for the rectangle's border (RGBA)
   cairo_set_source_rgba(cr, 1.0, 1.0, 1.0, 1.0);

   // Set the line width for the border
   cairo_set_line_width(cr, 1.7);

   // Draw the dashed rectangle to frame the text
   cairo_rectangle(cr, frameLeft, frameTop, frameWidth, frameHeight); // x, y, width, height

   // Draw the outline of the rectangle
   cairo_stroke(cr);

   // The length of the side of each small square box
   const double squareSize = 7.0;

   // Function to draw a small square box
   auto draw_square_box = [&](double x, double y)
   {
      // Clear the dash pattern for the border of squares
      cairo_set_dash(cr, nullptr, 0, 0);

      // Start path for the square
      cairo_new_path(cr);

      // Draw the solid square
      cairo_rectangle(cr, x - squareSize / 2.0, y - squareSize / 2.0, squareSize, squareSize);

      // Set the color to transparent and fill to clear the square area
      cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 0.0);
      cairo_fill_preserve(cr);

      // Set the color for the squares' border to white and stroke
      cairo_set_source_rgba(cr, 1.0, 1.0, 1.0, 1.0);
      cairo_set_line_width(cr, 1.2);
      cairo_stroke(cr);
   };

   // Draw square boxes at each corner of the rectangle
   draw_square_box(frameLeft, frameTop);
   draw_square_box(frameRight, frameTop);
   draw_square_box(frameLeft, frameBottom);
   draw_square_box(frameRight, frameBottom);

   // Draw square boxes at the midpoints of each side of the rectangle
   draw_square_box(frameLeft + frameWidth / 2.0, frameTop);               // Top midpoint
   draw_square_box(frameLeft, frameTop + frameHeight / 2.0);              // Left midpoint
   draw_square_box(frameLeft + frameWidth, frameTop + frameHeight / 2.0); // Right midpoint
   draw_square_box(frameLeft + frameWidth / 2.0, frameTop + frameHeight); // Bottom midpoint
}

// Function to handle keyboard input for text editing inside the frame
void handleKeyboardInput(ImGuiIO &io, std::string &editableText, size_t &cursorPosition)
{
   // Handling arrow keys and character input
   for (unsigned int c : io.InputQueueCharacters)
   {
      if (c == '')
         continue;
      if (c >= 32 && c != 127)
      { // Printable characters excluding Delete
         editableText.insert(cursorPosition++, 1, c);
      }
   }

   // Check for Enter key press to insert newline
   if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Enter)) || ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_KeyPadEnter)))
   {
      editableText.insert(cursorPosition++, 1, 'n');
   }

   // Arrow keys for moving the cursor left and right
   if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_LeftArrow)) && cursorPosition > 0)
   {
      cursorPosition--;
   }
   if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_RightArrow)) && cursorPosition < editableText.size())
   {
      cursorPosition++;
   }

   // Handling Backspace key press to delete characters
   if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Backspace)) && cursorPosition > 0)
   {
      editableText.erase(cursorPosition - 1, 1);
      std::cout << "Cursor position: " << cursorPosition << std::endl;

      cursorPosition--;
   }
}

// draw cursor on the cairo surface at the specified position
void drawCursor(cairo_t *cr, double cursorX, double cursorY, const cairo_font_extents_t &font_extents, bool afterChar = true)
{
   double x;
   if (afterChar)
   {
      x = cursorX; // Draw cursor to the right side of the character (after character)
   }
   else
   {
      x = cursorX - 1; // Draw cursor to the left side of the character (before character)
   }
   std::cout << "Cursor position ::::::::: " << x << std::endl;

   cairo_move_to(cr, x, cursorY - font_extents.ascent);
   cairo_line_to(cr, x, cursorY + font_extents.descent);
   cairo_stroke(cr);
}

// Function to draw text on the cairo surface using cairo functions
void drawTextWithCairo(cairo_t *cr, const std::string &font_name, double fontSize, const std::vector<float> &textColor, const std::string &textToDisplay, double frameLeft, double frameTop, double frameRight, double frameBottom, double wordSpacingValue, const cairo_font_extents_t &font_extents, float leading, size_t &cursorPosition, bool isEditingEnabled, size_t selectionStart, size_t selectionEnd, bool isTextSelected)
{
   cairo_select_font_face(cr, font_name.c_str(), CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
   cairo_set_font_size(cr, fontSize);
   cairo_set_source_rgba(cr, textColor[2], textColor[1], textColor[0], textColor[3]);

   double x = frameLeft;
   double y = frameTop + font_extents.ascent;
   size_t accumulatedChars = 0;

   double blinkTime = ImGui::GetTime();
   bool isCursorVisible = fmod(blinkTime, 1.0) < 0.5;

   std::istringstream iss(textToDisplay);
   std::string word;
   while (std::getline(iss, word, ' '))
   {
      cairo_text_extents_t word_extents;
      cairo_text_extents(cr, word.c_str(), &word_extents);

      if (x + word_extents.x_advance > frameRight)
      {
         x = frameLeft;
         y += leading;
      }

      for (size_t i = 0; i < word.length(); ++i)
      {
         cairo_text_extents_t char_extents;
         cairo_text_extents(cr, std::string(1, word[i]).c_str(), &char_extents);

         // Highlight selected text
         if (isTextSelected)
         {
            cairo_set_source_rgba(cr, 0.8, 0.8, 1.0, 0.65); // Change color for selection
            cairo_rectangle(cr, x, y - font_extents.ascent, char_extents.x_advance, font_extents.height);
            cairo_fill(cr);
            cairo_set_source_rgba(cr, textColor[2], textColor[1], textColor[0], textColor[3]); // Reset text color
         }

         cairo_move_to(cr, x, y);
         cairo_show_text(cr, std::string(1, word[i]).c_str());

         if (isEditingEnabled && isCursorVisible && accumulatedChars == cursorPosition)
         {
            double charX = x; // Adjust cursor position to be after the character
            std::cout << "Drawing cursor at: " << charX << ", " << y << std::endl;
            drawCursor(cr, charX, y, font_extents);
            //ImGui::GetWindowDrawList()->AddCircle(ImVec2(charX+204, y+54.5), 2, IM_COL32(255, 255, 255, 255), 12, 2.0f);
         }

         x += char_extents.x_advance;
         accumulatedChars++;
      }

      x += wordSpacingValue * 10;
   }

   // Draw cursor at the end if it's the last position
   if (isEditingEnabled && isCursorVisible && cursorPosition == accumulatedChars)
   {
      drawCursor(cr, x, y, font_extents);
   }
}

// Function to calculate the cursor position within a line of text
size_t calculateCursorPositionWithinLine(cairo_t *cr, const std::string &line, double clickX)
{
   double accumulatedWidth = 0.0;
   cairo_text_extents_t char_extents;

   for (size_t i = 0; i < line.length(); ++i)
   {
      cairo_text_extents(cr, std::string(1, line[i]).c_str(), &char_extents);
      accumulatedWidth += char_extents.x_advance;

      if (accumulatedWidth > clickX)
      {
         return i;
      }
   }

   return line.length();
}

// Function to display text on the viewport using cairo
void TextDisplay::proofose_display_text_contents_on_viewport(ImVec2 &imageStartPosition, ImVec2 &imageEndPosition, const TextData &textData, const GridInfo &gridInfo)
{
   // Get the window drawlist to draw the image
   ImDrawList *drawList = ImGui::GetWindowDrawList();
   //std::cout<<"Image Start Position: "<<imageStartPosition.x<<", "<<imageStartPosition.y<<std::endl;

   // Store the retrieved text in a string(utf16 format)
   std::string retrivedText = textData.text;

   // Get the text to display(every second character in the UTF-16 string)
   std::string textToDisplay = "";
   // Loop through content's second character(UTF-8 Unicode code point)
   for (int i = 1; i < retrivedText.length(); i += 2)
   {
      textToDisplay.append(1, retrivedText[i]);
   }

   // Variables to store the RGBA color values
   float Red, Green, Blue, Alpha;
   // Check if the StyleRun has properties
   if (!textData.styleRun.properties.empty())
   {
      // Access the fillColor of the first StyleProperties in the StyleRun
      ColorDetails fillColor = textData.styleRun.properties[0].fillColor;

      // Access RGBA color values from fillColor.values
      if (fillColor.values.size() >= 4)
      {
         Alpha = fillColor.values[0];
         Red = fillColor.values[1];
         Green = fillColor.values[2];
         Blue = fillColor.values[3];
      }
   }
   // Set the color of the text to be displayed (BGR format)
   std::vector<float> textColor = {Red, Green, Blue, Alpha};

   // Get the font size and font name
   float fontSize = textData.styleRun.properties[0].fontSize;
   float leading = textData.styleRun.properties[0].leading;
   float tracking = textData.styleRun.properties[0].tracking;

   // font name in a string(utf16 format)
   std::string retrivedFontUtf16 = textData.fontSet.fonts[0].name;
   // Convert name from UTF-16 to UTF-8
   std::string font_name = "";
   // Loop through every second character(UTF-8 Unicode code point)
   for (int i = 3; i < retrivedFontUtf16.length(); i += 2)
   {
      font_name.append(1, retrivedFontUtf16[i]);
   }

   // Image dimensions to draw the text on the cairo surface
   ImVec2 startPos = imageStartPosition;
   ImVec2 endPos = imageEndPosition;
   int bgWidth = static_cast<int>(endPos.x - startPos.x);
   int bgHeight = static_cast<int>(endPos.y - startPos.y);

   // Store the text frame dimensions
   int frameWidth = static_cast<int>(textData.width);
   int frameHeight = static_cast<int>(textData.height + textData.top);
   int frameLeft = static_cast<int>(textData.transformdata[4]);
   int frameTop = static_cast<int>(textData.transformdata[5]) + frameHeight * 1.2;
   int frameRight = frameLeft + frameWidth;
   int frameBottom = frameTop + frameHeight;

   // For mouse click detection and text frame visibility control variables
   ImVec2 frameRegionTopLeft = ImVec2(imageStartPosition.x + frameLeft, imageStartPosition.y + frameTop);
   ImVec2 frameRegionBottomRight = ImVec2(imageStartPosition.x + frameRight, imageStartPosition.y + frameBottom);

   float wordSpacingValue = 0.0f; // Initialize with a default value
   std::vector<float> letterspacing;
   std::vector<float> glyphSpacings;
   std::vector<float> wordSpacings;

   if (!textData.paragraphRun.properties.empty())
   {
      // Access the word spacing of the first ParagraphProperties in the ParagraphRun
      const std::vector<float> &wordSpacing = textData.paragraphRun.properties[0].wordSpacing;
      const std::vector<float> &letterSpacing = textData.paragraphRun.properties[0].letterSpacing;
      const std::vector<float> &glyphSpacing = textData.paragraphRun.properties[0].glyphSpacing;

      // Access word spacing values from wordSpacing
      if (!wordSpacing.empty())
      {
         // Set the word spacing value
         wordSpacingValue = wordSpacing[0];
      }
      // Access letter spacing values from letterSpacing
      if (!letterSpacing.empty())
      {
         for (int i = 0; i < letterSpacing.size(); i++)
         {
            letterspacing.push_back(letterSpacing[i]);
         }
      }

      // Access glyph spacing values from glyphSpacing
      if (!glyphSpacing.empty())
      {
         for (int i = 0; i < glyphSpacing.size(); i++)
         {
            glyphSpacings.push_back(glyphSpacing[i]);
         }
      }

      // Acess word spacing values from wordSpacing
      if (!wordSpacing.empty())
      {
         for (int i = 0; i < wordSpacing.size(); i++)
         {
            wordSpacings.push_back(wordSpacing[i]);
         }
      }
   }

   // print word spacing value
   for (int i = 0; i < wordSpacings.size(); i++)
   {
      // std::cout << "Word Spacing: " << wordSpacings[i] << std::endl;
   }

   // Initialize text editing variables
   static bool isTextEditing = false;
   size_t selectionStart = 0;
   size_t selectionEnd = 0;
   static bool isTextSelected = false;

   try
   {
      // Create a temporary surface and context to calculate text dimensions and metrics
      cairo_surface_t *temp_surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 1, 1);
      cairo_t *temp_cr = cairo_create(temp_surface);

      // Set font face and size
      cairo_select_font_face(temp_cr, font_name.c_str(), CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
      cairo_set_font_size(temp_cr, fontSize);

      // Calculate text dimensions
      cairo_text_extents_t extents;
      cairo_text_extents(temp_cr, textToDisplay.c_str(), &extents);

      // Calculate additional metrics
      cairo_font_extents_t font_extents;
      cairo_font_extents(temp_cr, &font_extents);

      // Check if calculated dimensions are valid and return if they are not
      if (frameWidth <= 0 || frameHeight <= 0)
      {
         std::cerr << "Calculated text dimensions are invalid." << 'n';
         cairo_destroy(temp_cr);
         cairo_surface_destroy(temp_surface);
         return;
      }

      // Create a surface to cover the whole background image and a context to draw on it
      cairo_surface_t *surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, bgWidth, bgHeight);
      cairo_t *cr = cairo_create(surface);

      // Set the cairo surface to transparent
      cairo_set_source_rgba(cr, 0, 0, 0, 0.3);
      cairo_paint(cr);

      // Variable to track if the text frame should be displayed
      static bool isTextFrameVisible = false;
      static std::string latestText = textToDisplay;

      // Check for a single click outside the text frame bounds to disable text editing
      if (ImGui::IsMouseClicked(0))
      {
         ImVec2 mousePos = ImGui::GetMousePos();
         if (!(mousePos.x >= frameRegionTopLeft.x && mousePos.x <= frameRegionBottomRight.x && mousePos.y >= frameRegionTopLeft.y && mousePos.y <= frameRegionBottomRight.y))
         {
            isTextFrameVisible = false; // Hide text frame if clicked outside
            isTextEditing = false;      // Disable text editing if clicked outside
            isTextSelected = false;     // Deselect text if clicked outside
         }
         if (isTextSelected)
         {
            isTextSelected = false;
         }
      }

      // Check for a double click within the text frame bounds to enable text editing
      if (ImGui::IsMouseDoubleClicked(0))
      {
         ImVec2 mousePos = ImGui::GetMousePos();
         if (mousePos.x >= frameRegionTopLeft.x && mousePos.x <= frameRegionBottomRight.x &&
             mousePos.y >= frameRegionTopLeft.y && mousePos.y <= frameRegionBottomRight.y)
         {
            isTextFrameVisible = true;
            isTextEditing = true;
            strncpy(editedTextBuffer, latestText.c_str(), sizeof(editedTextBuffer)); // Sync buffer
            editableText = editedTextBuffer;                                         // Sync editable text
            cursorPosition = editableText.length();                                  // Position cursor at end

            // Select all text
            selectionStart = 0;
            selectionEnd = textToDisplay.length();
            isTextSelected = true;
            cursorPosition = selectionEnd; // Move cursor to the end of the text
         }
         else
         {
            isTextFrameVisible = false;
            isTextEditing = false;
         }
      }

      // Change the cursor to ImGuiMouseCursor_TextInput if hovering over the frame region and text editing is enabled
      if (ImGui::IsMouseHoveringRect(frameRegionTopLeft, frameRegionBottomRight) && isTextFrameVisible && isTextEditing)
      {
         ImGui::SetMouseCursor(ImGuiMouseCursor_TextInput);
      }

      // Only proceed to draw the text frame if isTextFrameVisible is true
      if (isTextFrameVisible)
      {
         // Draw the text frame
         drawTextFrame(cr, frameLeft, frameTop, frameRight, frameBottom, frameWidth, frameHeight);
      }

      // Before rendering text
      if (isTextEditing)
      {
         if (ImGui::IsMouseClicked(0) && isTextEditing)
         {
            ImVec2 mousePos = ImGui::GetMousePos();
            std::cout << "Mouse position: " << mousePos.x << ", " << mousePos.y << std::endl;
            ImVec2 relativeMousePos = ImVec2(mousePos.x - frameRegionTopLeft.x, mousePos.y - frameRegionTopLeft.y);

            double accumulatedHeight = 0; // Initialize accumulated height
            size_t newPosition = 0;
            bool foundPosition = false;

            // Split the text into lines
            std::vector<std::string> lines;
            std::istringstream iss(textToDisplay);
            std::string line;
            while (std::getline(iss, line, 'n'))
            {
               lines.push_back(line);
            }

            cairo_set_font_size(cr, fontSize);
            cairo_select_font_face(cr, font_name.c_str(), CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);

            // Get the height of a line
            cairo_font_extents_t font_extents;
            cairo_font_extents(cr, &font_extents);
            double lineHeight = font_extents.height;

            // Calculate the line number based on the vertical position of the click
            int lineNumber = relativeMousePos.y / lineHeight;

            // Ensure the line number is within the bounds of the lines vector and calculate the cursor position
            if (lineNumber >= 0 && lineNumber < lines.size())
            {
               // Calculate the cursor position within the line
               newPosition = calculateCursorPositionWithinLine(cr, lines[lineNumber], relativeMousePos.x);

               // Add the lengths of the previous lines to the cursor position
               for (int i = 0; i < lineNumber; ++i)
               {
                  newPosition += lines[i].length() + 1; // +1 for the newline character
               }

               foundPosition = true;
            }

            // If the position was found, update the cursor position
            if (foundPosition)
            {
               cursorPosition = newPosition;
            }
            std::cout << "Cursor position: " << cursorPosition << std::endl;
         }

         // Handle keyboard input for text editing
         handleKeyboardInput(ImGui::GetIO(), editableText, cursorPosition); // Update text based on input events
         latestText = editableText;                                         // Sync latest text for rendering
      }

      // Call the function to draw text on the cairo surface
      drawTextWithCairo(cr, font_name, fontSize, textColor, latestText, frameLeft, frameTop, frameRight, frameBottom, wordSpacingValue, font_extents, leading, cursorPosition, isTextEditing, selectionStart, selectionEnd, isTextSelected);

      // Create texture and draw in ImGui window if the text frame is visible
      ImTextureID textureID = createTextureForFillingTheShape(cairo_image_surface_get_data(surface), bgWidth, bgHeight);

      // Check if texture creation failed and return if it did
      if (textureID == 0)
      {
         std::cerr << "Texture creation failed." << 'n';
         cairo_destroy(cr);
         cairo_surface_destroy(surface);
         return;
      }

      // Flush changes to the surface and clean up resources
      cairo_surface_flush(surface);
      cairo_destroy(cr);
      cairo_surface_destroy(surface);

      // Get the screen size
      ImVec2 screenSize = ImGui::GetIO().DisplaySize;

      // Calculate text position
      ImVec2 textPos = ImVec2((screenSize.x - frameWidth) / 2, (screenSize.y - frameHeight) / 2);

      // Check if text position is within viewport bounds
      if (textPos.x < 0 || textPos.y < 0)
      {
         std::cerr << "Text position is out of viewport bounds." << 'n';
         return;
      }

      // Add the texture to the ImGui draw list to cover the background image
      ImVec2 imagePos = ImVec2(startPos.x, startPos.y); // Position where background image starts
      drawList->AddImage(textureID, imagePos, ImVec2(imagePos.x + bgWidth, imagePos.y + bgHeight));
   }
   catch (const std::exception &e)
   {
      // Handle any exceptions that occur during the process
      std::cerr << "Error displaying text: " << e.what() << 'n';
   }
}

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật