New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Wraps the dear ImGui custom drawing API #111
Conversation
The build is failing as below:
I don't really understand what's the issue. The build succeeds locally when I run |
https://doc.rust-lang.org/stable/rustdoc/documentation-tests.html#pre-processing-examples What's happening is that the doctest is being wrapped in a
|
@Lymia Thanks! I just fixed the examples as you suggested. Travis CI should get a successful build now. Actually the build succeeded locally in my environment because I used the nightly compiler. On stable, it fails as in the CI. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! 😄
src/lib.rs
Outdated
@@ -82,6 +84,15 @@ pub fn get_version() -> &'static str { | |||
} | |||
} | |||
|
|||
/// Represents one of the buttons of the mouse | |||
pub enum ImMouseButton { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Derive at least Copy, Clone, Eq, PartialEq, Debug
src/window_draw_list.rs
Outdated
/// `From<[f32; 3]>`, `From<(f32, f32, f32, f32)>` and `From<(f32, f32, f32)>` | ||
/// for convenience. If alpha is not provided, it is assumed to be 1.0 (255). | ||
#[derive(Copy, Clone)] | ||
pub struct ImColor(ImU32); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Derive Eq, PartialEq, Debug
src/window_draw_list.rs
Outdated
/// All types from which ImGui's custom draw API can be used implement this | ||
/// trait. This trait is internal to this library and implemented by | ||
/// `WindowDrawList` and `ChannelsSplit`. | ||
pub trait DrawAPI<'ui> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The lifetime parameter 'ui
is not used at all in the trait so it's not necessary
src/window_draw_list.rs
Outdated
|
||
/// Object implementing the custom draw API. | ||
pub struct WindowDrawList<'ui> { | ||
draw_list: *mut ImDrawList, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs a very thorough study, since it seems that we can have more than one WindowDrawList at a time. We won't have multiple mutable borrows in the normal Rust way, but multiple mutable raw pointers can still end up doing bad things.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you know how lib wrappers usually deal with this kind of issues? It seems difficult to enforce such check at compile time.
I propose to enforce the *mut ImDrawList
borrow check at run time. using a static
variable to store the state. This may not be a perfect solution, but I hope it is optimal for a rusty wrapper of imgui. I implement this idea in patch 586e102, please let me know your thoughts.
src/window_draw_list.rs
Outdated
}; | ||
} | ||
|
||
impl_draw_list_methods!(WindowDrawList); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couldn't the methods be on the trait instead of manually duplicating everything with macros? I know this pattern has been used before with some input components in imgui-rs, but in retrospect I don't think it was the right way to do it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually thought about doing that, so let me explain the reasoning behind my choice.
If the methods are defined on the trait, then the user of the lib will be required to import the DrawAPI
trait to be able to use any of the methods. As adding a supplementary import is not convenient I chose to duplicate all method definitions with macros. With this design,DrawAPI
is an internal trait used only internally for its draw_list
method.
What do you think would be the best for the user's interest?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I understand this reasoning and I've seen this pattern in some libs before (including imgui-rs itself ;) ). However, using macros to copy-paste code means that we're also ignoring potentially beneficial abstraction.
In this case, imagine that you want to write a function that draws something:
fn draw_my_amazing_thing<'ui>(dl: &WindowDrawList<'ui>) {
dl.add_line(...);
// ...
}
This is all fine and you might be happy with this function, but now we've tied this function to WindowDrawList, so it won't work with ChannelsSplit even though it too has the add_line
method. This seems strange if WindowDrawList and ChannelsSplit are supposed to have the same functionality.
What if you write a new trait to support both types? That seems very strange since we already have DrawAPI which could be used to abstract the common functionality.
What if you require that dl
implements DrawAPI
and use the Line::new
function in the implementation instead of add_line
? That works because it supports anything that implements DrawAPI. But now what's the point of having add_line
?
In my opinion using macros to copy-paste code only makes sense when you want to copy some boilerplate into types that are so different that you know there's no need to abstract over them.
So, I'd prefer just having the functions in DrawAPI to avoid all these issues. Yes, the user has to import the trait, but this is just how things work in Rust. Is it a problem that you need to import std::io::Read
to read from a std::fs::File
?
However, don't start changing things just yet, because I think we can have a slightly different approach and abolish DrawAPI completely. I'll explain this in another comment 😃
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That makes so much sense. Thank you very much for the explanation :) . I feel a better Rustacean now. Looking forward to your idea about a different approach then!
src/window_draw_list.rs
Outdated
} | ||
} | ||
|
||
/// Represents a circle about to be drawn on the window |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
circle?
src/window_draw_list.rs
Outdated
impl<'ui> ChannelsSplit<'ui> { | ||
/// Change current channel | ||
pub fn channels_set_current(&self, channel_index: u32) { | ||
unsafe { sys::ImDrawList_ChannelsSetCurrent(self.draw_list(), channel_index as i32) }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
channel_index
has an assertion in the underlying library. If we can't check the condition with a "nice" Rust panic, the abort should be documented.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right. I will refactor to get a "nice" rust panic if the condition is unmet.
src/lib.rs
Outdated
/// }); | ||
/// } | ||
/// ``` | ||
pub fn with_window_draw_list<F>(&self, f: F) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using the with_
+ closure method here feels strange, because there's no cleanup function that needs to be called. Also, this doesn't add any extra safety because it can be called recursively multiple times. WindowDrawList::new
is also public, so this is not the only way to construct a WindowDrawList
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure enough. I propose then to rename the function to get_window_draw_list
and have it return a WindowDrawList
object.
As you said, WindowDrawList::new
should only be public within the crate.
cc878b4 fixed this issue.
Thanks for the review, I incorporated your comments in the PR and answered to your inquiries. |
@Gekkio There are some conflicts now. What is your policy regarding the solving of conflicts? |
Personally I prefer rebases for handling conflicts in short-lived pull requests, but you can choose whichever feels easier |
Then I'll do a rebase. Rebases are better in the long term, as we get a cleaner git history. I will overwrite this PR with a rebased branch. |
This patch makes the basic structure for a wrapper around Dear ImGui's drawing API. 1. Implement `with_window_draw_list` method on Ui. Call this method to get access to the `WindowDrawList` object. This object holds the methods to access ImGui's drawing API. 2. Dear ImGui uses the ImU32 (an unsigned c_int) to represent colors in the drawing API. This commit wraps this type with ImColor for convenience. Any color representation (3or4-tuples, 3or4-arrays, ImU32 or ImVec4) can be converted into ImColor for convenience. 3. Difference between WindowDrawList and ChannelsSplit: Most drawing functions can be called on WindowDrawList and ChannelsSplit objects. However for safety, some functions can only be called on WindowDrawList or ChannelsSplit instance. For example `channels_set_current` can only be called after channels have been split. To avoid code duplication, functions common to WindowDrawList and ChannelsSplit are implemented within the `impl_draw_list_methods` macro. 4. Implement drawing functions (in this commit, add_line only). Calling `add_line` returns a default representation of the line to be drawn, but does not draw it. Then parameters, such as thickness, can be set. You must call `build` to draw the line. All drawing functions will be implemented following this pattern.
This patch uses bitflags to set the whether the corners are rounded. Hence the `ImDrawCornerFlags` struct is defined, but only used internally. Externally, the valule of the flags can be changed with methods on the `Rect` structure such as `round_top_right` or `round_bot_left`. This patch wraps both ImDrawList_AddRectFilled and ImDrawList_AddRect. ImDrawList_AddRectFilled is seen as a particular case of `add_rect` where `filled` is set to `true`.
As `add_rect_filled_multicolor` does not have any option, the `build` pattern is not used and calling `add_rect_filled_multicolor` directly draws the rectangle on the window.
ImDrawList_AddCircle was missing an argument in the bindings, resulting in UB. This patches includes it and wrap the AddCircle API.
This commit adds two methods to the drawing APIs: with_clip_rect and with_clip_rect_intersect. Both wrap ImDrawList_PushClipRect and ImDrawList_PopClipRect. However, with_clip_rect_intersect sets the `intersect` argument of ImDrawList_PushClipRect to `true`.
This commit defines a new enum: `ImMouseButton`, which is used in the public-facing API of mouse-related methods. The discriminant values of the ImMouseButton enum are the value used internally by ImGui to represent the buttons of the mouse.
…endering show_example_app_custom_rendering is implemented exactly as it is in the original Dear ImGui in C++. The result should be the same. The only difference is that `DragFloat`, used to control the size of the drawings, is not implement as of now. This example demonstrates how the custom drawing API can be used.
…canvas Include drawing canvas example into show_example_app_custom_rendering. The example contains now everything included in the original C++ example provided with dear imgui.
As channels_split may be difficult to understand, this commit adds a simple example of its usage.
The test was failing because no `main` function was defined, as explained in the rustdoc documentation [1]. Add the "no_run" flag. This way, there is no attempt to run the test code. Only compile checks are done. Thus defining a `main` function is unnecessary. Moreover "export crate imgui" is implicit, so removed. [1] https://doc.rust-lang.org/stable/rustdoc/documentation-tests.html#pre-processing-examples.
For convenience.
At run time, the environment checks that at most one instance of WindowDrawList exists using a static boolean: WINDOW_DRAW_LIST_LOADED. If two WindowDrawList could exist at the same time, there would be several instances of the same `*mut ImDrawList`, which could lead to unfathomable bad things. When a WindowDrawList is created, WINDOW_DRAW_LIST_LOADED is set to true. And when it is dropped, WINDOW_DRAW_LIST_LOADED is set to false. Creating a new WindowDrawList while WINDOW_DRAW_LIST_LOADED is true causes a panic. AtomicBool could have been used instead of a bool for WINDOW_DRAW_LIST_LOADED. Though it allows to avoid the use of `unsafe { }`, the construct we are doing is already inherently unsafe. WindowDrawList and Ui are !Send and !Sync, so they cannot anyway be shared among threads. So we'd better be explicit and use an unsafe block with a normal bool.
👍 |
Rebase done. I found what seemed to be a bug while rebasing. The bug does not seem related to this PR. I opened a different issue: #115 |
What's the status on this? This would be useful for my use case too. |
Since there are no mutable borrows in the API, couldn't we remove all draw functions from So, you would use it like this: let draw_list = ui.get_window_draw_list();
draw_list.channels_split(2, |channels| {
const RADIUS: f32 = 100.0;
let canvas_pos = ui.get_cursor_screen_pos();
channels.set_current(1);
draw_list
.add_line(
canvas_pos,
[canvas_pos.0 + RADIUS, canvas_pos.1 + RADIUS],
RED,
)
.thickness(5.0)
.build();
channels.set_current(0);
let center = (canvas_pos.0 + RADIUS, canvas_pos.1 + RADIUS);
draw_list
.add_circle(center, RADIUS, WHITE)
.thickness(10.0)
.num_segments(50)
.build();
}); This approach makes PS. I've noticed some lifetime issues that we need to think about, but let's first focus on the basic API. |
Your proposition is a great idea! Thanks! Going into that direction tremendously reduces the complexity of the code. What are the lifetime issues that you are hinting at? |
It seems that a draw list is conceptually tied to a window, but let draw_list;
ui.window(im_str!("Uh oh"))
.build(|| {
draw_list = ui.get_window_draw_list();
// lifetime says it's ok for draw_list to outlive the corresponding window
});
draw_list.add_line(...); // ???
ui.window(im_str!("Uh oh 2"))
.build(|| {
draw_list.add_line(...); // ???
}); |
Thanks. I will investigate that what happens if a If this is indeed an issue, we could imagine getting the draw list via a ui.window(im_str!("Uh oh"))
.build_with_draw_list(|draw_list| {
// use draw_list
// draw list is destroyed when it goes out of scope
});
This would as well protect us from having several instances of Are there cases where we could draw outside a window? I guess no. |
Since there are no mutable borrows in the WindowDrawList API, we can remove all draw functions from ChannelsSplit and just use the same `draw_list' all the time, as shown in the `WindowDrawList::channels_split` example. This approach makes the `DrawAPI` trait pointless so it can be removed, and ChannelsSplit is reduced to a type with just one function: `set_current`.
I pushed the code with the approach you proposed that gets rid of Regarding the lifetime issues, I did the following experiment: // Get draw_list in the context of the "Debug" window:
let draw_list = ui.get_window_draw_list();
// Make another "Uh oh!" window
ui.window(im_str!("Uh oh!"))
.build(|| {
// use draw_list inside the context of the "Uh oh!" window
}); As you can see, the content is drawn on the current window at the time Maybe we should just ignore these life time issues and just precise in the documentation that the draw_list instance returned by |
IMHO, the closure is definitely a great fit for this feature. The method name can be a bike shed, but it seems like if you want a |
There actually is (sort of), because imgui adds a debug window by default if you don't explicitly create a window yourself. So it's ok to get a drawlist without ever creating a window. But this is a bit of a special case that we don't necessarily have to support. Anyway...I think we can go without the closure for now, so I'll merge this pull request. If this approach turns out to be unsafe, we can change the API to the closure version later. |
This is an attempt at wrapping ImGui's custom drawing API.
The custom rendering example provided with ImGui has been completely implemented. The result is as below:
Each patch should be self-explanatory and can be reviewed separately.
This collection of patches can be split into 4 sections.
window_draw_list.rs
, the API itselfUi
. These functions will be used for the examples.show_example_app_custom_rendering
intest_window_impl
test_drawing_channels_split
, showing how channels_split can be used. => The example can be removed from this PR if you do not like it.This is an improvement over #102. With support for safe use of
channels_split
and generics to allow a more convenient use of the API.