diff --git a/docs/src/_changelog.md b/docs/src/_changelog.md index e5ef133..308b41c 100644 --- a/docs/src/_changelog.md +++ b/docs/src/_changelog.md @@ -6,6 +6,14 @@ CurrentModule = CImGui This documents notable changes in CImGui.jl. The format is based on [Keep a Changelog](https://keepachangelog.com). +## Unreleased + +### Changed +- Modified [`MakieFigure`](@ref) to not rely on the `id` for its internal state + so that it's safe to call it on different figures with the same ID (not at + the same time of course). Also implemented [`delete_figure!()`](@ref) to allow + for cleaning up the internal state. + ## [v6.2.0] - 2025-09-27 ### Fixed diff --git a/docs/src/api.md b/docs/src/api.md index 7c0c9f5..8b7267e 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -17,5 +17,5 @@ imgui_version ```@autodocs Modules = [CImGui] Order = [:constant, :function, :type] -Filter = t -> nameof(t) ∉ (:imgui_version, :render, :set_backend, :MakieFigure) +Filter = t -> nameof(t) ∉ (:imgui_version, :render, :set_backend, :MakieFigure, :delete_figure!) ``` diff --git a/docs/src/makie.md b/docs/src/makie.md index 2fc4e9b..618bdbc 100644 --- a/docs/src/makie.md +++ b/docs/src/makie.md @@ -39,4 +39,5 @@ None of this is thread-safe. Here's what you can and can't do: ```@docs MakieFigure +delete_figure! ``` diff --git a/examples/makie_demo.jl b/examples/makie_demo.jl index ad94eb5..b9307f0 100644 --- a/examples/makie_demo.jl +++ b/examples/makie_demo.jl @@ -74,7 +74,13 @@ function makie_demo(; engine=nothing, spawn=1) @c ig.Checkbox("Stream data", &stream_data) if stream_data && isnothing(data_task) - data_task = errormonitor(Threads.@spawn live_data(data)) + # We have to launch the task on the same thread because it modifies + # observables, which will end up modifying thread-unsafe OpenGL + # things. Hence we use @async instead of Threads.@spawn. If you're + # plotting live data being read from a different thread, consider + # putting all the updates into a channel that your render function + # empties within the render loop. + data_task = errormonitor(@async live_data(data)) end ig.MakieFigure("plot", f; auto_resize_x, auto_resize_y, tooltip, stats) diff --git a/ext/MakieIntegration.jl b/ext/MakieIntegration.jl index b884dde..542a5d3 100644 --- a/ext/MakieIntegration.jl +++ b/ext/MakieIntegration.jl @@ -6,6 +6,7 @@ import Statistics: mean, std import CImGui as ig import ModernGL as gl import GLFW + import GLMakie import GLMakie.Makie as Makie @@ -30,11 +31,11 @@ mutable struct ImMakieFigure end end -const makie_context = Dict{ig.ImGuiID, ImMakieFigure}() +const makie_context = IdDict{GLMakie.Figure, ImMakieFigure}() function destroy_context() - for imfigure in values(makie_context) - empty!(imfigure.figure) + for f in keys(makie_context) + empty!(f) end empty!(makie_context) @@ -42,15 +43,15 @@ end Base.isopen(window::ImMakieWindow) = isopen(window.glfw_window) GLMakie.framebuffer_size(window::ImMakieWindow) = GLMakie.framebuffer_size(window.glfw_window) -GLMakie.scale_factor(window::ImMakieWindow) = GLMakie.scale_factor(window.glfw_window) -GLMakie.was_destroyed(window::ImMakieWindow) = GLMakie.was_destroyed(window.glfw_window) +GLMakie.scale_factor(window::ImMakieWindow)::Float32 = GLMakie.scale_factor(window.glfw_window) +GLMakie.was_destroyed(window::ImMakieWindow)::Bool = GLMakie.was_destroyed(window.glfw_window) GLMakie.reopen!(x::GLMakie.Screen{ImMakieWindow}) = x GLMakie.destroy!(::ImMakieWindow) = nothing GLMakie.set_screen_visibility!(::GLMakie.Screen{ImMakieWindow}, ::Bool) = nothing # 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 +GLMakie.ShaderAbstractions.native_context_alive(x::ImMakieWindow)::Bool = 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 @@ -129,24 +130,29 @@ function draw_popup(axis) end end +function ig.delete_figure!(f::GLMakie.Figure) + delete!(makie_context, f) + return nothing +end + function ig.MakieFigure(title_id::String, f::GLMakie.Figure; auto_resize_x=true, auto_resize_y=false, tooltip=true, stats=false) ig.PushID(title_id) id = ig.GetID(title_id) - if !haskey(makie_context, id) + if !haskey(makie_context, f) window = ig.current_window() makie_window = ImMakieWindow(window) screen = GLMakie.Screen(; window=makie_window, start_renderloop=false) - makie_context[id] = ImMakieFigure(f, screen) + makie_context[f] = ImMakieFigure(f, screen) scene = Makie.get_scene(f) scene.events.window_open[] = true display(screen, f) end - imfigure = makie_context[id] + imfigure = makie_context[f] scene = Makie.get_scene(f) region_avail = ig.GetContentRegionAvail() diff --git a/src/CImGui.jl b/src/CImGui.jl index 180d412..3be2328 100644 --- a/src/CImGui.jl +++ b/src/CImGui.jl @@ -93,8 +93,8 @@ function pintask!(task::Task, tid::Integer) end end -# 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. +# The Makie functions are 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, @@ -137,6 +137,18 @@ Known issues: """ function MakieFigure end +""" + delete_figure!(f::GLMakie.Figure) + +Delete a figure from the internal CImGui state. + +When [`MakieFigure()`](@ref) is called it will create some internal state for +the figure. If your application ever 'deletes' a figure (i.e. hiding it so it +shouldn't be displayed again) then you should call this function to clear the +internal state and allow the figure to be GC'd. +""" +function delete_figure! end + ## Backends const _exit_handlers = Function[] @@ -272,9 +284,10 @@ function __init__() if isempty(methods(exc.f)) print(io, "\nrender() cannot be called yet. You must load the packages for supported backends, e.g. `import ModernGL, GLFW` for the GLFW/OpenGL3 backend.") end - elseif exc.f === MakieFigure + elseif exc.f === MakieFigure || exc.f === delete_figure! + name = nameof(exc.f) if isempty(methods(exc.f)) - print(io, "\nMakieFigure() cannot be called yet, you must load GLMakie with e.g. `import GLMakie`.") + print(io, "\n$(name)() cannot be called yet, you must load GLMakie with e.g. `import GLMakie`.") end end end