diff --git a/.github/workflows/glmakie.yaml b/.github/workflows/glmakie.yaml index 90114e784eb..c494ba57d05 100644 --- a/.github/workflows/glmakie.yaml +++ b/.github/workflows/glmakie.yaml @@ -44,7 +44,7 @@ jobs: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - uses: julia-actions/cache@v1 - - run: sudo apt-get update && sudo apt-get install -y xorg-dev mesa-utils xvfb libgl1 freeglut3-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxext-dev + - run: sudo apt-get update && sudo apt-get install -y xorg-dev mesa-utils xvfb libgl1 freeglut3-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxext-dev xsettingsd x11-xserver-utils - name: Install Julia dependencies shell: julia --project=monorepo {0} run: | diff --git a/CairoMakie/src/CairoMakie.jl b/CairoMakie/src/CairoMakie.jl index b5995fe1f33..195c35956b5 100644 --- a/CairoMakie/src/CairoMakie.jl +++ b/CairoMakie/src/CairoMakie.jl @@ -29,6 +29,7 @@ include("infrastructure.jl") include("utils.jl") include("primitives.jl") include("overrides.jl") +include("glmakie_integration.jl") function __init__() activate!() diff --git a/CairoMakie/src/glmakie_integration.jl b/CairoMakie/src/glmakie_integration.jl new file mode 100644 index 00000000000..9529166038d --- /dev/null +++ b/CairoMakie/src/glmakie_integration.jl @@ -0,0 +1,168 @@ +# Upstreamable code + +# TODO: make this function more efficient! +function alpha_colorbuffer(Backend, screen::MakieScreen) + img = try + scene = screen.root_scene + display(screen) + bg = scene.backgroundcolor[] + scene.backgroundcolor[] = RGBAf(0, 0, 0, 1) + b1 = Makie.colorbuffer(screen) + scene.backgroundcolor[] = RGBAf(1, 1, 1, 1) + b2 = Makie.colorbuffer(screen) + scene.backgroundcolor[] = bg + map(infer_alphacolor, b1, b2) + catch e + println("Error: something failed in alpha colorbuffer!") + rethrow(e) + finally + end + + return img +end + +function infer_alphacolor(rgb1, rgb2) + rgb1 == rgb2 && return RGBAf(rgb1.r, rgb1.g, rgb1.b, 1) + c1 = Float64.((rgb1.r, rgb1.g, rgb1.b)) + c2 = Float64.((rgb2.r, rgb2.g, rgb2.b)) + alpha = @. 1 - (c1 - c2) * -1 # ( / (0 - 1)) + meanalpha = clamp(sum(alpha) / 3, 0, 1) + meanalpha == 0 && return RGBAf(0, 0, 0, 0) + c = @. clamp((c1 / meanalpha), 0, 1) + return RGBAf(c..., meanalpha) +end + +function create_render_scene(plot::Combined; scale::Real = 1) + scene = Makie.parent_scene(plot) + w, h = Int.(scene.px_area[].widths) + + # We create a dummy scene to render to, which inherits its parent's + # transformation and camera. + # WARNING: lights, SSAO and axis do not update when the original Scene's + # attributes do. This is because they are stored as fields in the Scene + # struct, not as attributes. + render_scene = Makie.Scene( + camera = Makie.camera(scene), + lights = scene.lights, + ssao = scene.ssao, + show_axis = false, + backgroundcolor = :transparent + ) + + # continually keep the pixel area updated + on(pixelarea(scene); update = true) do px_area + Makie.resize!(render_scene, (px_area.widths .* scale)...) + end + + # link the transofrmation attributes + Makie.Observables.connect!(render_scene.transformation.transform_func, scene.transformation.transform_func) + Makie.Observables.connect!(render_scene.transformation.rotation , scene.transformation.rotation) + Makie.Observables.connect!(render_scene.transformation.scale , scene.transformation.scale) + Makie.Observables.connect!(render_scene.transformation.translation , scene.transformation.translation) + Makie.Observables.connect!(render_scene.transformation.model , scene.transformation.model) + + # push only the relevant plot to the scene + push!(render_scene, plot) + + return render_scene + +end + +""" + plot2img(backend::Makie.AbstractBackend, plot::Combined; scale::Real = 1, use_backgroundcolor = false) +""" +function plot2img(Backend, plot::Combined; scale::Real = 1, use_backgroundcolor = false) + parent = Makie.parent_scene(plot) + # obtain or create the render scene + screen_ind = findfirst(x -> x isa Backend.Screen, parent.current_screens) + render_screen = if isnothing(screen_ind) + Backend.Screen(parent; visible = false, px_per_unit = scale) + else + parent.current_screens[screen_ind] + end + + img = if use_backgroundcolor + Makie.colorbuffer(render_screen) + else # render with transparency, using the alpha-colorbuffer hack + alpha_colorbuffer(Backend, render_screen) + end + + return img +end + +# Utility function to remove rendercaches +function purge_render_cache!(sc::Scene) + haskey(scene.attributes, :_render_scenes) && delete!(scene.attributes, :_render_scenes) + purge_render_cache!.(scene.children) +end +purge_render_cache!(fig::Figure) = purge_render_cache!(fig.scene) + +# Rendering pipeline + +# This goes as follows: +# The first time a plot is encountered which has to be rasterized, +# we create the rasterization scene, and connect it to the original Scene's +# attributes. +# Then, we retrieve this scene from `plot._render_scene`, an attribute of the plot +# which we set in the previous step to contain the scene. +# This retrieval + +function draw_plot_as_image_with_backend(Backend, scene::Scene, screen::Screen, plot; scale = 1) + + # special case if CairoMakie is the backend, since + # we don't want an infinite loop. + if Backend == @__MODULE__ + draw_plot_as_image(scene, screen, plot, scale) + return + end + + w, h = Int.(scene.px_area[].widths) + + img = plot2img(Backend, plot; scale = scale, use_backgroundcolor = false) + Makie.save("hi.png", img) # cache for debugging + + surf = Cairo.CairoARGBSurface(to_uint32_color.(img)) + + Cairo.rectangle(screen.context, 0, 0, w, h) + Cairo.save(screen.context) + Cairo.scale(screen.context, w / surf.width, h / surf.height) + Cairo.set_source_surface(screen.context, surf, 0, 0) + p = Cairo.get_source(screen.context) + Cairo.pattern_set_extend(p, Cairo.EXTEND_PAD) # avoid blurry edges + Cairo.pattern_set_filter(p, Cairo.FILTER_NEAREST) + Cairo.fill(screen.context) + Cairo.restore(screen.context) + + return + +end + +function draw_scene_as_image(Backend, scene::Scene, screen::Screen; scale = 1) + w, h = Int.(scene.px_area[].widths) + + render_scene = create_render_scene(scene.plots[begin]; scale = scale) + if length(scene.plots) > 1 + push!.(Ref(render_scene), scene.plots[(begin+1):end]) + end + + img = if RGBAf(Makie.to_color(scene.backgroundcolor[])) == RGBAf(0,0,0,0) + alpha_colorbuffer(Backend, scene) + else + Makie.colorbuffer(scene) + end + + Makie.save("hi.png", img) # cache for debugging + + surf = Cairo.CairoARGBSurface(to_uint32_color.(img)) + + Cairo.rectangle(screen.context, 0, 0, w, h) + Cairo.save(screen.context) + Cairo.scale(screen.context, w / surf.width, h / surf.height) + Cairo.set_source_surface(screen.context, surf, 0, 0) + p = Cairo.get_source(screen.context) + Cairo.pattern_set_extend(p, Cairo.EXTEND_PAD) # avoid blurry edges + Cairo.pattern_set_filter(p, Cairo.FILTER_NEAREST) + Cairo.fill(screen.context) + Cairo.restore(screen.context) + +end diff --git a/CairoMakie/src/infrastructure.jl b/CairoMakie/src/infrastructure.jl index ffe6741e10d..3f21e7e6c98 100644 --- a/CairoMakie/src/infrastructure.jl +++ b/CairoMakie/src/infrastructure.jl @@ -41,9 +41,20 @@ function cairo_draw(screen::Screen, scene::Scene) # rasterize it when plotting to vector backends, by using the `rasterize` # keyword argument. This can be set to a Bool or an Int which describes # the density of rasterization (in terms of a direct scaling factor.) - if to_value(get(p, :rasterize, false)) != false && should_rasterize - draw_plot_as_image(pparent, screen, p, p[:rasterize][]) - else # draw vector + # This can also be set to a `Module` (i.e., ) + if to_value(get(p, :rasterize, false)) != false + if p.rasterize[] isa Union{Bool, Int} && should_rasterize + draw_plot_as_image(pparent, screen, p, p[:rasterize][]) + elseif p.rasterize[] isa Union{<: Module, Tuple{<: Module, Int}} + backend = p.rasterize[] isa Module ? p.rasterize[] : p.rasterize[][1] + scale = p.rasterize[] isa Module ? 1 : p.rasterize[][2] + draw_plot_as_image_with_backend(backend, pparent, screen, p; scale = scale) + else # rasterization option was not recognized, or should_rasterize + # was false and backend was not selected. + draw_plot(pparent, screen, p) + end + else # draw vector, only if a parent plot has not been rasterized + draw_plot(pparent, screen, p) end Cairo.restore(screen.context) diff --git a/GLMakie/Project.toml b/GLMakie/Project.toml index dd4074be8cb..185f43f9eac 100644 --- a/GLMakie/Project.toml +++ b/GLMakie/Project.toml @@ -27,7 +27,7 @@ Colors = "0.11, 0.12" FileIO = "1.6" FixedPointNumbers = "0.7, 0.8" FreeTypeAbstraction = "0.10" -GLFW = "3" +GLFW = "3.3" GeometryBasics = "0.4.1" Makie = "=0.19.2" MeshIO = "0.4" diff --git a/GLMakie/assets/shader/distance_shape.frag b/GLMakie/assets/shader/distance_shape.frag index df3e66e064c..18c750e5a41 100644 --- a/GLMakie/assets/shader/distance_shape.frag +++ b/GLMakie/assets/shader/distance_shape.frag @@ -25,6 +25,7 @@ uniform float stroke_width; uniform float glow_width; uniform int shape; // shape is a uniform for now. Making them a in && using them for control flow is expected to kill performance uniform vec2 resolution; +uniform float px_per_unit; uniform bool transparent_picking; flat in float f_viewport_from_u_scale; @@ -97,7 +98,9 @@ void stroke(vec4 strokecolor, float signed_distance, float width, inout vec4 col void glow(vec4 glowcolor, float signed_distance, float inside, inout vec4 color){ if (glow_width > 0.0){ - float outside = (abs(signed_distance)-stroke_width)/glow_width; + float s_stroke_width = px_per_unit * stroke_width; + float s_glow_width = px_per_unit * glow_width; + float outside = (abs(signed_distance)-s_stroke_width)/s_glow_width; float alpha = 1-outside; color = mix(vec4(glowcolor.rgb, glowcolor.a*alpha), color, inside); } @@ -145,13 +148,14 @@ void main(){ // See notes in geometry shader where f_viewport_from_u_scale is computed. signed_distance *= f_viewport_from_u_scale; - float inside_start = max(-stroke_width, 0.0); + float s_stroke_width = px_per_unit * stroke_width; + float inside_start = max(-s_stroke_width, 0.0); float inside = aastep(inside_start, signed_distance); vec4 final_color = f_bg_color; fill(f_color, image, tex_uv, inside, final_color); - stroke(f_stroke_color, signed_distance, -stroke_width, final_color); - glow(f_glow_color, signed_distance, aastep(-stroke_width, signed_distance), final_color); + stroke(f_stroke_color, signed_distance, -s_stroke_width, final_color); + glow(f_glow_color, signed_distance, aastep(-s_stroke_width, signed_distance), final_color); // TODO: In 3D, we should arguably discard fragments outside the sprite // But note that this may interfere with object picking. // if (final_color == f_bg_color) diff --git a/GLMakie/assets/shader/sprites.geom b/GLMakie/assets/shader/sprites.geom index 850b700bd20..cae60a31faf 100644 --- a/GLMakie/assets/shader/sprites.geom +++ b/GLMakie/assets/shader/sprites.geom @@ -38,6 +38,7 @@ uniform float stroke_width; uniform float glow_width; uniform int shape; // for RECTANGLE hack below uniform vec2 resolution; +uniform float px_per_unit; uniform float depth_shift; in int g_primitive_index[]; @@ -138,7 +139,7 @@ void main(void) 0.0, 1.0/vclip.w, 0.0, 0.0, 0.0, 0.0, 1.0/vclip.w, 0.0, -vclip.xyz/(vclip.w*vclip.w), 0.0); - mat2 dxyv_dxys = diagm(0.5*resolution) * mat2(d_ndc_d_clip*trans); + mat2 dxyv_dxys = diagm(0.5*px_per_unit*resolution) * mat2(d_ndc_d_clip*trans); // Now, our buffer size is expressed in viewport pixels but we get back to // the sprite coordinate system using the scale factor of the // transformation (for isotropic transformations). For anisotropic diff --git a/GLMakie/src/GLAbstraction/GLUniforms.jl b/GLMakie/src/GLAbstraction/GLUniforms.jl index 17f83cc7ec8..288b736d558 100644 --- a/GLMakie/src/GLAbstraction/GLUniforms.jl +++ b/GLMakie/src/GLAbstraction/GLUniforms.jl @@ -200,6 +200,11 @@ gl_convert(s::Vector{Matrix{T}}) where {T<:Colorant} = Texture(s) gl_convert(s::Nothing) = s +# special overloads for rasterization +gl_convert(s::Module) = nothing +gl_convert(s::Tuple{Module, Any}) = nothing + + isa_gl_struct(x::AbstractArray) = false isa_gl_struct(x::NATIVE_TYPES) = false isa_gl_struct(x::Colorant) = false diff --git a/GLMakie/src/drawing_primitives.jl b/GLMakie/src/drawing_primitives.jl index 56727ae6ebd..8650111570a 100644 --- a/GLMakie/src/drawing_primitives.jl +++ b/GLMakie/src/drawing_primitives.jl @@ -107,6 +107,7 @@ function cached_robj!(robj_func, screen, scene, x::AbstractPlot) gl_attributes[:ambient] = ambientlight.color end gl_attributes[:track_updates] = screen.config.render_on_demand + gl_attributes[:px_per_unit] = screen.px_per_unit robj = robj_func(gl_attributes) diff --git a/GLMakie/src/events.jl b/GLMakie/src/events.jl index fced12ae9af..3992d28d6fa 100644 --- a/GLMakie/src/events.jl +++ b/GLMakie/src/events.jl @@ -36,50 +36,50 @@ function Makie.disconnect!(window::GLFW.Window, ::typeof(window_open)) GLFW.SetWindowCloseCallback(window, nothing) end -function window_position(window::GLFW.Window) - xy = GLFW.GetWindowPos(window) - (xy.x, xy.y) -end +function Makie.window_area(scene::Scene, screen::Screen) + disconnect!(screen, window_area) -struct WindowAreaUpdater - window::GLFW.Window - dpi::Observable{Float64} - area::Observable{GeometryBasics.HyperRectangle{2, Int64}} -end + # TODO: Figure out which monitor the window is on and react to DPI changes + monitor = GLFW.GetPrimaryMonitor() + props = MonitorProperties(monitor) + scene.events.window_dpi[] = minimum(props.dpi) -function (x::WindowAreaUpdater)(::Nothing) - ShaderAbstractions.switch_context!(x.window) - rect = x.area[] - # TODO put back window position, but right now it makes more trouble than it helps# - # x, y = GLFW.GetWindowPos(window) - # if minimum(rect) != Vec(x, y) - # event[] = Recti(x, y, framebuffer_size(window)) - # end - w, h = GLFW.GetFramebufferSize(x.window) - if Vec(w, h) != widths(rect) - monitor = GLFW.GetPrimaryMonitor() - props = MonitorProperties(monitor) - # dpi of a monitor should be the same in x y direction. - # if not, minimum seems to be a fair default - x.dpi[] = minimum(props.dpi) - x.area[] = Recti(minimum(rect), w, h) - end - return -end + function windowsizecb(window, width::Cint, height::Cint) + area = scene.events.window_area + sf = screen.scalefactor[] -function Makie.window_area(scene::Scene, screen::Screen) - disconnect!(screen, window_area) + ShaderAbstractions.switch_context!(window) + winscale = sf / (@static Sys.isapple() ? scale_factor(window) : 1) + winw, winh = round.(Int, (width, height) ./ winscale) + if Vec(winw, winh) != widths(area[]) + area[] = Recti(minimum(area[]), winw, winh) + end + return + end + # TODO put back window position, but right now it makes more trouble than it helps + #function windowposcb(window, x::Cint, y::Cint) + # area = scene.events.window_area + # ShaderAbstractions.switch_context!(window) + # winscale = screen.scalefactor[] / (@static Sys.isapple() ? scale_factor(window) : 1) + # xs, ys = round.(Int, (x, y) ./ winscale) + # if Vec(xs, ys) != minimum(area[]) + # area[] = Recti(xs, ys, widths(area[])) + # end + # return + #end - updater = WindowAreaUpdater( - to_native(screen), scene.events.window_dpi, scene.events.window_area - ) - on(updater, screen.render_tick) + window = to_native(screen) + GLFW.SetWindowSizeCallback(window, (win, w, h) -> windowsizecb(win, w, h)) + #GLFW.SetWindowPosCallback(window, (win, x, y) -> windowposcb(win, x, y)) + windowsizecb(window, Cint.(window_size(window))...) return end function Makie.disconnect!(screen::Screen, ::typeof(window_area)) - filter!(p -> !isa(p[2], WindowAreaUpdater), screen.render_tick.listeners) + window = to_native(screen) + #GLFW.SetWindowPosCallback(window, nothing) + GLFW.SetWindowSizeCallback(window, nothing) return end function Makie.disconnect!(::GLFW.Window, ::typeof(window_area)) @@ -167,44 +167,28 @@ function Makie.disconnect!(window::GLFW.Window, ::typeof(unicode_input)) GLFW.SetCharCallback(window, nothing) end -# TODO memoise? Or to bug ridden for the small performance gain? -function retina_scaling_factor(w, fb) - (w[1] == 0 || w[2] == 0) && return (1.0, 1.0) - fb ./ w -end - -# TODO both of these methods are slow! -# ~90µs, ~80µs -# This is too slow for events that may happen 100x per frame -function framebuffer_size(window::GLFW.Window) - wh = GLFW.GetFramebufferSize(window) - (wh.width, wh.height) -end -function window_size(window::GLFW.Window) - wh = GLFW.GetWindowSize(window) - (wh.width, wh.height) -end -function retina_scaling_factor(window::GLFW.Window) - w, fb = window_size(window), framebuffer_size(window) - retina_scaling_factor(w, fb) -end - -function correct_mouse(window::GLFW.Window, w, h) - ws, fb = window_size(window), framebuffer_size(window) - s = retina_scaling_factor(ws, fb) - (w * s[1], fb[2] - (h * s[2])) +function correct_mouse(screen::Screen, w, h) + nw = to_native(screen) + sf = screen.scalefactor[] / (@static Sys.isapple() ? scale_factor(nw) : 1) + _, winh = window_size(nw) + @static if Sys.isapple() + return w, (winh / sf) - h + else + return w / sf, (winh - h) / sf + end end struct MousePositionUpdater - window::GLFW.Window + screen::Screen mouseposition::Observable{Tuple{Float64, Float64}} hasfocus::Observable{Bool} end function (p::MousePositionUpdater)(::Nothing) !p.hasfocus[] && return - x, y = GLFW.GetCursorPos(p.window) - pos = correct_mouse(p.window, x, y) + nw = to_native(p.screen) + x, y = GLFW.GetCursorPos(nw) + pos = correct_mouse(p.screen, x, y) if pos != p.mouseposition[] @print_error p.mouseposition[] = pos # notify!(e.mouseposition) @@ -221,7 +205,7 @@ which is not in scene coordinates, with the upper left window corner being 0 function Makie.mouse_position(scene::Scene, screen::Screen) disconnect!(screen, mouse_position) updater = MousePositionUpdater( - to_native(screen), scene.events.mouseposition, scene.events.hasfocus + screen, scene.events.mouseposition, scene.events.hasfocus ) on(updater, screen.render_tick) return diff --git a/GLMakie/src/glwindow.jl b/GLMakie/src/glwindow.jl index d643f609c5b..3ccf5ca1ef0 100644 --- a/GLMakie/src/glwindow.jl +++ b/GLMakie/src/glwindow.jl @@ -124,15 +124,13 @@ function GLFramebuffer(fb_size::NTuple{2, Int}) ) end -function Base.resize!(fb::GLFramebuffer, window_size) - ws = Int.((window_size[1], window_size[2])) - if ws != size(fb) && all(x-> x > 0, window_size) - for (name, buffer) in fb.buffers - resize_nocopy!(buffer, ws) - end - fb.resolution[] = ws +function Base.resize!(fb::GLFramebuffer, w::Int, h::Int) + (w > 0 && h > 0 && (w, h) != size(fb)) || return + for (name, buffer) in fb.buffers + resize_nocopy!(buffer, (w, h)) end - nothing + fb.resolution[] = (w, h) + return nothing end @@ -188,10 +186,21 @@ function destroy!(nw::GLFW.Window) was_current && ShaderAbstractions.switch_context!() end -function windowsize(nw::GLFW.Window) +function window_size(nw::GLFW.Window) + was_destroyed(nw) && return (0, 0) + return Tuple(GLFW.GetWindowSize(nw)) +end +function window_position(nw::GLFW.Window) was_destroyed(nw) && return (0, 0) - size = GLFW.GetFramebufferSize(nw) - return (size.width, size.height) + return Tuple(GLFW.GetWindowPos(window)) +end +function framebuffer_size(nw::GLFW.Window) + was_destroyed(nw) && return (0, 0) + return Tuple(GLFW.GetFramebufferSize(nw)) +end +function scale_factor(nw::GLFW.Window) + was_destroyed(nw) && return 1f0 + return minimum(GLFW.GetWindowContentScale(nw)) end function Base.isopen(window::GLFW.Window) diff --git a/GLMakie/src/picking.jl b/GLMakie/src/picking.jl index d83c4d6f9d2..0a7b74346e7 100644 --- a/GLMakie/src/picking.jl +++ b/GLMakie/src/picking.jl @@ -6,16 +6,16 @@ function pick_native(screen::Screen, rect::Rect2i) isopen(screen) || return Matrix{SelectionID{Int}}(undef, 0, 0) ShaderAbstractions.switch_context!(screen.glscreen) - window_size = size(screen) fb = screen.framebuffer buff = fb.buffers[:objectid] glBindFramebuffer(GL_FRAMEBUFFER, fb.id[1]) glReadBuffer(GL_COLOR_ATTACHMENT1) rx, ry = minimum(rect) rw, rh = widths(rect) - w, h = window_size - sid = zeros(SelectionID{UInt32}, widths(rect)...) + w, h = size(screen.root_scene) if rx > 0 && ry > 0 && rx + rw <= w && ry + rh <= h + rx, ry, rw, rh = round.(Int, screen.px_per_unit[] .* (rx, ry, rw, rh)) + sid = zeros(SelectionID{UInt32}, rw, rh) glReadPixels(rx, ry, rw, rh, buff.format, buff.pixeltype, sid) return sid else @@ -26,15 +26,15 @@ end function pick_native(screen::Screen, xy::Vec{2, Float64}) isopen(screen) || return SelectionID{Int}(0, 0) ShaderAbstractions.switch_context!(screen.glscreen) - sid = Base.RefValue{SelectionID{UInt32}}() - window_size = size(screen) fb = screen.framebuffer buff = fb.buffers[:objectid] glBindFramebuffer(GL_FRAMEBUFFER, fb.id[1]) glReadBuffer(GL_COLOR_ATTACHMENT1) x, y = floor.(Int, xy) - w, h = window_size + w, h = size(screen.root_scene) if x > 0 && y > 0 && x <= w && y <= h + x, y = round.(Int, screen.px_per_unit[] .* (x, y)) + sid = Base.RefValue{SelectionID{UInt32}}() glReadPixels(x, y, 1, 1, buff.format, buff.pixeltype, sid) return convert(SelectionID{Int}, sid[]) end @@ -65,7 +65,7 @@ end # Skips one set of allocations function Makie.pick_closest(scene::Scene, screen::Screen, xy, range) isopen(screen) || return (nothing, 0) - w, h = size(screen) + w, h = size(scene) ((1.0 <= xy[1] <= w) && (1.0 <= xy[2] <= h)) || return (nothing, 0) x0, y0 = max.(1, floor.(Int, xy .- range)) @@ -95,7 +95,7 @@ end # Skips some allocations function Makie.pick_sorted(scene::Scene, screen::Screen, xy, range) isopen(screen) || return (nothing, 0) - w, h = size(screen) + w, h = size(scene) if !((1.0 <= xy[1] <= w) && (1.0 <= xy[2] <= h)) return Tuple{AbstractPlot, Int}[] end diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index 3c295d158a6..fa55afd4bfd 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -285,14 +285,11 @@ function to_screen_postprocessor(framebuffer, shader_cache, screen_fb_id = nothi pass.postrenderfunction = () -> draw_fullscreen(pass.vertexarray.id) full_render = screen -> begin - fb = screen.framebuffer - w, h = size(fb) - # transfer everything to the screen default_id = isnothing(screen_fb_id) ? 0 : screen_fb_id[] # GLFW uses 0, Gtk uses a value that we have to probe at the beginning of rendering glBindFramebuffer(GL_FRAMEBUFFER, default_id) - glViewport(0, 0, w, h) + glViewport(0, 0, framebuffer_size(screen.glscreen)...) glClear(GL_COLOR_BUFFER_BIT) GLAbstraction.render(pass) # copy postprocess end diff --git a/GLMakie/src/rendering.jl b/GLMakie/src/rendering.jl index 811bb8094f0..e9bc680913a 100644 --- a/GLMakie/src/rendering.jl +++ b/GLMakie/src/rendering.jl @@ -1,14 +1,14 @@ - -function setup!(screen) +function setup!(screen::Screen) glEnable(GL_SCISSOR_TEST) - if isopen(screen) - glScissor(0, 0, size(screen)...) + if isopen(screen) && !isnothing(screen.root_scene) + sf = screen.px_per_unit[] + glScissor(0, 0, round.(Int, size(screen.root_scene) .* sf)...) glClearColor(1, 1, 1, 1) glClear(GL_COLOR_BUFFER_BIT) for (id, scene) in screen.screens if scene.visible[] a = pixelarea(scene)[] - rt = (minimum(a)..., widths(a)...) + rt = (round.(Int, sf .* minimum(a))..., round.(Int, sf .* widths(a))...) glViewport(rt...) if scene.clear c = scene.backgroundcolor[] @@ -45,11 +45,10 @@ function render_frame(screen::Screen; resize_buffers=true) # render order here may introduce artifacts because of that. fb = screen.framebuffer - if resize_buffers - wh = Int.(framebuffer_size(nw)) - resize!(fb, wh) + if resize_buffers && !isnothing(screen.root_scene) + sf = screen.px_per_unit[] + resize!(fb, round.(Int, sf .* size(screen.root_scene))...) end - w, h = size(fb) # prepare stencil (for sub-scenes) glBindFramebuffer(GL_FRAMEBUFFER, fb.id) @@ -121,8 +120,9 @@ function GLAbstraction.render(filter_elem_func, screen::Screen) found, scene = id2scene(screen, screenid) found || continue scene.visible[] || continue + sf = screen.px_per_unit[] a = pixelarea(scene)[] - glViewport(minimum(a)..., widths(a)...) + glViewport(round.(Int, sf .* minimum(a))..., round.(Int, sf .* widths(a))...) render(elem) end catch e diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 4afc2844a57..6aa9013a23e 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -8,19 +8,19 @@ function renderloop end """ ## Renderloop -* `renderloop = GLMakie.renderloop`: sets a function `renderloop(::GLMakie.Screen)` which starts a renderloop for the screen. +* `renderloop = GLMakie.renderloop`: Sets a function `renderloop(::GLMakie.Screen)` which starts a renderloop for the screen. - - !!! warning +!!! warning The below are not effective if renderloop isn't set to `GLMakie.renderloop`, unless implemented in custom renderloop: - * `pause_renderloop = false`: creates a screen with paused renderlooop. Can be started with `GLMakie.start_renderloop!(screen)` or paused again with `GLMakie.pause_renderloop!(screen)`. * `vsync = false`: enables vsync for the window. * `render_on_demand = true`: renders the scene only if something has changed in it. * `framerate = 30.0`: sets the currently rendered frames per second. +* `px_per_unit = automatic`: Sets the ratio between the number of rendered pixels and the `Makie` resolution. It defaults to the value of `scalefactor` but may be any positive real number. ## GLFW window attributes + * `float = false`: Lets the opened window float above anything else. * `focus_on_show = false`: Focusses the window when newly opened. * `decorated = true`: shows the window decorations or not. @@ -28,6 +28,8 @@ function renderloop end * `fullscreen = false`: Starts the window in fullscreen. * `debugging = false`: Starts the GLFW.Window/OpenGL context with debug output. * `monitor::Union{Nothing, GLFW.Monitor} = nothing`: Sets the monitor on which the Window should be opened. +* `visible = true`: Sets whether the window is user-visible. +* `scalefactor = automatic`: Sets the window scaling factor, such as `2.0` on HiDPI/Retina displays. It is set automatically based on the display, but may be any positive real number. ## Postprocessor * `oit = false`: Enles order independent transparency for the window. @@ -43,6 +45,7 @@ mutable struct ScreenConfig vsync::Bool render_on_demand::Bool framerate::Float64 + px_per_unit::Union{Nothing, Float32} # GLFW window attributes float::Bool @@ -53,6 +56,7 @@ mutable struct ScreenConfig debugging::Bool monitor::Union{Nothing, GLFW.Monitor} visible::Bool + scalefactor::Union{Nothing, Float32} # Postprocessor oit::Bool @@ -67,6 +71,7 @@ mutable struct ScreenConfig vsync::Bool, render_on_demand::Bool, framerate::Number, + px_per_unit::Union{Makie.Automatic, Number}, # GLFW window attributes float::Bool, focus_on_show::Bool, @@ -76,6 +81,7 @@ mutable struct ScreenConfig debugging::Bool, monitor::Union{Nothing, GLFW.Monitor}, visible::Bool, + scalefactor::Union{Makie.Automatic, Number}, # Preproccessor oit::Bool, @@ -90,6 +96,7 @@ mutable struct ScreenConfig vsync, render_on_demand, framerate, + px_per_unit isa Makie.Automatic ? nothing : Float32(px_per_unit), # GLFW window attributes float, focus_on_show, @@ -99,6 +106,7 @@ mutable struct ScreenConfig debugging, monitor, visible, + scalefactor isa Makie.Automatic ? nothing : Float32(scalefactor), # Preproccessor oit, fxaa, @@ -148,6 +156,7 @@ mutable struct Screen{GLWindow} <: MakieScreen config::Union{Nothing, ScreenConfig} stop_renderloop::Bool rendertask::Union{Task, Nothing} + px_per_unit::Observable{Float32} screen2scene::Dict{WeakRef, ScreenID} screens::Vector{ScreenArea} @@ -158,6 +167,7 @@ mutable struct Screen{GLWindow} <: MakieScreen framecache::Matrix{RGB{N0f8}} render_tick::Observable{Nothing} window_open::Observable{Bool} + scalefactor::Observable{Float32} root_scene::Union{Scene, Nothing} reuse::Bool @@ -186,10 +196,10 @@ mutable struct Screen{GLWindow} <: MakieScreen screen = new{GLWindow}( glscreen, shader_cache, framebuffer, config, stop_renderloop, rendertask, - screen2scene, + Observable(0f0), screen2scene, screens, renderlist, postprocessors, cache, cache2plot, Matrix{RGB{N0f8}}(undef, s), Observable(nothing), - Observable(true), nothing, reuse, true, false + Observable(true), Observable(0f0), nothing, reuse, true, false ) push!(ALL_SCREENS, screen) # track all created screens return screen @@ -214,6 +224,8 @@ function empty_screen(debugging::Bool; reuse=true) (GLFW.STENCIL_BITS, 0), (GLFW.AUX_BUFFERS, 0), + + (GLFW.SCALE_TO_MONITOR, true), ] resolution = (10, 10) window = try @@ -262,7 +274,9 @@ function empty_screen(debugging::Bool; reuse=true) Dict{UInt32, AbstractPlot}(), reuse, ) - GLFW.SetWindowRefreshCallback(window, window -> refreshwindowcb(window, screen)) + GLFW.SetWindowRefreshCallback(window, refreshwindowcb(screen)) + GLFW.SetWindowContentScaleCallback(window, scalechangecb(screen)) + return screen end @@ -276,6 +290,7 @@ function reopen!(screen::Screen) end @assert isempty(screen.window_open.listeners) screen.window_open[] = true + on(scalechangeobs(screen), screen.scalefactor) @assert isopen(screen) return screen end @@ -302,8 +317,6 @@ function singleton_screen(debugging::Bool) return reopen!(screen) end -const GLFW_FOCUS_ON_SHOW = 0x0002000C - function Makie.apply_screen_config!(screen::Screen, config::ScreenConfig, scene::Scene, args...) apply_config!(screen, config) end @@ -311,7 +324,7 @@ end function apply_config!(screen::Screen, config::ScreenConfig; start_renderloop::Bool=true) glw = screen.glscreen ShaderAbstractions.switch_context!(glw) - GLFW.SetWindowAttrib(glw, GLFW_FOCUS_ON_SHOW, config.focus_on_show) + GLFW.SetWindowAttrib(glw, GLFW.FOCUS_ON_SHOW, config.focus_on_show) GLFW.SetWindowAttrib(glw, GLFW.DECORATED, config.decorated) GLFW.SetWindowAttrib(glw, GLFW.FLOATING, config.float) GLFW.SetWindowTitle(glw, config.title) @@ -319,6 +332,8 @@ function apply_config!(screen::Screen, config::ScreenConfig; start_renderloop::B if !isnothing(config.monitor) GLFW.SetWindowMonitor(glw, config.monitor) end + screen.scalefactor[] = !isnothing(config.scalefactor) ? config.scalefactor : scale_factor(glw) + screen.px_per_unit[] = !isnothing(config.px_per_unit) ? config.px_per_unit : screen.scalefactor[] function replace_processor!(postprocessor, idx) fb = screen.framebuffer @@ -355,10 +370,10 @@ function Screen(; # Screen config is managed by the current active theme, so managed by Makie config = Makie.merge_screen_config(ScreenConfig, screen_config) screen = screen_from_pool(config.debugging) + apply_config!(screen, config; start_renderloop=start_renderloop) if !isnothing(resolution) resize!(screen, resolution...) end - apply_config!(screen, config; start_renderloop=start_renderloop) return screen end @@ -553,7 +568,10 @@ function destroy!(screen::Screen) # otherwise, during rendertask clean up we may run into a destroyed window wait(screen) screen.rendertask = nothing - destroy!(screen.glscreen) + window = screen.glscreen + GLFW.SetWindowRefreshCallback(window, nothing) + GLFW.SetWindowContentScaleCallback(window, nothing) + destroy!(window) # Since those are sets, we can just delete them from there, even if they weren't in there (e.g. reuse=false) delete!(SCREEN_REUSE_POOL, screen) delete!(ALL_SCREENS, screen) @@ -575,6 +593,8 @@ function Base.close(screen::Screen; reuse=true) screen.window_open[] = false end empty!(screen) + Observables.clear(screen.px_per_unit) + Observables.clear(screen.scalefactor) if reuse && screen.reuse push!(SCREEN_REUSE_POOL, screen) end @@ -600,24 +620,30 @@ function closeall() return end -function resize_native!(window::GLFW.Window, resolution...) - if isopen(window) - ShaderAbstractions.switch_context!(window) - oldsize = windowsize(window) - retina_scale = retina_scaling_factor(window) - w, h = resolution ./ retina_scale - if oldsize == (w, h) - return - end - GLFW.SetWindowSize(window, round(Int, w), round(Int, h)) +function Base.resize!(screen::Screen, w::Int, h::Int) + window = to_native(screen) + (w > 0 && h > 0 && isopen(window)) || return nothing + + # Resize the window which appears on the user desktop (if necessary). + # + # On OSX with a Retina display, the window size is given in logical dimensions and + # is automatically scaled by the OS. To support arbitrary scale factors, we must account + # for the native scale factor when calculating the effective scaling to apply. + # + # On Linux and Windows, scale from the logical size to the pixel size. + ShaderAbstractions.switch_context!(window) + winscale = screen.scalefactor[] / (@static Sys.isapple() ? scale_factor(window) : 1) + winw, winh = round.(Int, winscale .* (w, h)) + if window_size(window) != (winw, winh) + GLFW.SetWindowSize(window, winw, winh) end -end -function Base.resize!(screen::Screen, w, h) - nw = to_native(screen) - resize_native!(nw, w, h) - fb = screen.framebuffer - resize!(fb, (w, h)) + # Then resize the underlying rendering framebuffers as well, which can be scaled + # independently of the window scale factor. + fbscale = screen.px_per_unit[] + fbw, fbh = round.(Int, fbscale .* (w, h)) + resize!(screen.framebuffer, fbw, fbh) + return nothing end function fast_color_data!(dest::Array{RGB{N0f8}, 2}, source::Texture{T, 2}) where T @@ -662,8 +688,7 @@ function Makie.colorbuffer(screen::Screen, format::Makie.ImageStorageFormat = Ma # polling may change window size, when its bigger than monitor! # we still need to poll though, to get all the newest events! # GLFW.PollEvents() - # keep current buffer size to allows larger-than-window renders - render_frame(screen, resize_buffers=false) # let it render + render_frame(screen, resize_buffers=true) # let it render glFinish() # block until opengl is done rendering if size(ctex) != size(screen.framecache) screen.framecache = Matrix{RGB{N0f8}}(undef, size(ctex)) @@ -784,12 +809,32 @@ function set_framerate!(screen::Screen, fps=30) screen.config.framerate = fps end -function refreshwindowcb(window, screen) +function refreshwindowcb(screen, window) screen.render_tick[] = nothing render_frame(screen) GLFW.SwapBuffers(window) return end +refreshwindowcb(screen) = window -> refreshwindowcb(screen, window) + +function scalechangecb(screen, window, xscale, yscale) + sf = min(xscale, yscale) + if isnothing(screen.config.px_per_unit) && screen.scalefactor[] == screen.px_per_unit[] + screen.px_per_unit[] = sf + end + screen.scalefactor[] = sf + return +end +scalechangecb(screen) = (window, xscale, yscale) -> scalechangecb(screen, window, xscale, yscale) + +function scalechangeobs(screen, _) + if !isnothing(screen.root_scene) + resize!(screen, size(screen.root_scene)...) + end + return nothing +end +scalechangeobs(screen) = scalefactor -> scalechangeobs(screen, scalefactor) + # TODO add render_tick event to scene events function vsynced_renderloop(screen) diff --git a/GLMakie/test/runtests.jl b/GLMakie/test/runtests.jl index d73329c6825..437f1022c5b 100644 --- a/GLMakie/test/runtests.jl +++ b/GLMakie/test/runtests.jl @@ -16,7 +16,7 @@ reference_tests_dir = normpath(joinpath(dirname(pathof(Makie)), "..", "Reference Pkg.develop(PackageSpec(path = reference_tests_dir)) using ReferenceTests -GLMakie.activate!(framerate=1.0) +GLMakie.activate!(framerate=1.0, scalefactor=1.0) @testset "mimes" begin f, ax, pl = scatter(1:4) diff --git a/GLMakie/test/unit_tests.jl b/GLMakie/test/unit_tests.jl index ea16da8f284..45325358606 100644 --- a/GLMakie/test/unit_tests.jl +++ b/GLMakie/test/unit_tests.jl @@ -242,6 +242,8 @@ end @test isempty(screen.window_open.listeners) @test isempty(screen.render_tick.listeners) + @test isempty(screen.px_per_unit.listeners) + @test isempty(screen.scalefactor.listeners) @test screen.root_scene === nothing @test screen.rendertask === nothing @@ -254,3 +256,120 @@ end # now every screen should be gone @test isempty(GLMakie.SCREEN_REUSE_POOL) end + +@testset "HiDPI displays" begin + import FileIO: @format_str, File, load + GLMakie.closeall() + + W, H = 400, 400 + N = 51 + x = collect(range(0.0, 2π, length=N)) + y = sin.(x) + fig, ax, pl = scatter(x, y, figure = (; resolution = (W, H))); + hidedecorations!(ax) + + # On OSX, the native window size has an underlying scale factor that we need to account + # for when interpreting native window sizes with respect to the desired figure size + # and desired scaling factor. + function scaled(screen::GLMakie.Screen, dims::Tuple{Vararg{Int}}) + sf = screen.scalefactor[] / (Sys.isapple() ? GLMakie.scale_factor(screen.glscreen) : 1) + return round.(Int, dims .* sf) + end + + screen = display(GLMakie.Screen(visible = false, scalefactor = 2), fig) + @test screen.scalefactor[] === 2f0 + @test screen.px_per_unit[] === 2f0 # inherited from scale factor + @test size(screen.framebuffer) == (2W, 2H) + @test GLMakie.window_size(screen.glscreen) == scaled(screen, (W, H)) + + # check that picking works through the resized GL buffers + GLMakie.Makie.colorbuffer(screen) # force render + # - point pick + point_px = project_sp(ax.scene, Point2f(x[end÷2], y[end÷2])) + elem, idx = pick(ax.scene, point_px) + @test elem === pl + @test idx == length(x) ÷ 2 + # - area pick + bottom_px = project_sp(ax.scene, Point2f(π, -1)) + right_px = project_sp(ax.scene, Point2f(2π, 0)) + quadrant = Rect2i(round.(bottom_px)..., round.(right_px - bottom_px)...) + picks = pick(ax.scene, quadrant) + points = Set(Int(p[2]) for p in picks if p[1] isa Scatter) + @test points == Set(((N+1)÷2):N) + + # render at lower resolution + screen = display(GLMakie.Screen(visible = false, scalefactor = 2, px_per_unit = 1), fig) + @test screen.scalefactor[] === 2f0 + @test screen.px_per_unit[] === 1f0 + @test size(screen.framebuffer) == (W, H) + + # decrease the scale factor after-the-fact + screen.scalefactor[] = 1 + sleep(0.1) # TODO: Necessary?? Are observable callbacks asynchronous? + @test GLMakie.window_size(screen.glscreen) == scaled(screen, (W, H)) + + # save images of different resolutions + mktemp() do path, io + close(io) + file = File{format"PNG"}(path) + + # save at current size + @test screen.px_per_unit[] == 1 + save(file, fig) + img = load(file) + @test size(img) == (W, H) + + # save with a different resolution + save(file, fig, px_per_unit = 2) + img = load(file) + @test size(img) == (2W, 2H) + # writing to file should not effect the visible figure + @test_broken screen.px_per_unit[] == 1 + end + + if Sys.islinux() + # Test that GLMakie is correctly getting the default scale factor from X11 in a + # HiDPI environment. + + checkcmd = `which xrdb` & `which xsettingsd` + checkcmd = pipeline(ignorestatus(checkcmd), stdout = devnull, stderr = devnull) + hasxrdb = success(run(checkcmd)) + + # Only continue if running within an Xvfb environment where the setting is + # empty by default. Overriding during a user's session could be problematic + # (i.e. if running interactively rather than in CI). + inxvfb = hasxrdb ? isempty(readchomp(`xrdb -query`)) : false + + if hasxrdb && inxvfb + # GLFW looks for Xft.dpi resource setting. Spawn a temporary xsettingsd daemon + # to be the X resource manager + xsettingsd = run(pipeline(`xsettingsd -c /dev/null`), wait = false) + try + # Then set the DPI to 192, i.e. 2 times the default of 96dpi + run(pipeline(`echo "Xft.dpi: 192"`, `xrdb -merge`)) + + # Print out the automatically-determined scale factor from the GLScreen + jlscript = raw""" + using GLMakie + fig, ax, pl = scatter(1:2, 3:4) + screen = display(GLMakie.Screen(visible = false), fig) + print(Int(screen.scalefactor[])) + """ + cmd = ``` + $(Base.julia_cmd()) + --project=$(Base.active_project()) + --eval $jlscript + ``` + scalefactor = readchomp(cmd) + @test scalefactor == "2" + finally + # cleanup: kill the daemon before continuing with more tests + kill(xsettingsd) + end + else + @test_broken hasxrdb && inxvfb + end + else + @test_broken Sys.islinux() + end +end diff --git a/NEWS.md b/NEWS.md index 1f29c947ae5..8e735bb873c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -5,6 +5,9 @@ - Added the `stephist` plotting function [#2408](https://github.com/JuliaPlots/Makie.jl/pull/2408). - Fixed an issue where `poly` plots with `Vector{<: MultiPolygon}` inputs with per-polygon color were mistakenly rendered as meshes using CairoMakie. [#2590](https://github.com/MakieOrg/Makie.jl/pulls/2478) - Fixed a small typo which caused an error in the `Stepper` constructor. [#2600](https://github.com/MakieOrg/Makie.jl/pulls/2478) +- GLMakie has gained support for HiDPI (aka Retina) screens. + This also enables saving images with higher resolution than screen pixel dimensions. + [#2544](https://github.com/MakieOrg/Makie.jl/pull/2544) - Fixed rectangle zoom for nonlinear axes [#2674](https://github.com/MakieOrg/Makie.jl/pull/2674) ## v0.19.1 diff --git a/docs/documentation/backends/glmakie.md b/docs/documentation/backends/glmakie.md index c270749dc8c..ff87cf7a6b6 100644 --- a/docs/documentation/backends/glmakie.md +++ b/docs/documentation/backends/glmakie.md @@ -15,6 +15,50 @@ println("~~~") ``` \textoutput{docs} +#### Window Scaling + +The sizes of figures are given in display-independent "logical" dimensions, and the +GLMakie backend will scale the size of the displayed window on HiDPI/Retina displays +automatically. +For example, the default `resolution = (800, 600)` will be shown in a 1600 × 1200 window +on a HiDPI display which is configured with a 200% scaling factor. + +The scaling factor may be overridden by displaying the figure with a different +`scalefactor` value: +```julia +fig = Figure(resolution = (800, 600)) +# ... +display(fig, scalefactor = 1.5) +``` + +If the scale factor is not changed from its default automatic configuration, the window +will be resized to maintain its apparent size when moved across displays with different +scaling factors on Windows and OSX. +(Independent scaling factors are not supported by X11, and at this time the underlying +GLFW library is not compiled with Wayland support.) + +#### Resolution Scaling + +Related to the window scaling factor, the mapping from figure sizes and positions to pixels +can be scaled to achieve HiDPI/Retina resolution renderings. +The resolution scaling defaults to the same factor as the window scaling, but it may +be independently overridden with the `px_per_unit` argument when showing a figure: +```julia +fig = Figure(resolution = (800, 600)) +# ... +display(fig, px_per_unit = 2) +``` + +The resolution scale factor may also be changed when saving pngs: +```julia +save("hires.png", fig, px_per_unit = 2) # 1600 × 1200 px png +save("lores.png", fig, px_per_unit = 0.5) # 400 × 300 px png +``` +If a script may run in interactive environments where the native screen DPI can vary, +you may want to explicitly set `px_per_unit = 1` when saving figures to ensure consistency +of results. + + #### Multiple Windows GLMakie has experimental support for displaying multiple independent figures (or scenes). To open a new window, use `display(GLMakie.Screen(), figure_or_scene)`. diff --git a/src/camera/projection_math.jl b/src/camera/projection_math.jl index 717224c6a6a..cba833091ed 100644 --- a/src/camera/projection_math.jl +++ b/src/camera/projection_math.jl @@ -316,7 +316,7 @@ end # project between different coordinate systems/spaces function space_to_clip(cam::Camera, space::Symbol, projectionview::Bool=true) - if is_data_space(space) + if is_data_space(space) || is_transformed_space(space) return projectionview ? cam.projectionview[] : cam.projection[] elseif is_pixel_space(space) return cam.pixel_space[] @@ -330,7 +330,7 @@ function space_to_clip(cam::Camera, space::Symbol, projectionview::Bool=true) end function clip_to_space(cam::Camera, space::Symbol) - if is_data_space(space) + if is_data_space(space) || is_transformed_space(space) return inv(cam.projectionview[]) elseif is_pixel_space(space) w, h = cam.resolution[] diff --git a/src/conversions.jl b/src/conversions.jl index 33597627f56..2bb06b1ad7c 100644 --- a/src/conversions.jl +++ b/src/conversions.jl @@ -300,12 +300,12 @@ end function edges(v::AbstractVector) l = length(v) if l == 1 - return [v[1] - 0.5, v[1] + 0.5] + return [v[begin] - 0.5, v[begin] + 0.5] else # Equivalent to # mids = 0.5 .* (v[1:end-1] .+ v[2:end]) # borders = [2v[1] - mids[1]; mids; 2v[end] - mids[end]] - borders = [0.5 * (v[max(1, i)] + v[min(end, i+1)]) for i in 0:length(v)] + borders = [0.5 * (v[max(begin, i)] + v[min(end, i+1)]) for i in (firstindex(v) - 1):lastindex(v)] borders[1] = 2borders[1] - borders[2] borders[end] = 2borders[end] - borders[end-1] return borders @@ -359,12 +359,11 @@ and stores the `ClosedInterval` to `n` and `m`, plus the original matrix in a Tu """ function convert_arguments(sl::SurfaceLike, data::AbstractMatrix) n, m = Float32.(size(data)) - convert_arguments(sl, 0f0 .. n, 0f0 .. m, el32convert(data)) + convert_arguments(sl, firstindex(data, 1) .. lastindex(data, 1), firstindex(data, 2) .. lastindex(data, 2), el32convert(collect(data))) end function convert_arguments(ds::DiscreteSurface, data::AbstractMatrix) - n, m = Float32.(size(data)) - convert_arguments(ds, edges(1:n), edges(1:m), el32convert(data)) + convert_arguments(ds, edges(axes(data, 1)), edges(axes(data, 2)), el32convert(collect(data))) end function convert_arguments(SL::SurfaceLike, x::AbstractVector{<:Number}, y::AbstractVector{<:Number}, z::AbstractVector{<:Number}) @@ -423,12 +422,16 @@ and stores the `ClosedInterval` to `n`, `m` and `k`, plus the original array in `P` is the plot Type (it is optional). """ function convert_arguments(::VolumeLike, data::AbstractArray{T, 3}) where T - n, m, k = Float32.(size(data)) - return (0f0 .. n, 0f0 .. m, 0f0 .. k, el32convert(data)) + return ( + Float32(firstindex(data, 1)) .. Float32(lastindex(data, 1)), + Float32(firstindex(data, 2)) .. Float32(lastindex(data, 2)), + Float32(firstindex(data, 3)) .. Float32(lastindex(data, 3)), + el32convert(collect(data)) + ) end function convert_arguments(::VolumeLike, x::RangeLike, y::RangeLike, z::RangeLike, data::AbstractArray{T, 3}) where T - return (x, y, z, el32convert(data)) + return (x, y, z, el32convert(collect(data))) end """ convert_arguments(P, x, y, z, i)::(Vector, Vector, Vector, Matrix) @@ -631,16 +634,6 @@ function tryrange(F, vec) error("$F is not a Function, or is not defined at any of the values $vec") end -# OffsetArrays conversions -function convert_arguments(sl::SurfaceLike, wm::OffsetArray) - x1, y1 = wm.offsets .+ 1 - nx, ny = size(wm) - x = range(x1, length = nx) - y = range(y1, length = ny) - v = parent(wm) - return convert_arguments(sl, x, y, v) -end - ################################################################################ # Helper Functions # ################################################################################ diff --git a/src/display.jl b/src/display.jl index 75e17dd02bc..16e01b32c1d 100644 --- a/src/display.jl +++ b/src/display.jl @@ -31,9 +31,18 @@ function push_screen!(scene::Scene, screen::MakieScreen) return end # Else we delete all other screens, only one screen per scene is allowed!! - while !isempty(scene.current_screens) - delete_screen!(scene, pop!(scene.current_screens)) + unique_screen_types = Set{DataType}() + indices_for_deletion = Int[] + + for (index, screen) in enumerate(scene.current_screens) + if typeof(screen) ∈ unique_screen_types # screen type already exists in current_screens, so delete this + push!(indices_for_deletion, index) + else # new type of screen + union!(unique_screen_types, (typeof(screen),)) + end end + # delete all screnes whose screentype already exists + delete_screen!.((scene,), scene.current_screens[indices_for_deletion]) # Now we push the screen :) push!(scene.current_screens, screen) diff --git a/src/scenes.jl b/src/scenes.jl index 8bb92be7286..a1583b7eb4d 100644 --- a/src/scenes.jl +++ b/src/scenes.jl @@ -358,7 +358,8 @@ getscreen(scene::SceneLike) = getscreen(rootparent(scene)) Base.iterate(scene::Scene, idx=1) = idx <= length(scene) ? (scene[idx], idx + 1) : nothing Base.length(scene::Scene) = length(scene.plots) Base.lastindex(scene::Scene) = length(scene.plots) -getindex(scene::Scene, idx::Integer) = scene.plots[idx] +Base.getindex(scene::Scene, idx::Integer) = scene.plots[idx] +Base.get!(scene::Scene, attr::Symbol, default) = Base.get!(scene.theme, attr, default) struct OldAxis end zero_origin(area) = Recti(0, 0, widths(area)) diff --git a/src/theming.jl b/src/theming.jl index 1cbbbe39d38..0098e0eea66 100644 --- a/src/theming.jl +++ b/src/theming.jl @@ -85,6 +85,7 @@ const minimal_default = Attributes( vsync = false, render_on_demand = true, framerate = 30.0, + px_per_unit = automatic, # GLFW window attributes float = false, @@ -95,6 +96,7 @@ const minimal_default = Attributes( debugging = false, monitor = nothing, visible = true, + scalefactor = automatic, # Postproccessor oit = true, diff --git a/src/units.jl b/src/units.jl index d93d71f8331..ed9f5268c6e 100644 --- a/src/units.jl +++ b/src/units.jl @@ -127,8 +127,9 @@ export px ######################################## -spaces() = (:data, :pixel, :relative, :clip) +spaces() = (:data, :pixel, :relative, :clip, :transformed) is_data_space(space) = space === :data is_pixel_space(space) = space === :pixel is_relative_space(space) = space === :relative is_clip_space(space) = space === :clip +is_transformed_space(space) = space === :transformed