As promised in the video: Click here for the FREE starter template here. This video’s source code (and much more) is available in the Kea Campus.
I decided to build my own paint program… not because the world needs another one, but because I needed something to play with Raylib and Dear ImGui. It’s the kind of project that’s simple, visual, and a great way to learn how to work with these libraries. I’ll be starting from a Raylib + ImGui template I made earlier — if you want to follow along or build your own project.
Alright, let’s go!
Using Raylib-cpp
The first thing I did was add the raylib-cpp library to the project. It wraps Raylib objects in C++ classes. Why use this? Basically, to offload the burden of memory management. For example, I can create a C++ Texture object up the top of a function, and C++ will call its destructor as soon as the function returns. I don’t need to stick UnloadTexture() calls everywhere where it goes out of scope.
Here’s the CMake script that imports this library:
# Fetches the version of raylib-cpp that we need
set(RAYLIB_CPP_VERSION 5.5.0)
FetchContent_Declare(
raylib_cpp
URL https://github.com/RobLoach/raylib-cpp/archive/refs/tags/v${RAYLIB_CPP_VERSION}.tar.gz
FIND_PACKAGE_ARGS ${RAYLIB_CPP_VERSION} EXACT
)
set(BUILD_RAYLIB_CPP_EXAMPLES OFF CACHE INTERNAL "")
FetchContent_MakeAvailable(raylib_cpp)
Now let’s take a step back, and map out what we’re trying to do. This is going to be a very simple paint program. No loading or saving. Just a few simple tools and a draw surface.
We’ll put a toolbar on the left, and the rest of the window will be taken up by the draw surface:

