diff --git a/CHANGELOG.md b/CHANGELOG.md index fb0a8d822..44ffc5fc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,71 +1,17 @@ -# Other branches (not yet merged) +# master -# `master` -- Changed default font to monospace in showlibrary (#190) -- Got `rotate!(Accum, ...)` working (#196) -- Updated Stepper documentation and allowed for custom file formats (#197) +## New features -# v0.9.10 -- Fixed two-dimensional limits when passed to `Scene` (#195) +- Added interactions model to LAxis. Interactions can be added with `register_interaction!` and removed with `deregister_interaction!`, as well as activated or deactivated temporarily with `activate_interaction!` and `deactivate_interaction!`. +- Added `labelslidergrid!`, a function to create an internally aligned grid of sliders with labels and value-labels. +- LAxis has attributes `xrectzoom` and `yrectzoom` that control if the rectangle zoom changes the respective dimension or not. -# v0.9.9 -- Added a custom docstring extension which allows the Attributes of a Recipe to be shown in - the help mode (#174). -- Documented a lot of internal features (#174). -- Added a new 3d camera type, `cam3d_cad!`(#161). -- Improved warning text when displaying to text or plotpane (#163). -- Ensured that unless `inline!(true)` was called, plots will always display in - interactive displays, even in Juno (#163). -- Added licenses for fonts shipped with AbstractPlotting (#160). -- Switched from using system `ffmpeg` to using `FFMPEG.jl` (#160). -- Better docstrings for recording functions (#160). -- Let certain attributes passed to mutating plot functions affect the Scene (#160). -- Changed the default theme for `colorlegend` so that it scales with the resolution of the scene. -- Provided a way to generate a lower quality texture atlas via `set_glyph_resolution!(Low)` - to make the WebGL backend more lightweight (#180). -- Fixed `scale_plot` not actually working (#180). -- Cleaned up theme merging & scene attribute composition, so this works (#180): - ```julia - scatter(rand(4), resolution = (200, 200)) - scatter(rand(4), limits = ((0, 0), (200, 200))) - ``` +## Improvements -## Internal changes -- Replaced the `nothing` conversion trait with a new `NoConversion` trait, for clarity (#150). -- A lot of backend API changes to accomodate `WGLMakie` (#160). -- Enabled Gitlab CI! +- Cleaned up LSlider style and implementation, LSlider doesn't react while below other objects anymore +- MakieLayout Mouse event types are now enum instances for less compilation +- MakieLayout Mouse events store position in both data and pixel coordinates +- Aligned colors of LMenu, LSlider, LButton etc better -# v0.9.8 -## User interface changes -- Recipe docstrings are now associated with two functions instead of six (#116). -- New title recipe! (#99) -- Fixed buttons not respecting some attributes, and add a padding option (#114). -- Added `showlibrary(grad::Symbol)` function to show color libraries in `PlotUtils.jl` (#116). -- Added `showgradients` function to show an Array of gradients, indicated by Symbols (#116). -- Fixed incorrect frame duration in `record`. (#132) -- Fixed `save(path, io::VideoStream)` when file type is `.mkv` (#137). -- Improveed camera zooming (#140). +## Bugfixes -## Internal/other changes -- Changeed default alignment of buttons to (:center, :center) (#115) -- Documented a lot of recipes and functions -- Exported `HyperRectangle`, `update_limits!`, `update!`, and more... -- Updated `VideoStream`, `save` docstrings. -- Reworked tests to use `MakieGallery` (pretty big feature on the backend) -- Enabled Travis CI! - -# Additions - -## streamplot - -```julia -using MakieGallery, Makie -run_example("streamplot") -``` - -## timeseries - -```julia -using MakieGallery, Makie -run_example("timeseries") -``` diff --git a/src/makielayout/MakieLayout.jl b/src/makielayout/MakieLayout.jl index c59dd631e..03aec2189 100644 --- a/src/makielayout/MakieLayout.jl +++ b/src/makielayout/MakieLayout.jl @@ -43,13 +43,14 @@ function __init__() end include("geometrybasics_extension.jl") +include("mousestatemachine.jl") include("types.jl") include("helpers.jl") -include("mousestatemachine.jl") include("ticklocators/linear.jl") include("ticklocators/wilkinson.jl") include("defaultattributes.jl") include("lineaxis.jl") +include("interactions.jl") include("lobjects/laxis.jl") include("lobjects/lcolorbar.jl") include("lobjects/ltext.jl") @@ -84,8 +85,10 @@ export tight_xticklabel_spacing!, tight_yticklabel_spacing!, tight_ticklabel_spa export layoutscene export set_close_to! export xaxis_bottom!, xaxis_top!, yaxis_left!, yaxis_right! -export labelslider! -export addmousestate! +export labelslider!, labelslidergrid! +export addmouseevents! +export interactions, register_interaction!, deregister_interaction!, activate_interaction!, deactivate_interaction! +export MouseEventTypes, MouseEvent, ScrollEvent, KeysEvent export hlines!, vlines! diff --git a/src/makielayout/defaultattributes.jl b/src/makielayout/defaultattributes.jl index 3b693bc47..5d55152d6 100644 --- a/src/makielayout/defaultattributes.jl +++ b/src/makielayout/defaultattributes.jl @@ -109,6 +109,10 @@ function default_attributes(::Type{LAxis}, scene) xzoomlock = false "Locks interactive zooming in the y direction." yzoomlock = false + "Controls if rectangle zooming affects the x dimension." + xrectzoom = true + "Controls if rectangle zooming affects the y dimension." + yrectzoom = true "The width of the axis spines." spinewidth = 1f0 "Controls if the x grid lines are visible." @@ -433,7 +437,7 @@ function default_attributes(::Type{LButton}, scene) "The color of the button border." strokecolor = :transparent "The color of the button." - buttoncolor = RGBf0(0.9, 0.9, 0.9) + buttoncolor = RGBf0(0.94, 0.94, 0.94) "The color of the label." labelcolor = :black "The color of the label when the mouse hovers over the button." @@ -495,14 +499,12 @@ end function default_attributes(::Type{LSlider}, scene) attrs, docdict, defaultdict = @documented_attributes begin - "The line width of the main slider line." - linewidth = 4f0 "The horizontal alignment of the slider in its suggested bounding box." halign = :center "The vertical alignment of the slider in its suggested bounding box." valign = :center "The width setting of the slider." - width = nothing + width = Auto() "The height setting of the slider." height = Auto() "The range of values that the slider can pick from." @@ -511,24 +513,20 @@ function default_attributes(::Type{LSlider}, scene) tellwidth = true "Controls if the parent layout can adjust to this element's height" tellheight = true - "The radius of the slider button." - buttonradius = 9f0 "The start value of the slider or the value that is closest in the slider range." startvalue = 0 "The current value of the slider." value = 0 + "The width of the slider line" + linewidth = 15 "The color of the slider when the mouse hovers over it." color_active_dimmed = COLOR_ACCENT_DIMMED[] "The color of the slider when the mouse clicks and drags the slider." color_active = COLOR_ACCENT[] "The color of the slider when it is not interacted with." - color_inactive = RGBf0(0.9, 0.9, 0.9) - "The color of the button when it is not interacted with." - buttoncolor_inactive = RGBf0(1, 1, 1) + color_inactive = RGBf0(0.94, 0.94, 0.94) "Controls if the slider has a horizontal orientation or not." horizontal = true - "The line width of the slider button's border." - buttonstrokewidth = 4f0 "The align mode of the slider in its parent GridLayout." alignmode = Inside() end @@ -554,28 +552,28 @@ function default_attributes(::Type{LToggle}, scene) "The width of the toggle." width = 60 "The height of the toggle." - height = 30 + height = 28 "Controls if the parent layout can adjust to this element's width" tellwidth = true "Controls if the parent layout can adjust to this element's height" tellheight = true "The number of poly segments in each rounded corner." - cornersegments = 10 + cornersegments = 15 # strokewidth = 2f0 # strokecolor = :transparent "The color of the border when the toggle is inactive." - framecolor_inactive = RGBf0(0.9, 0.9, 0.9) - "The color of the border when the toggle is active." - framecolor_active = COLOR_ACCENT[] + framecolor_inactive = RGBf0(0.94, 0.94, 0.94) + "The color of the border when the toggle is hovered." + framecolor_active = COLOR_ACCENT_DIMMED[] # buttoncolor = RGBf0(0.2, 0.2, 0.2) "The color of the toggle button." - buttoncolor = RGBf0(1, 1, 1) + buttoncolor = COLOR_ACCENT[] "Indicates if the toggle is active or not." active = false "The duration of the toggle animation." - toggleduration = 0.2 + toggleduration = 0.15 "The border width as a fraction of the toggle height " - rimfraction = 0.25 + rimfraction = 0.33 "The align mode of the toggle in its parent GridLayout." alignmode = Inside() end diff --git a/src/makielayout/helpers.jl b/src/makielayout/helpers.jl index 13d4f1694..05d667348 100644 --- a/src/makielayout/helpers.jl +++ b/src/makielayout/helpers.jl @@ -426,7 +426,7 @@ layout[1, 1] = ls.layout ``` """ function labelslider!(scene, label, range; format = string, - sliderkw = Dict(), labelkw = Dict(), valuekw = Dict(), layoutkw...) + sliderkw = Dict(), labelkw = Dict(), valuekw = Dict(), layoutkw...) slider = LSlider(scene; range = range, sliderkw...) label = LText(scene, label; labelkw...) valuelabel = LText(scene, lift(format, slider.value); valuekw...) @@ -435,6 +435,50 @@ function labelslider!(scene, label, range; format = string, end +""" + labelslidergrid!(scene, labels, ranges; formats = [string], + sliderkw = Dict(), labelkw = Dict(), valuekw = Dict(), layoutkw...) + +Construct a GridLayout with a column of label, a column of sliders and a column of value labels in `scene`. +The argument values are broadcast, so you can use scalars if you want to keep labels, ranges or formats constant across rows. + +Returns a `NamedTuple`: + +`(sliders = sliders, labels = labels, valuelabels = valuelabels, layout = layout)` + +Specify format functions for the value labels with the `formats` keyword. +The sliders are forwarded the keywords from `sliderkw`. +The labels are forwarded the keywords from `labelkw`. +The value labels are forwarded the keywords from `valuekw`. +All other keywords are forwarded to the `GridLayout`. + +Example: + +``` +ls = labelslidergrid!(scene, ["Voltage", "Ampere"], Ref(0:0.1:100); format = x -> "\$(x)V") +layout[1, 1] = ls.layout +``` +""" +function labelslidergrid!(scene, labels, ranges; formats = [string], + sliderkw = Dict(), labelkw = Dict(), valuekw = Dict(), layoutkw...) + + elements = broadcast(labels, ranges, formats) do label, range, format + slider = LSlider(scene; range = range, sliderkw...) + label = LText(scene, label; halign = :left, labelkw...) + valuelabel = LText(scene, lift(format, slider.value); halign = :right, valuekw...) + (; slider = slider, label = label, valuelabel = valuelabel) + end + + sliders = map(x -> x.slider, elements) + labels = map(x -> x.label, elements) + valuelabels = map(x -> x.valuelabel, elements) + + layout = grid!(hcat(labels, sliders, valuelabels)) + + (sliders = sliders, labels = labels, valuelabels = valuelabels, layout = layout) +end + + # helper function to create either h or vlines depending on `direction` # this works only with LAxes because it needs to react to limit changes diff --git a/src/makielayout/interactions.jl b/src/makielayout/interactions.jl new file mode 100644 index 000000000..0a94b722c --- /dev/null +++ b/src/makielayout/interactions.jl @@ -0,0 +1,270 @@ +# overloadable for other types that might want to offer similar interactions +function interactions end + +interactions(ax::LAxis) = ax.interactions + +""" + register_interaction!(parent, name::Symbol, interaction) + +Register `interaction` with `parent` under the name `name`. +The parent will call `process_interaction(interaction, event, parent)` +whenever suitable events happen. + +The interaction can be removed with `deregister_interaction!` or temporarily +toggled with `activate_interaction!` / `deactivate_interaction!`. +""" +function register_interaction!(parent, name::Symbol, interaction) + haskey(interactions(parent), name) && error("Interaction $name already exists.") + registration_setup!(parent, interaction) + push!(interactions(parent), name => (true, interaction)) + return interaction +end + +""" + register_interaction!(interaction::Function, parent, name::Symbol) + +Register `interaction` with `parent` under the name `name`. +The parent will call `process_interaction(interaction, event, parent)` +whenever suitable events happen. +This form with the first `Function` argument is especially intended for `do` syntax. + +The interaction can be removed with `deregister_interaction!` or temporarily +toggled with `activate_interaction!` / `deactivate_interaction!`. +""" +function register_interaction!(interaction::Function, parent, name::Symbol) + haskey(interactions(parent), name) && error("Interaction $name already exists.") + registration_setup!(parent, interaction) + push!(interactions(parent), name => (true, interaction)) + return interaction +end + +""" + deregister_interaction!(parent, name::Symbol) + +Deregister the interaction named `name` registered in `parent`. +""" +function deregister_interaction!(parent, name::Symbol) + !haskey(interactions(parent), name) && error("Interaction $name does not exist.") + _, interaction = interactions(parent)[name] + + deregistration_cleanup!(parent, interaction) + pop!(interactions(parent), name) + return interaction +end + +function registration_setup!(parent, interaction) + # do nothing in the default case +end + +function deregistration_cleanup!(parent, interaction) + # do nothing in the default case +end + +""" + activate_interaction!(parent, name::Symbol) + +Activate the interaction named `name` registered in `parent`. +""" +function activate_interaction!(parent, name::Symbol) + !haskey(interactions(parent), name) && error("Interaction $name does not exist.") + interactions(parent)[name] = (true, interactions(parent)[name][2]) + return nothing +end + +""" + deactivate_interaction!(parent, name::Symbol) + +Deactivate the interaction named `name` registered in `parent`. +It can be reactivated with `activate_interaction!`. +""" +function deactivate_interaction!(parent, name::Symbol) + !haskey(interactions(parent), name) && error("Interaction $name does not exist.") + interactions(parent)[name] = (false, interactions(parent)[name][2]) + return nothing +end + + +function process_interaction(@nospecialize args...) + # do nothing in the default case +end + +# a generic fallback for functions to have one really simple path to getting interactivity +# without needing to define a special type first +function process_interaction(f::Function, event, parent) + # in case f is only defined for a specific type of event + if applicable(f, event, parent) + f(event, parent) + end +end + + + +############################################################################ +# LAxis interactions # +############################################################################ + +function _chosen_limits(rz, ax) + + r = positivize(FRect2D(rz.from, rz.to .- rz.from)) + lims = ax.limits[] + # restrict to y change + if rz.restrict_x || !ax.xrectzoom[] + r = FRect2D(lims.origin[1], r.origin[2], widths(lims)[1], widths(r)[2]) + end + # restrict to x change + if rz.restrict_y || !ax.yrectzoom[] + r = FRect2D(r.origin[1], lims.origin[2], widths(r)[1], widths(lims)[2]) + end + return r +end + +function process_interaction(r::RectangleZoom, event::MouseEvent, ax::LAxis) + + if event.type === MouseEventTypes.leftdragstart + r.from = event.prev_data + r.to = event.data + r.rectnode[] = _chosen_limits(r, ax) + r.poly = poly!(ax.scene, r.rectnode, color = (COLOR_ACCENT[], 0.1), strokewidth = 2, strokecolor = COLOR_ACCENT[])[end] + r.active = true + + elseif event.type === MouseEventTypes.leftdrag + r.to = event.data + r.rectnode[] = _chosen_limits(r, ax) + + elseif event.type === MouseEventTypes.leftdragstop + newlims = r.rectnode[] + if !(0 in widths(newlims)) + ax.targetlimits[] = newlims + end + + if !isnothing(r.poly) + delete!(ax.scene, r.poly) + r.poly = nothing + end + r.active = false + end + + return nothing +end + +function process_interaction(r::RectangleZoom, event::KeysEvent, ax::LAxis) + r.restrict_y = Keyboard.x in event.keys + r.restrict_x = Keyboard.y in event.keys + r.active || return + + r.rectnode[] = _chosen_limits(r, ax) + return nothing +end + + +function positivize(r::FRect2D) + negwidths = r.widths .< 0 + newori = ifelse.(negwidths, r.origin .+ r.widths, r.origin) + newwidths = ifelse.(negwidths, -r.widths, r.widths) + FRect2D(newori, newwidths) +end + + +function process_interaction(l::LimitReset, event::MouseEvent, ax::LAxis) + + if event.type === MouseEventTypes.leftclick + if ispressed(ax.scene, Keyboard.left_control) + autolimits!(ax) + end + end + + return nothing +end + + +function process_interaction(s::ScrollZoom, event::ScrollEvent, ax::LAxis) + # use vertical zoom + zoom = event.y + + tlimits = ax.targetlimits + xzoomlock = ax.xzoomlock + yzoomlock = ax.yzoomlock + xzoomkey = ax.xzoomkey + yzoomkey = ax.yzoomkey + + scene = ax.scene + e = events(scene) + cam = camera(scene) + + if zoom != 0 + pa = pixelarea(scene)[] + + # don't let z go negative + z = max(0.1f0, 1f0 - (abs(zoom) * s.speed)) + if zoom > 0 + z = 1/z # sets the old to be a fraction of the new. This ensures zoom in & then out returns to original position. + end + + mp_axscene = Vec4f0((e.mouseposition[] .- pa.origin)..., 0, 1) + + # first to normal -1..1 space + mp_axfraction = (cam.pixel_space[] * mp_axscene)[1:2] .* + # now to 1..-1 if an axis is reversed to correct zoom point + (-2 .* ((ax.xreversed[], ax.yreversed[])) .+ 1) .* + # now to 0..1 + 0.5 .+ 0.5 + + xorigin = tlimits[].origin[1] + yorigin = tlimits[].origin[2] + + xwidth = tlimits[].widths[1] + ywidth = tlimits[].widths[2] + + newxwidth = xzoomlock[] ? xwidth : xwidth * z + newywidth = yzoomlock[] ? ywidth : ywidth * z + + newxorigin = xzoomlock[] ? xorigin : xorigin + mp_axfraction[1] * (xwidth - newxwidth) + newyorigin = yzoomlock[] ? yorigin : yorigin + mp_axfraction[2] * (ywidth - newywidth) + + timed_ticklabelspace_reset(ax, s.reset_timer, s.prev_xticklabelspace, s.prev_yticklabelspace, s.reset_delay) + + tlimits[] = if ispressed(scene, xzoomkey[]) + FRect(newxorigin, yorigin, newxwidth, ywidth) + elseif ispressed(scene, yzoomkey[]) + FRect(xorigin, newyorigin, xwidth, newywidth) + else + FRect(newxorigin, newyorigin, newxwidth, newywidth) + end + + end +end + +function process_interaction(dp::DragPan, event::MouseEvent, ax) + + if event.type !== MouseEventTypes.rightdrag + return nothing + end + + tlimits = ax.targetlimits + xpanlock = ax.xpanlock + ypanlock = ax.ypanlock + xpankey = ax.xpankey + ypankey = ax.ypankey + panbutton = ax.panbutton + + scene = ax.scene + + movement = AbstractPlotting.to_world(ax.scene, event.px) .- + AbstractPlotting.to_world(ax.scene, event.prev_px) + + xori, yori = Vec2f0(tlimits[].origin) .- movement + + if xpanlock[] || ispressed(scene, ypankey[]) + xori = tlimits[].origin[1] + end + + if ypanlock[] || ispressed(scene, xpankey[]) + yori = tlimits[].origin[2] + end + + timed_ticklabelspace_reset(ax, dp.reset_timer, dp.prev_xticklabelspace, dp.prev_yticklabelspace, dp.reset_delay) + + tlimits[] = FRect(Vec2f0(xori, yori), widths(tlimits[])) + + return nothing +end diff --git a/src/makielayout/lobjects/laxis.jl b/src/makielayout/lobjects/laxis.jl index 3e6d6574a..14827c907 100644 --- a/src/makielayout/lobjects/laxis.jl +++ b/src/makielayout/lobjects/laxis.jl @@ -294,16 +294,52 @@ function LAxis(parent::Scene; bbox = nothing, kwargs...) # layout layoutobservables.suggestedbbox[] = layoutobservables.suggestedbbox[] + mouseevents = addmouseevents!(scene) + scrollevents = Node(ScrollEvent(0, 0)) + keysevents = Node(KeysEvent(Set())) + + on(scene.events.scroll) do s + if is_mouseinside(scene) + scrollevents[] = ScrollEvent(s[1], s[2]) + end + end + + on(scene.events.keyboardbuttons) do buttons + keysevents[] = KeysEvent(buttons) + end + + interactions = Dict{Symbol, Tuple{Bool, Any}}() + la = LAxis(parent, scene, xaxislinks, yaxislinks, limits, - layoutobservables, attrs, block_limit_linking, decorations) + layoutobservables, attrs, block_limit_linking, decorations, + mouseevents, scrollevents, keysevents, interactions) + + + function process_event(event) + for (active, interaction) in values(la.interactions) + active && process_interaction(interaction, event, la) + end + end + + on(process_event, mouseevents) + on(process_event, scrollevents) + on(process_event, keysevents) + + register_interaction!(la, + :rectanglezoom, + RectangleZoom(false, false, false, nothing, nothing, Node(FRect2D(0, 0, 1, 1)), nothing)) - # add action that resets limits on ctrl + click - add_reset_limits!(la) - # add action that allows zooming using mouse scrolling - add_zoom!(la) - # add action that allows panning using a mouse button - add_pan!(la) + register_interaction!(la, + :limitreset, + LimitReset()) + register_interaction!(la, + :scrollzoom, + ScrollZoom(0.1, Ref{Any}(nothing), Ref{Any}(0), Ref{Any}(0), 0.2)) + + register_interaction!(la, + :dragpan, + DragPan(Ref{Any}(nothing), Ref{Any}(0), Ref{Any}(0), 0.2)) # compute limits that adhere to the limit aspect ratio whenever the targeted # limits or the scene size change, because both influence the displayed ratio @@ -314,6 +350,7 @@ function LAxis(parent::Scene; bbox = nothing, kwargs...) la end + function AbstractPlotting.plot!( la::LAxis, P::AbstractPlotting.PlotFunc, attributes::AbstractPlotting.Attributes, args...; @@ -602,139 +639,6 @@ function linkyaxes!(a::LAxis, others...) autolimits!(a) end -function add_pan!(ax::LAxis) - - tlimits = ax.targetlimits - xpanlock = ax.xpanlock - ypanlock = ax.ypanlock - xpankey = ax.xpankey - ypankey = ax.ypankey - panbutton = ax.panbutton - - scene = ax.scene - - reset_timer = Ref{Any}(nothing) - prev_xticklabelspace = Ref{Any}(0) - prev_yticklabelspace = Ref{Any}(0) - - startpos = Base.RefValue((0.0, 0.0)) - e = events(scene) - on( - camera(scene), - # Node.((scene, cam, startpos))..., - Node.((scene, startpos))..., - e.mousedrag - ) do scene, startpos, dragging - mp = e.mouseposition[] - if ispressed(scene, panbutton[]) && is_mouseinside(scene) - window_area = pixelarea(scene)[] - if dragging == Mouse.down - startpos[] = mp - elseif dragging == Mouse.pressed && ispressed(scene, panbutton[]) - diff = startpos[] .- mp - startpos[] = mp - pxa = scene.px_area[] - diff_fraction = Vec2f0(diff) ./ Vec2f0(widths(pxa)) - - diff_limits = diff_fraction .* widths(tlimits[]) - - # correct for reversals - reversals = (ax.xreversed[], ax.yreversed[]) - - diff_limits = diff_limits .* (-2 .* reversals .+ 1) - - xori, yori = Vec2f0(tlimits[].origin) .+ Vec2f0(diff_limits) - - if xpanlock[] || ispressed(scene, ypankey[]) - xori = tlimits[].origin[1] - end - - if ypanlock[] || ispressed(scene, xpankey[]) - yori = tlimits[].origin[2] - end - - timed_ticklabelspace_reset(ax, reset_timer, prev_xticklabelspace, prev_yticklabelspace, 0.1) - - tlimits[] = FRect(Vec2f0(xori, yori), widths(tlimits[])) - end - end - return - end -end - -function add_zoom!(ax::LAxis) - - tlimits = ax.targetlimits - xzoomlock = ax.xzoomlock - yzoomlock = ax.yzoomlock - xzoomkey = ax.xzoomkey - yzoomkey = ax.yzoomkey - - scene = ax.scene - - e = events(scene) - cam = camera(scene) - - reset_timer = Ref{Any}(nothing) - prev_xticklabelspace = Ref{Any}(0) - prev_yticklabelspace = Ref{Any}(0) - - on(cam, e.scroll) do x - # @extractvalue cam (zoomspeed, zoombutton, area) - zoomspeed = 0.10f0 - zoombutton = nothing - zoom = Float32(x[2]) - if zoom != 0 && ispressed(scene, zoombutton) && AbstractPlotting.is_mouseinside(scene) - pa = pixelarea(scene)[] - - # don't let z go negative - z = max(0.1f0, 1f0 - (abs(zoom) * zoomspeed)) - if zoom > 0 - z = 1/z # sets the old to be a fraction of the new. This ensures zoom in & then out returns to original position. - end - - mp_axscene = Vec4f0((e.mouseposition[] .- pa.origin)..., 0, 1) - - # first to normal -1..1 space - mp_axfraction = (cam.pixel_space[] * mp_axscene)[1:2] .* - # now to 1..-1 if an axis is reversed to correct zoom point - (-2 .* ((ax.xreversed[], ax.yreversed[])) .+ 1) .* - # now to 0..1 - 0.5 .+ 0.5 - - xorigin = tlimits[].origin[1] - yorigin = tlimits[].origin[2] - - xwidth = tlimits[].widths[1] - ywidth = tlimits[].widths[2] - - newxwidth = xzoomlock[] ? xwidth : xwidth * z - newywidth = yzoomlock[] ? ywidth : ywidth * z - - newxorigin = xzoomlock[] ? xorigin : xorigin + mp_axfraction[1] * (xwidth - newxwidth) - newyorigin = yzoomlock[] ? yorigin : yorigin + mp_axfraction[2] * (ywidth - newywidth) - - timed_ticklabelspace_reset(ax, reset_timer, prev_xticklabelspace, prev_yticklabelspace, 0.1) - - tlimits[] = if AbstractPlotting.ispressed(scene, xzoomkey[]) - FRect(newxorigin, yorigin, newxwidth, ywidth) - elseif AbstractPlotting.ispressed(scene, yzoomkey[]) - FRect(xorigin, newyorigin, xwidth, newywidth) - else - FRect(newxorigin, newyorigin, newxwidth, newywidth) - end - - end - - return - end - - # Also support rubber band selection - rect = select_rectangle(scene) - on(rect) do r - tlimits[] = r - end -end """ Keeps the ticklabelspace static for a short duration and then resets it to its previous diff --git a/src/makielayout/lobjects/lbutton.jl b/src/makielayout/lobjects/lbutton.jl index ea2ada6ac..141b01b2d 100644 --- a/src/makielayout/lobjects/lbutton.jl +++ b/src/makielayout/lobjects/lbutton.jl @@ -61,24 +61,24 @@ function LButton(scene::Scene; bbox = nothing, kwargs...) - mousestate = addmousestate!(scene, button, labeltext) + mouseevents = addmouseevents!(scene, button, labeltext) - onmouseover(mousestate) do state + onmouseover(mouseevents) do state bcolor[] = buttoncolor_hover[] lcolor[] = labelcolor_hover[] end - onmouseout(mousestate) do state + onmouseout(mouseevents) do state bcolor[] = buttoncolor[] lcolor[] = labelcolor[] end - onmouseleftup(mousestate) do state + onmouseleftup(mouseevents) do state bcolor[] = buttoncolor_hover[] lcolor[] = labelcolor_hover[] end - onmouseleftdown(mousestate) do state + onmouseleftdown(mouseevents) do state bcolor[] = buttoncolor_active[] lcolor[] = labelcolor_active[] clicks[] = clicks[] + 1 diff --git a/src/makielayout/lobjects/lmenu.jl b/src/makielayout/lobjects/lmenu.jl index 7d6ac19cc..fb8ba9384 100644 --- a/src/makielayout/lobjects/lmenu.jl +++ b/src/makielayout/lobjects/lmenu.jl @@ -228,14 +228,14 @@ function LMenu(parent::Scene; bbox = nothing, kwargs...) rowgap!(contentgrid, 0) - mousestates = [addmousestate!(scene, r.rect, t.textobject) for (r, t) in zip(allrects, alltexts)] + mouseevents_vector = [addmouseevents!(scene, r.rect, t.textobject) for (r, t) in zip(allrects, alltexts)] - for (i, (mousestate, r, t)) in enumerate(zip(mousestates, allrects, alltexts)) - onmouseover(mousestate) do state + for (i, (mouseevents, r, t)) in enumerate(zip(mouseevents_vector, allrects, alltexts)) + onmouseover(mouseevents) do events r.color = cell_color_hover[] end - onmouseout(mousestate) do state + onmouseout(mouseevents) do events if i == 1 r.color = selection_cell_color_inactive[] else @@ -244,7 +244,7 @@ function LMenu(parent::Scene; bbox = nothing, kwargs...) end end - onmouseleftdown(mousestate) do state + onmouseleftdown(mouseevents) do events r.color = cell_color_active[] if is_open[] # first item is already selected @@ -257,7 +257,7 @@ function LMenu(parent::Scene; bbox = nothing, kwargs...) end # close the menu if the user clicks somewhere else - onmousedownoutside(addmousestate!(scene)) do state + onmousedownoutside(addmouseevents!(scene)) do events if is_open[] is_open[] = !is_open[] end diff --git a/src/makielayout/lobjects/lslider.jl b/src/makielayout/lobjects/lslider.jl index 38592fb6e..b9809dba0 100644 --- a/src/makielayout/lobjects/lslider.jl +++ b/src/makielayout/lobjects/lslider.jl @@ -7,9 +7,8 @@ function LSlider(parent::Scene; bbox = nothing, kwargs...) decorations = Dict{Symbol, Any}() @extract attrs ( - halign, valign, linewidth, buttonradius, horizontal, - startvalue, value, color_active, color_active_dimmed, color_inactive, - buttonstrokewidth, buttoncolor_inactive + halign, valign, horizontal, linewidth, + startvalue, value, color_active, color_active_dimmed, color_inactive ) sliderrange = attrs.range @@ -18,34 +17,29 @@ function LSlider(parent::Scene; bbox = nothing, kwargs...) layoutobservables = LayoutObservables{LSlider}(attrs.width, attrs.height, attrs.tellwidth, attrs.tellheight, halign, valign, attrs.alignmode; suggestedbbox = bbox, protrusions = protrusions) - onany(buttonradius, horizontal, buttonstrokewidth) do br, hori, bstrw - layoutobservables.autosize[] = if hori - (nothing, 2 * (br + bstrw)) + onany(linewidth, horizontal) do lw, horizontal + if horizontal + layoutobservables.autosize[] = (nothing, Float32(lw)) else - (2 * (br + bstrw), nothing) + layoutobservables.autosize[] = (Float32(lw), nothing) end end - subarea = lift(layoutobservables.computedbbox) do bbox - round_to_IRect2D(bbox) - end - - # the slider gets its own subscene so a click doesn't have to hit the line - # perfectly but can be registered in the whole area that the slider scene has - subscene = Scene(parent, subarea, camera=campixel!) - - sliderbox = lift(bb -> Rect{2, Float32}(zeros(eltype(bb.origin), 2), bb.widths), layoutobservables.computedbbox) + sliderbox = lift(identity, layoutobservables.computedbbox) endpoints = lift(sliderbox, horizontal) do bb, horizontal + h = height(bb) + w = width(bb) + if horizontal - y = bottom(bb) + height(bb) / 2 - [Point2f0(left(bb), y), - Point2f0(right(bb), y)] + y = bottom(bb) + h / 2 + [Point2f0(left(bb) + h/2, y), + Point2f0(right(bb) - h/2, y)] else - x = left(bb) + width(bb) / 2 - [Point2f0(x, bottom(bb)), - Point2f0(x, top(bb))] + x = left(bb) + w / 2 + [Point2f0(x, bottom(bb) + w/2), + Point2f0(x, top(bb) - w/2)] end end @@ -82,57 +76,39 @@ function LSlider(parent::Scene; bbox = nothing, kwargs...) # initialize slider value with closest from range selected_index[] = closest_index(sliderrange[], startvalue[]) - buttonpoint = lift(sliderbox, horizontal, displayed_sliderfraction, buttonradius, - buttonstrokewidth) do bb, horizontal, sf, brad, bstw - - pad = brad + bstw - - if horizontal - [Point2f0(left(bb) + pad + (width(bb) - 2pad) * sf, bottom(bb) + height(bb) / 2)] - else - [Point2f0(left(bb) + 0.5f0 * width(bb), bottom(bb) + pad + (height(bb) - 2pad) * sf)] - end + middlepoint = lift(endpoints, displayed_sliderfraction) do ep, sf + Point2f0(ep[1] .+ sf .* (ep[2] .- ep[1])) end - linepoints = lift(endpoints, buttonpoint) do eps, bp - [eps[1], bp[1], bp[1], eps[2]] + linepoints = lift(endpoints, middlepoint) do eps, middle + [eps[1], middle, middle, eps[2]] end linecolors = lift(color_active_dimmed, color_inactive) do ca, ci [ca, ci] end - linesegs = linesegments!(subscene, linepoints, color = linecolors, linewidth = linewidth, raw = true)[end] - decorations[:linesegments] = linesegs - - linestate = addmousestate!(subscene, linesegs) - - bsize = @lift($buttonradius * 2f0) - - bcolor = Node{Any}(buttoncolor_inactive[]) + endbuttons = scatter!(parent, endpoints, color = linecolors, markersize = linewidth, strokewidth = 0, raw = true)[end] + decorations[:endbuttons] = endbuttons + linesegs = linesegments!(parent, linepoints, color = linecolors, linewidth = linewidth, raw = true)[end] + decorations[:linesegments] = linesegs - button = scatter!(subscene, buttonpoint, markersize = bsize, color = bcolor, marker = '⚫', - strokewidth = buttonstrokewidth, strokecolor = color_active_dimmed, raw = true)[end] + button_magnification = Node(1.0) + buttonsize = @lift($linewidth * $button_magnification) + button = scatter!(parent, middlepoint, color = color_active, strokewidth = 0, markersize = buttonsize, raw = true)[end] decorations[:button] = button + mouseevents = addmouseevents!(parent, linesegs, button) - scenestate = addmousestate!(subscene) - - onmouseleftup(scenestate) do state - bcolor[] = buttoncolor_inactive[] - end - - onmouseleftdrag(scenestate) do state - - pad = buttonradius[] + buttonstrokewidth[] + onmouseleftdrag(mouseevents) do event dragging[] = true - dif = state.pos - state.prev + dif = event.px - event.prev_px fraction = if horizontal[] - dif[1] / (width(sliderbox[]) - 2pad) + dif[1] / (endpoints[][2][1] - endpoints[][1][1]) else - dif[2] / (height(sliderbox[]) - 2pad) + dif[2] / (endpoints[][2][2] - endpoints[][1][2]) end if fraction != 0.0f0 newfraction = min(max(displayed_sliderfraction[] + fraction, 0f0), 1f0) @@ -145,55 +121,39 @@ function LSlider(parent::Scene; bbox = nothing, kwargs...) end end - onmouseleftdragstop(scenestate) do state + onmouseleftdragstop(mouseevents) do event dragging[] = false # adjust slider to closest legal value sliderfraction[] = sliderfraction[] + linecolors[] = [color_active_dimmed[], color_inactive[]] end - onmouseleftdown(scenestate) do state - - bcolor[] = color_active[] + onmouseleftdown(mouseevents) do event - pad = buttonradius[] + buttonstrokewidth[] - - pos = state.pos + pos = event.px dim = horizontal[] ? 1 : 2 - frac = (pos[dim] - endpoints[][1][dim] - pad) / (endpoints[][2][dim] - endpoints[][1][dim] - 2pad) + frac = (pos[dim] - endpoints[][1][dim]) / (endpoints[][2][dim] - endpoints[][1][dim]) selected_index[] = closest_fractionindex(sliderrange[], frac) + # linecolors[] = [color_active[], color_inactive[]] end - onmouseleftdoubleclick(scenestate) do state + onmouseleftdoubleclick(mouseevents) do event selected_index[] = closest_index(sliderrange[], startvalue[]) end - onmouseenter(scenestate) do state - # bcolor[] = color_active[] - linecolors[] = [color_active[], color_inactive[]] - button.strokecolor = color_active[] + onmouseenter(mouseevents) do event + button_magnification[] = 1.25 end - onmouseout(scenestate) do state - bcolor[] = buttoncolor_inactive[] + onmouseout(mouseevents) do event + button_magnification[] = 1.0 linecolors[] = [color_active_dimmed[], color_inactive[]] - button.strokecolor = color_active_dimmed[] - end - - onany(buttonradius, horizontal) do br, horizontal - protrusions[] = if horizontal - GridLayoutBase.RectSides{Float32}(br, br, 0, 0) - else - GridLayoutBase.RectSides{Float32}(0, 0, br, br) - end end - # trigger protrusions using one observable - buttonradius[] = buttonradius[] - - # trigger bbox - layoutobservables.suggestedbbox[] = layoutobservables.suggestedbbox[] + # trigger autosize through linewidth for first layout + linewidth[] = linewidth[] - LSlider(parent, subscene, layoutobservables, attrs, decorations) + LSlider(parent, layoutobservables, attrs, decorations) end function valueindex(sliderrange, value) diff --git a/src/makielayout/lobjects/ltextbox.jl b/src/makielayout/lobjects/ltextbox.jl index 18293cf93..2fb51a980 100644 --- a/src/makielayout/lobjects/ltextbox.jl +++ b/src/makielayout/lobjects/ltextbox.jl @@ -118,7 +118,7 @@ function LTextbox(parent::Scene; bbox = nothing, kwargs...) # trigger bbox layoutobservables.suggestedbbox[] = layoutobservables.suggestedbbox[] - mousestate = addmousestate!(scene) + mousestate = addmouseevents!(scene) onmouseleftdown(mousestate) do state focus!(ltextbox) diff --git a/src/makielayout/lobjects/ltoggle.jl b/src/makielayout/lobjects/ltoggle.jl index ee8bf3eda..2b7b4c662 100644 --- a/src/makielayout/lobjects/ltoggle.jl +++ b/src/makielayout/lobjects/ltoggle.jl @@ -47,14 +47,15 @@ function LToggle(parent::Scene; bbox = nothing, kwargs...) end end - buttonsize = lift(markersize, rimfraction) do ms, rf - ms * (1 - rf) + buttonfactor = Node(1.0) + buttonsize = lift(markersize, rimfraction, buttonfactor) do ms, rf, bf + ms * (1 - rf) * bf end - button = scatter!(parent, buttonpos, markersize = buttonsize, color = buttoncolor, raw = true)[end] + button = scatter!(parent, buttonpos, markersize = buttonsize, color = buttoncolor, strokewidth = 0, raw = true)[end] decorations[:button] = button - buttonstate = addmousestate!(parent, button, frame) + buttonstate = addmouseevents!(parent, button, frame) onmouseleftclick(buttonstate) do state if animating[] @@ -89,5 +90,13 @@ function LToggle(parent::Scene; bbox = nothing, kwargs...) end end + onmouseover(buttonstate) do state + buttonfactor[] = 1.15 + end + + onmouseout(buttonstate) do state + buttonfactor[] = 1.0 + end + LToggle(parent, layoutobservables, attrs, decorations) end diff --git a/src/makielayout/mousestatemachine.jl b/src/makielayout/mousestatemachine.jl index 8b4d9dede..d605f9576 100644 --- a/src/makielayout/mousestatemachine.jl +++ b/src/makielayout/mousestatemachine.jl @@ -1,47 +1,72 @@ -abstract type AbstractMouseState end +module MouseEventTypes + @enum MouseEventType begin + out + enter + over + leftdown + rightdown + middledown + leftup + rightup + middleup + leftdragstart + rightdragstart + middledragstart + leftdrag + rightdrag + middledrag + leftdragstop + rightdragstop + middledragstop + leftclick + rightclick + middleclick + leftdoubleclick + rightdoubleclick + middledoubleclick + downoutside + end + export MouseEventType +end + +using .MouseEventTypes """ - MouseState{T<:AbstractMouseState} + MouseEvent Describes a mouse state change. Fields: -- `typ`: Symbol describing the mouse state +- `type`: MouseEventType - `t`: Time of the event -- `pos`: Mouse position -- `tprev`: Time of previous event -- `prev`: Previous mouse position +- `data`: Mouse position in data coordinates +- `px`: Mouse position in px relative to scene origin +- `prev_t`: Time of previous event +- `prev_data`: Previous mouse position in data coordinates +- `prev_px`: Previous mouse position in data coordinates """ -struct MouseState{T<:AbstractMouseState} - typ::T +struct MouseEvent + type::MouseEventType t::Float64 - pos::Point2f0 - tprev::Float64 - prev::Point2f0 + data::Point2f0 + px::Point2f0 + prev_t::Float64 + prev_data::Point2f0 + prev_px::Point2f0 end -mousestates = (:MouseOut, :MouseEnter, :MouseOver, - :MouseLeftDown, :MouseRightDown, :MouseMiddleDown, - :MouseLeftUp, :MouseRightUp, :MouseMiddleUp, - :MouseLeftDragStart, :MouseRightDragStart, :MouseMiddleDragStart, - :MouseLeftDrag, :MouseRightDrag, :MouseMiddleDrag, - :MouseLeftDragStop, :MouseRightDragStop, :MouseMiddleDragStop, - :MouseLeftClick, :MouseRightClick, :MouseMiddleClick, - :MouseLeftDoubleclick, :MouseRightDoubleclick, :MouseMiddleDoubleclick, - :MouseDownOutside - ) - -for statetype in mousestates - onfunctionname = Symbol("on" * lowercase(String(statetype))) + + +for eventtype in instances(MouseEventType) + onfunctionname = Symbol("onmouse" * String(Symbol(eventtype))) @eval begin - struct $statetype <: AbstractMouseState end """ - Executes the function f whenever the `Node{MouseState}` statenode transitions - to `$($statetype)`. + Executes the function f whenever the `Node{MouseEvent}` statenode transitions + to `$($eventtype)`. """ - function $onfunctionname(f, statenode::Node{MouseState}) + function $onfunctionname(f, statenode::Node{MouseEvent}) on(statenode) do state - if state.typ isa $statetype + if state.type === $eventtype f(state) end end @@ -51,14 +76,10 @@ for statetype in mousestates end -function Base.show(io::IO, ms::MouseState{T}) where T - print(io, "$T(t: $(ms.t), pos: $(ms.pos[1]), $(ms.pos[2]), tprev: $(ms.tprev), prev: $(ms.prev[1]), $(ms.prev[2]))") -end - """ - addmousestate!(scene, elements...) + addmouseevents!(scene, elements...) -Returns an `Observable{MouseState}` which is triggered by all mouse +Returns an `Observable{MouseEvent}` which is triggered by all mouse interactions with the `scene` and optionally restricted to all given plot objects in `elements`. @@ -67,31 +88,32 @@ To react to mouse events, use the onmouse... handlers. Example: ``` -mousestate = addmousestate!(scene, scatterplot) +mouseevents = addmouseevents!(scene, scatterplot) -onmouseleftclick(mousestate) do state - # do something with the mousestate +onmouseleftclick(mouseevents) do event + # do something with the mouseevent end ``` """ -function addmousestate!(scene, elements...) +function addmouseevents!(scene, elements...) Mouse = AbstractPlotting.Mouse dblclick_max_interval = 0.2 - mousestate = Node{MouseState}(MouseState(MouseOut(), 0.0, Point2f0(0, 0), 0.0, Point2f0(0, 0))) + mouseevent = Node{MouseEvent}(MouseEvent(MouseEventTypes.out, 0.0, Point2f0(0, 0), Point2f0(0, 0), 0.0, Point2f0(0, 0), Point2f0(0, 0))) is_mouse_over_relevant_area() = isempty(elements) ? AbstractPlotting.is_mouseinside(scene) : mouseover(scene, elements...) # initialize state variables - last_mousestate = Ref{Mouse.DragEnum}(events(scene).mousedrag[]) - prev = Ref(mouseposition(AbstractPlotting.rootparent(scene))) + last_mouseevent = Ref{Mouse.DragEnum}(events(scene).mousedrag[]) + prev_data = Ref(mouseposition(scene)) + prev_px = Ref(AbstractPlotting.mouseposition_px(scene)) mouse_downed_inside = Ref(false) mouse_downed_button = Ref{Optional{Mouse.Button}}(nothing) drag_ongoing = Ref(false) mouse_was_inside = Ref(false) - tprev = Ref(0.0) + prev_t = Ref(0.0) t_last_click = Ref(0.0) b_last_click = Ref{Optional{Mouse.Button}}(nothing) last_click_was_double = Ref(false) @@ -101,60 +123,62 @@ function addmousestate!(scene, elements...) on(events(scene).mouseposition) do mp t = time() - pos = mouseposition(AbstractPlotting.rootparent(scene)) + data = mouseposition(scene) + px = AbstractPlotting.mouseposition_px(scene) mouse_inside = is_mouse_over_relevant_area() # movement while mouse is pressed - if last_mousestate[] == Mouse.pressed + if last_mouseevent[] == Mouse.pressed # must have been a registered drag (otherwise could have come from outside) if drag_ongoing[] event = @match mouse_downed_button[] begin - Mouse.left => MouseLeftDrag() - Mouse.right => MouseRightDrag() - Mouse.middle => MouseMiddleDrag() + Mouse.left => MouseEventTypes.leftdrag + Mouse.right => MouseEventTypes.rightdrag + Mouse.middle => MouseEventTypes.middledrag x => error("No recognized mouse button $x") end - mousestate[] = MouseState(event, t, pos, tprev[], prev[]) + mouseevent[] = MouseEvent(event, t, data, px, prev_t[], prev_data[], prev_px[]) end # mouse moved while just having been pressed down - elseif last_mousestate[] == Mouse.down + elseif last_mouseevent[] == Mouse.down # mouse must have been downed inside # that means a drag started if mouse_downed_inside[] drag_ongoing[] = true event = @match mouse_downed_button[] begin - Mouse.left => MouseLeftDragStart() - Mouse.right => MouseRightDragStart() - Mouse.middle => MouseMiddleDragStart() + Mouse.left => MouseEventTypes.leftdragstart + Mouse.right => MouseEventTypes.rightdragstart + Mouse.middle => MouseEventTypes.middledragstart x => error("No recognized mouse button $x") end - mousestate[] = MouseState(event, t, pos, tprev[], prev[]) + mouseevent[] = MouseEvent(event, t, data, px, prev_t[], prev_data[], prev_px[]) event = @match mouse_downed_button[] begin - Mouse.left => MouseLeftDrag() - Mouse.right => MouseRightDrag() - Mouse.middle => MouseMiddleDrag() + Mouse.left => MouseEventTypes.leftdrag + Mouse.right => MouseEventTypes.rightdrag + Mouse.middle => MouseEventTypes.middledrag x => error("No recognized mouse button $x") end - mousestate[] = MouseState(event, t, pos, tprev[], prev[]) + mouseevent[] = MouseEvent(event, t, data, px, prev_t[], prev_data[], prev_px[]) end else if mouse_inside if mouse_was_inside[] - mousestate[] = MouseState(MouseOver(), t, pos, tprev[], prev[]) + mouseevent[] = MouseEvent(MouseEventTypes.over, t, data, px, prev_t[], prev_data[], prev_px[]) else - mousestate[] = MouseState(MouseEnter(), t, pos, tprev[], prev[]) + mouseevent[] = MouseEvent(MouseEventTypes.enter, t, data, px, prev_t[], prev_data[], prev_px[]) end else if mouse_was_inside[] - mousestate[] = MouseState(MouseOut(), t, pos, tprev[], prev[]) + mouseevent[] = MouseEvent(MouseEventTypes.out, t, data, px, prev_t[], prev_data[], prev_px[]) end end end mouse_was_inside[] = mouse_inside - prev[] = pos - tprev[] = t + prev_data[] = data + prev_px[] = px + prev_t[] = t end @@ -162,7 +186,8 @@ function addmousestate!(scene, elements...) on(events(scene).mousedrag) do mousedrag t = time() - pos = prev[] + data = prev_data[] + px = prev_px[] pressed_buttons = events(scene).mousebuttons[] @@ -175,16 +200,16 @@ function addmousestate!(scene, elements...) if mouse_was_inside[] event = @match mouse_downed_button[] begin - Mouse.left => MouseLeftDown() - Mouse.right => MouseRightDown() - Mouse.middle => MouseMiddleDown() + Mouse.left => MouseEventTypes.leftdown + Mouse.right => MouseEventTypes.rightdown + Mouse.middle => MouseEventTypes.middledown x => error("No recognized mouse button $x") end - mousestate[] = MouseState(event, t, pos, tprev[], prev[]) + mouseevent[] = MouseEvent(event, t, data, px, prev_t[], prev_data[], prev_px[]) mouse_downed_inside[] = true else mouse_downed_inside[] = false - mousestate[] = MouseState(MouseDownOutside(), t, pos, tprev[], prev[]) + mouseevent[] = MouseEvent(MouseEventTypes.downoutside, t, data, px, prev_t[], prev_data[], prev_px[]) end end elseif mousedrag == Mouse.up @@ -199,27 +224,27 @@ function addmousestate!(scene, elements...) if drag_ongoing[] event = @match mouse_downed_button[] begin - Mouse.left => MouseLeftDragStop() - Mouse.right => MouseRightDragStop() - Mouse.middle => MouseMiddleDragStop() + Mouse.left => MouseEventTypes.leftdragstop + Mouse.right => MouseEventTypes.rightdragstop + Mouse.middle => MouseEventTypes.middledragstop x => error("No recognized mouse button $x") end - mousestate[] = MouseState(event, t, pos, tprev[], prev[]) + mouseevent[] = MouseEvent(event, t, data, px, prev_t[], prev_data[], prev_px[]) drag_ongoing[] = false if mouse_was_inside[] # up after drag done over element event = @match mouse_downed_button[] begin - Mouse.left => MouseLeftUp() - Mouse.right => MouseRightUp() - Mouse.middle => MouseMiddleUp() + Mouse.left => MouseEventTypes.leftup + Mouse.right => MouseEventTypes.rightup + Mouse.middle => MouseEventTypes.middleup x => error("No recognized mouse button $x") end - mousestate[] = MouseState(event, t, pos, tprev[], prev[]) + mouseevent[] = MouseEvent(event, t, data, px, prev_t[], prev_data[], prev_px[]) else # mouse could be not over elements after drag is over - mousestate[] = MouseState(MouseOut(), t, pos, tprev[], prev[]) + mouseevent[] = MouseEvent(MouseEventTypes.out, t, data, px, prev_t[], prev_data[], prev_px[]) end else if mouse_was_inside[] @@ -232,21 +257,21 @@ function addmousestate!(scene, elements...) mouse_downed_button[] == b_last_click[] event = @match mouse_downed_button[] begin - Mouse.left => MouseLeftDoubleclick() - Mouse.right => MouseRightDoubleclick() - Mouse.middle => MouseMiddleDoubleclick() + Mouse.left => MouseEventTypes.leftdoubleclick + Mouse.right => MouseEventTypes.rightdoubleclick + Mouse.middle => MouseEventTypes.middledoubleclick x => error("No recognized mouse button $x") end - mousestate[] = MouseState(event, t, pos, tprev[], prev[]) + mouseevent[] = MouseEvent(event, t, data, px, prev_t[], prev_data[], prev_px[]) last_click_was_double[] = true else event = @match mouse_downed_button[] begin - Mouse.left => MouseLeftClick() - Mouse.right => MouseRightClick() - Mouse.middle => MouseMiddleClick() + Mouse.left => MouseEventTypes.leftclick + Mouse.right => MouseEventTypes.rightclick + Mouse.middle => MouseEventTypes.middleclick x => error("No recognized mouse button $x") end - mousestate[] = MouseState(event, t, pos, tprev[], prev[]) + mouseevent[] = MouseEvent(event, t, data, px, prev_t[], prev_data[], prev_px[]) last_click_was_double[] = false end # save what type the last downed button was @@ -256,13 +281,13 @@ function addmousestate!(scene, elements...) # up after click event = @match mouse_downed_button[] begin - Mouse.left => MouseLeftUp() - Mouse.right => MouseRightUp() - Mouse.middle => MouseMiddleUp() + Mouse.left => MouseEventTypes.leftup + Mouse.right => MouseEventTypes.rightup + Mouse.middle => MouseEventTypes.middleup x => error("No recognized mouse button $x") end - mousestate[] = MouseState(event, t, pos, tprev[], prev[]) + mouseevent[] = MouseEvent(event, t, data, px, prev_t[], prev_data[], prev_px[]) end end @@ -271,9 +296,9 @@ function addmousestate!(scene, elements...) end - last_mousestate[] = mousedrag - tprev[] = t + last_mouseevent[] = mousedrag + prev_t[] = t end - mousestate + mouseevent end diff --git a/src/makielayout/types.jl b/src/makielayout/types.jl index 4ab1108df..a11ee14d8 100644 --- a/src/makielayout/types.jl +++ b/src/makielayout/types.jl @@ -46,6 +46,42 @@ mutable struct LineAxis ticklabels::Node{Vector{String}} end +struct LimitReset end + +mutable struct RectangleZoom + active::Bool + restrict_x::Bool + restrict_y::Bool + from::Union{Nothing, Point2f0} + to::Union{Nothing, Point2f0} + rectnode::Observable{FRect2D} + poly::Union{Poly, Nothing} +end + +struct ScrollZoom + speed::Float32 + reset_timer::Ref{Any} + prev_xticklabelspace::Ref{Any} + prev_yticklabelspace::Ref{Any} + reset_delay::Float32 +end + +struct DragPan + reset_timer::Ref{Any} + prev_xticklabelspace::Ref{Any} + prev_yticklabelspace::Ref{Any} + reset_delay::Float32 +end + +struct ScrollEvent + x::Float32 + y::Float32 +end + +struct KeysEvent + keys::Set{AbstractPlotting.Keyboard.Button} +end + abstract type LObject end mutable struct LAxis <: AbstractPlotting.AbstractScene @@ -58,6 +94,10 @@ mutable struct LAxis <: AbstractPlotting.AbstractScene attributes::Attributes block_limit_linking::Node{Bool} decorations::Dict{Symbol, Any} + mouseevents::Observable{MouseEvent} + scrollevents::Observable{ScrollEvent} + keysevents::Observable{KeysEvent} + interactions::Dict{Symbol, Tuple{Bool, Any}} end mutable struct LColorbar <: LObject @@ -83,7 +123,6 @@ end struct LSlider <: LObject parent::Scene - scene::Scene layoutobservables::LayoutObservables attributes::Attributes decorations::Dict{Symbol, Any}