1 /* combobox-like widget - select one of N texts
2  *
3  * Copyright (C) 2013-2016 Robin Gareus <robin@gareus.org>
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2, or (at your option)
8  * any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software Foundation,
17  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18  */
19 
20 #ifndef _ROB_TK_SELECTOR_H_
21 #define _ROB_TK_SELECTOR_H_
22 
23 #include "robtk_label.h"
24 
25 struct select_item {
26 	RobTkLbl* lbl;
27 	float value;
28 	int width;
29 };
30 
31 typedef struct {
32 	RobWidget* rw;
33 	struct select_item *items;
34 
35 	bool sensitive;
36 	bool prelight;
37 	int  lightarr;
38 
39 	bool wraparound;
40 	cairo_pattern_t* btn_bg;
41 
42 	bool (*cb) (RobWidget* w, void* handle);
43 	void* handle;
44 
45 	void (*touch_cb) (void*, uint32_t, bool);
46 	void*    touch_hd;
47 	uint32_t touch_id;
48 	bool     touching;
49 
50 	int active_item;
51 	int item_count;
52 	int dfl;
53 
54 	pthread_mutex_t _mutex;
55 	float w_width, w_height;
56 	float t_width, t_height;
57 	float scale;
58 } RobTkSelect;
59 
60 /******************************************************************************
61  * child callbacks
62  */
63 
robtk_select_expose_event(RobWidget * handle,cairo_t * cr,cairo_rectangle_t * ev)64 static bool robtk_select_expose_event(RobWidget* handle, cairo_t* cr, cairo_rectangle_t* ev) {
65 	RobTkSelect * d = (RobTkSelect *)GET_HANDLE(handle);
66 	assert(d->items != NULL);
67 	assert(d->active_item < d->item_count);
68 
69 	if (!d->btn_bg) {
70 		float c_bg[4]; get_color_from_theme(1, c_bg);
71 		d->btn_bg = cairo_pattern_create_linear (0.0, 0.0, 0.0, d->w_height);
72 		cairo_pattern_add_color_stop_rgb (d->btn_bg, ISBRIGHT(c_bg) ? 0.5 : 0.0, SHADE_RGB(c_bg, 1.95));
73 		cairo_pattern_add_color_stop_rgb (d->btn_bg, ISBRIGHT(c_bg) ? 0.0 : 0.5, SHADE_RGB(c_bg, 0.75));
74 	}
75 
76 	cairo_rectangle (cr, ev->x, ev->y, ev->width, ev->height);
77 	cairo_clip (cr);
78 	cairo_scale (cr, d->rw->widget_scale, d->rw->widget_scale);
79 
80 	rounded_rectangle(cr, 2.5, 2.5, d->w_width - 4, d->w_height -4, C_RAD);
81 	cairo_clip(cr);
82 
83 	/* background */
84 	float cbg[4], cfg[4];
85 	get_color_from_theme(0, cfg);
86 	get_color_from_theme(1, cbg);
87 	cairo_set_source_rgb (cr, cbg[0], cbg[1], cbg[2]);
88 
89 	rounded_rectangle(cr, 2.5, 2.5, d->w_width - 4, d->w_height -4, C_RAD);
90 	cairo_fill(cr);
91 
92 	/* draw arrows left + right */
93 	int w_width = d->w_width;
94 	const int w_h2 = d->w_height / 2;
95 
96 	cairo_set_line_width(cr, 1.0);
97 
98 	cairo_set_source(cr, d->btn_bg);
99 	cairo_rectangle(cr, 2.5, 2.5, 14, d->w_height - 4);
100 	if (d->sensitive && d->prelight && d->lightarr == -1) {
101 		cairo_fill_preserve(cr);
102 		if (ISBRIGHT(cbg)) {
103 			cairo_set_source_rgba (cr, 0.0, 0.0, 0.0, .1);
104 		} else {
105 			cairo_set_source_rgba (cr, 1.0, 1.0, 1.0, .1);
106 		}
107 	}
108 	cairo_fill(cr);
109 
110 	if (d->sensitive && (d->wraparound || d->active_item != 0)) {
111 		cairo_set_source_rgba(cr, cfg[0], cfg[1], cfg[2], 1.0);
112 		cairo_move_to(cr, 12, w_h2 - 3.5);
113 		cairo_line_to(cr,  8, w_h2 + 0.5);
114 		cairo_line_to(cr, 12, w_h2 + 4.5);
115 		cairo_stroke(cr);
116 	}
117 
118 	cairo_set_source(cr, d->btn_bg);
119 	cairo_rectangle(cr, w_width - 15.5, 2.5, 14, d->w_height - 4);
120 	if (d->prelight && d->lightarr == 1) {
121 		cairo_fill_preserve(cr);
122 		if (ISBRIGHT(cbg)) {
123 			cairo_set_source_rgba (cr, 0.0, 0.0, 0.0, .1);
124 		} else {
125 			cairo_set_source_rgba (cr, 1.0, 1.0, 1.0, .1);
126 		}
127 	}
128 	cairo_fill(cr);
129 
130 	if (d->sensitive && (d->wraparound || d->active_item != d->item_count -1)) {
131 		cairo_set_source_rgba(cr, cfg[0], cfg[1], cfg[2], 1.0);
132 		cairo_move_to(cr, w_width - 10.5, w_h2 - 3.5);
133 		cairo_line_to(cr, w_width -  6.5, w_h2 + 0.5);
134 		cairo_line_to(cr, w_width - 10.5, w_h2 + 4.5);
135 		cairo_stroke(cr);
136 	}
137 
138 	cairo_save(cr);
139 	const float off = 16 + (d->w_width - 36 - d->items[d->active_item].width) / 2.0;
140 	cairo_scale (cr, 1.0 / d->rw->widget_scale, 1.0 / d->rw->widget_scale);
141 	cairo_translate(cr, floor (off * d->rw->widget_scale), floor(3. * d->rw->widget_scale));
142 	cairo_rectangle_t a;
143 	a.x=0; a.width = ceil (d->items[d->active_item].width * d->rw->widget_scale);
144 	a.y=0; a.height = ceil (d->t_height * d->rw->widget_scale);
145 	robtk_lbl_expose_event(d->items[d->active_item].lbl->rw, cr, &a);
146 	cairo_restore(cr);
147 
148 	cairo_set_line_width (cr, .75);
149 	rounded_rectangle(cr, 2.5, 2.5, d->w_width - 4, d->w_height -4, C_RAD);
150 	cairo_set_line_width(cr, 1.0);
151 	cairo_set_source_rgba(cr, .0, .0, .0, 1.0);
152 	cairo_stroke(cr);
153 
154 	if (!d->sensitive) {
155 		cairo_set_source_rgba(cr, SHADE_RGB(cbg, .9) , 0.5);
156 		cairo_rectangle(cr, 0, 0, w_width, d->w_height);
157 		cairo_fill(cr);
158 	}
159 
160 	return TRUE;
161 }
162 
robtk_select_set_active_item(RobTkSelect * d,int i)163 static void robtk_select_set_active_item(RobTkSelect *d, int i) {
164 	if (i<0 || i >= d->item_count) return;
165 	if (i == d->active_item) return;
166 	d->active_item = i;
167 	if (d->cb) d->cb(d->rw, d->handle);
168 	queue_draw(d->rw);
169 }
170 
robtk_select_mousedown(RobWidget * handle,RobTkBtnEvent * ev)171 static RobWidget* robtk_select_mousedown(RobWidget* handle, RobTkBtnEvent *ev) {
172 	RobTkSelect * d = (RobTkSelect *)GET_HANDLE(handle);
173 	if (!d->sensitive) { return NULL; }
174 	if (!d->prelight) { return NULL; }
175 	if (d->touch_cb) {
176 		d->touch_cb (d->touch_hd, d->touch_id, true);
177 	}
178 	return NULL;
179 }
180 
robtk_select_mouseup(RobWidget * handle,RobTkBtnEvent * ev)181 static RobWidget* robtk_select_mouseup(RobWidget* handle, RobTkBtnEvent *ev) {
182 	RobTkSelect * d = (RobTkSelect *)GET_HANDLE(handle);
183 	if (!d->sensitive) { return NULL; }
184 	if (!d->prelight) {
185 		if (d->touch_cb) {
186 			d->touch_cb (d->touch_hd, d->touch_id, false);
187 		}
188 		return NULL;
189 	}
190 
191 	if (ev->state & ROBTK_MOD_SHIFT) {
192 		robtk_select_set_active_item(d, d->dfl);
193 		return NULL;
194 	}
195 
196 	int active_item = d->active_item;
197 	if (ev->x <= 18 * d->rw->widget_scale) {
198 		if (d->wraparound) {
199 			active_item = (d->active_item + d->item_count - 1) % d->item_count;
200 		} else {
201 				active_item--;
202 		}
203 	} else if (ev->x >= (d->w_width - 18) * d->rw->widget_scale) {
204 		if (d->wraparound) {
205 			active_item = (d->active_item + 1) % d->item_count;
206 		} else {
207 			active_item++;
208 		}
209 	}
210 
211 	robtk_select_set_active_item(d, active_item);
212 
213 	if (d->touch_cb) {
214 		d->touch_cb (d->touch_hd, d->touch_id, false);
215 	}
216 	return NULL;
217 }
218 
robtk_select_mousemove(RobWidget * handle,RobTkBtnEvent * ev)219 static RobWidget* robtk_select_mousemove(RobWidget* handle, RobTkBtnEvent *ev) {
220 	RobTkSelect * d = (RobTkSelect *)GET_HANDLE(handle);
221 	if (!d->sensitive) { return NULL; }
222 	int pla = 0;
223 	if (ev->x <= 18 * d->rw->widget_scale) {
224 		if (d->wraparound || d->active_item != 0) pla = -1;
225 	} else if (ev->x >= (d->w_width - 18) * d->rw->widget_scale) {
226 		if (d->wraparound || d->active_item != d->item_count -1) pla = 1;
227 	}
228 	if (pla != d->lightarr) {
229 		d->lightarr = pla;
230 		queue_draw(d->rw);
231 	}
232 	return NULL;
233 }
234 
robtk_select_scroll(RobWidget * handle,RobTkBtnEvent * ev)235 static RobWidget* robtk_select_scroll(RobWidget* handle, RobTkBtnEvent *ev) {
236 	RobTkSelect * d = (RobTkSelect *)GET_HANDLE(handle);
237 	if (!d->sensitive) { return NULL; }
238 
239 	int active_item = d->active_item;
240 	switch (ev->direction) {
241 		case ROBTK_SCROLL_RIGHT:
242 		case ROBTK_SCROLL_UP:
243 			if (d->wraparound) {
244 				active_item = (d->active_item + 1) % d->item_count;
245 			} else {
246 				active_item++;
247 			}
248 			break;
249 		case ROBTK_SCROLL_LEFT:
250 		case ROBTK_SCROLL_DOWN:
251 			if (d->wraparound) {
252 				active_item = (d->active_item + d->item_count - 1) % d->item_count;
253 			} else {
254 				active_item--;
255 			}
256 			break;
257 		default:
258 			break;
259 	}
260 	if (d->touch_cb && !d->touching) {
261 		d->touch_cb (d->touch_hd, d->touch_id, true);
262 		d->touching = TRUE;
263 	}
264 
265 	robtk_select_set_active_item(d, active_item);
266 	return handle;
267 }
268 
robtk_select_enter_notify(RobWidget * handle)269 static void robtk_select_enter_notify(RobWidget *handle) {
270 	RobTkSelect * d = (RobTkSelect *)GET_HANDLE(handle);
271 	if (!d->prelight) {
272 		d->prelight = TRUE;
273 		queue_draw(d->rw);
274 	}
275 }
276 
robtk_select_leave_notify(RobWidget * handle)277 static void robtk_select_leave_notify(RobWidget *handle) {
278 	RobTkSelect * d = (RobTkSelect *)GET_HANDLE(handle);
279 	if (d->touch_cb && d->touching) {
280 		d->touch_cb (d->touch_hd, d->touch_id, false);
281 		d->touching = FALSE;
282 	}
283 	if (d->prelight) {
284 		d->prelight = FALSE;
285 		queue_draw(d->rw);
286 	}
287 }
288 
289 
290 static void
robtk_select_size_request(RobWidget * handle,int * w,int * h)291 robtk_select_size_request(RobWidget* handle, int *w, int *h) {
292 	RobTkSelect * d = (RobTkSelect *)GET_HANDLE(handle);
293 	if (d->scale != d->rw->widget_scale) {
294 		d->scale = d->rw->widget_scale;
295 		for (int i = 0; i < d->item_count; ++i) {
296 			d->items[i].lbl->rw->widget_scale = d->scale;
297 		}
298 	}
299 	// currently assumes  text widgets are instantiated with scale 1.0
300 	*w = d->rw->widget_scale * (d->t_width + 36);
301 	*h = d->rw->widget_scale * MAX(16, 6 + d->t_height);
302 }
303 
304 static void
robtk_select_size_allocate(RobWidget * handle,int w,int h)305 robtk_select_size_allocate(RobWidget* handle, int w, int h) {
306 	RobTkSelect * d = (RobTkSelect *)GET_HANDLE(handle);
307 	d->w_width = w / d->rw->widget_scale;
308 	d->w_height = MAX(16, 6 + d->t_height);
309 	robwidget_set_size(handle, w, h);
310 }
311 
312 
313 /******************************************************************************
314  * public functions
315  */
316 
robtk_select_new()317 static RobTkSelect * robtk_select_new() {
318 	RobTkSelect *d = (RobTkSelect *) malloc(sizeof(RobTkSelect));
319 
320 	d->sensitive = TRUE;
321 	d->prelight = FALSE;
322 	d->lightarr = 0;
323 	d->cb = NULL;
324 	d->handle = NULL;
325 	d->touch_cb = NULL;
326 	d->touch_hd = NULL;
327 	d->touch_id = 0;
328 	d->touching = FALSE;
329 	d->scale = 1.0;
330 	pthread_mutex_init (&d->_mutex, 0);
331 
332 	d->wraparound = FALSE;
333 	d->items = NULL;
334 	d->btn_bg = NULL;
335 	d->item_count = d->active_item = d->dfl = 0;
336 	d->w_width = d->w_height = 0;
337 	d->t_width = d->t_height = 0;
338 
339 	d->rw = robwidget_new(d);
340 	ROBWIDGET_SETNAME(d->rw, "select");
341 	robwidget_set_expose_event(d->rw, robtk_select_expose_event);
342 	robwidget_set_mouseup(d->rw, robtk_select_mouseup);
343 	robwidget_set_mousedown(d->rw, robtk_select_mousedown);
344 	robwidget_set_mousemove(d->rw, robtk_select_mousemove);
345 	robwidget_set_mousescroll(d->rw, robtk_select_scroll);
346 	robwidget_set_enter_notify(d->rw, robtk_select_enter_notify);
347 	robwidget_set_leave_notify(d->rw, robtk_select_leave_notify);
348 	return d;
349 }
350 
robtk_select_destroy(RobTkSelect * d)351 static void robtk_select_destroy(RobTkSelect *d) {
352 	int i;
353 	for (i=0; i < d->item_count ; ++i) {
354 		robtk_lbl_destroy(d->items[i].lbl);
355 	}
356 	robwidget_destroy(d->rw);
357 	if (d->btn_bg) cairo_pattern_destroy(d->btn_bg);
358 	free(d->items);
359 	pthread_mutex_destroy(&d->_mutex);
360 
361 	free(d);
362 }
363 
robtk_select_set_alignment(RobTkSelect * d,float x,float y)364 static void robtk_select_set_alignment(RobTkSelect *d, float x, float y) {
365 	robwidget_set_alignment(d->rw, x, y);
366 }
367 
robtk_select_add_item(RobTkSelect * d,float val,const char * txt)368 static void robtk_select_add_item(RobTkSelect *d, float val, const char *txt) {
369 	d->items = (struct select_item *) realloc(d->items, sizeof(struct select_item) * (d->item_count + 1));
370 	d->items[d->item_count].value = val;
371 	d->items[d->item_count].lbl = robtk_lbl_new(txt);
372 	int w, h;
373 	priv_lbl_size_request(d->items[d->item_count].lbl->rw, &w, &h);
374 	assert (d->rw->widget_scale  == 1.0); // XXX
375 	d->t_width = MAX(d->t_width, w);
376 	d->t_height = MAX(d->t_height, h);
377 	d->items[d->item_count].width = w;
378 	d->item_count++;
379 	robwidget_set_size_request(d->rw, robtk_select_size_request);
380 	robwidget_set_size_allocate(d->rw, robtk_select_size_allocate);
381 }
382 
robtk_select_widget(RobTkSelect * d)383 static RobWidget * robtk_select_widget(RobTkSelect *d) {
384 	return d->rw;
385 }
386 
robtk_select_set_callback(RobTkSelect * d,bool (* cb)(RobWidget * w,void * handle),void * handle)387 static void robtk_select_set_callback(RobTkSelect *d, bool (*cb) (RobWidget* w, void* handle), void* handle) {
388 	d->cb = cb;
389 	d->handle = handle;
390 }
391 
robtk_select_set_touch(RobTkSelect * d,void (* cb)(void *,uint32_t,bool),void * handle,uint32_t id)392 static void robtk_select_set_touch(RobTkSelect *d, void (*cb) (void*, uint32_t, bool), void* handle, uint32_t id) {
393 	d->touch_cb = cb;
394 	d->touch_hd = handle;
395 	d->touch_id = id;
396 }
397 
robtk_select_set_default_item(RobTkSelect * d,int i)398 static void robtk_select_set_default_item(RobTkSelect *d, int i) {
399 	d->dfl = i;
400 }
401 
robtk_select_set_item(RobTkSelect * d,int i)402 static void robtk_select_set_item(RobTkSelect *d, int i) {
403 	robtk_select_set_active_item(d, i);
404 }
405 
robtk_select_set_value(RobTkSelect * d,float v)406 static void robtk_select_set_value(RobTkSelect *d, float v) {
407 	assert(d->item_count > 0);
408 	int i;
409 	int s = 0;
410 	float diff = fabsf(v - d->items[0].value);
411 	for (i=1; i < d->item_count; ++i) {
412 		float df = fabsf(v - d->items[i].value);
413 		if (df < diff) {
414 			s = i;
415 			diff = df;
416 		}
417 	}
418 	robtk_select_set_active_item(d, s);
419 }
420 
421 
robtk_select_set_sensitive(RobTkSelect * d,bool s)422 static void robtk_select_set_sensitive(RobTkSelect *d, bool s) {
423 	if (d->sensitive != s) {
424 		d->sensitive = s;
425 	}
426 	queue_draw(d->rw);
427 }
428 
robtk_select_get_item(RobTkSelect * d)429 static int robtk_select_get_item(RobTkSelect *d) {
430 	return d->active_item;
431 }
432 
robtk_select_get_value(RobTkSelect * d)433 static float robtk_select_get_value(RobTkSelect *d) {
434 	return d->items[d->active_item].value;
435 }
436 
robtk_select_set_wrap(RobTkSelect * d,bool en)437 static void robtk_select_set_wrap(RobTkSelect *d, bool en) {
438 	d->wraparound = en;
439 }
440 
robtk_select_get_wrap(RobTkSelect * d)441 static bool robtk_select_get_wrap(RobTkSelect *d) {
442 	return d->wraparound;
443 }
444 #endif
445