Drag-and-drop is often mentioned as one of the things that are “tricky” to implement in an immediate mode GUI. In this post I’ll describe how we do it in The Machinery.
Our goal is to create a generic drag-and-drop system that works well for all possible kinds of dragged data and drop targets.
Representing dragged objects in data
When designing a new system or feature I always find it helpful to start from a data perspective. Once we know how we want to represent the feature in data it is relatively easy to add an API for manipulating that data and UI that interacts with the API. Focusing on just the data representation prevents the task from feeling too overwhelming.
For a drag-and-drop operation we need to know whether a drag operation is in progress or not and what objects are being dragged. Since “no drag operation” can be represented as an empty list of dragged objects, we only really need a single thing:
- A list of the object(s) that are currently being dragged.
That’s all well and good, but what is an “object” in this case? We want to support dragging for lots of different “things”, so our “object” has to be something pretty generic. There are a lot of different ways of representing such arbitrary things, some of the more popular ones are:
- An object inheriting from a global DraggableObject class. The DraggableObject class has methods for inspecting exactly what kind of object it is.
- A type identifier and a
void *
that we can cast to the right pointer type. - A type identifier and a raw data blob that could be a struct, a pointer or something else — how to interpret the content is determined by the type identifier.
- Serialized object data — for example in JSON format.
Any of these approaches can work well, depending on the architecture of your application. Personally, I’m partial to (3) — it is simple, flexible and easy to extend. It can also encompass both option (2) — by storing a pointer in the blob and option (4) — by storing the serialized data in the blob. I don’t like (1) since it tends to create tight coupling between unrelated classes that all have to inherit from DraggableObject.
In The Machinery we have a unified data model that we call The Truth. All the data managed by the application is stored in The Truth and we can reference individual Truth objects by their ID. The Truth keeps track of the object types and manages their data. This means that we don’t have to use any of the options above, we can just represent objects by their ID in The Truth.
In fact, we can go one step further. Since objects in The Truth can contain sets of references to other objects we can represent the whole set of dragged objects as a single object in The Truth. So all we need to know is the ID of that object.
When dragging from an external source, such as the file system, we can create an object in The Truth to represent the dragged items. It could for example contain the paths to all the files.
So we only need a single ID variable to represent the data. Where should this variable be stored? In our IMGUI system each window is its own separate UI context, but we want to support dragging objects between different UI windows. This means that the dragged object must be stored outside the UI context. Since the dragged object can be dropped anywhere in the application, it makes sense to treat it as a global property.
So our representation is a global ID that references the dragged objects in The Truth. In other words, this is our data model:
uint64_t dragged_objects;
And here is our API:
void start_dragging(uint64_t objects)
{
dragged_objects = objects;
}
void stop_dragging()
{
dragged_objects = 0;
}
uint64_t get_dragged_objects()
{
return dragged_objects;
}
The case where no objects are being dragged is represented by the NULL reference, which is just the zero ID.
We can see here how the shared data model we implemented with The Truth really pays off. It allows us to use a super simple representation for the dragged data.
We can also see that starting with the data representation indeed was a smart move. Since the data model turned out so simple, we are pretty confident that its easy to build on top of.
Initiating a drag operation
A drag operation is typically initiated by clicking on a draggable object and then, while keeping the mouse pressed, dragging outside its boundary. Nothing prevents us from also supporting other ways of initiating drag, but let’s focus on this typical case.
Here is some pseudocode, showing how we implement this in the IMGUI code for a draggable UI item with ID item_id
, representing a truth object with ID object_id
.
// If the mouse is pressed on the item, prepare it for dragging.
if (ui->hover == item_id && ui->left_mouse_pressed)
ui->prepare_drag = item_id;
// If the mouse button is lifted, abort dragging.
if (!ui->left_mouse_is_down)
ui->prepare_drag = 0;
// If this item was clicked on (prepared for dragging), but the mouse is now
// outside the item -- start dragging.
if (ui->prepare_drag == item_id && ui->hover != item_id) {
tm_drag_api->start_dragging(object_id);
ui->prepare_drag = 0;
}
Here, ui->hover
is a variable in the UI that keeps track of which item the mouse is currently hovering over.
Detecting drops
The code for an IMGUI drop target is similar. We check if we are hovering over the item and if we are currently dragging an object of the right type. If we are, we draw the appropriate highlighting to indicate that the item is a valid drop target.
const uint64_t dragged = tm_drag_api->dragged_objects();
if (dragged && ui->hover == item_id) {
const uint64_t type = tm_the_truth_api->object_type(tt, dragged);
if (type == ACCEPTED_TYPE) {
// INSERT CODE: Draw drop target highlight
if (ui->left_mouse_released) {
// INSERT CODE: Handle drag and drop
tm_drag_api->stop_dragging();
}
}
}
If the mouse button is released over the target we implement the drop action. Exactly what happens depends on what is dropped where, but it is typically an operation that only involves the data model. The UI will simply reflect the changes made to the data. Since we use an immediate model this happens automatically with no need for state synchronization.
We also need to handle the case when the mouse is released over an invalid target. In this case we want to stop the dragging without any data change.
It is a bit unclear who should handle this. Since the dragged state is a global property it has to be a part of the global update loop. We simply have this at the end of the main update loop:
if (!left_mouse_button_down)
tm_ui_drag_api->stop_dragging();
This illustrates what I’ve found to be one of the trickier parts of IMGUI programming — processing events in the right order. For example, it is important that all the UIs get a chance to react to the mouse up event, so they can accept valid drops before we conclude that an invalid drop happened and discard the drag state.
Another tricky ordering thing that can happen is that sometimes an input event causes a new UI object to be created (for example when expanding a tree control) and then that control reacts to the very event that created it. I’ve sometimes found the need to frame delay certain actions to prevent things like that from happening, which doesn’t feel very elegant. I might go into greater detail on this in a future blog post.