1 /* This file is part of Ganv.
2  * Copyright 2007-2015 David Robillard <http://drobilla.net>
3  *
4  * Ganv is free software: you can redistribute it and/or modify it under the
5  * terms of the GNU General Public License as published by the Free Software
6  * Foundation, either version 3 of the License, or any later version.
7  *
8  * Ganv is distributed in the hope that it will be useful, but WITHOUT ANY
9  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
10  * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for details.
11  *
12  * You should have received a copy of the GNU General Public License along
13  * with Ganv.  If not, see <http://www.gnu.org/licenses/>.
14  */
15 
16 #include "boilerplate.h"
17 #include "color.h"
18 #include "ganv-private.h"
19 #include "gettext.h"
20 
21 #include "ganv/box.h"
22 #include "ganv/item.h"
23 #include "ganv/node.h"
24 #include "ganv/types.h"
25 
26 #include <cairo.h>
27 #include <glib-object.h>
28 #include <glib.h>
29 #include <gtk/gtk.h>
30 
31 #include <math.h>
32 #include <string.h>
33 
34 static const double STACKED_OFFSET = 4.0;
35 
36 G_DEFINE_TYPE_WITH_CODE(GanvBox, ganv_box, GANV_TYPE_NODE,
37                         G_ADD_PRIVATE(GanvBox))
38 
39 static GanvNodeClass* parent_class;
40 
41 enum {
42 	PROP_0,
43 	PROP_X1,
44 	PROP_Y1,
45 	PROP_X2,
46 	PROP_Y2,
47 	PROP_RADIUS_TL,
48 	PROP_RADIUS_TR,
49 	PROP_RADIUS_BR,
50 	PROP_RADIUS_BL,
51 	PROP_STACKED,
52 	PROP_BEVELED
53 };
54 
55 static void
ganv_box_init(GanvBox * box)56 ganv_box_init(GanvBox* box)
57 {
58 	box->impl = (GanvBoxPrivate*)ganv_box_get_instance_private(box);
59 
60 	memset(&box->impl->coords, '\0', sizeof(GanvBoxCoords));
61 
62 	box->impl->coords.border_width = GANV_NODE(box)->impl->border_width;
63 	box->impl->old_coords          = box->impl->coords;
64 	box->impl->radius_tl           = 0.0;
65 	box->impl->radius_tr           = 0.0;
66 	box->impl->radius_br           = 0.0;
67 	box->impl->radius_bl           = 0.0;
68 	box->impl->beveled             = FALSE;
69 }
70 
71 static void
ganv_box_destroy(GtkObject * object)72 ganv_box_destroy(GtkObject* object)
73 {
74 	g_return_if_fail(object != NULL);
75 	g_return_if_fail(GANV_IS_BOX(object));
76 
77 	if (GTK_OBJECT_CLASS(parent_class)->destroy) {
78 		(*GTK_OBJECT_CLASS(parent_class)->destroy)(object);
79 	}
80 }
81 
82 static void
ganv_box_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)83 ganv_box_set_property(GObject*      object,
84                       guint         prop_id,
85                       const GValue* value,
86                       GParamSpec*   pspec)
87 {
88 	g_return_if_fail(object != NULL);
89 	g_return_if_fail(GANV_IS_BOX(object));
90 
91 	GanvBox*        box    = GANV_BOX(object);
92 	GanvBoxPrivate* impl   = box->impl;
93 	GanvBoxCoords*  coords = &impl->coords;
94 
95 	switch (prop_id) {
96 		SET_CASE(X1, double, coords->x1)
97 		SET_CASE(Y1, double, coords->y1)
98 		SET_CASE(X2, double, coords->x2)
99 		SET_CASE(Y2, double, coords->y2)
100 		SET_CASE(RADIUS_TL, double, impl->radius_tl)
101 		SET_CASE(RADIUS_TR, double, impl->radius_tr)
102 		SET_CASE(RADIUS_BR, double, impl->radius_br)
103 		SET_CASE(RADIUS_BL, double, impl->radius_bl)
104 		SET_CASE(STACKED, boolean, coords->stacked)
105 		SET_CASE(BEVELED, boolean, impl->beveled)
106 	default:
107 		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
108 		break;
109 	}
110 }
111 
112 static void
ganv_box_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)113 ganv_box_get_property(GObject*    object,
114                       guint       prop_id,
115                       GValue*     value,
116                       GParamSpec* pspec)
117 {
118 	g_return_if_fail(object != NULL);
119 	g_return_if_fail(GANV_IS_BOX(object));
120 
121 	GanvBox*        box    = GANV_BOX(object);
122 	GanvBoxPrivate* impl   = box->impl;
123 	GanvBoxCoords*  coords = &impl->coords;
124 
125 	switch (prop_id) {
126 		GET_CASE(X1, double, coords->x1)
127 		GET_CASE(X2, double, coords->x2)
128 		GET_CASE(Y1, double, coords->y1)
129 		GET_CASE(Y2, double, coords->y2)
130 		GET_CASE(RADIUS_TL, double, impl->radius_tl)
131 		GET_CASE(RADIUS_TR, double, impl->radius_tr)
132 		GET_CASE(RADIUS_BR, double, impl->radius_br)
133 		GET_CASE(RADIUS_BL, double, impl->radius_bl)
134 		GET_CASE(STACKED, boolean, impl->coords.stacked)
135 		GET_CASE(BEVELED, boolean, impl->beveled)
136 	default:
137 		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
138 		break;
139 	}
140 }
141 
142 static void
ganv_box_bounds_item(const GanvBoxCoords * coords,double * x1,double * y1,double * x2,double * y2)143 ganv_box_bounds_item(const GanvBoxCoords* coords,
144                      double* x1, double* y1,
145                      double* x2, double* y2)
146 {
147 	*x1 = coords->x1 - coords->border_width;
148 	*y1 = coords->y1 - coords->border_width;
149 	*x2 = coords->x2 + coords->border_width + (coords->stacked * STACKED_OFFSET);
150 	*y2 = coords->y2 + coords->border_width + (coords->stacked * STACKED_OFFSET);
151 }
152 
153 void
ganv_box_request_redraw(GanvItem * item,const GanvBoxCoords * coords,gboolean world)154 ganv_box_request_redraw(GanvItem*            item,
155                         const GanvBoxCoords* coords,
156                         gboolean             world)
157 {
158 	double x1 = 0.0;
159 	double y1 = 0.0;
160 	double x2 = 0.0;
161 	double y2 = 0.0;
162 	ganv_box_bounds_item(coords, &x1, &y1, &x2, &y2);
163 
164 	if (!world) {
165 		// Convert from item-relative coordinates to world coordinates
166 		ganv_item_i2w_pair(item, &x1, &y1, &x2, &y2);
167 	}
168 
169 	ganv_canvas_request_redraw_w(item->impl->canvas, x1, y1, x2, y2);
170 }
171 
172 static void
coords_i2w(GanvItem * item,GanvBoxCoords * coords)173 coords_i2w(GanvItem* item, GanvBoxCoords* coords)
174 {
175 	ganv_item_i2w_pair(item, &coords->x1, &coords->y1, &coords->x2, &coords->y2);
176 }
177 
178 static void
ganv_box_bounds(GanvItem * item,double * x1,double * y1,double * x2,double * y2)179 ganv_box_bounds(GanvItem* item,
180                 double* x1, double* y1,
181                 double* x2, double* y2)
182 {
183 	// Note this will not be correct if children are outside the box bounds
184 	GanvBox* box = (GanvBox*)item;
185 	ganv_box_bounds_item(&box->impl->coords, x1, y1, x2, y2);
186 }
187 
188 static void
ganv_box_update(GanvItem * item,int flags)189 ganv_box_update(GanvItem* item, int flags)
190 {
191 	GanvBox*        box  = GANV_BOX(item);
192 	GanvBoxPrivate* impl = box->impl;
193 	impl->coords.border_width = box->node.impl->border_width;
194 
195 	// Request redraw of old location
196 	ganv_box_request_redraw(item, &impl->old_coords, TRUE);
197 
198 	// Store old coordinates in world relative coordinates in case the
199 	// group we are in moves between now and the next update
200 	impl->old_coords = impl->coords;
201 	coords_i2w(item, &impl->old_coords);
202 
203 	// Call parent class update method, resizing if necessary
204 	GANV_ITEM_CLASS(parent_class)->update(item, flags);
205 	ganv_box_normalize(box);
206 
207 	// Update world-relative bounding box
208 	ganv_box_bounds(item, &item->impl->x1, &item->impl->y1, &item->impl->x2, &item->impl->y2);
209 	ganv_item_i2w_pair(item, &item->impl->x1, &item->impl->y1, &item->impl->x2, &item->impl->y2);
210 
211 	// Request redraw of new location
212 	ganv_box_request_redraw(item, &impl->coords, FALSE);
213 }
214 
215 void
ganv_box_path(GanvBox * box,cairo_t * cr,double x1,double y1,double x2,double y2,double dr)216 ganv_box_path(GanvBox* box,
217               cairo_t* cr, double x1, double y1, double x2, double y2,
218               double dr)
219 {
220 	static const double degrees = G_PI / 180.0;
221 
222 	GanvBoxPrivate* impl = box->impl;
223 
224 	if (impl->radius_tl == 0.0 && impl->radius_tr == 0.0
225 	    && impl->radius_br == 0.0 && impl->radius_bl == 0.0) {
226 		// Simple rectangle
227 		cairo_rectangle(cr, x1, y1, x2 - x1, y2 - y1);
228 	} else if (impl->beveled) {
229 		// Beveled rectangle
230 		cairo_new_sub_path(cr);
231 		cairo_move_to(cr, x1 + impl->radius_tl, y1);
232 		cairo_line_to(cr, x2 - impl->radius_tr, y1);
233 		cairo_line_to(cr, x2,                   y1 + impl->radius_tr);
234 		cairo_line_to(cr, x2,                   y2 - impl->radius_br);
235 		cairo_line_to(cr, x2 - impl->radius_br, y2);
236 		cairo_line_to(cr, x1 + impl->radius_bl, y2);
237 		cairo_line_to(cr, x1,                   y2 - impl->radius_bl);
238 		cairo_line_to(cr, x1,                   y2 - impl->radius_bl);
239 		cairo_line_to(cr, x1,                   y1 + impl->radius_tl);
240 		cairo_close_path(cr);
241 	} else {
242 		// Rounded rectangle
243 		cairo_new_sub_path(cr);
244 		cairo_arc(cr,
245 		          x2 - impl->radius_tr - dr,
246 		          y1 + impl->radius_tr + dr,
247 		          impl->radius_tr + dr, -90 * degrees, 0 * degrees);
248 		cairo_arc(cr,
249 		          x2 - impl->radius_br - dr, y2 - impl->radius_br - dr,
250 		          impl->radius_br + dr, 0 * degrees, 90 * degrees);
251 		cairo_arc(cr,
252 		          x1 + impl->radius_bl + dr, y2 - impl->radius_bl - dr,
253 		          impl->radius_bl + dr, 90 * degrees, 180 * degrees);
254 		cairo_arc(cr,
255 		          x1 + impl->radius_tl + dr, y1 + impl->radius_tl + dr,
256 		          impl->radius_tl + dr, 180 * degrees, 270 * degrees);
257 		cairo_close_path(cr);
258 	}
259 }
260 
261 static void
ganv_box_draw(GanvItem * item,cairo_t * cr,double cx,double cy,double cw,double ch)262 ganv_box_draw(GanvItem* item,
263               cairo_t* cr, double cx, double cy, double cw, double ch)
264 {
265 	GanvBox*        box  = GANV_BOX(item);
266 	GanvBoxPrivate* impl = box->impl;
267 
268 	double x1 = impl->coords.x1;
269 	double y1 = impl->coords.y1;
270 	double x2 = impl->coords.x2;
271 	double y2 = impl->coords.y2;
272 	ganv_item_i2w_pair(item, &x1, &y1, &x2, &y2);
273 
274 	double dash_length  = 0.0;
275 	double border_color = 0.0;
276 	double fill_color   = 0.0;
277 	ganv_node_get_draw_properties(
278 		&box->node, &dash_length, &border_color, &fill_color);
279 
280 	double r = 0.0;
281 	double g = 0.0;
282 	double b = 0.0;
283 	double a = 0.0;
284 
285 	for (int i = (impl->coords.stacked ? 1 : 0); i >= 0; --i) {
286 		const double x = 0.0 - (STACKED_OFFSET * i);
287 		const double y = 0.0 - (STACKED_OFFSET * i);
288 
289 		// Trace basic box path
290 		ganv_box_path(box, cr, x1 - x, y1 - y, x2 - x, y2 - y, 0.0);
291 
292 		// Fill
293 		color_to_rgba(fill_color, &r, &g, &b, &a);
294 		cairo_set_source_rgba(cr, r, g, b, a);
295 
296 		// Border
297 		if (impl->coords.border_width > 0.0) {
298 			cairo_fill_preserve(cr);
299 			color_to_rgba(border_color, &r, &g, &b, &a);
300 			cairo_set_source_rgba(cr, r, g, b, a);
301 			cairo_set_line_width(cr, impl->coords.border_width);
302 			if (dash_length > 0) {
303 				cairo_set_dash(cr, &dash_length, 1, box->node.impl->dash_offset);
304 			} else {
305 				cairo_set_dash(cr, &dash_length, 0, 0);
306 			}
307 			cairo_stroke(cr);
308 		} else {
309 			cairo_fill(cr);
310 		}
311 	}
312 
313 	GanvItemClass* item_class = GANV_ITEM_CLASS(parent_class);
314 	item_class->draw(item, cr, cx, cy, cw, ch);
315 }
316 
317 static double
ganv_box_point(GanvItem * item,double x,double y,GanvItem ** actual_item)318 ganv_box_point(GanvItem* item, double x, double y, GanvItem** actual_item)
319 {
320 	GanvBox*        box  = GANV_BOX(item);
321 	GanvBoxPrivate* impl = box->impl;
322 
323 	*actual_item = NULL;
324 
325 	double x1 = 0.0;
326 	double y1 = 0.0;
327 	double x2 = 0.0;
328 	double y2 = 0.0;
329 	ganv_box_bounds_item(&impl->coords, &x1, &y1, &x2, &y2);
330 
331 	// Point is inside the box (distance 0)
332 	if ((x >= x1) && (y >= y1) && (x <= x2) && (y <= y2)) {
333 		*actual_item = item;
334 		return 0.0;
335 	}
336 
337 	// Point is outside the box
338 	double dx = 0.0;
339 	double dy = 0.0;
340 
341 	// Find horizontal distance to nearest edge
342 	if (x < x1) {
343 		dx = x1 - x;
344 	} else if (x > x2) {
345 		dx = x - x2;
346 	}
347 
348 	// Find vertical distance to nearest edge
349 	if (y < y1) {
350 		dy = y1 - y;
351 	} else if (y > y2) {
352 		dy = y - y2;
353 	}
354 
355 	return sqrt((dx * dx) + (dy * dy));
356 }
357 
358 static gboolean
ganv_box_is_within(const GanvNode * self,double x1,double y1,double x2,double y2)359 ganv_box_is_within(const GanvNode* self,
360                    double          x1,
361                    double          y1,
362                    double          x2,
363                    double          y2)
364 {
365 	double bx1 = 0.0;
366 	double by1 = 0.0;
367 	double bx2 = 0.0;
368 	double by2 = 0.0;
369 	g_object_get(G_OBJECT(self),
370 	             "x1", &bx1,
371 	             "y1", &by1,
372 	             "x2", &bx2,
373 	             "y2", &by2,
374 	             NULL);
375 
376 	ganv_item_i2w_pair(GANV_ITEM(self), &bx1, &by1, &bx2, &by2);
377 
378 	return (   bx1 >= x1
379 	           && by2 >= y1
380 	           && bx2 <= x2
381 	           && by2 <= y2);
382 }
383 
384 static void
ganv_box_default_set_width(GanvBox * box,double width)385 ganv_box_default_set_width(GanvBox* box, double width)
386 {
387 	box->impl->coords.x2 = ganv_box_get_x1(box) + width;
388 	ganv_item_request_update(GANV_ITEM(box));
389 }
390 
391 static void
ganv_box_default_set_height(GanvBox * box,double height)392 ganv_box_default_set_height(GanvBox* box, double height)
393 {
394 	box->impl->coords.y2 = ganv_box_get_y1(box) + height;
395 	ganv_item_request_update(GANV_ITEM(box));
396 }
397 
398 static void
ganv_box_class_init(GanvBoxClass * klass)399 ganv_box_class_init(GanvBoxClass* klass)
400 {
401 	GObjectClass*   gobject_class = (GObjectClass*)klass;
402 	GtkObjectClass* object_class  = (GtkObjectClass*)klass;
403 	GanvItemClass*  item_class    = (GanvItemClass*)klass;
404 	GanvNodeClass*  node_class    = (GanvNodeClass*)klass;
405 
406 	parent_class = GANV_NODE_CLASS(g_type_class_peek_parent(klass));
407 
408 	gobject_class->set_property = ganv_box_set_property;
409 	gobject_class->get_property = ganv_box_get_property;
410 
411 	g_object_class_install_property(
412 		gobject_class, PROP_X1, g_param_spec_double(
413 			"x1",
414 			_("x1"),
415 			_("Top left x coordinate."),
416 			-G_MAXDOUBLE, G_MAXDOUBLE,
417 			0.0,
418 			G_PARAM_READWRITE));
419 
420 	g_object_class_install_property(
421 		gobject_class, PROP_Y1, g_param_spec_double(
422 			"y1",
423 			_("y1"),
424 			_("Top left y coordinate."),
425 			-G_MAXDOUBLE, G_MAXDOUBLE,
426 			0.0,
427 			G_PARAM_READWRITE));
428 
429 	g_object_class_install_property(
430 		gobject_class, PROP_X2, g_param_spec_double(
431 			"x2",
432 			_("x2"),
433 			_("Bottom right x coordinate."),
434 			-G_MAXDOUBLE, G_MAXDOUBLE,
435 			0.0,
436 			G_PARAM_READWRITE));
437 
438 	g_object_class_install_property(
439 		gobject_class, PROP_Y2, g_param_spec_double(
440 			"y2",
441 			_("y2"),
442 			_("Bottom right y coordinate."),
443 			-G_MAXDOUBLE, G_MAXDOUBLE,
444 			0.0,
445 			G_PARAM_READWRITE));
446 
447 	g_object_class_install_property(
448 		gobject_class, PROP_RADIUS_TL, g_param_spec_double(
449 			"radius-tl",
450 			_("Top left radius"),
451 			_("The radius of the top left corner."),
452 			0.0, G_MAXDOUBLE,
453 			0.0,
454 			G_PARAM_READWRITE));
455 
456 	g_object_class_install_property(
457 		gobject_class, PROP_RADIUS_TR, g_param_spec_double(
458 			"radius-tr",
459 			_("Top right radius"),
460 			_("The radius of the top right corner."),
461 			0.0, G_MAXDOUBLE,
462 			0.0,
463 			G_PARAM_READWRITE));
464 
465 	g_object_class_install_property(
466 		gobject_class, PROP_RADIUS_BR, g_param_spec_double(
467 			"radius-br",
468 			_("Bottom right radius"),
469 			_("The radius of the bottom right corner."),
470 			0.0, G_MAXDOUBLE,
471 			0.0,
472 			G_PARAM_READWRITE));
473 
474 	g_object_class_install_property(
475 		gobject_class, PROP_RADIUS_BL, g_param_spec_double(
476 			"radius-bl",
477 			_("Bottom left radius"),
478 			_("The radius of the bottom left corner."),
479 			0.0, G_MAXDOUBLE,
480 			0.0,
481 			G_PARAM_READWRITE));
482 
483 	g_object_class_install_property(
484 		gobject_class, PROP_STACKED, g_param_spec_boolean(
485 			"stacked",
486 			_("Stacked"),
487 			_("Show box with a stacked appearance."),
488 			FALSE,
489 			G_PARAM_READWRITE));
490 
491 	g_object_class_install_property(
492 		gobject_class, PROP_BEVELED, g_param_spec_boolean(
493 			"beveled",
494 			_("Beveled"),
495 			_("Show radiused corners with a sharp bevel."),
496 			FALSE,
497 			G_PARAM_READWRITE));
498 
499 	object_class->destroy = ganv_box_destroy;
500 
501 	item_class->update = ganv_box_update;
502 	item_class->bounds = ganv_box_bounds;
503 	item_class->point  = ganv_box_point;
504 	item_class->draw   = ganv_box_draw;
505 
506 	node_class->is_within = ganv_box_is_within;
507 
508 	klass->set_width  = ganv_box_default_set_width;
509 	klass->set_height = ganv_box_default_set_height;
510 }
511 
512 void
ganv_box_normalize(GanvBox * box)513 ganv_box_normalize(GanvBox* box)
514 {
515 	if (box->impl->coords.x2 < box->impl->coords.x1) {
516 		const double tmp = box->impl->coords.x1;
517 		box->impl->coords.x1 = box->impl->coords.x2;
518 		box->impl->coords.x2 = tmp;
519 	}
520 	if (box->impl->coords.y2 < box->impl->coords.y1) {
521 		const double tmp = box->impl->coords.y1;
522 		box->impl->coords.y1 = box->impl->coords.y2;
523 		box->impl->coords.y2 = tmp;
524 	}
525 }
526 
527 double
ganv_box_get_x1(const GanvBox * box)528 ganv_box_get_x1(const GanvBox* box)
529 {
530 	return box->impl->coords.x1;
531 }
532 
533 double
ganv_box_get_y1(const GanvBox * box)534 ganv_box_get_y1(const GanvBox* box)
535 {
536 	return box->impl->coords.y1;
537 }
538 
539 double
ganv_box_get_x2(const GanvBox * box)540 ganv_box_get_x2(const GanvBox* box)
541 {
542 	return box->impl->coords.x2;
543 }
544 
545 double
ganv_box_get_y2(const GanvBox * box)546 ganv_box_get_y2(const GanvBox* box)
547 {
548 	return box->impl->coords.y2;
549 }
550 
551 double
ganv_box_get_width(const GanvBox * box)552 ganv_box_get_width(const GanvBox* box)
553 {
554 	return box->impl->coords.x2 - box->impl->coords.x1;
555 }
556 
557 void
ganv_box_set_width(GanvBox * box,double width)558 ganv_box_set_width(GanvBox* box,
559                    double   width)
560 {
561 	GANV_BOX_GET_CLASS(box)->set_width(box, width);
562 }
563 
564 double
ganv_box_get_height(const GanvBox * box)565 ganv_box_get_height(const GanvBox* box)
566 {
567 	return box->impl->coords.y2 - box->impl->coords.y1;
568 }
569 
570 void
ganv_box_set_height(GanvBox * box,double height)571 ganv_box_set_height(GanvBox* box,
572                     double   height)
573 {
574 	GANV_BOX_GET_CLASS(box)->set_height(box, height);
575 }
576 
577 double
ganv_box_get_border_width(const GanvBox * box)578 ganv_box_get_border_width(const GanvBox* box)
579 {
580 	return box->impl->coords.border_width;
581 }
582