Skip to content
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

Merged
merged 28 commits into from Apr 29, 2018
Merged

Conversation

malikolivier
Copy link
Collaborator

@malikolivier malikolivier commented Mar 30, 2018

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:

image

Each patch should be self-explanatory and can be reviewed separately.

This collection of patches can be split into 4 sections.

  • Implementation of window_draw_list.rs, the API itself
  • Add a few helper methods in Ui. These functions will be used for the examples.
  • Add show_example_app_custom_rendering in test_window_impl
  • Add another example 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.

@malikolivier
Copy link
Collaborator Author

The build is failing as below:

---- src/lib.rs - Ui<'ui>::with_window_draw_list (line 1187) stdout ----
	error[E0432]: unresolved import `imgui`
 --> src/lib.rs:1189:5
  |
4 | use imgui::*;
  |     ^^^^^ Maybe a missing `extern crate imgui;`?

thread 'rustc' panicked at 'couldn't compile the test', librustdoc/test.rs:298:13

I don't really understand what's the issue. The build succeeds locally when I run cargo test --all.

@Lymia
Copy link
Contributor

Lymia commented Apr 7, 2018

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 fn main(), (since they contain none), and the use doesn't work because imgui is in the scope of main, not the module.

Add a # fn main() { } line to the end, I guess, since you can't reasonably test this? Or a hidden main that runs the function in a imgui frame also works. Edit: Actually, removing the # extern crate imgui; line should work just as well. Doctests implicitly add one.

@malikolivier
Copy link
Collaborator Author

@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.

Copy link
Contributor

@Gekkio Gekkio left a 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 {
Copy link
Contributor

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

/// `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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Derive Eq, PartialEq, Debug

/// 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> {
Copy link
Contributor

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


/// Object implementing the custom draw API.
pub struct WindowDrawList<'ui> {
draw_list: *mut ImDrawList,
Copy link
Contributor

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.

Copy link
Collaborator Author

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.

};
}

impl_draw_list_methods!(WindowDrawList);
Copy link
Contributor

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.

Copy link
Collaborator Author

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?

Copy link
Contributor

@Gekkio Gekkio Apr 22, 2018

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 😃

Copy link
Collaborator Author

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!

}
}

/// Represents a circle about to be drawn on the window
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

circle?

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) };
Copy link
Contributor

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.

Copy link
Collaborator Author

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)
Copy link
Contributor

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

Copy link
Collaborator Author

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.

@malikolivier
Copy link
Collaborator Author

Thanks for the review, I incorporated your comments in the PR and answered to your inquiries.

@malikolivier
Copy link
Collaborator Author

@Gekkio There are some conflicts now. What is your policy regarding the solving of conflicts?
Should I commit a merge or should I rebase?

@Gekkio
Copy link
Contributor

Gekkio commented Apr 16, 2018

Personally I prefer rebases for handling conflicts in short-lived pull requests, but you can choose whichever feels easier

@malikolivier
Copy link
Collaborator Author

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.
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.
@Gekkio
Copy link
Contributor

Gekkio commented Apr 16, 2018

👍

@malikolivier
Copy link
Collaborator Author

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

@Lymia
Copy link
Contributor

Lymia commented Apr 22, 2018

What's the status on this? This would be useful for my use case too.

@Gekkio
Copy link
Contributor

Gekkio commented Apr 22, 2018

Since there are no mutable borrows in the API, couldn't we remove all draw functions from ChannelsSplit and just use the same draw list all the time?

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 DrawAPI pointless so it can be removed, and ChannelsSplit is reduced to a type with just one function: set_current (renamed from channels_set_current). All the XXX::new functions would then take WindowDrawList as an argument directly. Do you see any problems with this approach?

PS. I've noticed some lifetime issues that we need to think about, but let's first focus on the basic API.

@malikolivier
Copy link
Collaborator Author

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?

@Gekkio
Copy link
Contributor

Gekkio commented Apr 22, 2018

It seems that a draw list is conceptually tied to a window, but 'ui in WindowDrawList<'ui> is the lifetime of the current frame. So, what happens if you use a WindowDrawList after the corresponding window is gone? I don't have time to investigate this right now, but imagine code like this (doesn't actually compile but the idea is valid):

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(...); // ???
    });

@malikolivier
Copy link
Collaborator Author

malikolivier commented Apr 22, 2018

Thanks. I will investigate that what happens if a WindowDrawList is used after the corresponding window is gone, so that we can come up with a solution.

If this is indeed an issue, we could imagine getting the draw list via a Window::build_with_draw_list method which could be used as follows:

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

Ui::get_window_draw_list would then not be useful. We could delete it.

This would as well protect us from having several instances of WindowDrawList and allows us not to to have to use the dirty work-around I implemented in 454e980.

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`.
@malikolivier
Copy link
Collaborator Author

malikolivier commented Apr 24, 2018

I pushed the code with the approach you proposed that gets rid of DrawAPI.

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

"Oh uh!" window on top
"Debug" window on top

As you can see, the content is drawn on the current window at the time Ui::get_window_draw_list was called (here the "Debug" window), not on the new window (here the "Uh oh!" window).

Maybe we should just ignore these life time issues and just precise in the documentation that the draw_list instance returned by Ui::get_window_draw_list is tied to the current UI window? What do you think?

@parasyte
Copy link
Contributor

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 draw_list reference, you should be able to request one on a window, and receive it in a closure.

@Gekkio
Copy link
Contributor

Gekkio commented Apr 29, 2018

Are there cases where we could draw outside a window? I guess no.

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.

@Gekkio Gekkio merged commit 544d7de into imgui-rs:master Apr 29, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants