Skip to content

Commit 67e6cc0

Browse files
committed
Docs: add walk-through examples on Canvas redraws
1 parent 702b814 commit 67e6cc0

File tree

2 files changed

+331
-0
lines changed

2 files changed

+331
-0
lines changed

doc/imageview_warmups.md

Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
If you're new to Reactive or functionally-reactive programming in
2+
general, it pays to spend some time exploring Reactive's programming
3+
paradigm. This documents a set of experiments conducted while
4+
rewriting ImageView.jl using Reactive and Gtk; perhaps they will serve
5+
as useful demonstrations for others.
6+
7+
## Core concept: deferred execution
8+
9+
Actions that you take on Reactive nodes are `push!`ed to a queue and
10+
run at some later time:
11+
12+
```jl
13+
julia> using Reactive
14+
15+
julia> n = Node(0)
16+
Node{Int64}(0, nactions=0)
17+
18+
julia> push!(n, 1)
19+
20+
julia> value(n)
21+
0
22+
23+
julia> Reactive.run_till_now()
24+
25+
julia> value(n)
26+
1
27+
```
28+
29+
You can cause these updates to be processed at regular intervals---if
30+
julia isn't busy doing something else---as follows:
31+
```jl
32+
eventloop = Timer(_->Reactive.run_till_now(), 0.001, 0.001)
33+
```
34+
35+
If you want to stop checking for queued messages, call
36+
`close(eventloop)`.
37+
38+
## Example 1: throttling redraws
39+
40+
In an interactive image viewer, drawing an image on the canvas might
41+
depend on many variables: the current contrast settings, the x- and
42+
y-intervals selected by zooming, and (for images with transparency)
43+
the choice of a colored or checkerboard background. Whenever one of
44+
these variables updates, we want to redraw the image. However, quite
45+
commonly we might update several of these variables "simultaneously":
46+
for example, selecting a zoom region with the mouse will update both
47+
the x- and y-intervals. While we could write a function
48+
49+
set_x_and_y!(imagecanvas, xlim, ylim)
50+
imagecanvas.xlim = xlim
51+
imagecanvas.ylim = ylim
52+
draw(imagecanvas)
53+
end
54+
55+
this approach has disadvantages particularly when changes to
56+
`imagecanvas` are generated from code rather than user interaction:
57+
while updates of `xlim` and `ylim` are now coupled, code that affects
58+
both `xlim` and, say, the contrast settings will generate needless
59+
redraws.
60+
61+
A simple approach that does not require any coupling is to use
62+
*scheduled* redraws: any time a relevant setting changes, indicate
63+
that the canvas needs to be redrawn, but don't perform that redraw
64+
immediately. Instead, schedule it for some (short) time in the
65+
future, and limit (`throttle`) redraws to some minimum time interval.
66+
67+
Let's explore a couple of different implementations of this basic
68+
idea. First, let's look at an approach that uses Reactive just to
69+
manage the updates---this is a hybrid between reactive-programming and
70+
state-dependent approaches.
71+
72+
```jl
73+
module TwoStates
74+
75+
using Reactive
76+
77+
export Canvas, state1!, state2!
78+
79+
type Canvas{S<:IO}
80+
io::S
81+
state1::Int
82+
state2::Int
83+
update::Node{Bool} # push! a value here any time the canvas needs redrawing
84+
85+
function Canvas(io::S, s1::Integer, s2::Integer)
86+
update = Node(true)
87+
c = new(io, s1, s2, update)
88+
throttled = throttle(1/60, update)
89+
# The node that gets returned by `map` will be garbage-collected
90+
# unless we call `preserve` on it. An alternative is to store
91+
# this node somewhere (e.g., see the "TwoSharedNodes" example below).
92+
Reactive.preserve(map(x->println(c.io, "state1: $(c.state1); state2: $(c.state2)"), throttled))
93+
c
94+
end
95+
end
96+
97+
Canvas(io::IO, s1, s2) = Canvas{typeof(io)}(io, s1, s2)
98+
99+
state1!(c::Canvas, val) = (c.state1 = val; push!(c.update, true); val)
100+
state2!(c::Canvas, val) = (c.state2 = val; push!(c.update, true); val)
101+
102+
end # module
103+
104+
using Reactive, TwoStates
105+
106+
eventqueue = Timer(_->Reactive_run_till_now(), 0.001, 0.001)
107+
108+
# OK, let's try it!
109+
c = Canvas(STDOUT, 1, 1)
110+
state1!(c, 5)
111+
sleep(0.1)
112+
113+
state1!(c, 7)
114+
state2!(c, -3)
115+
```
116+
117+
Here's the output:
118+
```jl
119+
julia> include("twostates.jl")
120+
state1: 1; state2: 1
121+
state1: 5; state2: 1
122+
-3
123+
124+
julia> state1: 7; state2: -3
125+
julia>
126+
```
127+
Note that your exact output can depend on the details of timing between
128+
`eventloop` and the REPL code.
129+
130+
The first output line was triggered by creating the `Canvas`. The
131+
second line was triggered by the first `state1!` call; most likely,
132+
both of these outputs were produced during the `sleep`. The next line
133+
is the returned value from `state2!(c, -3)`, and control returns to
134+
the julia REPL. However, roughly a millisecond later the event queue
135+
fires and processes updates; it produces a single line of output for
136+
the updates to both states. Without the `throttle`, we instead would
137+
have gotten something like
138+
139+
```jl
140+
julia> include("twostates.jl")
141+
state1: 1; state2: 1
142+
state1: 5; state2: 1
143+
state1: 7; state2: -3
144+
-3state1: 7; state2: -3
145+
146+
julia>
147+
```
148+
149+
Note that unlike the previous case using `throttle`, here each
150+
`state!` call generated a corresponding line of output.
151+
152+
153+
Interestingly, the output produced by the second update of `signal1`
154+
also included the consequences of updating `signal2`: this is because
155+
the updates happened before the event loop fired again, so by the time
156+
the `println` statement ran both values had already been updated.
157+
In some circumstances (like this one), this behavior might be fine or
158+
even desirable; in other cases, such behavior could be a source of
159+
bugs.
160+
161+
So in the spirit of exploration, let's look at a second implementation
162+
that preserves history:
163+
164+
```jl
165+
module TwoNodes
166+
167+
using Reactive
168+
169+
export Canvas, state1!, state2!
170+
171+
type Canvas{S<:IO}
172+
io::S
173+
state1::Node{Int}
174+
state2::Node{Int}
175+
176+
function Canvas(io::S, s1::Integer, s2::Integer)
177+
n1, n2 = Node(Int, s1), Node(Int, s2)
178+
c = new(io, n1, n2)
179+
combined = merge(n1, n2)
180+
throttled = throttle(1/60, combined)
181+
Reactive.preserve(map(x->println(c.io, "state1: $(value(c.state1)); state2: $(value(c.state2))"), throttled))
182+
c
183+
end
184+
end
185+
186+
Canvas(io::IO, s1, s2) = Canvas{typeof(io)}(io, s1, s2)
187+
188+
state1!(c::Canvas, val) = push!(c.state1, val)
189+
state2!(c::Canvas, val) = push!(c.state2, val)
190+
191+
end # module
192+
193+
using Reactive, TwoNodes
194+
195+
tmr = Timer(_->Reactive.run_till_now(), 0.001, 0.001)
196+
197+
c = Canvas(STDOUT, 1, 1)
198+
state1!(c, 5)
199+
sleep(0.1)
200+
201+
state1!(c, 7)
202+
state2!(c, -3)
203+
```
204+
205+
Note that the implementation of the `state!` functions was simpler
206+
here. With `throttle` we again get the same output:
207+
208+
```jl
209+
julia> include("twonodes.jl")
210+
state1: 1; state2: 1
211+
state1: 5; state2: 1
212+
213+
julia> state1: 7; state2: -3
214+
julia>
215+
```
216+
217+
but this time, without `throttle` we get output that respects the history:
218+
219+
```jl
220+
julia> include("twonodes.jl")
221+
state1: 1; state2: 1
222+
state1: 5; state2: 1
223+
state1: 7; state2: 1
224+
225+
julia> state1: 7; state2: -3
226+
julia>
227+
```
228+
229+
## Example 2: sharing state
230+
231+
Suppose we have two `Canvas`es that we want to couple together: for
232+
example, you might want to show two views of the same image, one in
233+
"raw" form and the other "annotated" by some kind of image processing
234+
algorithm. If you zoom in on one canvas, you might like to
235+
automatically zoom in on the same region in the other canvas.
236+
237+
```jl
238+
module TwoSharedNodes
239+
240+
using Reactive
241+
242+
export Canvas, state1!, state2!, relink!
243+
244+
type Canvas{S<:IO}
245+
io::S
246+
name::ASCIIString
247+
state1::Node{Int}
248+
state2::Node{Int}
249+
update::Node{Void}
250+
251+
Canvas(io::S, name, s1, s2) = Canvas(io, name, node(Int, s1), node(Int, s2))
252+
function Canvas(io::S, name, n1::Node{Int}, n2::Node{Int})
253+
c = new(io, name, n1, n2)
254+
relink!(c)
255+
end
256+
end
257+
258+
node{T}(::Type{T}, val) = Node(T, val)
259+
node{T}(::Type{T}, n::Node{T}) = n
260+
261+
function relink!(c::Canvas)
262+
isdefined(c, :update) && close(c.update)
263+
combined = merge(c.state1, c.state2)
264+
throttled = throttle(1/60, combined)
265+
c.update = map(x->println(c.io, "$(c.name): state1=$(value(c.state1)), state2=$(value(c.state2))"), throttled)
266+
c
267+
end
268+
269+
Canvas(io::IO, name, s1, s2) = Canvas{typeof(io)}(io, name, s1, s2)
270+
271+
state1!(c::Canvas, val) = push!(c.state1, val)
272+
state2!(c::Canvas, val) = push!(c.state2, val)
273+
274+
end # module
275+
276+
using Reactive, TwoSharedNodes
277+
278+
tmr = Timer(_->Reactive.run_till_now(), 0.001, 0.001)
279+
280+
n2 = Node(22)
281+
c1 = Canvas(STDOUT, "canvas1", 1, n2)
282+
c2 = Canvas(STDOUT, "canvas2", 2, n2)
283+
state2!(c1, 33)
284+
sleep(1.0)
285+
println("slept")
286+
287+
state1!(c1, 7)
288+
state2!(c2, -3)
289+
sleep(1.0)
290+
println("slept")
291+
292+
# Now let's disconnect the two
293+
println("decoupling canvases")
294+
c1.state2 = Node(11)
295+
relink!(c1)
296+
relink!(c2)
297+
sleep(1.0)
298+
println("slept")
299+
state2!(c2, -5)
300+
sleep(1.0)
301+
println("slept")
302+
state2!(c1, -7)
303+
```
304+
305+
The output from this script should be something like
306+
307+
```jl
308+
julia> include("twosharednodes.jl")
309+
canvas1: state1=1, state2=22
310+
canvas2: state1=2, state2=22
311+
canvas1: state1=1, state2=33
312+
canvas2: state1=2, state2=33
313+
slept
314+
canvas1: state1=7, state2=-3
315+
canvas2: state1=2, state2=-3
316+
slept
317+
decoupling canvases
318+
canvas1: state1=7, state2=11
319+
canvas2: state1=2, state2=-3
320+
slept
321+
canvas2: state1=2, state2=-5
322+
slept
323+
324+
julia> canvas1: state1=7, state2=-7
325+
julia>
326+
```
327+
328+
You can see that until we decoupled them, updating state2 triggered
329+
updates to both Canvases.

doc/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,8 @@ Reactive is a great substrate to build interactive GUI libraries. Here are a few
285285

286286
It could also be potentially used for other projects that require any kind of event handling: controlling robots, making music or simulations.
287287

288+
See also a set of [experiments using Reactive for drawing control](imageview_warmups.md).
289+
288290
# Reporting Bugs
289291

290292
Let me know about any bugs, counterintuitive behavior, or enhancements you'd like by [filing a bug](https://github.com/shashi/Reactive.jl/issues/new) on github.

0 commit comments

Comments
 (0)