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';
}
}