My template code already has that basic setup. I just need to delete the stuff I don’t want, and replace it with my tool buttons and draw surface.
I decided to create the tool buttons first. This took a lot longer than I expected. That’s because I’m new to ImGui, and couldn’t find any detailed documentation. So, I spent a lot of time scouring the internet, looking at ImGui’s source code, followed by a lot of trial and error
I decided to use FontAwesome for the tool icons, because it’s awesome, but had trouble resizing the icons to the size I wanted. Eventually I discovered that you must call rlImGuiReloadFonts() after adding a new font or font size (see below).
ImFont *iconFont = setupIconFont(iconFontSize);
rlImGuiReloadFonts();
With a bit more experimentation, I got a basic toolbar working, with two tool icons, a pencil for the freehand drawing tool, and a slash for straight line drawing. There’s also a colour selection widget.
Here ‘s the toolbar in hard-coded form:
// The toolbar
ImVec2 toolButtonSize = calcToolButtonSize(iconFont, ICON_FA_PAINTBRUSH);
float toolbarWidth = TOOL_BUTTONS_PER_ROW * (toolButtonSize.x + ImGui::GetStyle().ItemSpacing.x);
ImGui::BeginChild("Toolbar", ImVec2(toolbarWidth, 0.0f));
ImGui::PushFont(iconFont);
ImGui::Button(ICON_FA_PENCIL, toolButtonSize);
ImGui::PushStyleColor(ImGuiCol_Button, inactiveToolButtonColour);
ImGui::SameLine();
ImGui::Button(ICON_FA_SLASH, toolButtonSize);
ImGui::PopStyleColor();
ImGui::PopFont();
ImGui::NewLine();
ImGui::Text("Colour");
ImGui::ColorEdit3("##Colour RGB", (float*)&toolColour, ImGuiColorEditFlags_NoInputs); // Edit 3 floats representing a color
ImGui::EndChild();
Designing the Draw Tool Interface
At this stage it looks a bit like a paint program, but it’s all hardcoded, and you can’t do any actual drawing with it. So that’s what I want to add now.
I decided to create a DrawTool class to define the interface for how a draw tool works. That way the main program doesn’t need to know how to use each draw tool. It interacts with any draw tool the same way.
Getting this interface right, will make building a large fully featured paint program much easier.
However, do NOT overthink this interface. You could waste days trying to come up with the perfect interface on which you could build the next Photoshop. But, you’ll probably still screw it up because you don’t have enough information, to create the perfect interface.
Instead, design something that looks reasonable quickly, and try it out. You’ll move faster and gain much more experience by trying, failing, redesigning and trying again, until you get it right. Rapid prototyping and iteration.
So this is what I came up with:
/** Base class for draw tools.
* As the name suggests, draw tools allow the user to draw to a draw surface (i.e., a texture/bitmap).
*/
class DrawTool {
public:
/** Create a new draw tool.
*/
DrawTool() = default;
virtual ~DrawTool() = default;
/** Returns the font character code for this tool's icon (e.g., ICON_FA_PENCIL).
*/
virtual const char* getIconCode() const = 0;
/** Process input events (i.e., read mouse/keyboard/touch input).
*
* @param camera use the camera's GetScreenToWorld() function to convert mouse/touch coordinates to
* draw surface coordinates
* @param colour the tool colour as set by the UI
*
* @return bool true if the tool needs to write to the draw surface (e.g., finished a line draw), and
* false otherwise
*/
virtual bool processInput(const raylib::Camera2D &camera, const Color &colour) = 0;
/** Draws the tool to the screen (e.g., the line drawing tool draws the in-progress line).
*
* NOTE: You're drawing in the draw surface's coordinate system (to a texture that's the same size and
* resolution).
*
*/
virtual void drawTool() const = 0;
/** Draw to the draw surface (e.g., draw the line to the actual bitmap that the user is editing).
* This is called when processInput() returns true. The render context will be set up to draw to the
* drawing surface.
*/
virtual void drawToSurface() const = 0;
// ##### FIXME! ##### How to handle mouse pointer? Raylib can't use custom mouse pointers
};
A draw tool has 4 things to do:
- Provide a tool icon (getIconCode())
- Handle user input when it’s active (processInput()). The camera parameter is for adding the ability to zoom in and out, and pan around. Notice how processInput() returns true when the tool needs to draw to the actual draw surface
- Draw itself on-screen (drawTool())
- Draw to the surface (drawToSurface())
I decided to side-step the issue of different mouse pointers for now, because Raylib currently doesn’t support custom mouse pointers. The two tools I’m implementing today all use cross-hairs, so I’m hard-coding that for now, like this:
ImGui::Image((ImTextureID)&dispTex, ImVec2(dispTex.width, dispTex.height));
if(ImGui::IsItemHovered()) {
// The mouse is over the draw surface. Show the tool's mouse cursor
// ##### FIXME! ##### Rethink when different tools need different pointers
mouseCursor = MOUSE_CURSOR_CROSSHAIR;
} else {
mouseCursor = MOUSE_CURSOR_DEFAULT;
}
if(prevMouseCursor != mouseCursor) {
SetMouseCursor(mouseCursor);
prevMouseCursor = mouseCursor;
}
This code switches the mouse pointer to the cross-hairs when the mouse pointer is over the draw surface. Custom mouse pointers can always be added later.
Implementing Drawing
Now we need to get the actual drawing code done. I’ve reworked main() to get the tool icons from the tools themselves, and added the ability to select the active tool. I’ve also created skeleton PencilTool and LineTool classes.
Displaying the draw surface and tool is a two step process. Here’s how it works internally: We have two RenderTextures:
- The draw surface. This is the image that the user is drawing
- A display surface. This is the same size as the draw surface, and is used when calling drawTool()
When we update the display, the draw surface is copied to the display surface. Next, drawTool() is called to draw the current tool, and then the display surface is drawn into the window’s main area via ImGui::Image().
This isn’t the most efficient way to do this, but it’s simple and it works. Modern computers have fast GPUs and plenty of VRAM, so it works just fine.
Implementing Freehand Drawing (a.k.a., The Pencil Tool)
Lets get the freehand drawing tool working. Here’s the PencilTool’s code:
const char* PencilTool::getIconCode() const {
return ICON_FA_PENCIL;
}
bool PencilTool::processInput(const raylib::Camera2D &camera, const Color &colour) {
this->colour = colour;
prevPoint = currPoint;
this->currPoint = camera.GetScreenToWorld(GetMousePosition());
if(!drawing) {
// Not drawing, so keep moving the previous point with the current one
prevPoint = currPoint;
}
// The pencil draws when the mouse button is down
drawing = IsMouseButtonDown(MOUSE_BUTTON_LEFT);
return drawing;
}
void PencilTool::drawTool() const {
// This is a preview of what it would look like on the actual drawing surface
drawToSurface();
}
void PencilTool::drawToSurface() const {
if(prevPoint != currPoint) {
DrawLine(prevPoint.x, prevPoint.y, currPoint.x, currPoint.y, colour);
} else {
DrawPixel(currPoint.x, currPoint.y, colour);
}
}
ProcessInput() saves the current colour and previous mouse position. Next, it reads the current mouse position. If drawing isn’t active, then it sets the previous position to the current mouse position. Finally, it checks if the left mouse button is down, and sets the drawing flag. It returns true if the user is holding down the mouse
Both drawTool() and drawToSurface() draw a line between prevPoint and currPoint. Or, draw a single pixel.
Here’s the code in main() that drives all of this:
ImGui::BeginChild("Main", ImVec2(0.0f, 0.0f),
ImGuiChildFlags_None, ImGuiWindowFlags_HorizontalScrollbar);
// Handle the draw tool
auto &dSurfaceTex = drawSurface.GetTexture();
bool haveDrawTool = currDrawTool != drawTools.end();
if(haveDrawTool && ImGui::IsWindowHovered()) {
ImVec2 mainPos = ImGui::GetWindowPos();
raylib::Camera2D camera(raylib::Vector2(mainPos.x, mainPos.y), raylib::Vector2());
bool drawToSurface = (*currDrawTool)->processInput(camera, toolColour);
if(drawToSurface) {
drawSurface.BeginMode();
(*currDrawTool)->drawToSurface();
drawSurface.EndMode();
}
}
The next step is to display everything. Here’s the code that does that:
// Draw the display
displaySurface.BeginMode();
dSurfaceTex.Draw(raylib::Vector2(0.0f, 0.0f));
// Need to flip the y-axis, or the tool gets drawn upside down
if(haveDrawTool) {
rlPushMatrix();
rlTranslatef(0.0f, dSurfaceTex.height, 0.0f);
rlScalef(1.0, -1.0, 1.0);
(*currDrawTool)->drawTool();
rlPopMatrix();
}
displaySurface.EndMode();
auto &dispTex = displaySurface.GetTexture();
ImGui::Image((ImTextureID)&dispTex, ImVec2(dispTex.width, dispTex.height));
This is a three step process. First, we draw the draw surface to the display surface. Next, we draw the tool (by calling drawTool()), and finally, the image is drawn to screen inside the main display.
You can see that I had to flip the y-axis to get the draw tool displayed the right way round. That’s because Raylib uses OpenGL under-the-hood (seee the rlTranslatef() and rlScalef() calls in the code above), and it’s default coordinate system puts the origin in the bottom left, whereas we’re measuring everything from the top-left corner. So, we flip the y-axis
Implementing Line Drawing
We have one draw tool working. Awesome! Now lets get line drawing working too.
Line drawing works almost the same as the pencil tool. The only difference is that we don’t draw anything until the user releases the mouse button. The code looks very similar to PencilTool, except the code to handle the mouse button. LineTool’s processInput() method only returns true when the mouse button state goes from down to up:
bool LineTool::processInput(const raylib::Camera2D &camera, const Color &colour) {
this->colour = colour;
this->endPoint = camera.GetScreenToWorld(GetMousePosition());
if(!inProgress) {
// Not drawing, so keep moving the start point to the current position
startPoint = endPoint;
}
bool wasInProgress = inProgress;
inProgress = IsMouseButtonDown(MOUSE_BUTTON_LEFT);
// A line is drawn when the mouse button is released
return wasInProgress && !inProgress;
}
I’ve also renamed prevPoint to startPoint, and currPoint to endPoint, because it makes more sense for straight line drawing.
Conclusion & Next Steps
And just like that, we have a fully working mini paint program. I can draw freehand with the pencil tool, draw straight lines, and choose the draw colour. It’s all working, just as designed.

What’s next? There are a few options:
- You could build your own mini paint app using the Raylib + Dear ImGui template. Click here to get it
- Or, Kea Campus Creator and higher members can download the source code to the mini-paint app that I just built, and have a go at extending it. Here are some suggestions:
- Add loading & saving, and creating a new image
- Remove the hard-coded draw surface size
- Add a few more tools, like drawing rectangles & circles
- If you want something harder, try adding text drawing or flood fills
The choice is yours. Here’s a link to the Kea Campus for those who want to build on my code. Otherwise, get the starter template, and play with that.
I’ll probably add more features to this paint program myself. But not today. That’s it for now. See you next time.