Skip to content

Commit

Permalink
Implement basic Makie integration
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesWrigley committed Jul 29, 2024
1 parent 37319a8 commit e4c0af3
Show file tree
Hide file tree
Showing 8 changed files with 349 additions and 4 deletions.
3 changes: 3 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,18 @@ CSyntax = "ea656a56-6ca6-5dda-bba5-7b6963a5f74c"

[weakdeps]
GLFW = "f7f18e0c-5ee9-5ccd-a5bf-e8befd85ed98"
GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a"
ModernGL = "66fc600b-dfda-50eb-8b99-91cfa97b1301"

[extensions]
GlfwOpenGLBackend = ["GLFW", "ModernGL"]
MakieIntegration = ["GLFW", "ModernGL", "GLMakie"]

[compat]
CEnum = "0.4, 0.5"
CImGuiPack_jll = "0.3.0"
CSyntax = "0.4"
GLFW = "3"
GLMakie = "0.10.5"
ModernGL = "1"
julia = "1.9"
2 changes: 1 addition & 1 deletion docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ makedocs(;
size_threshold=500000,
size_threshold_warn=400000
),
pages=["index.md", "api.md", "backends.md", "changelog.md"]
pages=["index.md", "api.md", "backends.md", "makie.md", "changelog.md"]
)

