|
| 1 | +""" |
| 2 | +Makie backend handler for LineCableModels. |
| 3 | +
|
| 4 | +Design goals: |
| 5 | +- Precompile in any environment (never touch GL/WGL at load-time). |
| 6 | +- Default to CairoMakie as a safe backend. |
| 7 | +- Offer a single `set_backend!` API (no user `using` needed). |
| 8 | +- In headless (e.g., Literate → Documenter), display PNG inline. |
| 9 | +
|
| 10 | +How to wire it (minimal integration): |
| 11 | +
|
| 12 | +1) Add this helper to your package module once: |
| 13 | +
|
| 14 | + include(joinpath(@__DIR__, "..", "@INPROGRESS", "makie_backend_alt.jl")) |
| 15 | + using .BackendHandler |
| 16 | +
|
| 17 | +2) In Makie-based preview entrypoints, ensure a backend is active: |
| 18 | +
|
| 19 | + # Use caller keyword `backend::Union{Nothing,Symbol}` if you keep it |
| 20 | + BackendHandler.ensure_backend!(backend === nothing ? :cairo : backend) |
| 21 | +
|
| 22 | +3) For GL interactive windows without directly referencing GLMakie: |
| 23 | +
|
| 24 | + if BackendHandler.current_backend_symbol() == :gl |
| 25 | + if (scr = BackendHandler.gl_screen("Title")) !== nothing |
| 26 | + display(scr, fig) |
| 27 | + else |
| 28 | + display(fig) |
| 29 | + end |
| 30 | + else |
| 31 | + display(fig) |
| 32 | + end |
| 33 | +
|
| 34 | +4) For docs/headless builds (Literate/Documenter) PNG inline display: |
| 35 | +
|
| 36 | + # in your final display branch |
| 37 | + BackendHandler.renderfig(fig) |
| 38 | +
|
| 39 | +5) Let users select backends interactively (no extra imports): |
| 40 | +
|
| 41 | + BackendHandler.set_backend!(:gl) # or :wgl, :cairo |
| 42 | +
|
| 43 | +Notes: |
| 44 | +- No `@eval import` anywhere; backends are loaded via `Base.require` using PkgId. |
| 45 | +- Calls into newly loaded modules go through `Base.invokelatest` to avoid |
| 46 | + world-age issues. |
| 47 | +""" |
| 48 | +module BackendHandler |
| 49 | + |
| 50 | +using Makie |
| 51 | +using UUIDs |
| 52 | +using ..Utils: is_headless |
| 53 | + |
| 54 | +export set_backend!, |
| 55 | + ensure_backend!, current_backend_symbol, |
| 56 | + backend_available, gl_screen, renderfig, |
| 57 | + next_fignum, reset_fignum! |
| 58 | + |
| 59 | +# --------------------------------------------------------------------------- |
| 60 | +# Backend registry |
| 61 | +# --------------------------------------------------------------------------- |
| 62 | + |
| 63 | +const _BACKENDS = Dict{Symbol, Tuple{UUID, String}}( |
| 64 | + :cairo => (UUID("13f3f980-e62b-5c42-98c6-ff1f3baf88f0"), "CairoMakie"), |
| 65 | + :gl => (UUID("e9467ef8-e4e7-5192-8a1a-b1aee30e663a"), "GLMakie"), |
| 66 | + :wgl => (UUID("276b4fcb-3e11-5398-bf8b-a0c2d153d008"), "WGLMakie"), |
| 67 | +) |
| 68 | + |
| 69 | +_pkgid(sym::Symbol) = begin |
| 70 | + tup = get(_BACKENDS, sym, nothing) |
| 71 | + tup === nothing && throw( |
| 72 | + ArgumentError("Unknown backend: $(sym). Valid: $(collect(keys(_BACKENDS)))"), |
| 73 | + ) |
| 74 | + Base.PkgId(tup[1], tup[2]) |
| 75 | +end |
| 76 | + |
| 77 | +"""Return true if a backend package exists in the environment.""" |
| 78 | +backend_available(backend::Symbol) = Base.find_package(last(_BACKENDS[backend])) !== nothing |
| 79 | + |
| 80 | +# Track the last activated backend symbol (separate from Makie.internal state) |
| 81 | +const _active_backend = Base.RefValue{Symbol}(:none) |
| 82 | + |
| 83 | +# --------------------------------------------------------------------------- |
| 84 | +# Activation core (lazy, world-age safe) |
| 85 | +# --------------------------------------------------------------------------- |
| 86 | + |
| 87 | +function _activate_backend!(backend::Symbol; allow_interactive_in_headless::Bool = false) |
| 88 | + if is_headless() && backend != :cairo && !allow_interactive_in_headless |
| 89 | + @warn "Headless environment: forcing :cairo instead of $(backend)." |
| 90 | + return _activate_backend!(:cairo; allow_interactive_in_headless) |
| 91 | + end |
| 92 | + |
| 93 | + pid = _pkgid(backend) |
| 94 | + # Load the backend module into Julia's module world; idempotent if already loaded |
| 95 | + mod = Base.require(pid) |
| 96 | + # Call `activate!` safely with world-age correctness |
| 97 | + Base.invokelatest(getproperty(mod, :activate!)) |
| 98 | + _active_backend[] = backend |
| 99 | + return backend |
| 100 | +end |
| 101 | + |
| 102 | +"""Ensure a backend is active. Defaults to :cairo the first time.""" |
| 103 | +function ensure_backend!(backend::Union{Nothing, Symbol} = nothing) |
| 104 | + if backend === nothing |
| 105 | + return _active_backend[] == :none ? _activate_backend!(:cairo) : _active_backend[] |
| 106 | + else |
| 107 | + return set_backend!(backend) |
| 108 | + end |
| 109 | +end |
| 110 | + |
| 111 | +"""Activate a specific backend (:cairo, :gl, :wgl). |
| 112 | +
|
| 113 | +In headless, :gl/:wgl requests fall back to :cairo unless `force=true`. |
| 114 | +No `using` required by callers. |
| 115 | +""" |
| 116 | +function set_backend!(backend::Symbol; force::Bool = false) |
| 117 | + haskey(_BACKENDS, backend) || throw( |
| 118 | + ArgumentError("Unknown backend: $(backend). Valid: $(collect(keys(_BACKENDS)))"), |
| 119 | + ) |
| 120 | + if backend != :cairo && !backend_available(backend) |
| 121 | + @warn "Backend $(last(_BACKENDS[backend])) not in environment; using :cairo." |
| 122 | + return _activate_backend!(:cairo) |
| 123 | + end |
| 124 | + return _activate_backend!(backend; allow_interactive_in_headless = force) |
| 125 | +end |
| 126 | + |
| 127 | +"""Symbol of the current Makie backend (:cairo, :gl, :wgl, :unknown, :none).""" |
| 128 | +function current_backend_symbol() |
| 129 | + try |
| 130 | + nb = nameof(Makie.current_backend()) |
| 131 | + nb === :CairoMakie && return :cairo |
| 132 | + nb === :GLMakie && return :gl |
| 133 | + nb === :WGLMakie && return :wgl |
| 134 | + return :unknown |
| 135 | + catch |
| 136 | + return :none |
| 137 | + end |
| 138 | +end |
| 139 | + |
| 140 | +"""Create a GLMakie screen if GL backend is active; otherwise return nothing.""" |
| 141 | +function gl_screen(title::AbstractString) |
| 142 | + if current_backend_symbol() == :gl |
| 143 | + mod = Base.require(_pkgid(:gl)) |
| 144 | + ctor = getproperty(mod, :Screen) |
| 145 | + return Base.invokelatest(ctor; title = String(title)) |
| 146 | + end |
| 147 | + return nothing |
| 148 | +end |
| 149 | + |
| 150 | +"""Display a figure appropriately in headless docs or interactive sessions. |
| 151 | +
|
| 152 | +- Headless: returns `DisplayAs.Text(DisplayAs.PNG(fig))` if `DisplayAs` exists; |
| 153 | + otherwise attempts to rasterize via CairoMakie and returns nothing. |
| 154 | +- Interactive: calls `display(fig)` and returns its result. |
| 155 | +""" |
| 156 | +function renderfig(fig) |
| 157 | + if is_headless() |
| 158 | + try |
| 159 | + D = Base.require( |
| 160 | + Base.PkgId(UUID("0b91fe84-8a4c-11e9-3e1d-67c38462b6d6"), "DisplayAs"), |
| 161 | + ) |
| 162 | + return D.Text(D.PNG(fig)) |
| 163 | + catch |
| 164 | + try |
| 165 | + ensure_backend!(:cairo) |
| 166 | + cm = Base.require(_pkgid(:cairo)) |
| 167 | + savef = getproperty(cm, :save) |
| 168 | + io = IOBuffer() |
| 169 | + Base.invokelatest(savef, io, fig) |
| 170 | + return nothing |
| 171 | + catch |
| 172 | + return nothing |
| 173 | + end |
| 174 | + end |
| 175 | + else |
| 176 | + return display(fig) |
| 177 | + end |
| 178 | +end |
| 179 | + |
| 180 | +const FIG_NO = Base.Threads.Atomic{Int}(1) |
| 181 | +next_fignum() = Base.Threads.atomic_add!(FIG_NO, 1) |
| 182 | +reset_fignum!(n::Int = 1) = (FIG_NO[] = n) |
| 183 | + |
| 184 | +# --------------------------------------------------------------------------- |
| 185 | +# Default backend at runtime load (safe: CairoMakie only) |
| 186 | +# --------------------------------------------------------------------------- |
| 187 | + |
| 188 | +function __init__() |
| 189 | + try |
| 190 | + # Only set a default if nothing looks active yet |
| 191 | + if current_backend_symbol() in (:none, :unknown) |
| 192 | + ensure_backend!(:cairo) |
| 193 | + end |
| 194 | + catch e |
| 195 | + @warn "Failed to initialize default CairoMakie" exception=(e, catch_backtrace()) |
| 196 | + end |
| 197 | +end |
| 198 | + |
| 199 | +end # module |
| 200 | + |
0 commit comments