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