deploydocs(;
Expand Down
2 changes: 1 addition & 1 deletion docs/src/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ imgui_version
```@autodocs
Modules = [CImGui]
Order = [:constant, :function, :type]
Filter = t -> nameof(t) ∉ (:imgui_version, :render, :set_backend)
Filter = t -> nameof(t) ∉ (:imgui_version, :render, :set_backend, :MakieFigure)
```
86 changes: 86 additions & 0 deletions examples/makie_demo.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import GLFW
using GLMakie
import GLMakie.Makie as Makie
import CImGui as ig
import CImGui.CSyntax: @c
import ModernGL as gl


ig.set_backend(:GlfwOpenGL3)

function generate_data(type::Symbol=:random, N=1000)
if type === :random
[Point2f(i, rand()) for i in 1:N]
end
end

function HelpMarker(msg)
ig.TextDisabled("(?)");

if ig.IsItemHovered() && ig.BeginTooltip()
ig.PushTextWrapPos(ig.GetFontSize() * 35.0);
ig.TextUnformatted(msg);
ig.PopTextWrapPos();
ig.EndTooltip();
end
end

function makie_demo(; engine=nothing)
# Create a plot
f = Figure()
scene = Makie.get_scene(f)
ax1 = Axis(f[1, 1]; title="Random data")
data = Observable(generate_data())
lines!(ax1, data)
data2 = Observable(generate_data())
ax2 = Axis(f[2, 1])
lines!(ax2, data2)

ctx = ig.CreateContext()
io = ig.GetIO()
io.ConfigFlags = unsafe_load(io.ConfigFlags) | ig.lib.ImGuiConfigFlags_DockingEnable
io.ConfigFlags = unsafe_load(io.ConfigFlags) | ig.lib.ImGuiConfigFlags_ViewportsEnable

ax1_tight_spacing = true
auto_resize_x = true
auto_resize_y = false

# Start the GUI
ig.render(ctx; engine, window_size=(1280, 760), window_title="ImGui Window") do
ig.Begin("Makie demo")

if ig.Button("Random data")
data[] = generate_data()
end

@c ig.Checkbox("Ax1 tight tick spacing", &ax1_tight_spacing)
ig.SameLine()
HelpMarker("""
Try zooming into the top plot, if this option is disabled
the axis will not resize itself to stop clipping the tick labels on the Y axis.
""")

@c ig.Checkbox("Auto resize X", &auto_resize_x)
ig.SameLine()
@c ig.Checkbox("Auto resize Y", &auto_resize_y)

if ig.MakieFigure("plot", f; auto_resize_x, auto_resize_y)
if ax1_tight_spacing
Makie.tight_ticklabel_spacing!(ax1)
end

Makie.tight_ticklabel_spacing!(ax2)
end

ig.Text("Mouse position in scene: $(scene.events.mouseposition[])")
ig.Text("Scene size: $(size(scene))")
ig.Text("Mouse position in ax1: $(mouseposition(ax1))")

ig.End()
end
end

# Run automatically if the script is launched from the command-line
if !isempty(Base.PROGRAM_FILE)
makie_demo()
end
201 changes: 201 additions & 0 deletions ext/MakieIntegration.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
module MakieIntegration

import CImGui as ig
import ModernGL as gl
import GLFW
import GLMakie
import GLMakie.Makie as Makie


# Represents a single Figure to be shown as an ImGui image texture
struct ImMakieWindow
glfw_window::GLFW.Window # Only needed for supporting GLMakie requirements
end

struct ImMakieFigure
figure::GLMakie.Figure
screen::GLMakie.Screen{ImMakieWindow}
end

const makie_context = Dict{ig.ImGuiID, ImMakieFigure}()

function destroy_context()
for imfigure in values(makie_context)
empty!(imfigure.figure)
end

empty!(makie_context)
end

Base.isopen(window::ImMakieWindow) = isopen(window.glfw_window)

# Specialization of Base.resize(::GLMakie.Screen, ::Int, ::Int) to not do GLFW things
# See: https://github.com/MakieOrg/Makie.jl/blob/4c4eaa1f3a7f7b3777a4b8ab38388a48c0eee6ce/GLMakie/src/screen.jl#L664
function Base.resize!(screen::GLMakie.Screen{ImMakieWindow}, w::Int, h::Int)
fbscale = screen.px_per_unit[]
fbw, fbh = round.(Int, fbscale .* (w, h))
resize!(screen.framebuffer, fbw, fbh)
end

# Not sure if this is correct, it should probably be the figure size
GLMakie.framebuffer_size(window::ImMakieWindow) = GLMakie.framebuffer_size(window.glfw_window)

# ShaderAbstractions support
GLMakie.ShaderAbstractions.native_switch_context!(x::ImMakieWindow) = GLFW.MakeContextCurrent(x.glfw_window)
GLMakie.ShaderAbstractions.native_context_alive(x::ImMakieWindow) = GLFW.is_initialized() && x.glfw_window != C_NULL

# This is called by GLMakie.display() to set up connections to GLFW for
# mouse/keyboard events etc. We disable it explicitly because we deliver the
# events in an immediate-mode fashion within MakieFigure().
GLMakie.connect_screen(::GLMakie.Scene, ::GLMakie.Screen{ImMakieWindow}) = nothing

# Modified copy of apply_config!() with all GLFW/renderloop things removed
# See: https://github.com/MakieOrg/Makie.jl/blob/4c4eaa1f3a7f7b3777a4b8ab38388a48c0eee6ce/GLMakie/src/screen.jl#L343
function apply_config!(screen::GLMakie.Screen, config::GLMakie.ScreenConfig)
screen.scalefactor[] = !isnothing(config.scalefactor) ? config.scalefactor : 1
screen.px_per_unit[] = !isnothing(config.px_per_unit) ? config.px_per_unit : screen.scalefactor[]
function replace_processor!(postprocessor, idx)
fb = screen.framebuffer
shader_cache = screen.shader_cache
post = screen.postprocessors[idx]
if post.constructor !== postprocessor
GLMakie.destroy!(screen.postprocessors[idx])
screen.postprocessors[idx] = postprocessor(fb, shader_cache)
end

nothing
end

replace_processor!(config.ssao ? GLMakie.ssao_postprocessor : GLMakie.empty_postprocessor, 1)
replace_processor!(config.oit ? GLMakie.OIT_postprocessor : GLMakie.empty_postprocessor, 2)
replace_processor!(config.fxaa ? GLMakie.fxaa_postprocessor : GLMakie.empty_postprocessor, 3)

# Set the config
screen.config = config
end

function ig.MakieFigure(title_id::String, f::GLMakie.Figure; auto_resize_x=true, auto_resize_y=false)
ig.PushID(title_id)
id = ig.GetID(title_id)

if !haskey(makie_context, id)
# The code in this block combines the screen creation code from
# GLMakie.empty_screen() and the screen configuration code from
# GLMakie.Screen().
# See:
# - https://github.com/MakieOrg/Makie.jl/blob/4c4eaa1f3a7f7b3777a4b8ab38388a48c0eee6ce/GLMakie/src/screen.jl#L223
# - https://github.com/MakieOrg/Makie.jl/blob/4c4eaa1f3a7f7b3777a4b8ab38388a48c0eee6ce/GLMakie/src/screen.jl#L388
window = ig.current_window()
makie_window = ImMakieWindow(window)
GLMakie.ShaderAbstractions.switch_context!(makie_window)
shader_cache = GLMakie.GLAbstraction.ShaderCache(makie_window)

fb = GLMakie.GLFramebuffer((10, 10))
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0) # Have to unbind after creating the framebuffer
postprocessors = [
GLMakie.empty_postprocessor(),
GLMakie.empty_postprocessor(),
GLMakie.empty_postprocessor(),
GLMakie.to_screen_postprocessor(fb, shader_cache)
]

screen = GLMakie.Screen(makie_window, shader_cache, fb,
nothing, false,
nothing,
Dict{WeakRef, GLMakie.ScreenID}(),
GLMakie.ScreenArea[],
Tuple{GLMakie.ZIndex, GLMakie.ScreenID, GLMakie.RenderObject}[],
postprocessors,
Dict{UInt64, GLMakie.RenderObject}(),
Dict{UInt32, GLMakie.AbstractPlot}(),
true)
config = Makie.merge_screen_config(GLMakie.ScreenConfig, Dict{Symbol, Any}())
apply_config!(screen, config)

makie_context[id] = ImMakieFigure(f, screen)
scene = Makie.get_scene(f)
scene.events.window_open[] = true
display(screen, f)
end

imfigure = makie_context[id]
scene = Makie.get_scene(f)

region_avail = ig.GetContentRegionAvail()
region_size = (Int(region_avail.x), Int(region_avail.y))
scene_size = size(scene)
new_size = (auto_resize_x ? region_size[1] : scene_size[1],
auto_resize_y ? region_size[2] : scene_size[2])

