From 54cdb9b605c4caaf6801ea70821aff8da890760d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B9=E3=83=8E=E3=83=AB?= Date: Wed, 7 Dec 2022 04:53:10 -0500 Subject: [PATCH] Dispatch & Timeout (#9) * dispatch/timeout on Linux. * Windows. * Windows tests. * Apple. * Finish. * Debug macOS. * Use `NSValue` as user info. * Finish. --- Project.toml | 2 + src/API.jl | 48 +++++++++++++------ src/Utils.jl | 63 ++++++++++++++++++++++--- src/platforms/apple/Impl.jl | 71 ++++++++++++++++++++++++---- src/platforms/common.jl | 52 ++------------------ src/platforms/common_bind.jl | 28 +++++++++++ src/platforms/linux/Impl.jl | 43 ++++++++++++++--- src/platforms/windows/Impl.jl | 76 +++++++++++++++++++++++------- src/platforms/windows/win_types.jl | 1 + test/runtests.jl | 12 +++-- 10 files changed, 293 insertions(+), 103 deletions(-) create mode 100644 src/platforms/common_bind.jl diff --git a/Project.toml b/Project.toml index a28c157..c854c6c 100644 --- a/Project.toml +++ b/Project.toml @@ -5,6 +5,7 @@ version = "1.0.0" [deps] Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6" +FunctionWrappers = "069b7b12-0de2-55c6-9aab-29f3d0a68a2e" JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" @@ -13,6 +14,7 @@ SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" [compat] Downloads = "1.6" +FunctionWrappers = "1.1" JSON3 = "1.12" Reexport = "1.2" julia = "1.8" diff --git a/src/API.jl b/src/API.jl index 6c336f8..1f655fd 100644 --- a/src/API.jl +++ b/src/API.jl @@ -11,7 +11,9 @@ export terminate, eval!, bind_raw, unbind, - return_raw + return_raw, + set_timeout, + clear_timeout using JSON3: JSON3 @@ -77,17 +79,15 @@ function Base.run(w::AbstractWebview) end """ - dispatch(f::Function, w::Webview, [arg]) + dispatch(f::Function, w::Webview) Posts a function to be executed on the main thread. You normally do not need to call this function, unless you want to tweak the native window. -The function `f` will be called with two arguments: the webview and the `arg`. +The function `f` should be callable without arguments. """ -function dispatch(f::Function, w::AbstractWebview, arg=nothing) - dispatch(w.platform) do - f(w, arg) - end +function dispatch(f::Function, w::AbstractWebview) + dispatch(f, w.platform) nothing end @@ -122,8 +122,8 @@ Sets the native window size. """ function Base.resize!( w::AbstractWebview, - size::Tuple{Integer, Integer}; - hint::Union{WindowSizeHint, Nothing}=WEBVIEW_HINT_NONE + size::Tuple{Integer,Integer}; + hint::Union{WindowSizeHint,Nothing}=WEBVIEW_HINT_NONE ) resize!(w.platform, size, hint=hint) w @@ -161,7 +161,7 @@ is ignored. Use `bind` if you want to receive notifications about the results of @forward eval!(w, js::AbstractString) """ - bind_raw(f::Function, w::Webview, name::AbstractString, [arg]) + bind_raw(f::Function, w::Webview, name::AbstractString) Binds a callback so that it will appear in the webview with the given name as a global async JavaScript function. Callback receives a seq and req value. @@ -169,7 +169,7 @@ The seq parameter is an identifier for using `Webviews.return_raw` to return a value while the req parameter is a string of an JSON array representing the arguments passed from the JavaScript function call. -The callback function must has the method `f(seq::String, req::String, [arg::Any])`. +The callback function must has the method `f(seq::String, req::String)`. """ function bind_raw end @@ -185,7 +185,7 @@ return value to the webview. The callback function must handle a `Tuple` as its argument. """ function Base.bind(f::Function, w::AbstractWebview, name::AbstractString) - bind_raw(w, name) do seq, args, _ + bind_raw(w, name) do seq, args try result = f(Tuple(args)) _return(w, seq, true, result) @@ -209,11 +209,11 @@ Allows to return a value from the native binding. Original request pointer must be provided to help internal RPC engine match requests with responses. """ function return_raw(w::AbstractWebview, seq::Int, success::Bool, result_or_err::AbstractString) - dispatch(w, nothing) do w, _ + dispatch(w) do if success - eval!(w, "window._rpc[$seq].resolve($result_or_err); delete window._rpc[$seq]"); + eval!(w, "window._rpc[$seq].resolve($result_or_err); delete window._rpc[$seq]") else - eval!(w, "window._rpc[$seq].reject($result_or_err); delete window._rpc[$seq]"); + eval!(w, "window._rpc[$seq].reject($result_or_err); delete window._rpc[$seq]") end end end @@ -232,6 +232,24 @@ function _return(w::AbstractWebview, seq::Int, success::Bool, result) return_raw(w, seq, success, s) end +""" + set_timeout(f::Function, w::Webview, interval::Real; [repeat::Bool=false]) + +Sets a function to be called after the given interval in webview's event loop. +If `repeat` is `true`, `f` will be called repeatedly. +The function `f` should be callable without arguments. + +This function returns a `timer_id::Ptr{Cvoid}` which can be used in `clear_timeout(webview, timer_id)`. +""" +set_timeout(f::Function, w::AbstractWebview, interval::Real; repeat::Bool=false) = + set_timeout(f, w.platform, interval; repeat) + +""" + clear_timeout(w::Webview, timer_id::Ptr{Cvoid}) +Clears a previously set timeout. +""" +@forward clear_timeout(w, timer_id::Ptr{Cvoid}) + const run = Base.run const size = Base.size const resize! = Base.resize! diff --git a/src/Utils.jl b/src/Utils.jl index 9ef5f9d..8ff2ff4 100644 --- a/src/Utils.jl +++ b/src/Utils.jl @@ -1,13 +1,26 @@ module Utils +using FunctionWrappers: FunctionWrapper using JSON3: JSON3 using ..API +const MessageCallback = FunctionWrapper{Nothing,Tuple{Int,Vector{Any}}} + +# JuliaLang/julia #269 +mutable struct _DispatchCallback{T} + const func::FunctionWrapper{Nothing,Tuple{}} + const ch::T +end + mutable struct CallbackHandler - const callbacks::Dict{String,Base.RefValue{Tuple{Function,Any}}} - CallbackHandler() = new(Dict()) + const callbacks::Dict{String,MessageCallback} + # func -> seq + const dispatched::Dict{_DispatchCallback{CallbackHandler}, UInt64} + + CallbackHandler() = new(Dict(), Dict()) end +const DispatchCallback = _DispatchCallback{CallbackHandler} function on_message(ch::CallbackHandler, s::Ptr{Cchar}) try @@ -16,17 +29,20 @@ function on_message(ch::CallbackHandler, s::Ptr{Cchar}) haskey(ch.callbacks, name) || return seq = msg.id args = msg.params - f, arg = ch.callbacks[name][] - f(seq, copy(args), arg) + f = ch.callbacks[name] + f(seq, copy(args)) catch e @debug "Error occured while handling message: $e" end nothing end -function API.bind_raw(f::Function, ch::CallbackHandler, name::AbstractString, arg=nothing) +function API.bind_raw(f::Function, ch::CallbackHandler, name::AbstractString) haskey(ch.callbacks, name) && return - ch.callbacks[name] = Ref{Tuple{Function,Any}}((f, arg)) + ch.callbacks[name] = MessageCallback() do seq, args + f(seq, args) + nothing + end nothing end @@ -35,4 +51,39 @@ function API.unbind(ch::CallbackHandler, name::AbstractString) nothing end +# Returns a pointer to the function wrapper as the ID. +function setup_dispatch(f::Function, ch::CallbackHandler) + func = FunctionWrapper{Nothing,Tuple{}}() do + try + f() + catch e + @debug "Error occured while dispatching: $e" + end + nothing + end + dcb = DispatchCallback(func, ch) + ch.dispatched[dcb] = 0 + ptr = pointer_from_objref(dcb) +end + +function set_dispatch_id(ptr::Ptr{Cvoid}, id::Integer) + dcb = unsafe_pointer_to_objref(ptr)::DispatchCallback + dcb.ch.dispatched[dcb] = UInt64(id) + nothing +end + +function call_dispatch(ptr::Ptr{Cvoid}) + dcb = unsafe_pointer_to_objref(ptr)::DispatchCallback + dcb.func() +end + +function clear_dispatch(ptr::Ptr{Cvoid}) + dcb = unsafe_pointer_to_objref(ptr)::DispatchCallback + dispatched = dcb.ch.dispatched + haskey(dispatched, dcb) || return nothing + id = dispatched[dcb] + delete!(dispatched, dcb) + id +end + end diff --git a/src/platforms/apple/Impl.jl b/src/platforms/apple/Impl.jl index 6cb8df3..b8c9cd2 100644 --- a/src/platforms/apple/Impl.jl +++ b/src/platforms/apple/Impl.jl @@ -2,6 +2,7 @@ module AppleImpl # TODO include("../common.jl") +include("../common_bind.jl") include("./objc.jl") @@ -17,7 +18,7 @@ function setup_platform() # Register the yielder in the shared `NSApplication`. app = get_shared_application() @ccall class_replaceMethod( - a"NSApplication"cls::Ptr{Cvoid}, a"webviewsjlTick:"sel::Ptr{Cvoid}, + a"NSApplication"cls::Ptr{Cvoid}, a"webviewsjlYielder:"sel::Ptr{Cvoid}, @cfunction(_event_loop_timeout, Cvoid, (Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid}))::Ptr{Cvoid}, "v@:@"::Cstring )::Ptr{Cvoid} @@ -27,10 +28,12 @@ function setup_platform() a"scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:"sel, (TIMEOUT_INTERVAL / 1000)::Cdouble, app, - a"webviewsjlTick:"sel, + a"webviewsjlYielder:"sel, C_NULL, true::Bool ) + prepare_timeout() + nothing end Base.@kwdef mutable struct Webview <: AbstractPlatformImpl @@ -38,7 +41,6 @@ Base.@kwdef mutable struct Webview <: AbstractPlatformImpl const debug::Bool const callback_handler::CallbackHandler const main_queue::ID = cglobal(:_dispatch_main_q) - const dispatched::Set{Base.RefValue{Tuple{Webview,Function}}} = Set() window::ID = C_NULL config::ID = C_NULL manager::ID = C_NULL @@ -77,7 +79,7 @@ end API.window_handle(w::Webview) = w.window # TODO: support multiple windows. -API.terminate(w::Webview) = +API.terminate(::Webview) = let app = get_shared_application() # Stop the main event loop instead of terminating the process. @msg_send Cvoid app a"stop:"sel C_NULL @@ -89,14 +91,20 @@ API.run(::Webview) = let app = get_shared_application() @msg_send Cvoid app a"run"sel end + +function _dispatch(ptr::Ptr{Cvoid}) + call_dispatch(ptr) + clear_dispatch(ptr) + nothing +end + function API.dispatch(f::Function, w::Webview) - ref = Ref{Tuple{Webview,Function}}((w, f)) - push!(w.dispatched, ref) - ptr = pointer_from_objref(ref) + cf = @cfunction(_dispatch, Cvoid, (Ptr{Cvoid},)) + ptr = setup_dispatch(f, w.callback_handler) @ccall dispatch_async_f( w.main_queue::ID, ptr::Ptr{Cvoid}, - @cfunction((arg) -> (_dispatch(arg); nothing), Cvoid, (Ptr{Cvoid},))::Ptr{Cvoid} + cf::Ptr{Cvoid} )::Cvoid end @@ -163,4 +171,51 @@ function API.eval!(w::Webview, js::AbstractString) w end +function _timeout(_1, _2, timer::ID) + valid = @msg_send Bool timer a"isValid"sel + valid || return + user_info = @msg_send ID timer a"userInfo"sel + fp = @msg_send Ptr{Cvoid} user_info a"pointerValue"sel + call_dispatch(fp) + interval = @msg_send Cdouble timer a"timeInterval"sel + interval > 0 || return + _clear_timeout(fp) + nothing +end + +function prepare_timeout() + @ccall class_replaceMethod( + a"NSApplication"cls::Ptr{Cvoid}, a"webviewsjlTimeout:"sel::Ptr{Cvoid}, + @cfunction(_timeout, Cvoid, (Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid}))::Ptr{Cvoid}, + "v@:@"::Cstring + )::Ptr{Cvoid} + nothing +end + +function API.set_timeout(f::Function, w::Webview, interval::Real; repeat=false) + fp = setup_dispatch(f, w.callback_handler) + user_info = @msg_send ID a"NSValue"cls a"valueWithPointer:"sel fp + app = get_shared_application() + timer_id = @msg_send( + ID, + a"NSTimer"cls, + a"scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:"sel, + interval::Cdouble, + app, + a"webviewsjlTimeout:"sel, + user_info, + repeat::Bool + ) + set_dispatch_id(fp, UInt64(timer_id)) + fp +end + +function _clear_timeout(timer_id::Ptr{Cvoid}) + timer = clear_dispatch(timer_id) + isnothing(timer) && return + @msg_send Cvoid Ptr{Cvoid}(timer) a"invalidate"sel + nothing +end +API.clear_timeout(::Webview, timer_id::Ptr{Cvoid}) = _clear_timeout(timer_id) + end diff --git a/src/platforms/common.jl b/src/platforms/common.jl index dea2fa9..a5fc888 100644 --- a/src/platforms/common.jl +++ b/src/platforms/common.jl @@ -6,7 +6,10 @@ using ..Consts using ..Consts: TIMEOUT_INTERVAL using ..API using ..API: AbstractWebview, AbstractPlatformImpl -using ..Utils: CallbackHandler, on_message +using ..Utils: MessageCallback, CallbackHandler, + on_message, + setup_dispatch, call_dispatch, clear_dispatch, + set_dispatch_id _event_loop_timeout(_...) = (yield(); nothing) @@ -22,50 +25,3 @@ function _check_dependency(lib) Libdl.dlclose(hdl) end end - -function _dispatch(p::Ptr{Cvoid}) - cd = nothing - w = nothing - try - cd = unsafe_pointer_to_objref(Ptr{Tuple{Webview,Function}}(p)) - w, f = cd[] - f() - finally - if !isnothing(cd) && !isnothing(w) - delete!(w.dispatched, cd) - end - end - Cint(false) -end - -@static if !Sys.iswindows() - -# Since we are using a workaround on Windows. -function API.bind_raw(f::Function, w::AbstractWebview, name::AbstractString, arg=nothing) - bind_raw(f, w.callback_handler, name, arg) - js = "((function() { var name = '$name'; - var RPC = window._rpc = (window._rpc || {nextSeq: 1}); - window[name] = function() { - var seq = RPC.nextSeq++; - var promise = new Promise(function(resolve, reject) { - RPC[seq] = { - resolve: resolve, - reject: reject, - }; - }); - window.external.invoke(JSON.stringify({ - id: seq, - method: name, - params: Array.prototype.slice.call(arguments), - })); - return promise; - } - })())" - init!(w, js) - eval!(w, js) - nothing -end - -API.unbind(w::AbstractWebview, name::AbstractString) = unbind(w.callback_handler, name) - -end diff --git a/src/platforms/common_bind.jl b/src/platforms/common_bind.jl new file mode 100644 index 0000000..a9e5b81 --- /dev/null +++ b/src/platforms/common_bind.jl @@ -0,0 +1,28 @@ +# This file is only loaded on non-Windows platforms. + +function API.bind_raw(f::Function, w::AbstractWebview, name::AbstractString) + bind_raw(f, w.callback_handler, name) + js = "((function() { var name = '$name'; + var RPC = window._rpc = (window._rpc || {nextSeq: 1}); + window[name] = function() { + var seq = RPC.nextSeq++; + var promise = new Promise(function(resolve, reject) { + RPC[seq] = { + resolve: resolve, + reject: reject, + }; + }); + window.external.invoke(JSON.stringify({ + id: seq, + method: name, + params: Array.prototype.slice.call(arguments), + })); + return promise; + } + })())" + init!(w, js) + eval!(w, js) + nothing +end + +API.unbind(w::AbstractWebview, name::AbstractString) = unbind(w.callback_handler, name) diff --git a/src/platforms/linux/Impl.jl b/src/platforms/linux/Impl.jl index 6bde104..798a512 100644 --- a/src/platforms/linux/Impl.jl +++ b/src/platforms/linux/Impl.jl @@ -1,6 +1,7 @@ module LinuxImpl include("../common.jl") +include("../common_bind.jl") const libwebkit2gtk = "libwebkit2gtk-4.0.so.37" @@ -27,7 +28,7 @@ end mutable struct Webview <: AbstractPlatformImpl const gtk_window_handle::Ptr{Cvoid} const webview_handle::Ptr{Cvoid} - const dispatched::Set{Base.RefValue{Tuple{Webview,Function}}} + const callback_handler::CallbackHandler function Webview( callback_handler::CallbackHandler, @@ -43,7 +44,7 @@ mutable struct Webview <: AbstractPlatformImpl unsafe_window_handle end webview = @gcall webkit_web_view_new()::Ptr{Cvoid} - w = new(window, webview, Set()) + w = new(window, webview, callback_handler) @g_signal_connect( window, "destroy", w, @@ -92,7 +93,7 @@ function setup_platform() TIMEOUT_INTERVAL::Cuint, cb::Ptr{Cvoid}, C_NULL::Ptr{Cvoid}, - )::UInt64 + )::Cuint nothing end @@ -112,11 +113,19 @@ API.terminate(::Webview) = @gcall gtk_main_quit() API.is_shown(::Webview) = 0 ≠ @gcall gtk_main_level()::Cuint API.run(::Webview) = @gcall gtk_main() +function _dispatch(p::Ptr{Cvoid}) + call_dispatch(p) + clear_dispatch(p) + Cint(false) # G_SOURCE_REMOVE +end +function _dispatch_repeat(p::Ptr{Cvoid}) + call_dispatch(p) + Cint(true) # G_SOURCE_CONTINUE +end + function API.dispatch(f::Function, w::Webview) cf = @cfunction(_dispatch, Cint, (Ptr{Cvoid},)) - ref = Ref{Tuple{Webview,Function}}((w, f)) - ptr = pointer_from_objref(ref) - push!(w.dispatched, ref) + ptr = setup_dispatch(f, w.callback_handler) @gcall g_idle_add_full( 100::Cint, # G_PRIORITY_HIGH_IDLE cf::Ptr{Cvoid}, @@ -220,4 +229,26 @@ API.eval!(w::Webview, js::AbstractString) = (@gcall webkit_web_view_run_javascri C_NULL::Ptr{Cvoid} )::Cvoid; w) +function API.set_timeout(f::Function, w::Webview, interval::Real; repeat=false) + fp = setup_dispatch(f, w.callback_handler) + timer_id = @gcall g_timeout_add( + round(Cuint, interval * 1000)::Cuint, + if repeat + @cfunction(_dispatch_repeat, Cint, (Ptr{Cvoid},)) + else + @cfunction(_dispatch, Cint, (Ptr{Cvoid},)) + end::Ptr{Cvoid}, + fp::Ptr{Cvoid} + )::Cuint + set_dispatch_id(fp, timer_id) + fp +end + +function API.clear_timeout(::Webview, timer_id::Ptr{Cvoid}) + id = clear_dispatch(timer_id) + isnothing(id) && return + @gcall g_source_remove(id::Cuint)::Cint + nothing +end + end \ No newline at end of file diff --git a/src/platforms/windows/Impl.jl b/src/platforms/windows/Impl.jl index 74857b1..816b69b 100644 --- a/src/platforms/windows/Impl.jl +++ b/src/platforms/windows/Impl.jl @@ -41,11 +41,11 @@ mutable struct Webview <: AbstractPlatformImpl const ptr::Ptr{Cvoid} const timer_id::Cuint const main_thread::DWORD - const dispatched::Set{Base.RefValue{Tuple{Webview,Function}}} + const callback_handler::CallbackHandler end function Webview( - _callback_handler::CallbackHandler, + callback_handler::CallbackHandler, debug::Bool, unsafe_window_handle::Ptr{Cvoid} ) @@ -56,10 +56,10 @@ function Webview( window = @ccall libwebview.webview_get_window(ptr::Ptr{Cvoid})::Ptr{Cvoid} timer_id = @ccall "user32".SetTimer( window::Ptr{Cvoid}, 0::UInt, TIMEOUT_INTERVAL::Cuint, - @cfunction(_event_loop_timeout, Cvoid, (Ptr{Cvoid}, Cuint, UInt, UInt32))::Ptr{Cvoid}::Ptr{Cvoid} + @cfunction(_event_loop_timeout, Cvoid, (Ptr{Cvoid}, Cuint, UInt, UInt32))::Ptr{Cvoid} )::UInt main_thread = @ccall GetCurrentThreadId()::DWORD - Webview(ptr, timer_id, main_thread, Set()) + Webview(ptr, timer_id, main_thread, callback_handler) end Base.cconvert(::Type{Ptr{Cvoid}}, w::Webview) = w.ptr @@ -74,14 +74,14 @@ function API.run(::Webview) res = @ccall "user32".GetMessageW(ref::Ptr{MSG}, C_NULL::Ptr{Cvoid}, 0::Cuint, 0::Cuint)::Cint ) ≠ -1 msg = ref[] - if msg.hwnd ≢ C_NULL + if msg.hwnd ≢ C_NULL || msg.message == WM_TIMER @ccall "user32".TranslateMessage(ref::Ptr{MSG})::Bool @ccall "user32".DispatchMessageW(ref::Ptr{MSG})::Clong continue end if msg.message == WM_APP ptr = Ptr{Cvoid}(msg.lParam) - _dispatch(ptr) + call_dispatch(ptr) elseif msg.message == WM_QUIT return end @@ -89,9 +89,7 @@ function API.run(::Webview) end function API.dispatch(f::Function, w::Webview) - ref = Ref{Tuple{Webview,Function}}((w, f)) - push!(w.dispatched, ref) - ptr = pointer_from_objref(ref) + ptr = setup_dispatch(f, w.callback_handler) ret = @ccall "user32".PostThreadMessageW( w.main_thread::DWORD, WM_APP::Cuint, @@ -99,7 +97,7 @@ function API.dispatch(f::Function, w::Webview) LPARAM(ptr)::LPARAM )::Bool if !ret - pop!(w.dispatched, ref) + clear_dispatch(ptr) @warn "Failed to dispatch function" end end @@ -135,26 +133,25 @@ API.init!(w::Webview, js::AbstractString) = ( API.eval!(w::Webview, js::AbstractString) = ( (@ccall libwebview.webview_eval(w::Ptr{Cvoid}, js::Cstring)::Cvoid); w) -_binding_wrapper(seq::Ptr{Cchar}, req::Ptr{Cchar}, ref::Ptr{Cvoid}) = begin +function _binding_wrapper(seq::Ptr{Cchar}, req::Ptr{Cchar}, ptr::Ptr{Cvoid}) try - cd = unsafe_pointer_to_objref(Ptr{Tuple{Function,Any}}(ref)) - f, arg = cd + f = unsafe_pointer_to_objref(ptr)::MessageCallback seq_id = JSON3.read(unsafe_string(seq)) args = JSON3.read(unsafe_string(req)) - f(seq_id, copy(args), arg) + f(seq_id, copy(args)) catch e @debug e end nothing end -function API.bind_raw(f::Function, w::AbstractWebview, name::AbstractString, arg=nothing) - API.bind_raw(f, w.callback_handler, name, arg) +function API.bind_raw(f::Function, w::AbstractWebview, name::AbstractString) + API.bind_raw(f, w.callback_handler, name) ref = w.callback_handler.callbacks[name] @ccall libwebview.webview_bind( w.platform::Ptr{Cvoid}, name::Cstring, @cfunction(_binding_wrapper, Cvoid, (Ptr{Cchar}, Ptr{Cchar}, Ptr{Cvoid}))::Ptr{Cvoid}, - ref::Ptr{Cvoid} + pointer_from_objref(ref)::Ptr{Cvoid} )::Cvoid nothing end @@ -164,4 +161,49 @@ function API.unbind(w::AbstractWebview, name::AbstractString) unbind(w.callback_handler, name) end +const GlobalTimers = Dict{UInt,Ptr{Cvoid}}() + +function _clear_timeout(ptr::Ptr{Cvoid}) + id = clear_dispatch(ptr) + isnothing(id) && return + @ccall "user32".KillTimer(C_NULL::Ptr{Cvoid}, id::UInt)::Bool + delete!(GlobalTimers, id) + nothing +end + +function _timeout(_1, _2, timer_id, _4) + haskey(GlobalTimers, timer_id) || return + ptr = GlobalTimers[timer_id] + _clear_timeout(ptr) + call_dispatch(ptr) + nothing +end +function _timeout_repeat(_1, _2, timer_id, _4) + haskey(GlobalTimers, timer_id) || return + call_dispatch(GlobalTimers[timer_id]) + nothing +end + +function API.set_timeout(f::Function, w::Webview, interval::Real; repeat=false) + fp = setup_dispatch(f, w.callback_handler) + timer_id = @ccall "user32".SetTimer( + C_NULL::Ptr{Cvoid}, 0::UInt, round(Cuint, interval * 1000)::Cuint, + if repeat + @cfunction(_timeout_repeat, Cvoid, (Ptr{Cvoid}, Cuint, UInt, UInt32)) + else + @cfunction(_timeout, Cvoid, (Ptr{Cvoid}, Cuint, UInt, UInt32)) + end::Ptr{Cvoid} + )::UInt + if timer_id == 0 + clear_dispatch(fp) + @warn "Failed to set timer" + return 0 + end + set_dispatch_id(fp, timer_id) + GlobalTimers[timer_id] = fp + fp +end + +API.clear_timeout(::Webview, timer_id::Ptr{Cvoid}) = _clear_timeout(timer_id) + end \ No newline at end of file diff --git a/src/platforms/windows/win_types.jl b/src/platforms/windows/win_types.jl index 7563a2f..7d6095f 100644 --- a/src/platforms/windows/win_types.jl +++ b/src/platforms/windows/win_types.jl @@ -13,6 +13,7 @@ end const DWORD = Culong const WM_APP = 0x8000 const WM_QUIT = 0x0012 +const WM_TIMER = 0x0113 const UINT_PTR = UInt const LONG_PTR = Int diff --git a/test/runtests.jl b/test/runtests.jl index c82af49..1350424 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -21,11 +21,16 @@ using Webviews @test size(webview) == (320, 240) resize!(webview, (240, 240)) resize!(webview, (500, 500); hint=WEBVIEW_HINT_MAX) - html!(webview, html) + timer = set_timeout(webview, 0.5, repeat=true) do + @test true + clear_timeout(webview, timer) + html!(webview, html) + end + @test timer ≢ C_NULL elseif step == 2 @test size(webview) == (240, 240) - dispatch(webview, true) do w, _ - @test w == webview + dispatch(webview) do + @test true end navigate!(webview, "http://localhost:8080") elseif step == 3 @@ -45,4 +50,5 @@ using Webviews init!(webview, "run_test().catch(console.error)") navigate!(webview, "data:text/html,$(HTTP.escapeuri(html))") run(webview) + @test Test.get_testset().n_passed == 7 end