Skip to content

Commit

Permalink
Implement record & replay functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
serenity4 committed Nov 11, 2023
1 parent 9c992dd commit 01dea9c
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 22 deletions.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Xorg_xcb_util_jll = "2def613f-5ad1-5310-b15b-b15d46f528f5"
BitMasks = "0.1"
DocStringExtensions = "0.8, 0.9"
Reexport = "1"
WindowAbstractions = "0.7"
WindowAbstractions = "0.7.1"
XKeyboard = "0.1"
Xorg_libxcb_jll = "1.13"
Xorg_xcb_util_jll = "0.4"
Expand Down
4 changes: 3 additions & 1 deletion src/XCB.jl
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ import WindowAbstractions: set_title,
KeyEvent,
ModifierState,
Event,
EventQueue
EventQueue,
save_history,
replay_history

include("LibXCB.jl")
@reexport using .LibXCB
Expand Down
8 changes: 5 additions & 3 deletions src/events.jl
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,13 @@ Event(wm::XWindowManager, win::XCBWindow, event::xcb_focus_in_event_t, t) =
Event(response_type(event) == XCB_FOCUS_IN ? WINDOW_GAINED_FOCUS : WINDOW_LOST_FOCUS, nothing, (0.0, 0.0), t, win)

function is_delete_request(event::xcb_client_message_event_t, win::XCBWindow)
ed_8 = Int.(event.data.data8)
event_data32_1 = ed_8[1] + ed_8[2] * 2^8 + ed_8[3] * 2^16 + ed_8[4] * 2^24
event_data32_1 == win.delete_request
data = deserialize_delete_request_data(event.data.data8)
data == win.delete_request
end

serialize_delete_request_data(delete_request::xcb_atom_t) = ntuple(i -> i 4 ? UInt8((delete_request << 8(4 - i)) >> 24) : 0x00, 20)
deserialize_delete_request_data(bytes) = xcb_atom_t(Int(bytes[1]) + Int(bytes[2]) << 8 + Int(bytes[3]) << 16 + Int(bytes[4]) << 24)

function poll_for_events!(queue::EventQueue{XWindowManager})
event = xcb_poll_for_event(queue.wm.conn)
event == C_NULL && return false
Expand Down
67 changes: 64 additions & 3 deletions src/testing.jl
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ function response_type_xcb(event::Event)
event.type == POINTER_EXITED && return XCB_LEAVE_NOTIFY
event.type == WINDOW_RESIZED && return XCB_CONFIGURE_NOTIFY
event.type == WINDOW_EXPOSED && return XCB_EXPOSE
event.type == WINDOW_GAINED_FOCUS && return XCB_FOCUS_IN
event.type == WINDOW_LOST_FOCUS && return XCB_FOCUS_OUT
event.type == WINDOW_CLOSED && return XCB_CLIENT_MESSAGE
error("No response type corresponding to $(event.type)")
end

Expand All @@ -33,6 +36,8 @@ function event_type_xcb(event::Event)
event.type in POINTER_EVENT && return xcb_enter_notify_event_t
event.type == WINDOW_RESIZED && return xcb_configure_notify_event_t
event.type == WINDOW_EXPOSED && return xcb_expose_event_t
event.type in WINDOW_GAINED_FOCUS | WINDOW_LOST_FOCUS && return xcb_focus_in_event_t
event.type == WINDOW_CLOSED && return xcb_client_message_event_t
error("No event type corresponding to $(event.type)")
end

Expand All @@ -55,9 +60,14 @@ end

function event_xcb(wm::XWindowManager, e::Event)
T = event_type_xcb(e)
T === xcb_expose_event_t && return T(response_type_xcb(e), 0, 0, e.win.id, e.location..., extent(e.win)..., 0, 0)
T === xcb_configure_notify_event_t && return T(response_type_xcb(e), 0, 0, e.win.id, e.win.id, 0, e.location..., extent(e.win)..., 0, 0, 0)
T(response_type_xcb(e), detail_xcb(wm, e), 0, e.time, e.win.parent_id, e.win.id, 0, 0, 0, e.location..., state_xcb(e), true, false)
wx, wy = extent(e.win)
x, y = (wx, wy) .* e.location
x, y = round(Int16, x), round(Int16, y)
T === xcb_expose_event_t && return T(response_type_xcb(e), 0, 0, e.win.id, x, y, wx, wy, 0, (0, 0))
T === xcb_configure_notify_event_t && return T(response_type_xcb(e), 0, 0, e.win.id, e.win.id, 0, x, y, wx, wy, 0, 0, 0)
T === xcb_focus_in_event_t && return T(response_type_xcb(e), detail_xcb(wm, e), 0, e.win.id, 0, (0, 0, 0))
e.type == WINDOW_CLOSED && return T(response_type_xcb(e), 32, 0, e.win.id, 0x00000183, xcb_client_message_data_t(serialize_delete_request_data(e.win.delete_request)))
T(response_type_xcb(e), detail_xcb(wm, e), 0, 0, e.win.parent_id, e.win.id, 0, 0, 0, x, y, state_xcb(e), true, false)
end

