9527cfe12d313f2281adbfc4396ea5fc7093fd8c
[terminatorX.git] / src / tX_widget.c
1 /*
2     terminatorX - realtime audio scratching software
3     Copyright (C) 1999-2016  Alexander K├Ânig
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 of the License, or
8     (at your option) 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, see <http://www.gnu.org/licenses/>.
17  
18     File: tX_widget.c
19  
20     Description: This contains the implementation of the tx_widget.
21                  This file is based on the GTK+ widget example from
22                  the GTK+ 1.2 tutorial.
23 */
24
25 #include <math.h>
26
27 #include <gtk/gtk.h>
28 #include "tX_widget.h"
29 #include "tX_types.h"
30 #include "tX_global.h"
31 #include <malloc.h>
32 #include <stdlib.h>
33 #include <string.h>
34
35 #ifndef WIN32
36 #include <unistd.h>
37 #endif
38
39 #define TX_DEFAULT_SIZE_X 100
40 #define TX_DEFAULT_SIZE_Y 11
41
42 /* forward declaration */
43 static void gtk_tx_class_init(GtkTxClass *);
44 static void gtk_tx_init(GtkTx * tx);
45 GtkWidget *gtk_tx_new(int16_t * wavdata, int wavsamples);
46 static void gtk_tx_destroy(GtkWidget * widget);
47 void gtk_tx_set_data(GtkTx * tx, int16_t * wavdata, int wavsamples);
48 static void gtk_tx_realize(GtkWidget * widget);
49
50 static void gtk_tx_get_preferred_width (GtkWidget *widget, gint *minimal_height, gint *natural_height);
51 static void gtk_tx_get_preferred_height (GtkWidget *widget, gint *minimal_height, gint *natural_height);
52
53 static void gtk_tx_size_allocate(GtkWidget * widget, GtkAllocation * allocation);
54 static gboolean gtk_tx_draw(GtkWidget * widget, cairo_t *cr);
55 static void gtk_tx_update(GtkTx * tx);
56 static void gtk_tx_prepare(GtkWidget * widget);
57
58 /* data */
59 static GtkWidgetClass *parent_class = NULL;
60
61 /* widget "methods" */
62
63 GType gtk_tx_get_type() {
64         static GType tx_type = 0;
65
66         if (!tx_type) {
67                 static const GTypeInfo tx_info = {
68                         sizeof (GtkTxClass),
69                         NULL,
70                         NULL,
71                         (GClassInitFunc) gtk_tx_class_init, 
72                         NULL,
73                         NULL,
74                         sizeof (GtkTx),
75                 0,
76                         (GInstanceInitFunc) gtk_tx_init,
77                 };
78
79                 tx_type = g_type_register_static(GTK_TYPE_WIDGET, "GtkTx", &tx_info, 0);
80     }
81         
82         return tx_type;
83 }
84
85 static void gtk_tx_class_init(GtkTxClass * gclass) {
86         GtkWidgetClass *widget_class;
87         
88         widget_class = (GtkWidgetClass *) gclass;
89         
90         parent_class = (GtkWidgetClass *) g_type_class_peek(gtk_widget_get_type());
91         
92         widget_class->destroy = gtk_tx_destroy;
93         
94         widget_class->realize = gtk_tx_realize;
95         widget_class->draw = gtk_tx_draw;
96         widget_class->get_preferred_height = gtk_tx_get_preferred_height;
97         widget_class->get_preferred_width = gtk_tx_get_preferred_width;
98         widget_class->size_allocate = gtk_tx_size_allocate;
99 //      widget_class->button_press_event = gtk_tx_button_press;
100 //      widget_class->button_release_event = gtk_tx_button_release;
101 //      widget_class->motion_notify_event = gtk_tx_motion_notify;
102 }
103
104 #define COL_BG_FOCUS     0
105 #define COL_BG_NO_FOCUS  1
106 #define COL_FG_FOCUS     2
107 #define COL_FG_NO_FOCUS  3
108 #define COL_CURSOR       4
109 #define COL_CURSOR_MUTE  5
110
111 void gtk_tx_update_colors(GtkTx *tx)
112 {
113         gdk_rgba_parse(&tx->colors[COL_BG_FOCUS], globals.wav_display_bg_focus);
114         tx->colors[COL_BG_FOCUS].alpha=1.0;
115         gdk_rgba_parse(&tx->colors[COL_BG_NO_FOCUS], globals.wav_display_bg_no_focus);
116         tx->colors[COL_BG_NO_FOCUS].alpha=1.0;
117         
118         gdk_rgba_parse(&tx->colors[COL_FG_FOCUS], globals.wav_display_fg_focus);
119         tx->colors[COL_FG_FOCUS].alpha=1.0;
120         gdk_rgba_parse(&tx->colors[COL_FG_NO_FOCUS], globals.wav_display_fg_no_focus);
121         tx->colors[COL_FG_NO_FOCUS].alpha=1.0;
122         
123         gdk_rgba_parse(&tx->colors[COL_CURSOR], globals.wav_display_cursor);
124         tx->colors[COL_CURSOR].alpha=1.0;
125         gdk_rgba_parse(&tx->colors[COL_CURSOR_MUTE], globals.wav_display_cursor_mute);
126         tx->colors[COL_CURSOR_MUTE].alpha=1.0;
127 }
128
129
130 static void gtk_tx_init(GtkTx * tx) {
131         tx->disp_data = NULL;
132         tx->data = NULL;
133         tx->samples = 0;
134         tx->do_showframe = 0;
135 #ifdef USE_DISPLAY_NORMALIZE
136         tx->max_value=-1;
137 #endif
138         
139         memset(tx->colors, 0, sizeof(tx->colors));
140         
141         gtk_tx_update_colors(tx);
142         
143         tx->current_fg=tx->audio_colors_focus;
144         tx->current_bg=&tx->colors[COL_BG_NO_FOCUS];
145         
146         tx->audio_colors_focus = NULL;
147         tx->audio_colors_nofocus = NULL;
148         
149         tx->spp=1;
150         tx->lastmute=-1;
151         tx->zoom=0;
152         tx->cursor_pos=0;
153         tx->cursor_x_pos=0;
154         
155         tx->surface = NULL;
156 }
157
158 GtkWidget *gtk_tx_new(int16_t * wavdata, int wavsamples) {
159         GtkTx *tx;
160
161         tx = (GtkTx *) g_object_new(gtk_tx_get_type (), NULL);
162
163         tx->data = wavdata;
164         tx->samples = wavsamples;
165         tx->zoom=0;
166         tx->display_x_offset=0;
167         
168         return GTK_WIDGET(tx);
169 }
170
171 static void gtk_tx_destroy(GtkWidget * widget) {
172         GtkTx *tx;
173         g_return_if_fail(widget != NULL);
174         g_return_if_fail(GTK_IS_TX(widget));
175
176         tx=GTK_TX(widget);
177         
178         if (tx->disp_data) { free(tx->disp_data); tx->disp_data=NULL; }
179         
180         if (GTK_WIDGET_CLASS(parent_class)->destroy) {
181                 (*GTK_WIDGET_CLASS(parent_class)->destroy) (widget);
182         }       
183 }
184
185 #define MAX_ZOOM_WIDTH 500000.0
186
187 void gtk_tx_set_data(GtkTx * tx, int16_t * wavdata, int wavsamples) {
188         g_return_if_fail(tx != NULL);
189         g_return_if_fail(GTK_IS_TX(tx));
190
191         tx->data = wavdata;
192         tx->samples = wavsamples;
193 #ifdef USE_DISPLAY_NORMALIZE    
194         tx->max_value=-1;
195 #endif
196         
197         gtk_tx_prepare(GTK_WIDGET(tx));
198         gtk_tx_update(tx);
199 }
200
201 static void gtk_tx_realize(GtkWidget * widget) {
202         GdkWindowAttr attributes;
203         gint attributes_mask;
204         GtkTx *tx;
205         
206         g_return_if_fail(widget != NULL);
207         g_return_if_fail(GTK_IS_TX(widget));
208
209         tx = GTK_TX(widget);
210         gtk_widget_set_realized(widget, TRUE);
211
212         GtkAllocation allocation;
213         gtk_widget_get_allocation(widget, &allocation);
214         attributes.x = allocation.x;
215         attributes.y = allocation.y;
216         attributes.width = allocation.width;
217         attributes.height = allocation.height;
218         attributes.wclass = GDK_INPUT_OUTPUT;
219         attributes.window_type = GDK_WINDOW_CHILD;
220         attributes.event_mask = gtk_widget_get_events(widget) | GDK_EXPOSURE_MASK;
221         attributes.visual = gtk_widget_get_visual(widget);
222         attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL;
223
224         gtk_widget_set_window(widget, gdk_window_new(gtk_widget_get_parent_window(widget), &attributes, attributes_mask));
225
226         gdk_window_set_user_data(gtk_widget_get_window(widget), widget);
227
228         gtk_style_context_set_background(gtk_widget_get_style_context(widget), gtk_widget_get_window(widget));
229         
230         if (tx->surface) {
231                 cairo_surface_destroy (tx->surface);
232         }
233         
234         tx->surface = gdk_window_create_similar_surface (gtk_widget_get_window(widget), CAIRO_CONTENT_COLOR, allocation.width, allocation.height);
235 }
236
237 static void gtk_tx_get_preferred_width (GtkWidget *widget, gint *minimal_width, gint *natural_width)
238 {
239   *minimal_width = *natural_width = TX_DEFAULT_SIZE_X;
240 }
241
242 static void gtk_tx_get_preferred_height (GtkWidget *widget, gint *minimal_height, gint *natural_height)
243 {
244     *minimal_height = *natural_height = TX_DEFAULT_SIZE_Y;
245 }
246
247 static void gtk_tx_prepare(GtkWidget * widget) {
248         int x, sample;
249         int16_t *ptr;
250         f_prec value;
251         GtkTx *tx;
252         int avg_pos, color;
253         gboolean *success;
254
255         g_return_if_fail(widget != NULL);
256         g_return_if_fail(GTK_IS_TX(widget));
257
258         tx = GTK_TX(widget);
259         
260         GtkAllocation allocation;
261         GdkRGBA midColor;
262         gboolean fg = (tx->current_fg == tx->audio_colors_focus);
263         
264         if (tx->audio_colors_focus) { 
265                 free(tx->audio_colors_focus); 
266                 free(tx->audio_colors_nofocus); 
267                 
268                 tx->audio_colors_focus = NULL; 
269                 tx->audio_colors_nofocus = NULL; 
270         } else {
271                 fg = FALSE;
272         }
273         
274         // update tx->yc
275         
276         gtk_widget_get_allocation(widget, &allocation);
277         tx->yc = allocation.height / 2;
278         
279         // allocate colors
280         
281         tx->audio_colors_focus = (GdkRGBA *) malloc(tx->yc * sizeof(GdkRGBA));
282         tx->audio_colors_nofocus = (GdkRGBA *) malloc(tx->yc * sizeof(GdkRGBA));
283         
284         success = (gboolean *) malloc(tx->yc * sizeof(gboolean));
285
286         // no focus colors
287
288         midColor.red = tx->colors[COL_BG_NO_FOCUS].red + (tx->colors[COL_FG_NO_FOCUS].red - tx->colors[COL_BG_NO_FOCUS].red) / 4;
289         midColor.green = tx->colors[COL_BG_NO_FOCUS].green + (tx->colors[COL_FG_NO_FOCUS].green - tx->colors[COL_BG_NO_FOCUS].green) / 4;
290         midColor.blue = tx->colors[COL_BG_NO_FOCUS].blue + (tx->colors[COL_FG_NO_FOCUS].blue - tx->colors[COL_BG_NO_FOCUS].blue) / 4;
291         
292         for (color=0 ; color < tx->yc; color++) {
293                 float dist = (float) color / (float) tx->yc;
294                 
295                 tx->audio_colors_nofocus[color].red = midColor.red + dist*(tx->colors[COL_FG_NO_FOCUS].red - midColor.red);
296                 tx->audio_colors_nofocus[color].green = midColor.green + dist*(tx->colors[COL_FG_NO_FOCUS].green - midColor.green);
297                 tx->audio_colors_nofocus[color].blue = midColor.blue + dist*(tx->colors[COL_FG_NO_FOCUS].blue - midColor.blue);
298                 tx->audio_colors_nofocus[color].alpha = 1.0;
299         }
300         // focus colors
301
302         midColor.red = tx->colors[COL_BG_FOCUS].red + (tx->colors[COL_FG_FOCUS].red - tx->colors[COL_BG_FOCUS].red) / 4;
303         midColor.green = tx->colors[COL_BG_FOCUS].green + (tx->colors[COL_FG_FOCUS].green - tx->colors[COL_BG_FOCUS].green) / 4;
304         midColor.blue = tx->colors[COL_BG_FOCUS].blue + (tx->colors[COL_FG_FOCUS].blue - tx->colors[COL_BG_FOCUS].blue) / 4;
305         
306         for (color=0 ; color < tx->yc; color++) {
307                 float dist = (float) color / (float) tx->yc;
308                 
309                 tx->audio_colors_focus[color].red = midColor.red + dist*(tx->colors[COL_FG_FOCUS].red - midColor.red);
310                 tx->audio_colors_focus[color].green = midColor.green + dist*(tx->colors[COL_FG_FOCUS].green - midColor.green);
311                 tx->audio_colors_focus[color].blue = midColor.blue + dist*(tx->colors[COL_FG_FOCUS].blue - midColor.blue);
312                 tx->audio_colors_focus[color].alpha = 1.0;
313         }
314         
315         if (fg) {
316                 tx->current_fg = tx->audio_colors_focus;
317         } else {
318                 tx->current_fg = tx->audio_colors_nofocus;
319         }
320         
321         // give up success
322         free(success);
323
324         if (tx->disp_data) { free(tx->disp_data); tx->disp_data=NULL; }
325
326         if (tx->data) {
327                 int max_spp=tx->samples/allocation.width;
328                 int min_spp=tx->samples/MAX_ZOOM_WIDTH;
329                 gdouble diff;
330                 
331                 if (min_spp==0) min_spp=1;
332                 
333                 diff=max_spp-min_spp;
334                 
335                 tx->spp=min_spp+diff*(1.0-tx->zoom);
336                 tx->display_width = tx->samples/tx->spp;
337                 
338 #ifdef USE_DISPLAY_NORMALIZE    
339                 tx->max_value=-1;
340 #endif          
341                 
342             tx->disp_data = (int16_t *) malloc(tx->display_width * sizeof(int16_t));
343
344             if (tx->disp_data) {                        
345 #ifdef USE_DISPLAY_NORMALIZE            
346                         if (tx->max_value==-1) {
347                                 /* We haven't figured a max value yet... */
348                                 //puts("searching max...");
349                 
350                                 for (x = 0, ptr = tx->disp_data; x < tx->display_width; ptr++, x++) {
351                                         value = 0;
352                                         for (sample = x * tx->spp, avg_pos=1; sample < (x + 1) * tx->spp; sample++) {
353                                                 value += (abs(tx->data[sample])-value)/(double) avg_pos++;
354                                         }
355                                         if (value>tx->max_value) tx->max_value=value;
356                                         tx->disp_data[x] = value; 
357                                 }
358                                 for (x = 0; x < tx->display_width; x++) {
359                                         f_prec t=tx->disp_data[x]/(double) tx->max_value;
360                                         tx->disp_data[x]=(int) (t * (f_prec) (tx->yc));
361                                 }
362                         } else {
363 #endif                          
364                                 //puts("have max...");
365                                 /* We have a max value... */
366                                 for (x = 0, ptr = tx->disp_data; x < tx->display_width; ptr++, x++) {
367                                         f_prec t;
368                                         value = 0;
369                                         for (sample = x * tx->spp, avg_pos=1; sample < (x + 1) * tx->spp; sample++) {
370                                                 value += (abs(tx->data[sample])-value)/(double) avg_pos++;
371                                         }
372 #ifdef USE_DISPLAY_NORMALIZE                                    
373                                         t=value/(double) tx->max_value;
374 #else
375                                         t=value/32768.0;
376 #endif                                  
377                                         tx->disp_data[x] = (int) (t * (f_prec) (tx->yc)); 
378                                 }
379 #ifdef USE_DISPLAY_NORMALIZE
380                         }
381 #endif                  
382                 }
383         } else {
384             tx->disp_data = NULL;
385         }
386         
387         tx->cursor_pos=-1;
388         tx->lastmute=-1;
389         
390         //tX_warning("spp: %i samples: %i width %i x %i", tx->spp, tx->samples, tx->display_width, x);
391 }
392
393 static void gtk_tx_size_allocate(GtkWidget * widget, GtkAllocation * allocation) {
394         g_return_if_fail(widget != NULL);
395         g_return_if_fail(GTK_IS_TX(widget));
396         g_return_if_fail(allocation != NULL);
397
398         gtk_widget_set_allocation(widget, allocation);
399
400         gtk_tx_prepare(widget);
401
402         if (gtk_widget_get_realized(widget)) {
403 #ifdef USE_DISPLAY_NORMALIZE            
404             GtkTx *tx = GTK_TX(widget);
405                 tx->max_value=-1;
406 #endif          
407             gdk_window_move_resize(gtk_widget_get_window(widget), allocation->x, allocation->y, allocation->width, allocation->height);
408         }
409 }
410
411 #define min(a,b) ((a) < (b) ? (a) : (b))
412 #define max(a,b) ((a) > (b) ? (a) : (b))
413
414 static gboolean gtk_tx_draw(GtkWidget * widget, cairo_t *cr) {
415         GtkTx *tx;
416         gint x;
417         GdkRectangle area;
418         
419         g_return_val_if_fail(widget != NULL, FALSE);
420         g_return_val_if_fail(GTK_IS_TX(widget), FALSE);
421
422         gdk_cairo_get_clip_rectangle(cr, &area);
423         
424         tx = GTK_TX(widget);
425         
426         cairo_set_antialias(cr, CAIRO_ANTIALIAS_NONE);
427         cairo_set_source_surface (cr, tx->surface, 0, 0);
428         cairo_set_line_width(cr,1);
429
430         gdk_cairo_set_source_rgba (cr, tx->current_bg);
431
432         cairo_rectangle(cr, area.x, area.y, area.width, area.height);
433         cairo_fill(cr);
434
435         if (tx->disp_data) {
436                 int max_x=area.x+area.width+1;
437
438             for (x =area.x; x < max_x; x++) {
439                         int dy = tx->disp_data[tx->display_x_offset+x];
440                         gdk_cairo_set_source_rgba (cr, &tx->current_fg[dy]);
441                         cairo_move_to (cr, x, tx->yc - dy);
442                         cairo_line_to (cr, x, tx->yc + dy+1);
443                         cairo_stroke (cr);
444             }
445         } else {
446                 GtkAllocation allocation;
447                 gtk_widget_get_allocation(widget, &allocation);
448                 
449                 gdk_cairo_set_source_rgba (cr, tx->current_fg);
450                 cairo_move_to (cr, 0, tx->yc);
451                 cairo_line_to (cr, allocation.width, tx->yc);
452                 cairo_stroke (cr);
453         }
454         
455         return FALSE;
456 }
457
458 void gtk_tx_set_zoom(GtkTx *tx, f_prec zoom) {
459         GtkWidget *wid=GTK_WIDGET(tx);
460         
461         tx->zoom=zoom;
462         gtk_tx_prepare(wid);
463         gtk_widget_queue_draw(wid);
464 }
465
466 static void gtk_tx_update(GtkTx * tx) {
467         g_return_if_fail(tx != NULL);
468         g_return_if_fail(GTK_IS_TX(tx));
469
470         gtk_widget_queue_draw(GTK_WIDGET(tx));
471 }
472
473 void gtk_tx_update_pos_display(GtkTx * tx, int sample, int mute) {
474         GtkWidget *widget;
475         GdkWindow *window;
476         cairo_t *cr;
477         
478         int x, y, yc, ymax, tmp;
479         int current_pos, current_pos_x, x_offset;
480         int force_draw=0;
481
482         /* Don't update if not required */
483
484         //current_x = sample / tx->spp + FR_SIZE;
485         current_pos = sample / tx->spp;
486         
487         if ((current_pos == tx->cursor_pos) && 
488                 (tx->lastmute == mute)) return;
489         tx->lastmute = mute;
490
491         /* speedup + easyness */
492
493         widget = GTK_WIDGET(tx);
494         window = gtk_widget_get_window(widget);
495
496         yc = tx->yc;
497         GtkAllocation allocation;
498         gtk_widget_get_allocation(widget, &allocation);
499         ymax = allocation.height-1;
500
501         /* clean up last pos */
502         
503         x = tx->cursor_x_pos;
504         
505         cr = gdk_cairo_create (gtk_widget_get_window(widget));
506         cairo_set_antialias(cr, CAIRO_ANTIALIAS_NONE);
507         cairo_set_source_surface (cr, tx->surface, 0, 0);
508         cairo_set_line_width(cr,1);
509         
510         if (x >= 0) {
511                 gdk_cairo_set_source_rgba (cr, tx->current_bg);
512                 
513                 cairo_move_to (cr, x, 0);
514                 cairo_line_to (cr, x, ymax);
515                 cairo_stroke (cr);
516                 
517             y = tx->disp_data[x+tx->display_x_offset];
518             gdk_cairo_set_source_rgba (cr, &tx->current_fg[y]);
519             
520                 cairo_move_to (cr, x, yc + y);
521                 cairo_line_to (cr, x, yc - y+1);
522                 cairo_stroke (cr);
523         }
524         
525         /* compute new position */
526         if (tx->zoom==0) {
527                 current_pos_x=current_pos;
528                 x_offset=0;             
529         } else {                
530                 tmp=allocation.width/2+1;
531                 
532                 if (current_pos>tmp) {
533                         x_offset=current_pos-tmp;
534                         
535                         if (x_offset+allocation.width>=tx->display_width) {
536                                 x_offset=tx->display_width-allocation.width;
537                         }
538                         
539                         current_pos_x=current_pos-x_offset;
540                 } else {
541                         x_offset=0;
542                         current_pos_x=current_pos;
543                 }
544                 
545                 if (x_offset!=tx->display_x_offset) {
546                         int x_move=tx->display_x_offset-x_offset;
547                         
548                         if (abs(x_move)<allocation.width) {
549                                 gdk_window_scroll(window, x_move, 0);
550                         } else {
551                                 /* we've moved so far that there's nothing to keep from our current display */
552                                 force_draw=1;
553                         }
554                 }
555         }
556         
557         /* store current_pos */
558
559         tx->cursor_pos = current_pos;
560         tx->cursor_x_pos = current_pos_x;
561         tx->display_x_offset = x_offset;
562         
563         /* not drawing current pos - let expose() take care of this... */
564
565         x = current_pos_x;
566
567         if (mute) gdk_cairo_set_source_rgba(cr, &tx->colors[COL_CURSOR_MUTE]);
568         else gdk_cairo_set_source_rgba(cr, &tx->colors[COL_CURSOR]);
569
570         cairo_move_to (cr, x, 0);
571         cairo_line_to (cr, x, ymax);
572         cairo_stroke (cr);
573         
574         cairo_destroy (cr);
575         
576         if (force_draw) {
577                 gtk_widget_queue_draw_area(widget, 0, 0, allocation.width, allocation.height);
578         }
579 }
580
581 void gtk_tx_cleanup_pos_display(GtkTx * tx) {
582         GtkWidget *widget;
583         GtkAllocation allocation;
584
585         widget = GTK_WIDGET(tx);
586         gtk_widget_get_allocation(widget, &allocation);
587
588         tx->display_x_offset=0;
589         tx->cursor_pos=-1;
590         tx->cursor_x_pos=-1;
591         tx->do_showframe=0;
592         //tx->current_fg=&tx->fg;
593         
594         gtk_widget_queue_draw(widget);
595 }
596
597 void gtk_tx_show_frame(GtkTx *tx, int show) {
598         if (show) {
599                 tx->current_fg=tx->audio_colors_focus;
600                 tx->current_bg=&tx->colors[COL_BG_FOCUS];
601         } else {
602                 tx->current_fg=tx->audio_colors_nofocus;
603                 tx->current_bg=&tx->colors[COL_BG_NO_FOCUS];
604         }
605         
606         gtk_widget_queue_draw(GTK_WIDGET(tx));  
607 }
608
609 f_prec gtk_tx_get_zoom(GtkTx *tx) {
610         return tx->zoom;
611 }