From 63e2b3ad17059951377a1263b037610a0c43ec61 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 21 Feb 2025 22:52:15 +0100 Subject: [PATCH 01/16] reduce messages on Textbox re-select (32 -> 4) --- src/makielayout/blocks/textbox.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/makielayout/blocks/textbox.jl b/src/makielayout/blocks/textbox.jl index 58f4252726c..af122ce0853 100644 --- a/src/makielayout/blocks/textbox.jl +++ b/src/makielayout/blocks/textbox.jl @@ -137,10 +137,12 @@ function initialize_block!(tbox::Textbox) onmouseleftdown(mouseevents) do state focus!(tbox) - if tbox.displayed_string[] == tbox.placeholder[] || tbox.displayed_string[] == " " + if tbox.displayed_string[] == tbox.placeholder[] tbox.displayed_string[] = " " cursorindex[] = 0 return Consume(true) + elseif tbox.displayed_string[] == " " + return Consume(true) end if typeof(tbox.width[]) <: Number From 1e2e53b2feb9bed3623ae379a4db7b9dc2e35ebe Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 21 Feb 2025 22:53:20 +0100 Subject: [PATCH 02/16] reduce select-textbox overhead (48 -> 38) --- src/makielayout/blocks/textbox.jl | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/makielayout/blocks/textbox.jl b/src/makielayout/blocks/textbox.jl index af122ce0853..2ce0c132a16 100644 --- a/src/makielayout/blocks/textbox.jl +++ b/src/makielayout/blocks/textbox.jl @@ -73,6 +73,7 @@ function initialize_block!(tbox::Textbox) charbbs(textplot) end + cursorsize = Observable(Vec2f(1, tbox.fontsize[])) cursorpoints = lift(topscene, cursorindex, displayed_charbbs) do ci, bbs textplot = t.blockscene.plots[1] @@ -90,16 +91,26 @@ function initialize_block!(tbox::Textbox) return end - if 0 < ci < length(bbs) - [leftline(bbs[ci+1])...] + line_ps = if 0 < ci < length(bbs) + leftline(bbs[ci+1]) elseif ci == 0 - [leftline(bbs[1])...] + leftline(bbs[1]) else - [leftline(bbs[ci])...] .+ Point2f(hadvances[ci], 0) + leftline(bbs[ci]) .+ (Point2f(hadvances[ci], 0),) end + + # could this be done statically as + # max_height = font.height / font.units_per_EM * fontsize + max_height = abs(line_ps[1][2] - line_ps[2][2]) + if !(cursorsize[][2] ≈ max_height) + cursorsize[] = Vec2f(1, max_height) + end + + return 0.5 * (line_ps[1] + line_ps[2]) end - cursor = linesegments!(scene, cursorpoints, color = tbox.cursorcolor, linewidth = 1, inspectable = false) + cursor = scatter!(scene, cursorpoints, marker = Rect, color = tbox.cursorcolor, + markersize = cursorsize, inspectable = false) on(cursorpoints) do cpts typeof(tbox.width[]) <: Number || return From 637f438bcda8f400c37d4789d80b3ba4a792b3c5 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 21 Feb 2025 23:08:21 +0100 Subject: [PATCH 03/16] skip duplicate validation results (33 -> 30 updates per char) --- src/makielayout/blocks/textbox.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/makielayout/blocks/textbox.jl b/src/makielayout/blocks/textbox.jl index 2ce0c132a16..95f1c2a8b86 100644 --- a/src/makielayout/blocks/textbox.jl +++ b/src/makielayout/blocks/textbox.jl @@ -20,7 +20,7 @@ function initialize_block!(tbox::Textbox) tbox.displayed_string[] = isnothing(tbox.stored_string[]) ? tbox.placeholder[] : tbox.stored_string[] - displayed_is_valid = lift(topscene, tbox.displayed_string, tbox.validator) do str, validator + displayed_is_valid = lift(topscene, tbox.displayed_string, tbox.validator, ignore_equal_values = true) do str, validator return validate_textbox(str, validator)::Bool end From df095ba72524b03e5b632aef2670e7e2ecbd1f0a Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 23 Feb 2025 14:18:31 +0100 Subject: [PATCH 04/16] avoid pick() in Menu --- src/makielayout/blocks/menu.jl | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/makielayout/blocks/menu.jl b/src/makielayout/blocks/menu.jl index 7d747f63b4b..663b80f282b 100644 --- a/src/makielayout/blocks/menu.jl +++ b/src/makielayout/blocks/menu.jl @@ -178,8 +178,9 @@ function initialize_block!(m::Menu; default = 1) is_over_button = false if Makie.is_mouseinside(menuscene) # the whole scene containing all options - # Is inside the expanded menu selection - if mouseover(menuscene, optionpolys, optiontexts) + # Is inside the expanded menu selection (the polys cover the whole + # selectable area and are in pixel space relative to menuscene) + if any(r -> mp in r, optionpolys[1][]) is_over_options = true was_inside_options = true # we either clicked on an item or hover it @@ -198,7 +199,8 @@ function initialize_block!(m::Menu; default = 1) return Consume(true) else # If not inside menuscene, we check the state for the menu button - if mouseover(blockscene, selectiontext, selectionpoly) + # (use position because selectionpoly is in blockscene) + if position in selectionpoly.converted[1][] # If over, we either click it to open/close the menu, or we just hover it is_over_button = true was_inside_button = true From 2e30415d77c6ea991997a1c3b8adc29f84d81ba9 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 23 Feb 2025 15:01:49 +0100 Subject: [PATCH 05/16] add pick tracking + tests --- src/interaction/interactive_api.jl | 34 +++++- test/interactivity/pick_tracking.jl | 156 ++++++++++++++++++++++++++++ 2 files changed, 186 insertions(+), 4 deletions(-) create mode 100644 test/interactivity/pick_tracking.jl diff --git a/src/interaction/interactive_api.jl b/src/interaction/interactive_api.jl index b3fdff252e4..6b27825598c 100644 --- a/src/interaction/interactive_api.jl +++ b/src/interaction/interactive_api.jl @@ -1,6 +1,9 @@ export mouseover, mouseposition, hovered_scene export select_rectangle, select_line, select_point +# for debug/test tracking of pick +const PICK_TRACKING = Ref(false) +const _PICK_COUNTER = Ref(0) """ mouseover(fig/ax/scene, plots::AbstractPlot...) @@ -56,13 +59,13 @@ end Returns the plot and element index under the given pixel position `xy = Vec(x, y)`. If `range` is given, the nearest plot up to a distance of `range` is returned instead. -The `plot` returned by this function is always a primitive plot, i.e. one that +The `plot` returned by this function is always a primitive plot, i.e. one that is not composed of other plot types. The index returned relates to the main input of the respective primitive plot. - For `scatter` and `meshscatter` it is an index into the positions given to the plot. - For `text` it is an index into the merged character array. -- For `lines` and `linesegments` it is the end position of the selected line segment. +- For `lines` and `linesegments` it is the end position of the selected line segment. - For `image`, `heatmap` and `surface` it is the linear index into the matrix argument of the plot (i.e. the given image, value or z-value matrix) that is closest to the selected position. - For `voxels` it is the linear index into the given 3D Array. - For `mesh` it is the largest vertex index of the picked triangle face. @@ -79,22 +82,33 @@ end pick(obj) = pick(get_scene(obj), events(obj).mouseposition[]) pick(obj, xy::VecTypes{2}) = pick(get_scene(obj), xy) function pick(scene::Scene, xy::VecTypes{2}) + PICK_TRACKING[] && (_PICK_COUNTER[] += 1) screen = getscreen(scene) screen === nothing && return (nothing, 0) - pick(scene, screen, Vec{2, Float64}(xy)) + return pick(scene, screen, Vec{2, Float64}(xy)) end pick(obj, range::Real) = pick(get_scene(obj), events(obj).mouseposition[], range) pick(obj, xy::VecTypes{2}, range::Real) = pick(get_scene(obj), xy, range) pick(obj, x::Real, y::Real, range::Real) = pick(get_scene(obj), Vec2(x, y), range) function pick(scene::Scene, xy::VecTypes{2}, range::Real) + PICK_TRACKING[] && (_PICK_COUNTER[] += 1) screen = getscreen(scene) screen === nothing && return (nothing, 0) - pick_closest(scene, screen, xy, range) + if PICK_TRACKING[] + # if the Makie implementation is used we'd double count if we just increment here + last = _PICK_COUNTER[] + result = pick_closest(scene, screen, xy, range) + _PICK_COUNTER[] = last + return result + else + return pick_closest(scene, screen, xy, range) + end end # The backend may handle this more optimally function pick_closest(scene::SceneLike, screen, xy, range) + PICK_TRACKING[] && (_PICK_COUNTER[] += 1) w, h = widths(screen) ((1.0 <= xy[1] <= w) && (1.0 <= xy[2] <= h)) || return (nothing, 0) x0, y0 = max.(1, floor.(Int, xy .- range)) @@ -126,12 +140,23 @@ Return all `(plot, index)` pairs in a `(xy .- range, xy .+ range)` region sorted by distance to `xy`. See [`pick`](@ref) for more details. """ function pick_sorted(scene::Scene, xy, range) + PICK_TRACKING[] && (_PICK_COUNTER[] += 1) screen = getscreen(scene) screen === nothing && return Tuple{AbstractPlot, Int}[] + if PICK_TRACKING[] + # if the Makie implementation is used we'd double count if we just increment here + last = _PICK_COUNTER[] + result = pick_sorted(scene, screen, xy, range) + _PICK_COUNTER[] = last + return result + else + return pick_sorted(scene, screen, xy, range) + end return pick_sorted(scene, screen, xy, range) end function pick_sorted(scene::Scene, screen, xy, range) + PICK_TRACKING[] && (_PICK_COUNTER[] += 1) w, h = size(scene) if !((1.0 <= xy[1] <= w) && (1.0 <= xy[2] <= h)) return Tuple{AbstractPlot, Int}[] @@ -168,6 +193,7 @@ screen boundaries. """ pick(x, rect::Rect2i) = pick(get_scene(x), rect) function pick(scene::Scene, rect::Rect2i) + PICK_TRACKING[] && (_PICK_COUNTER[] += 1) screen = getscreen(scene) screen === nothing && return Tuple{AbstractPlot, Int}[] return pick(scene, screen, rect) diff --git a/test/interactivity/pick_tracking.jl b/test/interactivity/pick_tracking.jl new file mode 100644 index 00000000000..2bc8fddcf37 --- /dev/null +++ b/test/interactivity/pick_tracking.jl @@ -0,0 +1,156 @@ +# pick() is fairly expensive operation as it requires data to be transferred +# from the GPU to the CPU side. Whenever possible we should prefer boundingbox/ +# area checks for performance. +# Note that pick-less code can also run without a backend whereas pick-full +# code requires the backend to correctly resolve actions. + +@testset "Widget pick() tracking" begin + + Makie.PICK_TRACKING[] = true + + # sanity check + init = Makie._PICK_COUNTER[] + scene = Scene() + pick(scene, (50, 50)) + pick(scene) + pick(scene, Rect2i(10,10,20,20)) + pick(scene, 10, 20, 5) + Makie.pick_sorted(scene, (10, 20), 5) + @test Makie._PICK_COUNTER[] == init + 5 + + init = Makie._PICK_COUNTER[] + + @testset "Menu" begin + prev = Makie._PICK_COUNTER[] + + f = Figure(size = (600, 450)) + m = Menu(f[1,1], options = string.(1:10)) + Makie.update_state_before_display!(f) + + # open menu + events(f).mouseposition[] = (300, 230) + events(f).mousebutton[] = Makie.MouseButtonEvent(Mouse.left, Mouse.press) + events(f).mousebutton[] = Makie.MouseButtonEvent(Mouse.left, Mouse.release) + + # select option + events(f).mouseposition[] = (300, 300) + events(f).mousebutton[] = Makie.MouseButtonEvent(Mouse.left, Mouse.press) + events(f).mousebutton[] = Makie.MouseButtonEvent(Mouse.left, Mouse.release) + + @test Makie._PICK_COUNTER[] == prev + end + + @testset "Button" begin + prev = Makie._PICK_COUNTER[] + + f = Figure(size = (100, 100)) + Makie.Button(f[1, 1]) + Makie.update_state_before_display!(f) + + # click button + events(f).mouseposition[] = (50, 50) + events(f).mousebutton[] = Makie.MouseButtonEvent(Mouse.left, Mouse.press) + events(f).mousebutton[] = Makie.MouseButtonEvent(Mouse.left, Mouse.release) + + @test Makie._PICK_COUNTER[] == prev + end + + @testset "Textbox" begin + prev = Makie._PICK_COUNTER[] + + f = Figure(size = (200, 100)) + Textbox(f[1, 1]) + Makie.update_state_before_display!(f) + + # enter text field + events(f).mouseposition[] = (300, 225) + events(f).mousebutton[] = Makie.MouseButtonEvent(Mouse.left, Mouse.press) + events(f).mousebutton[] = Makie.MouseButtonEvent(Mouse.left, Mouse.release) + + # type + events(f).unicode_input[] = 't' + events(f).keyboardbutton[] = Makie.KeyEvent(Keyboard.t, Keyboard.press) + events(f).keyboardbutton[] = Makie.KeyEvent(Keyboard.t, Keyboard.release) + + # exit + events(f).mouseposition[] = (0, 0) + events(f).mousebutton[] = Makie.MouseButtonEvent(Mouse.left, Mouse.press) + events(f).mousebutton[] = Makie.MouseButtonEvent(Mouse.left, Mouse.release) + + @test Makie._PICK_COUNTER[] == prev + end + + @testset "Slider" begin + prev = Makie._PICK_COUNTER[] + + f = Figure(size = (200, 100)) + Makie.Slider(f[1, 1]) + Makie.update_state_before_display!(f) + + # initiate drag + events(f).mouseposition[] = (21.0, 50.0) + events(f).mousebutton[] = Makie.MouseButtonEvent(Mouse.left, Mouse.press) + + # finalize drag + events(f).mouseposition[] = (105.0, 50.0) + events(f).mousebutton[] = Makie.MouseButtonEvent(Mouse.left, Mouse.release) + + @test Makie._PICK_COUNTER[] == prev + end + + @testset "Toggle" begin + prev = Makie._PICK_COUNTER[] + + f = Figure(size = (100, 100)) + Makie.Toggle(f[1, 1]) + Makie.update_state_before_display!(f) + + # click + events(f).mouseposition[] = (50.0, 50.0) + events(f).mousebutton[] = Makie.MouseButtonEvent(Mouse.left, Mouse.press) + events(f).mousebutton[] = Makie.MouseButtonEvent(Mouse.left, Mouse.release) + + @test Makie._PICK_COUNTER[] == prev + end + + @testset "Checkbox" begin + prev = Makie._PICK_COUNTER[] + + f = Figure(size = (100, 100)) + Makie.Checkbox(f[1, 1]) + f + Makie.update_state_before_display!(f) + + # click + events(f).mouseposition[] = (50.0, 50.0) + events(f).mousebutton[] = Makie.MouseButtonEvent(Mouse.left, Mouse.press) + events(f).mousebutton[] = Makie.MouseButtonEvent(Mouse.left, Mouse.release) + + @test Makie._PICK_COUNTER[] == prev + end + + @testset "IntervalSlider" begin + prev = Makie._PICK_COUNTER[] + + f = Figure(size = (200, 100)) + Makie.IntervalSlider(f[1, 1]) + f + Makie.update_state_before_display!(f) + + # left end drag + events(f).mouseposition[] = (20.0, 50.0) + events(f).mousebutton[] = Makie.MouseButtonEvent(Mouse.left, Mouse.press) + events(f).mouseposition[] = (70.0, 50.0) + events(f).mousebutton[] = Makie.MouseButtonEvent(Mouse.left, Mouse.release) + + # interval drag + events(f).mouseposition[] = (120.0, 50.0) + events(f).mousebutton[] = Makie.MouseButtonEvent(Mouse.left, Mouse.press) + events(f).mouseposition[] = (90.0, 50.0) + events(f).mousebutton[] = Makie.MouseButtonEvent(Mouse.left, Mouse.release) + + @test Makie._PICK_COUNTER[] == prev + end + + @test Makie._PICK_COUNTER[] == init +end \ No newline at end of file From b4b22bae64b63b66dc50b3ee393b578c2e3e64ed Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 23 Feb 2025 15:04:30 +0100 Subject: [PATCH 06/16] avoid pick in Checkbox --- src/makielayout/blocks/checkbox.jl | 4 ++-- test/interactivity/pick_tracking.jl | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/makielayout/blocks/checkbox.jl b/src/makielayout/blocks/checkbox.jl index 15167bb5ae5..3fbf70561a7 100644 --- a/src/makielayout/blocks/checkbox.jl +++ b/src/makielayout/blocks/checkbox.jl @@ -54,7 +54,7 @@ function initialize_block!(c::Checkbox) visible = @lift($ischecked || $ishovered), ) - mouseevents = addmouseevents!(scene, shp, sc) + mouseevents = addmouseevents!(scene, checkboxrect) onmouseleftclick(mouseevents) do _ newstatus = c.onchange[](c.checked[]) @@ -72,7 +72,7 @@ function initialize_block!(c::Checkbox) on(scene, c.size; update = true) do sz c.layoutobservables.autosize[] = Float64.((sz, sz)) end - + return end diff --git a/test/interactivity/pick_tracking.jl b/test/interactivity/pick_tracking.jl index 2bc8fddcf37..7e967975374 100644 --- a/test/interactivity/pick_tracking.jl +++ b/test/interactivity/pick_tracking.jl @@ -118,7 +118,6 @@ f = Figure(size = (100, 100)) Makie.Checkbox(f[1, 1]) - f Makie.update_state_before_display!(f) # click @@ -134,7 +133,6 @@ f = Figure(size = (200, 100)) Makie.IntervalSlider(f[1, 1]) - f Makie.update_state_before_display!(f) # left end drag From a2eea7c617a752e15a097430663a973999918e73 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 23 Feb 2025 15:27:28 +0100 Subject: [PATCH 07/16] track pixking in Axis, Axis3, Scene too --- test/interactivity/Axis.jl | 314 ++++++++++++++------------ test/interactivity/Axis3.jl | 6 + test/interactivity/camera_controls.jl | 5 + 3 files changed, 177 insertions(+), 148 deletions(-) diff --git a/test/interactivity/Axis.jl b/test/interactivity/Axis.jl index 3d35e5a1909..27ba8e92f7a 100644 --- a/test/interactivity/Axis.jl +++ b/test/interactivity/Axis.jl @@ -1,187 +1,205 @@ -# TODO: test more -# Note: zoom_pan.jl includes more tests for this -@testset "Axis Interaction Interface" begin - f = Figure(size = (400, 400)) - a = Axis(f[1, 1]) - e = events(f) - - names = (:rectanglezoom, :dragpan, :limitreset, :scrollzoom) - @test keys(a.interactions) == Set(names) - - types = (Makie.RectangleZoom, Makie.DragPan, Makie.LimitReset, Makie.ScrollZoom) - for (name, type) in zip(names, types) - @test a.interactions[name][1] == true - @test a.interactions[name][2] isa type - end +# TODO: test more? +@testset "Axis Interactions" begin + Makie.PICK_TRACKING[] = true + init = Makie._PICK_COUNTER[] + + @testset "Axis Interaction Interface" begin + f = Figure(size = (400, 400)) + a = Axis(f[1, 1]) + e = events(f) + + names = (:rectanglezoom, :dragpan, :limitreset, :scrollzoom) + @test keys(a.interactions) == Set(names) + + types = (Makie.RectangleZoom, Makie.DragPan, Makie.LimitReset, Makie.ScrollZoom) + for (name, type) in zip(names, types) + @test a.interactions[name][1] == true + @test a.interactions[name][2] isa type + end - blocked = Observable(true) - on(x -> blocked[] = false, e.scroll, priority = typemin(Int)) - - @assert !is_mouseinside(a.scene) - e.scroll[] = (0.0, 0.0) - @test !blocked[] - blocked[] = true - e.scroll[] = (0.0, 1.0) - @test !blocked[] - - blocked[] = true - e.mouseposition[] = (200, 200) - e.scroll[] = (0.0, 0.0) - @test blocked[] # TODO: should it block? - blocked[] = true - e.scroll[] = (0.0, 1.0) - @test blocked[] - - deactivate_interaction!.((a,), names) - - blocked[] = true - e.scroll[] = (0.0, 0.0) - @test !blocked[] - blocked[] = true - e.scroll[] = (0.0, 1.0) - @test !blocked[] -end - -function cleanaxes() - fig = Figure() - ax = Axis(fig[1, 1]) - axbox = viewport(ax.scene)[] - lim = ax.finallimits[] - e = events(ax) - return ax, axbox, lim, e -end - -@testset "Axis zoom interactions" begin - ax, axbox, lim, e = cleanaxes() - - @testset "Center zoom" begin - e.mouseposition[] = Tuple(axbox.origin + axbox.widths/2) + blocked = Observable(true) + on(x -> blocked[] = false, e.scroll, priority = typemin(Int)) - # zoom in + @assert !is_mouseinside(a.scene) + e.scroll[] = (0.0, 0.0) + @test !blocked[] + blocked[] = true e.scroll[] = (0.0, 1.0) - newlim = ax.finallimits[] - @test newlim.widths ≈ 0.9 * lim.widths - @test newlim.origin ≈ lim.origin + (lim.widths - newlim.widths)/2 - - # zoom out restores original position - e.scroll[] = (0.0, -1.0) - newlim = ax.finallimits[] - @test newlim.widths ≈ lim.widths - @test all(abs.(newlim.origin - lim.origin) .< 1e-7*lim.widths) - end + @test !blocked[] - ax.finallimits[] = lim + blocked[] = true + e.mouseposition[] = (200, 200) + e.scroll[] = (0.0, 0.0) + @test blocked[] # TODO: should it block? + blocked[] = true + e.scroll[] = (0.0, 1.0) + @test blocked[] - @testset "Corner zoom" begin - e.mouseposition[] = Tuple(axbox.origin) + deactivate_interaction!.((a,), names) - # zoom in + blocked[] = true + e.scroll[] = (0.0, 0.0) + @test !blocked[] + blocked[] = true e.scroll[] = (0.0, 1.0) - newlim = ax.finallimits[] - @test newlim.widths ≈ 0.9 * lim.widths - @test all(abs.(newlim.origin - lim.origin) .< 1e-7*lim.widths) - - # zoom out - e.scroll[] = (0.0, -1.0) - newlim = ax.finallimits[] - @test newlim.widths ≈ lim.widths - @test all(abs.(newlim.origin - lim.origin) .< 1e-7*lim.widths) + @test !blocked[] end - ax.finallimits[] = lim + @test init == Makie._PICK_COUNTER[] - # Zoom only x or y - for (lockname, idx, zoomkey) in ((:xzoomlock, 1, :yzoomkey), (:yzoomlock, 2, :xzoomkey)) - ax, axbox, lim, e = cleanaxes() + function cleanaxes() + fig = Figure() + ax = Axis(fig[1, 1]) + axbox = viewport(ax.scene)[] + lim = ax.finallimits[] + e = events(ax) + return ax, axbox, lim, e + end - @testset "$lockname" begin - lock = getproperty(ax, lockname) - @test !lock[] + @testset "Axis zoom interactions" begin + ax, axbox, lim, e = cleanaxes() - # Zoom with lock - lock[] = true + @testset "Center zoom" begin e.mouseposition[] = Tuple(axbox.origin + axbox.widths/2) + + # zoom in e.scroll[] = (0.0, 1.0) newlim = ax.finallimits[] - @test newlim.widths[idx] == lim.widths[idx] - @test newlim.widths[3-idx] ≈ 0.9 * lim.widths[3-idx] - @test newlim.origin[idx] == lim.origin[idx] - @test newlim.origin[3-idx] ≈ lim.origin[3-idx] + (lim.widths[3-idx] - newlim.widths[3-idx])/2 + @test newlim.widths ≈ 0.9 * lim.widths + @test newlim.origin ≈ lim.origin + (lim.widths - newlim.widths)/2 - # Revert zoom + # zoom out restores original position e.scroll[] = (0.0, -1.0) newlim = ax.finallimits[] @test newlim.widths ≈ lim.widths @test all(abs.(newlim.origin - lim.origin) .< 1e-7*lim.widths) - ax.finallimits[] = lim - lock[] = false end - @testset "$zoomkey" begin - @test isempty(e.keyboardstate) - e.keyboardbutton[] = KeyEvent(getproperty(ax, zoomkey)[], Keyboard.press) + @test init == Makie._PICK_COUNTER[] + ax.finallimits[] = lim + + @testset "Corner zoom" begin + e.mouseposition[] = Tuple(axbox.origin) - # zoom with restriction + # zoom in e.scroll[] = (0.0, 1.0) newlim = ax.finallimits[] - @test newlim.widths[idx] == lim.widths[idx] - @test newlim.widths[3-idx] ≈ 0.9 * lim.widths[3-idx] - @test newlim.origin[idx] == lim.origin[idx] - @test newlim.origin[3-idx] ≈ lim.origin[3-idx] + (lim.widths[3-idx] - newlim.widths[3-idx])/2 + @test newlim.widths ≈ 0.9 * lim.widths + @test all(abs.(newlim.origin - lim.origin) .< 1e-7*lim.widths) - # Revert zoom + # zoom out e.scroll[] = (0.0, -1.0) newlim = ax.finallimits[] @test newlim.widths ≈ lim.widths @test all(abs.(newlim.origin - lim.origin) .< 1e-7*lim.widths) end - end - # Rubber band selection - fig = Figure() - ax = Axis(fig[1, 1]) - plot!(ax, [10, 15, 20]) - Makie.update_state_before_display!(fig) - axbox = viewport(ax.scene)[] - lim = ax.finallimits[] - e = events(ax) + @test init == Makie._PICK_COUNTER[] + ax.finallimits[] = lim + + # Zoom only x or y + for (lockname, idx, zoomkey) in ((:xzoomlock, 1, :yzoomkey), (:yzoomlock, 2, :xzoomkey)) + ax, axbox, lim, e = cleanaxes() + + @testset "$lockname" begin + lock = getproperty(ax, lockname) + @test !lock[] + + # Zoom with lock + lock[] = true + e.mouseposition[] = Tuple(axbox.origin + axbox.widths/2) + e.scroll[] = (0.0, 1.0) + newlim = ax.finallimits[] + @test newlim.widths[idx] == lim.widths[idx] + @test newlim.widths[3-idx] ≈ 0.9 * lim.widths[3-idx] + @test newlim.origin[idx] == lim.origin[idx] + @test newlim.origin[3-idx] ≈ lim.origin[3-idx] + (lim.widths[3-idx] - newlim.widths[3-idx])/2 + + # Revert zoom + e.scroll[] = (0.0, -1.0) + newlim = ax.finallimits[] + @test newlim.widths ≈ lim.widths + @test all(abs.(newlim.origin - lim.origin) .< 1e-7*lim.widths) + ax.finallimits[] = lim + lock[] = false + end + + @testset "$zoomkey" begin + @test isempty(e.keyboardstate) + e.keyboardbutton[] = KeyEvent(getproperty(ax, zoomkey)[], Keyboard.press) + + # zoom with restriction + e.scroll[] = (0.0, 1.0) + newlim = ax.finallimits[] + @test newlim.widths[idx] == lim.widths[idx] + @test newlim.widths[3-idx] ≈ 0.9 * lim.widths[3-idx] + @test newlim.origin[idx] == lim.origin[idx] + @test newlim.origin[3-idx] ≈ lim.origin[3-idx] + (lim.widths[3-idx] - newlim.widths[3-idx])/2 + + # Revert zoom + e.scroll[] = (0.0, -1.0) + newlim = ax.finallimits[] + @test newlim.widths ≈ lim.widths + @test all(abs.(newlim.origin - lim.origin) .< 1e-7*lim.widths) + end + end - @testset "Selection Rectangle" begin - e.mouseposition[] = Tuple(axbox.origin) - e.mousebutton[] = MouseButtonEvent(Mouse.left, Mouse.press) - e.mouseposition[] = Tuple(axbox.origin + axbox.widths ./ Vec2(2, 3)) - e.mousebutton[] = MouseButtonEvent(Mouse.left, Mouse.release) - newlim = ax.finallimits[] - @test newlim.origin ≈ lim.origin atol = 1e-6 - @test newlim.widths ≈ lim.widths ./ Vec2(2, 3) atol = 1e-6 - end + @test init == Makie._PICK_COUNTER[] + + # Rubber band selection + fig = Figure() + ax = Axis(fig[1, 1]) + plot!(ax, [10, 15, 20]) + Makie.update_state_before_display!(fig) + axbox = viewport(ax.scene)[] + lim = ax.finallimits[] + e = events(ax) + + @testset "Selection Rectangle" begin + e.mouseposition[] = Tuple(axbox.origin) + e.mousebutton[] = MouseButtonEvent(Mouse.left, Mouse.press) + e.mouseposition[] = Tuple(axbox.origin + axbox.widths ./ Vec2(2, 3)) + e.mousebutton[] = MouseButtonEvent(Mouse.left, Mouse.release) + newlim = ax.finallimits[] + @test newlim.origin ≈ lim.origin atol = 1e-6 + @test newlim.widths ≈ lim.widths ./ Vec2(2, 3) atol = 1e-6 + end - # Ctrl-click to restore - @testset "Axis Reset" begin - @test isempty(e.keyboardstate) - e.keyboardbutton[] = KeyEvent(Keyboard.left_control, Keyboard.press) - @test isempty(e.mousebuttonstate) - e.mousebutton[] = MouseButtonEvent(Mouse.left, Mouse.press) - e.mousebutton[] = MouseButtonEvent(Mouse.left, Mouse.release) - e.keyboardbutton[] = KeyEvent(Keyboard.left_control, Keyboard.release) - newlim = ax.finallimits[] - @test lim ≈ newlim + @test init == Makie._PICK_COUNTER[] + + # Ctrl-click to restore + @testset "Axis Reset" begin + @test isempty(e.keyboardstate) + e.keyboardbutton[] = KeyEvent(Keyboard.left_control, Keyboard.press) + @test isempty(e.mousebuttonstate) + e.mousebutton[] = MouseButtonEvent(Mouse.left, Mouse.press) + e.mousebutton[] = MouseButtonEvent(Mouse.left, Mouse.release) + e.keyboardbutton[] = KeyEvent(Keyboard.left_control, Keyboard.release) + newlim = ax.finallimits[] + @test lim ≈ newlim + end + + @test init == Makie._PICK_COUNTER[] end -end -@testset "Axis Translation (pan)" begin - ax, axbox, lim, e = cleanaxes() + @test init == Makie._PICK_COUNTER[] + + @testset "Axis Translation (pan)" begin + ax, axbox, lim, e = cleanaxes() + + e.mouseposition[] = Tuple(axbox.origin + axbox.widths/2) + e.scroll[] = (0.0, 1.0) + newlim = ax.finallimits[] - e.mouseposition[] = Tuple(axbox.origin + axbox.widths/2) - e.scroll[] = (0.0, 1.0) - newlim = ax.finallimits[] + e.mouseposition[] = Tuple(axbox.origin) + e.mousebutton[] = MouseButtonEvent(ax.panbutton[], Mouse.press) + e.mouseposition[] = Tuple(axbox.origin + axbox.widths/10) + e.mousebutton[] = MouseButtonEvent(ax.panbutton[], Mouse.release) - e.mouseposition[] = Tuple(axbox.origin) - e.mousebutton[] = MouseButtonEvent(ax.panbutton[], Mouse.press) - e.mouseposition[] = Tuple(axbox.origin + axbox.widths/10) - e.mousebutton[] = MouseButtonEvent(ax.panbutton[], Mouse.release) + panlim = ax.finallimits[] + @test panlim.widths == newlim.widths + @test (5/4)*panlim.origin ≈ -newlim.origin atol = 1e-6 + end - panlim = ax.finallimits[] - @test panlim.widths == newlim.widths - @test (5/4)*panlim.origin ≈ -newlim.origin atol = 1e-6 -end + @test init == Makie._PICK_COUNTER[] +end \ No newline at end of file diff --git a/test/interactivity/Axis3.jl b/test/interactivity/Axis3.jl index 3eb820d553a..4b808945715 100644 --- a/test/interactivity/Axis3.jl +++ b/test/interactivity/Axis3.jl @@ -1,5 +1,9 @@ # TODO: test more @testset "Axis Interactions - Axis3" begin + + Makie.PICK_TRACKING[] = true + init = Makie._PICK_COUNTER[] + f = Figure(size = (400, 400)) a = Axis3(f[1, 1]) p = scatter!(a, Rect3f(Point3f(1,2,3), Vec3f(1,2,3))) @@ -66,4 +70,6 @@ e.mouseposition[] = (200, 200) e.scroll[] = (0.0, -4.0) @test a.targetlimits[] ≈ Rect3f(Point3f(0.95, 1.9, 2.85), Vec3f(1.1, 2.2, 3.3)) + + @test init == Makie._PICK_COUNTER[] end \ No newline at end of file diff --git a/test/interactivity/camera_controls.jl b/test/interactivity/camera_controls.jl index 05f1f988c2a..61ea40959e2 100644 --- a/test/interactivity/camera_controls.jl +++ b/test/interactivity/camera_controls.jl @@ -2,6 +2,9 @@ # This testset is based on the results the current camera system has. If # cam3d! is updated this is likely to break. @testset "cam3d!" begin + Makie.PICK_TRACKING[] = true + init = Makie._PICK_COUNTER[] + scene = Scene(size=(800, 600)); e = events(scene) cam3d!(scene, fixed_axis=true, cad=false, zoom_shift_lookat=false) @@ -95,4 +98,6 @@ @test cc.lookat[] ≈ Vec3f(0) @test cc.eyeposition[] ≈ 0.6830134f0 * Vec3f(3) @test cc.upvector[] ≈ Vec3f(0.0, 0.0, 1.0) + + @test init == Makie._PICK_COUNTER[] end \ No newline at end of file From e7c5450cf3a31818423226634bc69abd62108b28 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 24 Feb 2025 15:55:51 +0100 Subject: [PATCH 08/16] avoid Menu scene resizing when hidden 72 -> 48 updates per letter typed in Textbox --- src/makielayout/blocks/menu.jl | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/makielayout/blocks/menu.jl b/src/makielayout/blocks/menu.jl index 663b80f282b..0270acfb8b8 100644 --- a/src/makielayout/blocks/menu.jl +++ b/src/makielayout/blocks/menu.jl @@ -23,18 +23,25 @@ function initialize_block!(m::Menu; default = 1) end end - scenearea = lift(blockscene, m.layoutobservables.computedbbox, listheight, _direction, m.is_open; - ignore_equal_values=true) do bbox, h, d, open - !open ? - round_to_IRect2D(BBox(left(bbox), right(bbox), 0, 0)) : - round_to_IRect2D(BBox( + scenearea = Observable(Rect2i(0,0,0,0), ignore_equal_values=true) + map!(blockscene, scenearea, m.layoutobservables.computedbbox, listheight, _direction, m.is_open; + update = true) do bbox, h, d, open + if open + return round_to_IRect2D(BBox( left(bbox), right(bbox), d === :down ? max(0, bottom(bbox) - h) : top(bbox), - d === :down ? bottom(bbox) : min(top(bbox) + h, top(blockscene.viewport[])))) + d === :down ? bottom(bbox) : min(top(bbox) + h, top(blockscene.viewport[])) + )) + else + # If the scene is not visible the scene placement and size does not + # matter for rendering. We still need to set the size to 0 for + # interactions though. + return Rect2i(0,0,0,0) + end end - menuscene = Scene(blockscene, scenearea, camera = campixel!, clear=true) + menuscene = Scene(blockscene, scenearea, camera = campixel!, clear=true, visible = m.is_open) translate!(menuscene, 0, 0, 200) onany(blockscene, scenearea, listheight) do area, listheight From 55cd8a729d4d812669d42c6e2648c0f549bd10fa Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 24 Feb 2025 16:09:02 +0100 Subject: [PATCH 09/16] don't update options when they are hidden 48 -> 39 updates per letter typed in textbox with 100 options in Menu --- src/camera/camera.jl | 2 +- src/makielayout/blocks/menu.jl | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/camera/camera.jl b/src/camera/camera.jl index 96c3a458a5e..c92cd4d4cf8 100644 --- a/src/camera/camera.jl +++ b/src/camera/camera.jl @@ -118,7 +118,7 @@ Returns true if the current mouseposition is inside the given scene. """ is_mouseinside(x) = is_mouseinside(get_scene(x)) function is_mouseinside(scene::Scene) - return Vec(scene.events.mouseposition[]) in viewport(scene)[] + return scene.visible[] && in(Vec(scene.events.mouseposition[]), viewport(scene)[]) # Check that mouse is not inside any other screen # for child in scene.children # is_mouseinside(child) && return false diff --git a/src/makielayout/blocks/menu.jl b/src/makielayout/blocks/menu.jl index 0270acfb8b8..d349626507d 100644 --- a/src/makielayout/blocks/menu.jl +++ b/src/makielayout/blocks/menu.jl @@ -117,7 +117,10 @@ function initialize_block!(m::Menu; default = 1) optiontexts = text!(menuscene, textpositions, text = optionstrings, align = (:left, :center), fontsize = m.fontsize, inspectable = false) - onany(blockscene, optionstrings, m.textpadding, m.layoutobservables.computedbbox) do _, pad, bbox + onany(blockscene, optionstrings, m.textpadding, scenearea) do _, pad, bbox + # No need to update when the scene is hidden + widths(bbox) == Vec2i(0) && return + gcs = optiontexts.plots[1][1][]::Vector{GlyphCollection} bbs = map(x -> string_boundingbox(x, zero(Point3f), Quaternion(0, 0, 0, 0)), gcs) heights = map(bb -> height(bb) + pad[3] + pad[4], bbs) @@ -137,6 +140,7 @@ function initialize_block!(m::Menu; default = 1) update_option_colors!(0) notify(optionrects) + return end notify(optionstrings) From 7cd59776bb64daa40c2a5ec193dbc29526485887 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 24 Feb 2025 16:52:52 +0100 Subject: [PATCH 10/16] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4596871baf..74c91a96bb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ - Reverted change to `poly` which disallowed 3D geometries from being plotted [#4738](https://github.com/MakieOrg/Makie.jl/pull/4738) - Enabled autocompletion on Block types, e.g. `?Axis.xti...` [#4786](https://github.com/MakieOrg/Makie.jl/pull/4786) - Added `dpi` metadata to all rendered png files, where `px_per_unit = 1` means 96dpi, `px_per_unit = 2` means 192dpi, and so on. This gives frontends a chance to show plain Makie png images with the correct scaling [#4812](https://github.com/MakieOrg/Makie.jl/pull/4812). +- Improved performance of some Blocks, mainly `Textbox` and `Menu` [#4821](https://github.com/MakieOrg/Makie.jl/pull/4821) ## [0.22.1] - 2025-01-17 From 25f463c2db9333628584bc94d00ddb9107ad3a16 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 24 Feb 2025 17:48:52 +0100 Subject: [PATCH 11/16] add some message counting tests --- WGLMakie/test/message_counting.jl | 102 ++++++++++++++++++++++++++++++ WGLMakie/test/runtests.jl | 7 +- 2 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 WGLMakie/test/message_counting.jl diff --git a/WGLMakie/test/message_counting.jl b/WGLMakie/test/message_counting.jl new file mode 100644 index 00000000000..fc8506eea48 --- /dev/null +++ b/WGLMakie/test/message_counting.jl @@ -0,0 +1,102 @@ +# These tests are relevant for general Makie performance but only tested here +# because Bonito makes it easy to test. If the tests fail it is likely not +# WGLMakies fault. +# A lower number of messages maybe caused by optimizations or broken interactions. +# A higher number could come from added functionality or performance regressions. + + +@testset "TextBox with Menu" begin + f = Figure() + t1 = Textbox(f[1, 1]) + m = Menu(f[2, 1], options = string.(1:1000)) + display(edisplay, App(() -> f)) + + # trigger select + all_messages, summary_str = Bonito.collect_messages() do + events(f).mouseposition[] = (300, 250) + events(f).mousebutton[] = Makie.MouseButtonEvent(Mouse.left, Mouse.press) + events(f).mousebutton[] = Makie.MouseButtonEvent(Mouse.left, Mouse.release) + end + @test length(all_messages) == 44 + + # type text + for (char, expected) in zip(collect("test"), [18, 39, 39, 39]) + _key = eval(:(Makie.Keyboard.$(Symbol(char)))) + all_messages, summary_str = Bonito.collect_messages() do + events(f).keyboardbutton[] = Makie.KeyEvent(_key, Keyboard.press) + events(f).unicode_input[] = char + events(f).keyboardbutton[] = Makie.KeyEvent(_key, Keyboard.release) + end + @test length(all_messages) == expected + end +end + +@testset "Menu" begin + f = Figure() + m = Menu(f[1,1], options = string.(1:10)) + display(edisplay, App(() -> f)) + + # open menu + all_messages, summary_str = Bonito.collect_messages() do + events(f).mouseposition[] = (300, 230) + events(f).mousebutton[] = Makie.MouseButtonEvent(Mouse.left, Mouse.press) + events(f).mousebutton[] = Makie.MouseButtonEvent(Mouse.left, Mouse.release) + end + @test length(all_messages) == 102 + + # scroll items + all_messages, summary_str = Bonito.collect_messages() do + events(f).mouseposition[] = (300, 200) + events(f).scroll[] = (0.0, -1.0) + end + @test length(all_messages) == 16 + + # select item + all_messages, summary_str = Bonito.collect_messages() do + events(f).mousebutton[] = Makie.MouseButtonEvent(Mouse.left, Mouse.press) + events(f).mousebutton[] = Makie.MouseButtonEvent(Mouse.left, Mouse.release) + end + @test length(all_messages) == 29 +end + +@testset "Textbox" begin + f = Figure() + t1 = Textbox(f[1, 1], tellwidth = false) + display(edisplay, App(() -> f)) + + all_messages, summary_str = Bonito.collect_messages() do + events(f).mouseposition[] = (300, 225) + events(f).mousebutton[] = Makie.MouseButtonEvent(Mouse.left, Mouse.press) + events(f).mousebutton[] = Makie.MouseButtonEvent(Mouse.left, Mouse.release) + end + @test length(all_messages) == 34 + + all_messages, summary_str = Bonito.collect_messages() do + events(f).keyboardbutton[] = Makie.KeyEvent(Keyboard.t, Keyboard.press) + events(f).unicode_input[] = 't' + events(f).keyboardbutton[] = Makie.KeyEvent(Keyboard.t, Keyboard.release) + end + @test length(all_messages) == 18 + + all_messages, summary_str = Bonito.collect_messages() do + events(f).keyboardbutton[] = Makie.KeyEvent(Keyboard.e, Keyboard.press) + events(f).unicode_input[] = 'e' + events(f).keyboardbutton[] = Makie.KeyEvent(Keyboard.e, Keyboard.release) + end + @test length(all_messages) == 30 + + all_messages, summary_str = Bonito.collect_messages() do + events(f).keyboardbutton[] = Makie.KeyEvent(Keyboard.s, Keyboard.press) + events(f).unicode_input[] = 's' + events(f).keyboardbutton[] = Makie.KeyEvent(Keyboard.s, Keyboard.release) + end + @test length(all_messages) == 30 + + all_messages, summary_str = Bonito.collect_messages() do + events(f).keyboardbutton[] = Makie.KeyEvent(Keyboard.t, Keyboard.press) + events(f).unicode_input[] = 't' + events(f).keyboardbutton[] = Makie.KeyEvent(Keyboard.t, Keyboard.release) + end + @test length(all_messages) == 30 + +end \ No newline at end of file diff --git a/WGLMakie/test/runtests.jl b/WGLMakie/test/runtests.jl index 0ddbeeca214..48fb11b5d5a 100644 --- a/WGLMakie/test/runtests.jl +++ b/WGLMakie/test/runtests.jl @@ -37,14 +37,19 @@ Makie.inline!(Makie.automatic) edisplay = Bonito.use_electron_display(devtools=true) @testset "reference tests" begin + WGLMakie.activate!() @testset "refimages" begin - WGLMakie.activate!() ReferenceTests.mark_broken_tests(excludes) recorded_files, recording_dir = @include_reference_tests WGLMakie "refimages.jl" missing_images, scores = ReferenceTests.record_comparison(recording_dir, "WGLMakie") ReferenceTests.test_comparison(scores; threshold = 0.05) end + @testset "Message counts / Update frequencies" begin + # Uses edisplay + include("message_counting.jl") + end + @testset "memory leaks" begin Makie.CURRENT_FIGURE[] = nothing app = App(nothing) From b85f771e0c3ba37ad21429b9bbf21f7d52c152eb Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 25 Feb 2025 15:04:06 +0100 Subject: [PATCH 12/16] skip message counting tests for now --- WGLMakie/test/runtests.jl | 5 ----- 1 file changed, 5 deletions(-) diff --git a/WGLMakie/test/runtests.jl b/WGLMakie/test/runtests.jl index 48fb11b5d5a..7ea460e7b72 100644 --- a/WGLMakie/test/runtests.jl +++ b/WGLMakie/test/runtests.jl @@ -45,11 +45,6 @@ edisplay = Bonito.use_electron_display(devtools=true) ReferenceTests.test_comparison(scores; threshold = 0.05) end - @testset "Message counts / Update frequencies" begin - # Uses edisplay - include("message_counting.jl") - end - @testset "memory leaks" begin Makie.CURRENT_FIGURE[] = nothing app = App(nothing) From 7345642e9d6cc9758675a12f2db8bafc2a18c2b8 Mon Sep 17 00:00:00 2001 From: Frederic Freyer Date: Tue, 25 Feb 2025 16:07:04 +0100 Subject: [PATCH 13/16] replace eval with getproperty [skip ci] --- WGLMakie/test/message_counting.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WGLMakie/test/message_counting.jl b/WGLMakie/test/message_counting.jl index fc8506eea48..04e1b2d5981 100644 --- a/WGLMakie/test/message_counting.jl +++ b/WGLMakie/test/message_counting.jl @@ -21,7 +21,7 @@ # type text for (char, expected) in zip(collect("test"), [18, 39, 39, 39]) - _key = eval(:(Makie.Keyboard.$(Symbol(char)))) + _key = getproperty(Makie.Keyboard, Symbol(char)) all_messages, summary_str = Bonito.collect_messages() do events(f).keyboardbutton[] = Makie.KeyEvent(_key, Keyboard.press) events(f).unicode_input[] = char @@ -99,4 +99,4 @@ end end @test length(all_messages) == 30 -end \ No newline at end of file +end From 972368290a20ed79eb2664fd30ee6b4575457e99 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 25 Feb 2025 16:41:43 +0100 Subject: [PATCH 14/16] fix Menu direction --- src/makielayout/blocks/menu.jl | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/makielayout/blocks/menu.jl b/src/makielayout/blocks/menu.jl index d349626507d..4ea98fa0d6c 100644 --- a/src/makielayout/blocks/menu.jl +++ b/src/makielayout/blocks/menu.jl @@ -117,24 +117,33 @@ function initialize_block!(m::Menu; default = 1) optiontexts = text!(menuscene, textpositions, text = optionstrings, align = (:left, :center), fontsize = m.fontsize, inspectable = false) - onany(blockscene, optionstrings, m.textpadding, scenearea) do _, pad, bbox - # No need to update when the scene is hidden - widths(bbox) == Vec2i(0) && return - + # listheight needs to be up to date before showing the menuscene so that its + # direction is correct + gc_heights = map(blockscene, optiontexts.plots[1][1], m.textpadding) do gcs, pad gcs = optiontexts.plots[1][1][]::Vector{GlyphCollection} bbs = map(x -> string_boundingbox(x, zero(Point3f), Quaternion(0, 0, 0, 0)), gcs) heights = map(bb -> height(bb) + pad[3] + pad[4], bbs) - heights_cumsum = [zero(eltype(heights)); cumsum(heights)] h = sum(heights) + listheight[] = h + return (heights, h) + end + + onany(blockscene, gc_heights, scenearea) do (heights, h), bbox + # No need to update when the scene is hidden + widths(bbox) == Vec2i(0) && return + + pad = m.textpadding[] # gc_heights triggers on padding, so we don't need to react to it + # listheight[] = h + + heights_cumsum = [zero(eltype(heights)); cumsum(heights)] list_y_bounds[] = h .- heights_cumsum texts_y = @views h .- 0.5 .* (heights_cumsum[1:end-1] .+ heights_cumsum[2:end]) textpositions[] = Point2f.(pad[1], texts_y) - listheight[] = h w_bbox = width(bbox) # need to manipulate the vectors themselves, otherwise update errors when lengths change - resize!(optionrects.val, length(bbs)) + resize!(optionrects.val, length(heights)) - optionrects.val .= map(eachindex(bbs)) do i + optionrects.val .= map(eachindex(heights)) do i BBox(0, w_bbox, h - heights_cumsum[i+1], h - heights_cumsum[i]) end From 57928deeffa870847c76d4046641b46bb73ace59 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 25 Feb 2025 16:48:13 +0100 Subject: [PATCH 15/16] tweak Menu refimg test to include directions, simulated interaction, highlighting --- ReferenceTests/src/tests/figures_and_makielayout.jl | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/ReferenceTests/src/tests/figures_and_makielayout.jl b/ReferenceTests/src/tests/figures_and_makielayout.jl index 855964428c1..9605122a280 100644 --- a/ReferenceTests/src/tests/figures_and_makielayout.jl +++ b/ReferenceTests/src/tests/figures_and_makielayout.jl @@ -50,12 +50,21 @@ end [ Label(fig, "A", width = nothing) Label(fig, "C", width = nothing); menu1 menu3; + Box(fig, visible = false) Box(fig, visible = false); Label(fig, "B", width = nothing) Label(fig, "D", width = nothing); menu2 menu4; ] ) - menu2.is_open = true + + # simulated interaction + events(fig).mouseposition[] = (500, 380) + events(fig).mousebutton[] = Makie.MouseButtonEvent(Mouse.left, Mouse.press) + events(fig).mousebutton[] = Makie.MouseButtonEvent(Mouse.left, Mouse.release) + events(fig).mouseposition[] = (500, 320) + + menu1.is_open = true menu4.is_open = true + fig end From 685cb922793fb50cd5a8c8aebe62d24bf455309c Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 25 Feb 2025 18:55:43 +0100 Subject: [PATCH 16/16] remove simulated interactions --- ReferenceTests/src/tests/figures_and_makielayout.jl | 6 ------ 1 file changed, 6 deletions(-) diff --git a/ReferenceTests/src/tests/figures_and_makielayout.jl b/ReferenceTests/src/tests/figures_and_makielayout.jl index 9605122a280..129ba5b47f6 100644 --- a/ReferenceTests/src/tests/figures_and_makielayout.jl +++ b/ReferenceTests/src/tests/figures_and_makielayout.jl @@ -56,12 +56,6 @@ end ] ) - # simulated interaction - events(fig).mouseposition[] = (500, 380) - events(fig).mousebutton[] = Makie.MouseButtonEvent(Mouse.left, Mouse.press) - events(fig).mousebutton[] = Makie.MouseButtonEvent(Mouse.left, Mouse.release) - events(fig).mouseposition[] = (500, 320) - menu1.is_open = true menu4.is_open = true