send_event(wm::XWindowManager, e::Event) = send_event(e.win, event_xcb(wm, e))
Expand All @@ -77,3 +87,54 @@ function send_event(wm::XWindowManager, win::XCBWindow, event_type::EventType, d
end

send_event(wm::XWindowManager, win::XCBWindow) = (event_type, data = nothing; location = (0.0, 0.0)) -> send_event(wm, win, event_type, data; location)

struct WindowRef <: AbstractWindow
number::Int64
end

function save_history(wm::XWindowManager, queue::EventQueue{XWindowManager,XCBWindow})
events = Event{WindowRef}[]
windows = XCBWindow[]
for event in queue.history
i = findfirst(==(event.win), windows)
isnothing(i) && push!(windows, event.win)
winref = WindowRef(something(i, lastindex(windows)))
push!(events, Event(event.type, event.data, event.location, event.time, winref))
end
events
end

# FIXME: Events will be triggered multiple times if an event triggers another. How should we tackle that?
function replay_history(wm::XWindowManager, events::AbstractVector{Event{WindowRef}})
windows = Dict{WindowRef,XCBWindow}()
all_windows = xcb_window_t[]
replay_time = nothing
for event in events
win = get!(windows, event.win) do
# Assume that window IDs will be ordered chronologically.
union!(all_windows, keys(wm.windows))
@assert issorted(all_windows) "Window IDs do not appear to be sorted chronologically"
i = event.win.number
wm.windows[all_windows[i]]
end
wait_for(event.time - something(replay_time, event.time))
replay_time = event.time
event = Event(event.type, event.data, event.location, time(), win)
send_event(wm, event)
end
end

function wait_for(Δt)
start = time()

# Sleep if we need to wait for a while, to avoid holding onto CPU resources.
sleep_time = max(Δt - 0.002, 0)
if sleep_time > 0
sleep(sleep_time)
end

# Busy wait.
while time() < start + Δt
yield()
end
end
5 changes: 3 additions & 2 deletions src/window.jl
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,9 @@ end
function extent(win::XCBWindow)
geometry_cookie = xcb_get_geometry(win.conn, win.id)
geometry_reply = xcb_get_geometry_reply(win.conn, geometry_cookie, C_NULL)
geometry_reply == C_NULL && throw(InvalidWindow(win))
getproperty.(Ref(unsafe_load(geometry_reply)), (:width, :height))
geometry_reply == C_NULL && return (zero(UInt16), zero(UInt16))
data = unsafe_load(geometry_reply)
data.width, data.height
end

function resize(win::XCBWindow, extent)
Expand Down
37 changes: 25 additions & 12 deletions test/runtests.jl
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using XCB
using Test

function main(wm)
for event in EventQueue(wm)
function main(wm, queue = EventQueue(wm))
for event in queue
if event.type == WINDOW_CLOSED
close(wm, event.win)
elseif event.type == KEY_PRESSED
Expand Down Expand Up @@ -53,11 +53,6 @@ function on_pressed_key(wm, event)
write(io, String(wm.keymap))
end
end
if matches(key"f", event)
send = send_event(wm, win)
@info "Faking input: sending key AD01 to quit (requires an english keyboard layout to be translated to the relevant symbol 'q')"
return send(KEY_PRESSED, KeyEvent(wm.keymap, PhysicalKey(wm.keymap, :AD01), NO_MODIFIERS))
end

(; gc) = win
set_attributes(gc, [XCB.XCB_GC_FOREGROUND], [rand(1:16_777_215)])
Expand All @@ -74,12 +69,14 @@ function test()
ctx = GraphicsContext(win, attributes=[XCB.XCB_GC_FOREGROUND, XCB.XCB_GC_GRAPHICS_EXPOSURES], values=[screen.black_pixel, 0])
attach_graphics_context!(win, ctx)
send = send_event(wm, win)
queue = EventQueue(wm; record_history = true)

if interactive
main(wm)
main(wm, queue)
isfile("keymap.c") && rm("keymap.c")
else
@info "Running window asynchronously"
task = @async main(wm)
task = @async main(wm, queue)
@info "Sending fake inputs"
send(BUTTON_PRESSED, MouseEvent(BUTTON_LEFT, BUTTON_NONE))
send(BUTTON_RELEASED, MouseEvent(BUTTON_LEFT, BUTTON_LEFT))
Expand All @@ -92,17 +89,33 @@ function test()
send(KEY_RELEASED, KeyEvent(wm.keymap, PhysicalKey(wm.keymap, :AC09)))
send(KEY_PRESSED, KeyEvent(wm.keymap, PhysicalKey(wm.keymap, :AC02)))
send(KEY_RELEASED, KeyEvent(wm.keymap, PhysicalKey(wm.keymap, :AC02)))
send(KEY_PRESSED, KeyEvent(wm.keymap, PhysicalKey(wm.keymap, :AC04)))
send(KEY_RELEASED, KeyEvent(wm.keymap, PhysicalKey(wm.keymap, :AC04)))
sleep(0.5)
@info "Sending WINDOW_CLOSED event"
send(WINDOW_CLOSED)
@info "Waiting for window to close"
wait(task)
@test !istaskfailed(task)
@test isfile("keymap.c")
isfile("keymap.c") && rm("keymap.c")
end

events = save_history(wm, queue)

@info "Replaying events..."
wm = XWindowManager()
screen = current_screen(wm)
win = XCBWindow(wm; screen, x=0, y=1000, border_width=50, window_title="XCB window", icon_title="XCB", attributes=[XCB.XCB_CW_BACK_PIXEL], values=[screen.black_pixel])
ctx = GraphicsContext(win, attributes=[XCB.XCB_GC_FOREGROUND, XCB.XCB_GC_GRAPHICS_EXPOSURES], values=[screen.black_pixel, 0])
attach_graphics_context!(win, ctx)
task = @async main(wm)
replay_history(wm, events)
wait(task)
@test !istaskfailed(task)
isfile("keymap.c") && rm("keymap.c")

nothing
end

@testset "XCB.jl" begin
@test isnothing(test())
test()
end;

0 comments on commit 01dea9c

Please sign in to comment.