if scene_size != new_size && all(new_size .> 0)
@debug "resizing $(scene_size) -> $(new_size)"
scene.events.window_area[] = GLMakie.Rect2i(0, 0, Int(new_size[1]), Int(new_size[2]))
resize!(f, new_size[1], new_size[2])
end

do_render = GLMakie.requires_update(imfigure.screen)
if do_render
@debug "rendering"
GLMakie.render_frame(imfigure.screen)
end

# The color texture is what we need to render as an image. We add it to the
# drawlist and then create an InvisibleButton of the same size to create a
# space in the layout that can respond to key presses and clicks etc (which
# a regular Image() can't do).
color_buffer = imfigure.screen.framebuffer.buffers[:color]
drawlist = ig.GetWindowDrawList()
cursor_pos = ig.GetCursorScreenPos()
image_size = size(color_buffer)
ig.AddImage(drawlist,
Ptr{Cvoid}(Int(color_buffer.id)),
cursor_pos,
(cursor_pos.x + image_size[1], cursor_pos.y + image_size[2]),
(0, 1), (1, 0))
ig.InvisibleButton("figure_image", size(color_buffer))

# Update the scene events
if scene.events.hasfocus[] != ig.IsItemHovered()
scene.events.hasfocus[] = ig.IsItemHovered()
end
if scene.events.entered_window[] != ig.IsItemHovered()
scene.events.entered_window[] = ig.IsItemHovered()
end

io = ig.GetIO()
if ig.IsItemHovered()
pos = ig.GetMousePos()
cursor_pos = ig.GetCursorScreenPos()
item_spacing = unsafe_load(ig.GetStyle().ItemSpacing.y)
new_pos = (pos.x - cursor_pos.x, abs(pos.y - cursor_pos.y) - item_spacing)
if new_pos != scene.events.mouseposition[]
scene.events.mouseposition[] = new_pos
end

for (igkey, makiekey) in ((ig.ImGuiKey_MouseLeft, Makie.Mouse.left),
(ig.ImGuiKey_MouseRight, Makie.Mouse.right))
if ig.IsKeyPressed(igkey)
scene.events.mousebutton[] = Makie.MouseButtonEvent(makiekey, Makie.Mouse.press)
elseif ig.IsKeyReleased(igkey)
scene.events.mousebutton[] = Makie.MouseButtonEvent(makiekey, Makie.Mouse.release)
end
end

wheel_y = unsafe_load(io.MouseWheel)
wheel_x = unsafe_load(io.MouseWheelH)
if (wheel_x, wheel_y) != scene.events.scroll[]
scene.events.scroll[] = (wheel_x, wheel_y)
end

end

ig.PopID()

return do_render
end

function __init__()
ig.atrenderexit(destroy_context)
end

end
42 changes: 42 additions & 0 deletions src/CImGui.jl
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,48 @@ include("wrapper.jl")

const IMGUI_VERSION = unsafe_string(GetVersion())

# This is implemented by the MakieIntegration extension but we document it here
# so that we don't have to install GLMakie to build the docs.
"""
MakieFigure(id::String, f::GLMakie.Figure; auto_resize_x=true, auto_resize_y=false)
Display a Makie figure in ImGui. See `examples/makie_demo.jl` for an example of
how to use it. This supports all the interaction features in GLMakie:
- Scrolling to zoom
- Rectangle select to zoom
- RMB to pan
Note that scrolling to zoom will also cause the ImGui window to scroll, which
can be annoying. This may be fixed in the future by using some other key
combination for scrolling to zoom.
These are the [interaction
events](https://docs.makie.org/stable/explanations/events#The-Events-struct)
that are wired up and can be used:
- `hasfocus`
- `entered_window`
- `mousebutton`
- `mouseposition`
Known issues:
- Changing tick labels don't trigger the scene to be re-layouted, causing them
to be clipped if the labels change width. See `examples/makie_demo.jl` for an
example workaround using `Makie.tight_ticklabel_spacing!()`.
- The theming doesn't match the ImGui theme so plots look quite out of place by
default.
- Mouse events aren't delivered unless the mouse is hovered over the figure, so
dragging the mouse from within the figure to somewhere outside the figure will
keep the old mouse state. e.g. if you're RMB panning and the mouse goes
outside the figure, when it enters the figure again panning will resume even
the RMB was released.
!!! warning
This is very experimental, you will almost definitely encounter bugs (and if
so please submit an issue/PR). We don't consider this covered under semver
yet so there may be breaking changes in minor releases.
"""
function MakieFigure end

## Backends

const _exit_handlers = Function[]
Expand Down
1 change: 1 addition & 0 deletions test/Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[deps]
CImGui = "5d785b6c-b76f-510e-a07c-3070796c7e87"
GLFW = "f7f18e0c-5ee9-5ccd-a5bf-e8befd85ed98"
GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a"
ImGuiTestEngine = "464e2eba-0a11-4ed3-b274-413caa1a1cca"
ModernGL = "66fc600b-dfda-50eb-8b99-91cfa97b1301"
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
Expand Down
Loading

0 comments on commit e4c0af3

Please sign in to comment.