prepare meter-widget for dual background color
[ardour.git] / libs / gtkmm2ext / fastmeter.cc
1 /*
2     Copyright (C) 2003-2006 Paul Davis
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 2 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, write to the Free Software
16     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17
18     $Id$
19 */
20
21 #include <iostream>
22 #include <cmath>
23 #include <algorithm>
24 #include <cstring>
25
26 #include <gdkmm/rectangle.h>
27 #include <gtkmm2ext/fastmeter.h>
28 #include <gtkmm2ext/utils.h>
29
30 #define UINT_TO_RGB(u,r,g,b) { (*(r)) = ((u)>>16)&0xff; (*(g)) = ((u)>>8)&0xff; (*(b)) = (u)&0xff; }
31 #define UINT_TO_RGBA(u,r,g,b,a) { UINT_TO_RGB(((u)>>8),r,g,b); (*(a)) = (u)&0xff; }
32 using namespace Gtk;
33 using namespace Gdk;
34 using namespace Glib;
35 using namespace Gtkmm2ext;
36 using namespace std;
37
38 int FastMeter::min_pattern_metric_size = 16;
39 int FastMeter::max_pattern_metric_size = 1024;
40
41 FastMeter::Pattern10Map FastMeter::vm_pattern_cache;
42 FastMeter::PatternBgMap FastMeter::vb_pattern_cache;
43
44 FastMeter::FastMeter (long hold, unsigned long dimen, Orientation o, int len,
45                 int clr0, int clr1, int clr2, int clr3,
46                 int clr4, int clr5, int clr6, int clr7,
47                 int clr8, int clr9,
48                 int bgc0, int bgc1,
49                 int bgh0, int bgh1,
50                 float stp0, float stp1,
51                 float stp2, float stp3
52                 )
53 {
54         orientation = o;
55         hold_cnt = hold;
56         resized = true;
57         hold_state = 0;
58         current_peak = 0;
59         current_level = 0;
60         last_peak_rect.width = 0;
61         last_peak_rect.height = 0;
62
63         highlight = false;
64
65         _clr[0] = clr0;
66         _clr[1] = clr1;
67         _clr[2] = clr2;
68         _clr[3] = clr3;
69         _clr[4] = clr4;
70         _clr[5] = clr5;
71         _clr[6] = clr6;
72         _clr[7] = clr7;
73         _clr[8] = clr8;
74         _clr[9] = clr9;
75
76         _bgc[0] = bgc0;
77         _bgc[1] = bgc1;
78
79         _bgh[0] = bgh0;
80         _bgh[1] = bgh1;
81
82         _stp[0] = stp0;
83         _stp[1] = stp1;
84         _stp[2] = stp2;
85         _stp[3] = stp3;
86
87         set_events (BUTTON_PRESS_MASK|BUTTON_RELEASE_MASK);
88
89         pixrect.x = 1;
90         pixrect.y = 1;
91
92         if (!len) {
93                 len = 250;
94         }
95         fgpattern = request_vertical_meter(dimen, len, _clr, _stp, true);
96         bgpattern = request_vertical_background (dimen, len, _bgc, false);
97         pixheight = len;
98         pixwidth = dimen;
99
100         pixrect.width = pixwidth;
101         pixrect.height = pixheight;
102
103         request_width = pixrect.width + 2;
104         request_height= pixrect.height + 2;
105
106         queue_draw ();
107 }
108
109 FastMeter::~FastMeter ()
110 {
111 }
112
113 Cairo::RefPtr<Cairo::Pattern>
114 FastMeter::generate_meter_pattern (
115                 int width, int height, int *clr, float *stp, bool shade)
116 {
117         guint8 r,g,b,a;
118         double knee;
119         double soft = 1.5 / (double) height;
120
121         cairo_pattern_t* pat = cairo_pattern_create_linear (0.0, 0.0, 0.0, height);
122
123         /*
124           Cairo coordinate space goes downwards as y value goes up, so invert
125           knee-based positions by using (1.0 - y)
126         */
127
128         UINT_TO_RGBA (clr[9], &r, &g, &b, &a); // top/clip
129         cairo_pattern_add_color_stop_rgb (pat, 0.0,
130                                           r/255.0, g/255.0, b/255.0);
131
132         knee = ((float)height * stp[3] / 115.0f); // -0dB
133
134         UINT_TO_RGBA (clr[8], &r, &g, &b, &a);
135         cairo_pattern_add_color_stop_rgb (pat, 1.0 - (knee/(double)height) - soft,
136                                           r/255.0, g/255.0, b/255.0);
137
138         UINT_TO_RGBA (clr[7], &r, &g, &b, &a);
139         cairo_pattern_add_color_stop_rgb (pat, 1.0 - (knee/(double)height) + soft,
140                                           r/255.0, g/255.0, b/255.0);
141
142         knee = ((float)height * stp[2]/ 115.0f); // -3dB || -2dB
143
144         UINT_TO_RGBA (clr[6], &r, &g, &b, &a);
145         cairo_pattern_add_color_stop_rgb (pat, 1.0 - (knee/(double)height) - soft,
146                                           r/255.0, g/255.0, b/255.0);
147
148         UINT_TO_RGBA (clr[5], &r, &g, &b, &a);
149         cairo_pattern_add_color_stop_rgb (pat, 1.0 - (knee/(double)height) + soft,
150                                           r/255.0, g/255.0, b/255.0);
151
152         knee = ((float)height * stp[1] / 115.0f); // -9dB
153
154         UINT_TO_RGBA (clr[4], &r, &g, &b, &a);
155         cairo_pattern_add_color_stop_rgb (pat, 1.0 - (knee/(double)height) - soft,
156                                           r/255.0, g/255.0, b/255.0);
157
158         UINT_TO_RGBA (clr[3], &r, &g, &b, &a);
159         cairo_pattern_add_color_stop_rgb (pat, 1.0 - (knee/(double)height) + soft,
160                                           r/255.0, g/255.0, b/255.0);
161
162         knee = ((float)height * stp[0] / 115.0f); // -18dB
163
164         UINT_TO_RGBA (clr[2], &r, &g, &b, &a);
165         cairo_pattern_add_color_stop_rgb (pat, 1.0 - (knee/(double)height) - soft,
166                                           r/255.0, g/255.0, b/255.0);
167
168         UINT_TO_RGBA (clr[1], &r, &g, &b, &a);
169         cairo_pattern_add_color_stop_rgb (pat, 1.0 - (knee/(double)height) + soft,
170                                           r/255.0, g/255.0, b/255.0);
171
172         UINT_TO_RGBA (clr[0], &r, &g, &b, &a); // bottom
173         cairo_pattern_add_color_stop_rgb (pat, 1.0,
174                                           r/255.0, g/255.0, b/255.0);
175
176         if (shade) {
177                 cairo_pattern_t* shade_pattern = cairo_pattern_create_linear (0.0, 0.0, width, 0.0);
178                 cairo_pattern_add_color_stop_rgba (shade_pattern, 0, 1.0, 1.0, 1.0, 0.2);
179                 cairo_pattern_add_color_stop_rgba (shade_pattern, 1, 0.0, 0.0, 0.0, 0.3);
180
181                 cairo_surface_t* surface;
182                 cairo_t* tc = 0;
183                 surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height);
184                 tc = cairo_create (surface);
185                 cairo_set_source (tc, pat);
186                 cairo_rectangle (tc, 0, 0, width, height);
187                 cairo_fill (tc);
188                 cairo_set_source (tc, shade_pattern);
189                 cairo_rectangle (tc, 0, 0, width, height);
190                 cairo_fill (tc);
191
192                 cairo_pattern_destroy (pat);
193                 cairo_pattern_destroy (shade_pattern);
194
195                 pat = cairo_pattern_create_for_surface (surface);
196
197                 cairo_destroy (tc);
198                 cairo_surface_destroy (surface);
199         }
200
201         Cairo::RefPtr<Cairo::Pattern> p (new Cairo::Pattern (pat, false));
202
203         return p;
204 }
205
206
207 Cairo::RefPtr<Cairo::Pattern>
208 FastMeter::generate_meter_background (
209                 int width, int height, int *clr, bool shade)
210 {
211         guint8 r0,g0,b0,r1,g1,b1,a;
212
213         cairo_pattern_t* pat = cairo_pattern_create_linear (0.0, 0.0, 0.0, height);
214
215         UINT_TO_RGBA (clr[0], &r0, &g0, &b0, &a);
216         UINT_TO_RGBA (clr[1], &r1, &g1, &b1, &a);
217
218         cairo_pattern_add_color_stop_rgb (pat, 0.0,
219                                           r1/255.0, g1/255.0, b1/255.0);
220
221         cairo_pattern_add_color_stop_rgb (pat, 1.0,
222                                           r0/255.0, g0/255.0, b0/255.0);
223
224         if (shade) {
225                 cairo_pattern_t* shade_pattern = cairo_pattern_create_linear (0.0, 0.0, width, 0.0);
226                 cairo_pattern_add_color_stop_rgba (shade_pattern, 0.0, 1.0, 1.0, 1.0, 0.15);
227                 cairo_pattern_add_color_stop_rgba (shade_pattern, 0.6, 0.0, 0.0, 0.0, 0.10);
228                 cairo_pattern_add_color_stop_rgba (shade_pattern, 1.0, 1.0, 1.0, 1.0, 0.20);
229
230                 cairo_surface_t* surface;
231                 cairo_t* tc = 0;
232                 surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height);
233                 tc = cairo_create (surface);
234                 cairo_set_source (tc, pat);
235                 cairo_rectangle (tc, 0, 0, width, height);
236                 cairo_fill (tc);
237                 cairo_set_source (tc, shade_pattern);
238                 cairo_rectangle (tc, 0, 0, width, height);
239                 cairo_fill (tc);
240
241                 cairo_pattern_destroy (pat);
242                 cairo_pattern_destroy (shade_pattern);
243
244                 pat = cairo_pattern_create_for_surface (surface);
245
246                 cairo_destroy (tc);
247                 cairo_surface_destroy (surface);
248         }
249
250         Cairo::RefPtr<Cairo::Pattern> p (new Cairo::Pattern (pat, false));
251
252         return p;
253 }
254
255 Cairo::RefPtr<Cairo::Pattern>
256 FastMeter::request_vertical_meter(
257                 int width, int height, int *clr, float *stp, bool shade)
258 {
259         if (height < min_pattern_metric_size)
260                 height = min_pattern_metric_size;
261         if (height > max_pattern_metric_size)
262                 height = max_pattern_metric_size;
263
264         const Pattern10MapKey key (width, height,
265                         stp[0], stp[1], stp[2], stp[3],
266                         clr[0], clr[1], clr[2], clr[3],
267                         clr[4], clr[5], clr[6], clr[7],
268                         clr[8], clr[9]);
269
270         Pattern10Map::iterator i;
271         if ((i = vm_pattern_cache.find (key)) != vm_pattern_cache.end()) {
272                 return i->second;
273         }
274         // TODO flush pattern cache if it gets too large
275
276         Cairo::RefPtr<Cairo::Pattern> p = generate_meter_pattern (
277                 width, height, clr, stp, shade);
278         vm_pattern_cache[key] = p;
279
280         return p;
281 }
282
283 Cairo::RefPtr<Cairo::Pattern>
284 FastMeter::request_vertical_background(
285                 int width, int height, int *bgc, bool shade)
286 {
287         if (height < min_pattern_metric_size)
288                 height = min_pattern_metric_size;
289         if (height > max_pattern_metric_size)
290                 height = max_pattern_metric_size;
291
292         const PatternBgMapKey key (width, height, bgc[0], bgc[1]);
293         PatternBgMap::iterator i;
294         if ((i = vb_pattern_cache.find (key)) != vb_pattern_cache.end()) {
295                 return i->second;
296         }
297         // TODO flush pattern cache if it gets too large
298
299         Cairo::RefPtr<Cairo::Pattern> p = generate_meter_background (
300                 width, height, bgc, shade);
301         vb_pattern_cache[key] = p;
302
303         return p;
304 }
305
306
307 void
308 FastMeter::set_hold_count (long val)
309 {
310         if (val < 1) {
311                 val = 1;
312         }
313
314         hold_cnt = val;
315         hold_state = 0;
316         current_peak = 0;
317
318         queue_draw ();
319 }
320
321 void
322 FastMeter::on_size_request (GtkRequisition* req)
323 {
324         req->height = request_height;
325         req->height = max(req->height, min_pattern_metric_size);
326         req->height = min(req->height, max_pattern_metric_size);
327         req->height += 2;
328
329         req->width  = request_width;
330 }
331
332 void
333 FastMeter::on_size_allocate (Gtk::Allocation &alloc)
334 {
335         if (alloc.get_width() != request_width) {
336                 alloc.set_width (request_width);
337         }
338
339         int h = alloc.get_height();
340         h = max (h, min_pattern_metric_size + 2);
341         h = min (h, max_pattern_metric_size + 2);
342
343         if (h != alloc.get_height()) {
344                 alloc.set_height (h);
345         }
346
347         if (pixheight != h) {
348                 fgpattern = request_vertical_meter (request_width, h, _clr, _stp, true);
349                 bgpattern = request_vertical_background (request_width, h, highlight ? _bgh : _bgc, highlight);
350                 pixheight = h - 2;
351                 pixwidth  = request_width - 2;
352         }
353
354         DrawingArea::on_size_allocate (alloc);
355         resized = true;
356 }
357
358 bool
359 FastMeter::on_expose_event (GdkEventExpose* ev)
360 {
361         return vertical_expose (ev);
362 }
363
364 bool
365 FastMeter::vertical_expose (GdkEventExpose* ev)
366 {
367         Glib::RefPtr<Gdk::Window> win = get_window ();
368         gint top_of_meter;
369         GdkRectangle intersection;
370         GdkRectangle background;
371
372         cairo_t* cr = gdk_cairo_create (get_window ()->gobj());
373
374         if (resized) {
375                 cairo_set_source_rgb (cr, 0, 0, 0); // black
376                 rounded_rectangle (cr, 0, 0, pixrect.width + 2, pixheight + 2, 2);
377                 cairo_stroke (cr);
378                 //cairo_fill (cr);
379                 //resized = false;
380         }
381
382         cairo_rectangle (cr, ev->area.x, ev->area.y, ev->area.width, ev->area.height);
383         cairo_clip (cr);
384
385         top_of_meter = (gint) floor (pixheight * current_level);
386
387         /* reset the height & origin of the rect that needs to show the pixbuf
388          */
389
390         pixrect.height = top_of_meter;
391         pixrect.y = 1 + pixheight - top_of_meter;
392
393         background.x = 1;
394         background.y = 1;
395         background.width = pixrect.width;
396         background.height = pixheight - top_of_meter;
397
398         if (gdk_rectangle_intersect (&background, &ev->area, &intersection)) {
399                 cairo_set_source (cr, bgpattern->cobj());
400                 cairo_rectangle (cr, intersection.x, intersection.y, intersection.width, intersection.height);
401                 cairo_fill (cr);
402         }
403
404         if (gdk_rectangle_intersect (&pixrect, &ev->area, &intersection)) {
405                 // draw the part of the meter image that we need. the area we draw is bounded "in reverse" (top->bottom)
406                 cairo_set_source (cr, fgpattern->cobj());
407                 cairo_rectangle (cr, intersection.x, intersection.y, intersection.width, intersection.height);
408                 cairo_fill (cr);
409         }
410
411         // draw peak bar
412
413         if (hold_state) {
414                 last_peak_rect.x = 1;
415                 last_peak_rect.width = pixwidth;
416                 last_peak_rect.y = 1 + pixheight - (gint) floor (pixheight * current_peak);
417                 last_peak_rect.height = min(2, pixheight - last_peak_rect.y);
418
419                 cairo_set_source (cr, fgpattern->cobj());
420                 cairo_rectangle (cr, 1, last_peak_rect.y, pixwidth, last_peak_rect.height);
421                 cairo_fill (cr);
422
423         } else {
424                 last_peak_rect.width = 0;
425                 last_peak_rect.height = 0;
426         }
427
428         cairo_destroy (cr);
429
430         return TRUE;
431 }
432
433 void
434 FastMeter::set (float lvl)
435 {
436         float old_level = current_level;
437         float old_peak = current_peak;
438
439         current_level = lvl;
440
441         if (lvl > current_peak) {
442                 current_peak = lvl;
443                 hold_state = hold_cnt;
444         }
445
446         if (hold_state > 0) {
447                 if (--hold_state == 0) {
448                         current_peak = lvl;
449                 }
450         }
451
452         if (current_level == old_level && current_peak == old_peak && hold_state == 0) {
453                 return;
454         }
455
456
457         Glib::RefPtr<Gdk::Window> win;
458
459         if ((win = get_window()) == 0) {
460                 queue_draw ();
461                 return;
462         }
463
464         queue_vertical_redraw (win, old_level);
465 }
466
467 void
468 FastMeter::queue_vertical_redraw (const Glib::RefPtr<Gdk::Window>& win, float old_level)
469 {
470         GdkRectangle rect;
471
472         gint new_top = (gint) floor (pixheight * current_level);
473
474         rect.x = 1;
475         rect.width = pixwidth;
476         rect.height = new_top;
477         rect.y = 1 + pixheight - new_top;
478
479         if (current_level > old_level) {
480                 /* colored/pixbuf got larger, just draw the new section */
481                 /* rect.y stays where it is because of X coordinates */
482                 /* height of invalidated area is between new.y (smaller) and old.y
483                    (larger).
484                    X coordinates just make my brain hurt.
485                 */
486                 rect.height = pixrect.y - rect.y;
487         } else {
488                 /* it got smaller, compute the difference */
489                 /* rect.y becomes old.y (the smaller value) */
490                 rect.y = pixrect.y;
491                 /* rect.height is the old.y (smaller) minus the new.y (larger)
492                 */
493                 rect.height = pixrect.height - rect.height;
494         }
495
496         GdkRegion* region = 0;
497         bool queue = false;
498
499         if (rect.height != 0) {
500
501                 /* ok, first region to draw ... */
502
503                 region = gdk_region_rectangle (&rect);
504                 queue = true;
505         }
506
507         /* redraw the last place where the last peak hold bar was;
508            the next expose will draw the new one whether its part of
509            expose region or not.
510         */
511
512         if (last_peak_rect.width * last_peak_rect.height != 0) {
513                 if (!queue) {
514                         region = gdk_region_new ();
515                         queue = true;
516                 }
517                 gdk_region_union_with_rect (region, &last_peak_rect);
518         }
519
520         if (queue) {
521                 gdk_window_invalidate_region (win->gobj(), region, true);
522         }
523         if (region) {
524                 gdk_region_destroy(region);
525                 region = 0;
526         }
527 }
528
529 void
530 FastMeter::set_highlight (bool onoff)
531 {
532         if (highlight == onoff) {
533                 return;
534         }
535         highlight = onoff;
536         bgpattern = request_vertical_background (request_width, pixheight, highlight ? _bgh : _bgc, highlight);
537         resized = true;
538         queue_draw ();
539 }
540
541 void
542 FastMeter::clear ()
543 {
544         current_level = 0;
545         current_peak = 0;
546         hold_state = 0;
547         set_highlight(false);
548         resized = true;
549         queue_draw ();
550 }