cairo-gtk-animation.py: add a more efficient animation strategy
[experiments/cairo-gtk.git] / cairo-gtk-interactivity.py
1 #!/usr/bin/env python
2
3 # An example of mouse interaction with cairo and Gtk+
4 #
5 # Copyright (C) 2013  Antonio Ospite <ospite@studenti.unina.it>
6 #
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
20 # References:
21 # https://developer.gnome.org/gtk3/3.6/gtk-getting-started.html#idm140520417717488
22
23 import gi
24
25 gi.require_version('Gtk', '3.0')
26 from gi.repository import Gdk, Gtk, GObject, Gio
27
28 import math
29 import colorsys
30
31
32 # from http://stackoverflow.com/questions/481144/
33 def in_circle(c_x, c_y, r, x, y):
34     return math.hypot(c_x - x, c_y - y) <= r
35
36
37 def clamp(x, interval):
38     if x < interval[0]:
39         return interval[0]
40     elif x > interval[1]:
41         return interval[1]
42     else:
43         return x
44
45
46 class InteractiveCanvas(Gtk.DrawingArea):
47     def __init__(self, width, height, *args, **kwargs):
48         super(InteractiveCanvas, self).__init__(*args, **kwargs)
49
50         self.width = width
51         self.height = height
52
53         self.x_offset = self.width / 2.0
54         self.y_offset = self.height / 2.0
55         self.radius = 20
56         self.border = 4
57
58         self.selectable = False
59         self.selected = False
60         self.drag_offset_x = 0
61         self.drag_offset_y = 0
62
63         self.x = self.width / 2.0
64         self.y = self.height / 2.0
65
66         self.set_size_request(width, height)
67         self.connect('draw', self.on_draw)
68         self.connect('configure-event', self.on_configure_event)
69
70         self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK |
71                         Gdk.EventMask.BUTTON_RELEASE_MASK |
72                         Gdk.EventMask.POINTER_MOTION_MASK)
73         self.connect('button-press-event', self.on_button_press_event)
74         self.connect('button-release-event', self.on_button_release_event)
75         self.connect('motion-notify-event', self.on_motion_notify_event)
76
77     def on_configure_event(self, widget, event):
78         self.width = event.width
79         self.height = event.height
80
81     def on_draw(self, widget, cr):
82         cr.rectangle(0, 0, self.width, self.height)
83         cr.set_source_rgb(1.0, 0.5, 0.5)
84         cr.fill()
85
86         cr.arc(self.x, self.y, self.radius, 0, 2 * math.pi)
87
88         _h = self.x / float(self.width)
89         if self.selected:
90             _l = 0.6
91         else:
92             if self.selectable:
93                 _l = 0.5
94             else:
95                 _l = 0.2
96         _s = self.y / float(self.height)
97         _r, _g, _b = colorsys.hls_to_rgb(_h, _l, _s)
98         cr.set_source_rgb(_r, _g, _b)
99         cr.fill_preserve()
100
101         cr.set_source_rgb(0, 0, 0)
102         cr.set_line_width(self.border)
103         cr.stroke()
104
105     def on_button_press_event(self, widget, event):
106         if self.selectable:
107             self.selected = True
108             self.drag_offset_x = self.x - event.x
109             self.drag_offset_y = self.y - event.y
110             self.queue_draw()
111
112     def on_button_release_event(self, widget, event):
113         self.selected = False
114         self.queue_draw()
115
116     def on_motion_notify_event(self, widget, event):
117         if self.selected:
118             self.x = clamp(event.x + self.drag_offset_x, [0, self.width])
119             self.y = clamp(event.y + self.drag_offset_y, [0, self.height])
120             self.queue_draw()
121         else:
122             old_selectable = self.selectable
123             if in_circle(self.x, self.y, self.radius + self.border,
124                          event.x, event.y):
125                 self.selectable = True
126             else:
127                 self.selectable = False
128
129             if self.selectable != old_selectable:
130                 self.queue_draw()
131
132
133 class InteractiveApp(Gtk.Application):
134     def __init__(self, title, width, height, *args, **kwargs):
135         super(InteractiveApp, self).__init__(*args, **kwargs)
136         self.connect("activate", self.on_activate)
137         self.title = title
138         self.width = width
139         self.height = height
140
141     def on_activate(self, data=None):
142         window = Gtk.Window(type=Gtk.WindowType.TOPLEVEL)
143         window.set_title(self.title)
144
145         canvas = InteractiveCanvas(self.width, self.height)
146         window.add(canvas)
147         window.show_all()
148         self.add_window(window)
149
150
151 if __name__ == "__main__":
152     app = InteractiveApp(
153         "InteractiveApp", 640, 480,
154         application_id="apps.test.interactiveapp",
155         flags=Gio.ApplicationFlags.FLAGS_NONE)
156     app.run(None)