1 /* DrawOfPages: Take notes with touchscreen input.
2  * Copyright (C) 2017  Marko Semet(Marko10_000)
3  *
4  * This program is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
16  */
17 module drawofpages.gui.drawArea;
18 
19 private
20 {
21 	import cairo.Context;
22 	import cairo.ImageSurface;
23 	import cairo.Surface;
24 	import drawofpages.draw;
25 	import drawofpages.gui;
26 	import gdk.Device;
27 	import gtk.DrawingArea;
28 	import gdk.Event;
29 	import gtk.Widget;
30 	import std.math;
31 }
32 
33 private final class DrawPart
34 {
35 	package ImageSurface surface;
36 	private Context context;
37 
38 	public Point2D pos;
39 	public double scale;
40 	public Square box;
41 
42 	pragma(inline, true)
43 	private pure nothrow void updatePoint(ref Point2D p)
44 	{
45 		p = (p - this.pos) * this.scale;
46 	}
47 	pragma(inline, true)
48 	private pure nothrow void updateScale(ref double s)
49 	{
50 		s *= this.scale;
51 	}
52 
53 	public this(Point2D pos, double scale)
54 	{
55 		this.surface = ImageSurface.create(cairo_format_t.ARGB32, 512, 512);
56 		this.context = Context.create(this.surface);
57 		this.pos = pos;
58 		this.scale = scale;
59 
60 		// Set background color
61 		this.context.setSourceRgb(1, 1, 1);
62 		this.context.rectangle(0, 0, 512, 512);
63 		this.context.fill();
64 
65 		// Box
66 		this.box = Square(pos, pos + Point2D([512 / this.scale, 512 / this.scale]));
67 	}
68 	public ~this()
69 	{
70 		this.surface.destroy;
71 	}
72 
73 	public void drawLine(Point2D from, Point2D to, double size, Color color)
74 	{
75 		this.updatePoint(from);
76 		this.updatePoint(to);
77 		this.updateScale(size);
78 
79 		this.context.setSourceRgba(color.r, color.g, color.b, color.a);
80 		this.context.moveTo(from.dims[0], from.dims[1]);
81 		this.context.setLineWidth(size);
82 		this.context.lineTo(to.dims[0], to.dims[1]);
83 		this.context.stroke();
84 		this.context.moveTo(0, 0);
85 	}
86 }
87 
88 private final class PardDraw : Draw
89 {
90 	private DrawPart dp;
91 
92 	public this(DrawPart dp)
93 	{
94 		this.dp = dp;
95 	}
96 
97 	public void drawCirc(Point2D center, float radius, Color color)
98 	{
99 		assert(false);
100 	}
101 	public void drawLine(Point2D from, Point2D to, double size, Color color)
102 	{
103 		dp.drawLine(from, to, size, color);
104 	}
105 	public void drawRect(Square square, Color color)
106 	{
107 		assert(false); // TODO: draw rect
108 	}
109 	public void redraw() {}
110 }
111 
112 private final class Grid
113 {
114 	public DrawPart[][] parts;
115 
116 	public Point2D rel;
117 	public Point2D relZero;
118 	public double scale;
119 	public long x;
120 	public long y;
121 	public ulong width;
122 	public ulong height;
123 
124 	private pure void calcRelZero()
125 	{
126 		this.relZero = Point2D([this.x, this.y]) * this.scale;
127 	}
128 
129 	public this()
130 	{
131 		this.scale = 1;
132 		this.x = -256;
133 		this.y = -256;
134 		this.rel = Point2D([this.x, this.y]);
135 		this.width = 0;
136 		this.height = 0;
137 		this.parts = new DrawPart[][0];
138 		this.calcRelZero();
139 	}
140 
141 	public void resize(ulong width, ulong height, GuiInteraction interatcion)
142 	{
143 		synchronized
144 		{
145 			ulong partsX = (width - this.x) / 512 + ((width - this.x) % 512 == 0 ? 0 : 1);
146 			ulong partsY = (height - this.y) / 512 + ((height - this.y) % 512 == 0 ? 0 : 1);
147 			DrawPart[][] newParts = new DrawPart[][partsY];
148 			for(ulong y = 0; y < partsY; y++)
149 			{
150 				if(this.parts.length > y)
151 				{
152 					// Use old data
153 					DrawPart[] old = this.parts[y];
154 					DrawPart[] tmp = new DrawPart[partsX];
155 					for(ulong x = 0; x < partsX; x++)
156 					{
157 						if(old.length > x)
158 						{
159 							tmp[x] = old[x];
160 						}
161 						else
162 						{
163 							tmp[x] = new DrawPart(Point2D([x, y]) * (512 / this.scale) + this.rel, this.scale);
164 							interatcion.redraw(tmp[x].box, new PardDraw(tmp[x]));
165 						}
166 					}
167 					newParts[y] = tmp;
168 				}
169 				else
170 				{
171 					// Gen new array
172 					DrawPart[] tmp = new DrawPart[partsX];
173 					for(ulong x = 0; x < partsX; x++)
174 					{
175 						tmp[x] = new DrawPart(Point2D([x, y]) * (512 / this.scale) + this.rel, this.scale);
176 						interatcion.redraw(tmp[x].box, new PardDraw(tmp[x]));
177 					}
178 					newParts[y] = tmp;
179 				}
180 			}
181 
182 			// Set new data
183 			{
184 				DrawPart[][] tmp = this.parts;
185 				this.parts = newParts;
186 				tmp.destroy;
187 				this.width = width;
188 				this.height = height;
189 			}
190 		}
191 	}
192 }
193 
194 private final class DrawHandler : Draw
195 {
196 	private DrawElement de;
197 	package Grid grid;
198 
199 	public this(DrawElement de)
200 	{
201 		this.de = de;
202 		this.grid = new Grid();
203 	}
204 
205 	public void drawCirc(Point2D center, float radius, Color color)
206 	{
207 		assert(false);
208 	}
209 	public void drawLine(Point2D from, Point2D to, double size, Color color)
210 	{
211 		foreach(DrawPart[] dps; this.grid.parts)
212 		{
213 			foreach(DrawPart dp; dps)
214 			{
215 				dp.drawLine(from, to, size, color);
216 			}
217 		}
218 	}
219 	public void drawRect(Square square, Color color)
220 	{
221 		assert(false);
222 	}
223 	public void redraw()
224 	{
225 		this.de.queueDraw();
226 	}
227 
228 	package void _draw(Context cr)
229 	{
230 		for(long i = 0; i < this.grid.parts.length; i++)
231 		{
232 			DrawPart[] io = this.grid.parts[i];
233 			for(long j = 0; j < io.length; j++)
234 			{
235 				cr.setSourceSurface(io[j].surface, (j * 512) + (cast(long) this.grid.x), (i * 512) + (cast(long) this.grid.y));
236 				cr.rectangle((j * 512) + (cast(long) this.grid.x), (i * 512) + (cast(long) this.grid.y), 512, 512);
237 				cr.fill();
238 			}
239 		}
240 		cr.save();
241 		cr.destroy;
242 	}
243 }
244 
245 public class DrawElement : DrawingArea
246 {
247 	private DrawHandler dh;
248 	private GuiInteraction interatcion = null;
249 	private bool hasDown = false;
250 
251 	private static pure CURSOR_TYPE _getCursor(GdkInputSource gis)
252 	{
253 		if(gis == GdkInputSource.PEN)
254 		{
255 			return CURSOR_TYPE.PEN;
256 		}
257 		else if(gis == GdkInputSource.ERASER)
258 		{
259 			return CURSOR_TYPE.ERASER;
260 		}
261 		else if(gis == GdkInputSource.MOUSE)
262 		{
263 			return CURSOR_TYPE.MOUSE;
264 		}
265 		return CURSOR_TYPE.HAND;
266 	}
267 
268 	private struct _pointerData
269 	{
270 		string deviceID = null;
271 		double pressure;
272 		CURSOR_TYPE ctype;
273 
274 		this(Device device)
275 		{
276 			if(device.getDeviceType() != GdkDeviceType.MASTER)
277 			{
278 				this.deviceID = device.getProductId();
279 			}
280 			GdkAxisFlags axes = device.getAxes();
281 			this.pressure = axes.PRESSURE;
282 			this.ctype = _getCursor(device.getSource());
283 		}
284 	}
285 
286 	public this()
287 	{
288 		super();
289 		this.dh = new DrawHandler(this);
290 		this.addOnDraw(delegate bool(Context cr, Widget w) {
291 			synchronized
292 			{
293 				this._draw(cr);
294 			}
295 			return false;
296 		});
297 		this.addOnButtonPress(delegate bool(Event e, Widget w) {
298 			if(this.interatcion is null)
299 			{
300 				return false;
301 			}
302 
303 			GdkEventButton* geb = e.button();
304 			_pointerData tmp = _pointerData(e.getDevice());
305 			synchronized
306 			{
307 				this.interatcion.down(Point2D([geb.x, geb.y]), tmp.ctype, tmp.pressure, tmp.deviceID);
308 			}
309 			this.hasDown = true;
310 			return false;
311 		});
312 		this.addOnMotionNotify(delegate bool(Event e, Widget w) {
313 			if((this.interatcion is null) || (!this.hasDown))
314 			{
315 				return false;
316 			}
317 
318 			GdkEventMotion* motion = e.motion();
319 			_pointerData tmp = _pointerData(e.getDevice());
320 			synchronized
321 			{
322 				this.interatcion.contin(Point2D([motion.x, motion.y]), tmp.ctype, tmp.pressure, tmp.deviceID);
323 			}
324 			return false;
325 		});
326 		this.addOnButtonRelease(delegate bool(Event e, Widget w) {
327 			if(this.interatcion is null)
328 			{
329 				return false;
330 			}
331 
332 			GdkEventButton* geb = e.button();
333 			_pointerData tmp = _pointerData(e.getDevice());
334 			this.hasDown = false;
335 			synchronized
336 			{
337 				this.interatcion.up(Point2D([geb.x, geb.y]), tmp.ctype, tmp.pressure, tmp.deviceID);
338 			}
339 			return false;
340 		});
341 		this.addOnSizeAllocate(delegate void(Allocation alloc, Widget w) {
342 			synchronized(this)
343 			{
344 				this.dh.grid.resize(alloc.width, alloc.height, this.interatcion);
345 			}
346 		});
347 	}
348 
349 	private void _draw(Context cr)
350 	{
351 		this.dh._draw(cr);
352 	}
353 	public DrawHandler getDrawHanlder()
354 	{
355 		return this.dh;
356 	}
357 	public ref GuiInteraction getGuiInteraction()
358 	{
359 		return this.interatcion;
360 	}
361 }