Hello! Welcome to the next lesson on building our high-performance telemetry engine.
In our last lesson, we gave our custom plot a sense of scale by drawing axes, grid lines, and labels. While you can pan and zoom through this space, the plot itself remains static—it doesn't react to your specific actions. Today, we'll change that by making it truly interactive.
This lesson focuses on handling mouse input. You will learn to query the mouse's position, clicks, and scroll wheel state within ImGui to build interactive features. We will start by implementing a hover tooltip that displays data coordinates and then refine our pan and zoom logic, ensuring it feels intuitive and professional.

The Immediate Mode Input Model
Before we write any code, it's essential to understand ImGui's philosophy for handling input. Unlike traditional UI frameworks that use callbacks or event listeners (e.g., button.onClick = ...), ImGui uses an immediate mode or polling approach.
On every frame, you simply ask ImGui for the current state of the input devices:
- "Is the mouse button currently pressed?"
- "Where is the mouse cursor right now?"
- "Was this widget clicked in this frame?"
This removes the need to manage complex state or register and unregister event handlers. The UI's behavior is defined directly by the code that runs each frame.
Implementing basic interactions in IMGUI
To see this philosophy in action, let's look at a classic article explaining the core principles of an Immediate Mode GUI. Pay close attention to how a button click is detected.
Read the section titled "Implementing basic interactions". Notice how the button function's implementation is a simple check of the mouse's current position and button state within the same frame it's being drawn.
As you saw, the logic is as simple as if (mouse_clicked && mouse_in_bounds). We will apply this same principle to our plot.
Handling Mouse Position: Implementing a Hover Tooltip
Our first interactive feature will be a tooltip that appears when the user hovers the mouse over the plot, displaying the precise world coordinates under the cursor.
This requires three steps:
- Check if the mouse is inside our plot's boundaries.
- If it is, get the mouse's screen position and convert it to world coordinates using our
ScreenToWorldfunction. - Display these coordinates in a tooltip.
ImGui makes this straightforward. We can use ImGui::IsWindowHovered() to check if the mouse is within the current window's bounds. Since our plot canvas occupies the whole window, this works perfectly.
Here's how you can add this to your PlotMyData function. This code should go inside your ImGui::Begin("My Plot") block, right after you've defined your canvas boundaries (canvas_p0, canvas_p1).
// Inside PlotMyData, after getting canvas_p0 and canvas_p1
ImGuiIO& io = ImGui::GetIO();
// Check if the plot window is hovered
if (ImGui::IsWindowHovered())
{
// Get mouse position in screen coordinates
ImVec2 mouse_pos_screen = ImGui::GetMousePos();
// Convert to world coordinates
ImVec2 mouse_pos_world = ScreenToWorld(mouse_pos_screen, pan_offset, zoom_level, canvas_p0, canvas_p1);
// Display a tooltip
ImGui::BeginTooltip();
ImGui::Text("World Coords: (%.2f, %.2f)", mouse_pos_world.x, mouse_pos_world.y);
ImGui::EndTooltip();
}
When you run this, you'll see a small tooltip appear next to your cursor whenever it's over the plot window, showing the calculated world coordinates. This is the first step in connecting user actions to the data space.
Test your understanding!
To provide better visual feedback, let's draw a small crosshair at the mouse's world-space position. Using the mouse_pos_world variable from the code above, add code to draw a vertical and a horizontal line that intersect at the cursor's position.
Hint: You will need to convert the mouse_pos_world back to screen coordinates to draw it. You can do this with your WorldToScreen function. Use draw_list->AddLine().
Show answer
Here's one way to implement the crosshair. This code would go within the same if (ImGui::IsWindowHovered()) block.
// Continuing inside the if (ImGui::IsWindowHovered()) block...
// Convert the world position back to screen to draw the crosshair
ImVec2 crosshair_pos_screen = WorldToScreen(mouse_pos_world, pan_offset, zoom_level, canvas_p0, canvas_p1);
ImU32 crosshair_color = IM_COL32(255, 255, 0, 150); // A yellow color
// Draw vertical line
draw_list->AddLine(
ImVec2(crosshair_pos_screen.x, canvas_p0.y),
ImVec2(crosshair_pos_screen.x, canvas_p1.y),
crosshair_color, 1.0f);
// Draw horizontal line
draw_list->AddLine(
ImVec2(canvas_p0.x, crosshair_pos_screen.y),
ImVec2(canvas_p1.x, crosshair_pos_screen.y),
crosshair_color, 1.0f);
This draws two lines that span the entire canvas and intersect at the cursor, providing clear visual feedback.
Handling Clicks and Scrolls: The Math of Centered Zoom
You've already implemented panning and zooming. Let's formalize this within ImGui's input model and, more importantly, perfect the logic for cursor-centered zooming.
When you zoom with the mouse wheel, the point under the cursor should remain stationary. The world should appear to expand from or contract toward that point. Simply multiplying by a zoom factor scales the world around the origin (0,0), which feels unnatural.
The correct algorithm is:
- Get the mouse's position in world coordinates before the zoom is applied. Let's call this
world_pos_before_zoom. - Apply the zoom factor to update your
zoom_level. - Get the mouse's position in world coordinates after the zoom is applied. Let's call this
world_pos_after_zoom. The screen position of the mouse hasn't changed, but because the zoom level (and thus the transformation) has, the underlying world coordinate is now different. - Calculate the difference:
delta = world_pos_before_zoom - world_pos_after_zoom. - Add this
deltato yourpan_offset. This adjustment "pulls" the world back so that the original point is once again under the cursor.
This concept is much easier to grasp visually.
The following video from javidx9 provides an excellent conceptual and mathematical breakdown of implementing both panning and cursor-centered zooming from scratch.
Watch the segment on implementing cursor-centered zoom. Pay close attention to the logic of capturing the mouse world position before and after the zoom, and using the difference to adjust the offset.
Now, let's see how this exact logic is implemented in C++ with ImGui. This is a common problem, and studying existing solutions is a great way to learn.
Zoom in and out centered about the mouse cursor in 2D using ImGUI
This Stack Overflow question deals with the exact issue of implementing cursor-centered zoom in ImGui. The accepted answer provides a concise and correct code snippet.
Read the accepted answer. Compare the C++ code to the algorithm we just discussed and what you saw in the video. Identify which lines correspond to steps 1, 2, 3, and 5 of our algorithm.
Let's integrate this improved logic into our PlotMyData function. Find your existing mouse wheel handling code and replace it with this more robust version.
// Inside PlotMyData, in your input handling section...
if (ImGui::IsWindowHovered() && io.MouseWheel != 0.0f)
{
ImVec2 mouse_pos_screen = ImGui::GetMousePos();
// 1. Get world position before zoom
ImVec2 world_pos_before_zoom = ScreenToWorld(mouse_pos_screen, pan_offset, zoom_level, canvas_p0, canvas_p1);
// 2. Apply zoom
float zoom_delta = io.MouseWheel * 0.1f;
zoom_level *= std::pow(2.0f, zoom_delta);
// Clamp zoom level to avoid floating point issues
zoom_level = std::max(0.001f, std::min(zoom_level, 1000.0f));
// 3. Get world position after zoom
ImVec2 world_pos_after_zoom = ScreenToWorld(mouse_pos_screen, pan_offset, zoom_level, canvas_p0, canvas_p1);
// 4. & 5. Calculate difference and adjust pan
pan_offset += world_pos_before_zoom - world_pos_after_zoom;
}
This implementation correctly centers the zoom on the cursor, making navigation feel fluid and precise—a hallmark of a professional tool. Your panning logic (using IsMouseDragging and io.MouseDelta) should already work correctly with this, as it operates independently on the pan_offset.
Conclusion
In this lesson, you've taken a significant step toward making your plotter a powerful, interactive tool. You now understand how to use ImGui's immediate mode input model to react to user actions in a clean, stateless way.
Key Takeaways:
- ImGui's input model is based on polling (
Is...()functions) every frame, not event callbacks. - You can detect if the mouse is over a specific area using functions like
ImGui::IsWindowHovered(). - Interactive features like tooltips are built by combining hover detection, coordinate transformation (
ScreenToWorld), and ImGui's drawing APIs. - Cursor-centered zoom is achieved by calculating the shift in world space caused by a change in zoom level and compensating for it by adjusting the pan offset.
What's Next?
Our plot is now interactive, but it has a hidden flaw: it doesn't respond correctly if you resize the window. The canvas boundaries, and therefore all our coordinate calculations, are fixed at the start. In the next lesson, we will tackle this by learning how to ensure the visualization correctly adapts to window resizing and viewport updates. This will make our plotter robust and ready for integration into a real application.