/* This file is part of darktable, Copyright (C) 2010-2026 darktable developers. darktable is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. darktable is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with darktable. If not, see . */ #include "common/gdk_event_utils.h" #include #include #include "common/darktable.h" #include "common/gaussian.h" #include "common/math.h" #include "common/opencl.h" #include "control/conf.h" #include "control/control.h" #include "develop/develop.h" #include "develop/imageop.h" #include "develop/imageop_math.h" #include "dtgtk/drawingarea.h" #include "dtgtk/gradientslider.h" #include "dtgtk/togglebutton.h" #include "gui/gtk.h" #include "gui/presets.h" #include "iop/iop_api.h" #include // ugh, ugly hack. why do people break stuff all the time? #ifndef RSVG_CAIRO_H #include #endif DT_MODULE_INTROSPECTION(1, dt_iop_zonesystem_params_t) #define MAX_ZONE_SYSTEM_SIZE 24 /** gui params. */ typedef struct dt_iop_zonesystem_params_t { int size; // $DEFAULT: 10 float zone[MAX_ZONE_SYSTEM_SIZE + 1]; // $DEFAULT: -1.0 } dt_iop_zonesystem_params_t; /** and pixelpipe data is just the same */ typedef struct dt_iop_zonesystem_data_t { dt_iop_zonesystem_params_t params; float rzscale; float zonemap_offset[MAX_ZONE_SYSTEM_SIZE]; float zonemap_scale[MAX_ZONE_SYSTEM_SIZE]; } dt_iop_zonesystem_data_t; typedef struct dt_iop_zonesystem_global_data_t { int kernel_zonesystem; } dt_iop_zonesystem_global_data_t; typedef struct dt_iop_zonesystem_gui_data_t { guchar *in_preview_buffer; guchar *out_preview_buffer; int preview_width, preview_height; GtkWidget *preview; GtkWidget *zones; float press_x, press_y, mouse_x, mouse_y; gboolean hilite_zone; gboolean is_dragging; int current_zone; int zone_under_mouse; int mouse_over_output_zones; cairo_surface_t *image; guint8 *image_buffer; int image_width, image_height; } dt_iop_zonesystem_gui_data_t; const char *name() { return _("zone system"); } int flags() { return IOP_FLAGS_SUPPORTS_BLENDING | IOP_FLAGS_INCLUDE_IN_STYLES | IOP_FLAGS_ALLOW_TILING | IOP_FLAGS_PREVIEW_NON_OPENCL | IOP_FLAGS_DEPRECATED; } const char *deprecated_msg() { return _("this module is deprecated. please use the tone equalizer module instead."); } int default_group() { return IOP_GROUP_TONE | IOP_GROUP_GRADING; } dt_iop_colorspace_type_t default_colorspace(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { return IOP_CS_LAB; } /* get the zone index of pixel lightness from zonemap */ static inline int _iop_zonesystem_zone_index_from_lightness(float lightness, float *zonemap, int size) { for(int k = 0; k < size - 1; k++) if(zonemap[k + 1] >= lightness) return k; return size - 1; } static inline int _zonesystem_size(const int size) { // the size bounds the fixed zonemap arrays, and zone indices are clamped to size - 2 return CLAMP(size, 2, MAX_ZONE_SYSTEM_SIZE); } /* calculate a zonemap with scale values for each zone based on controlpoints from param */ static inline void _iop_zonesystem_calculate_zonemap(dt_iop_zonesystem_params_t *p, float *zonemap) { int steps = 0; int pk = 0; for(int k = 0; k < p->size; k++) { if((k > 0 && k < p->size - 1) && p->zone[k] == -1) steps++; else { /* set 0 and 1.0 for first and last element in zonesystem size, or the actually parameter value */ zonemap[k] = k == 0 ? 0.0 : k == (p->size - 1) ? 1.0 : p->zone[k]; /* for each step from pk to k, calculate values for now this is linear distributed */ for(int l = 1; l <= steps; l++) zonemap[pk + l] = zonemap[pk] + (((zonemap[k] - zonemap[pk]) / (steps + 1)) * l); /* store k into pk and reset zone steps for next range*/ pk = k; steps = 0; } } } static void process_common_setup(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, const void *const ivoid, void *const ovoid, const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out) { const int width = roi_out->width; const int height = roi_out->height; if(self->dev->gui_attached && dt_pipe_is_preview(piece->pipe)) { dt_iop_zonesystem_gui_data_t *g = self->gui_data; dt_iop_gui_enter_critical_section(self); if(g->in_preview_buffer == NULL || g->out_preview_buffer == NULL || g->preview_width != width || g->preview_height != height) { g_free(g->in_preview_buffer); g_free(g->out_preview_buffer); g->in_preview_buffer = g_malloc_n((size_t)width * height, sizeof(guchar)); g->out_preview_buffer = g_malloc_n((size_t)width * height, sizeof(guchar)); g->preview_width = width; g->preview_height = height; } dt_iop_gui_leave_critical_section(self); } } static void process_common_cleanup(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, const void *const ivoid, void *const ovoid, const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out) { dt_iop_zonesystem_data_t *d = piece->data; dt_iop_zonesystem_gui_data_t *g = self->gui_data; const int width = roi_out->width; const int height = roi_out->height; const size_t ch = piece->colors; const int size = d->params.size; if(piece->pipe->mask_display & DT_DEV_PIXELPIPE_DISPLAY_MASK) dt_iop_alpha_copy(ivoid, ovoid, width, height); /* if gui and have buffer lets gaussblur and fill buffer with zone indexes */ if(self->dev->gui_attached && dt_pipe_is_preview(piece->pipe) && g && g->in_preview_buffer && g->out_preview_buffer) { float Lmax[] = { 100.0f }; float Lmin[] = { 0.0f }; /* setup gaussian kernel */ const int radius = 8; const float sigma = 2.5 * (radius * roi_in->scale / piece->iscale); dt_gaussian_t *gauss = dt_gaussian_init(width, height, 1, Lmax, Lmin, sigma, DT_IOP_GAUSSIAN_ZERO); float *tmp = g_try_malloc_n((size_t)width * height, sizeof(float)); if(gauss && tmp) { DT_OMP_FOR() for(size_t k = 0; k < (size_t)width * height; k++) tmp[k] = ((float *)ivoid)[ch * k]; dt_gaussian_blur(gauss, tmp, tmp); /* create zonemap preview for input */ dt_iop_gui_enter_critical_section(self); DT_OMP_FOR() for(size_t k = 0; k < (size_t)width * height; k++) { g->in_preview_buffer[k] = CLAMPS(tmp[k] * (size - 1) / 100.0f, 0, size - 2); } dt_iop_gui_leave_critical_section(self); DT_OMP_FOR() for(size_t k = 0; k < (size_t)width * height; k++) tmp[k] = ((float *)ovoid)[ch * k]; dt_gaussian_blur(gauss, tmp, tmp); /* create zonemap preview for output */ dt_iop_gui_enter_critical_section(self); DT_OMP_FOR() for(size_t k = 0; k < (size_t)width * height; k++) { g->out_preview_buffer[k] = CLAMPS(tmp[k] * (size - 1) / 100.0f, 0, size - 2); } dt_iop_gui_leave_critical_section(self); } g_free(tmp); if(gauss) dt_gaussian_free(gauss); } } void process(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, const void *const ivoid, void *const ovoid, const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out) { if(!dt_iop_have_required_input_format(4 /*we need full-color pixels*/, self, piece->colors, ivoid, ovoid, roi_in, roi_out)) return; const dt_iop_zonesystem_data_t *const d = (const dt_iop_zonesystem_data_t *const)piece->data; process_common_setup(self, piece, ivoid, ovoid, roi_in, roi_out); const int size = d->params.size; const float *const restrict in = (const float *const)ivoid; float *const restrict out = (float *const)ovoid; const size_t npixels = (size_t)roi_out->width * roi_out->height; DT_OMP_FOR() for(size_t k = 0; k < (size_t)4 * npixels; k += 4) { /* remap lightness into zonemap and apply lightness */ const int rz = CLAMPS(in[k] * d->rzscale, 0, size - 2); // zone index const float zs = ((rz > 0) ? (d->zonemap_offset[rz] / in[k]) : 0) + d->zonemap_scale[rz]; for_each_channel(c,aligned(in,out)) { out[k+c] = in[k+c] * zs; } } process_common_cleanup(self, piece, ivoid, ovoid, roi_in, roi_out); } #ifdef HAVE_OPENCL int process_cl(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, cl_mem dev_in, cl_mem dev_out, const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out) { dt_iop_zonesystem_data_t *data = piece->data; dt_iop_zonesystem_global_data_t *gd = self->global_data; cl_mem dev_zmo, dev_zms = NULL; cl_int err = DT_OPENCL_DEFAULT_ERROR; const int devid = piece->pipe->devid; const int width = roi_in->width; const int height = roi_in->height; /* calculate zonemap */ const int size = data->params.size; float zonemap[MAX_ZONE_SYSTEM_SIZE] = { -1 }; float zonemap_offset[ROUNDUP(MAX_ZONE_SYSTEM_SIZE, 16)] = { -1 }; float zonemap_scale[ROUNDUP(MAX_ZONE_SYSTEM_SIZE, 16)] = { -1 }; _iop_zonesystem_calculate_zonemap(&(data->params), zonemap); /* precompute scale and offset */ for(int k = 0; k < size - 1; k++) zonemap_scale[k] = (zonemap[k + 1] - zonemap[k]) * (size - 1); for(int k = 0; k < size - 1; k++) zonemap_offset[k] = 100.0f * ((k + 1) * zonemap[k] - k * zonemap[k + 1]); dev_zmo = dt_opencl_copy_host_to_device_constant(devid, sizeof(float) * ROUNDUP(MAX_ZONE_SYSTEM_SIZE, 16), zonemap_offset); if(dev_zmo == NULL) goto error; dev_zms = dt_opencl_copy_host_to_device_constant(devid, sizeof(float) * ROUNDUP(MAX_ZONE_SYSTEM_SIZE, 16), zonemap_scale); if(dev_zms == NULL) goto error; err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_zonesystem, width, height, CLARG(dev_in), CLARG(dev_out), CLARG(width), CLARG(height), CLARG(size), CLARG(dev_zmo), CLARG(dev_zms)); error: dt_opencl_release_mem_object(dev_zmo); dt_opencl_release_mem_object(dev_zms); return err; } #endif void init_global(dt_iop_module_so_t *self) { const int program = 2; // basic.cl, from programs.conf dt_iop_zonesystem_global_data_t *gd = malloc(sizeof(dt_iop_zonesystem_global_data_t)); self->data = gd; gd->kernel_zonesystem = dt_opencl_create_kernel(program, "zonesystem"); } void cleanup_global(dt_iop_module_so_t *self) { dt_iop_zonesystem_global_data_t *gd = self->data; dt_opencl_free_kernel(gd->kernel_zonesystem); free(self->data); self->data = NULL; } void commit_params(dt_iop_module_t *self, dt_iop_params_t *p1, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { dt_iop_zonesystem_params_t *p = (dt_iop_zonesystem_params_t *)p1; dt_iop_zonesystem_data_t *d = piece->data; d->params = *p; d->params.size = _zonesystem_size(p->size); d->rzscale = (d->params.size - 1) / 100.0f; /* calculate zonemap */ float zonemap[MAX_ZONE_SYSTEM_SIZE] = { -1 }; _iop_zonesystem_calculate_zonemap(&(d->params), zonemap); const int size = d->params.size; // precompute scale and offset for(int k = 0; k < size - 1; k++) d->zonemap_scale[k] = (zonemap[k + 1] - zonemap[k]) * (size - 1); for(int k = 0; k < size - 1; k++) d->zonemap_offset[k] = 100.0f * ((k + 1) * zonemap[k] - k * zonemap[k + 1]); } void init_pipe(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { piece->data = calloc(1, sizeof(dt_iop_zonesystem_data_t)); } void cleanup_pipe(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { free(piece->data); piece->data = NULL; } void gui_update(dt_iop_module_t *self) { dt_iop_zonesystem_gui_data_t *g = self->gui_data; dt_iop_zonesystem_params_t *p = self->params; // the GUI handlers use the stored size directly p->size = _zonesystem_size(p->size); gtk_widget_queue_draw(GTK_WIDGET(g->zones)); } static void _iop_zonesystem_redraw_preview_callback(gpointer instance, dt_iop_module_t *self); static gboolean dt_iop_zonesystem_preview_draw(GtkWidget *widget, cairo_t *crf, dt_iop_module_t *self); static gboolean dt_iop_zonesystem_bar_draw(GtkWidget *widget, cairo_t *crf, dt_iop_module_t *self); static void dt_iop_zonesystem_bar_motion_notify(GtkEventControllerMotion *controller, gdouble x, gdouble y, dt_iop_module_t *self); static void dt_iop_zonesystem_bar_leave_notify(GtkEventControllerMotion *controller, dt_iop_module_t *self); static void dt_iop_zonesystem_bar_button_press(GtkGestureSingle *gesture, gint n_press, gdouble x, gdouble y, dt_iop_module_t *self); static void dt_iop_zonesystem_bar_button_release(GtkGestureSingle *gesture, gint n_press, gdouble x, gdouble y, dt_iop_module_t *self); static void dt_iop_zonesystem_bar_scrolled(GtkEventControllerScroll *controller, gdouble dx, gdouble dy, dt_iop_module_t *self); static void size_allocate_callback(GtkWidget *widget, GtkAllocation *allocation, dt_iop_module_t *self) { dt_iop_zonesystem_gui_data_t *g = self->gui_data; if(g->image) cairo_surface_destroy(g->image); free(g->image_buffer); /* load the dt logo as a background */ g->image = dt_util_get_logo(MIN(allocation->width, allocation->height) * 0.75); if(g->image) { g->image_buffer = cairo_image_surface_get_data(g->image); g->image_width = cairo_image_surface_get_width(g->image); g->image_height = cairo_image_surface_get_height(g->image); } else { g->image_buffer = NULL; g->image_width = 0; g->image_height = 0; } } void gui_init(dt_iop_module_t *self) { dt_iop_zonesystem_gui_data_t *g = IOP_GUI_ALLOC(zonesystem); g->in_preview_buffer = g->out_preview_buffer = NULL; g->is_dragging = FALSE; g->hilite_zone = FALSE; g->preview_width = g->preview_height = 0; g->mouse_over_output_zones = FALSE; g->preview = dtgtk_drawing_area_new_with_height(0); g_signal_connect(G_OBJECT(g->preview), "size-allocate", G_CALLBACK(size_allocate_callback), self); g_signal_connect(G_OBJECT(g->preview), "draw", G_CALLBACK(dt_iop_zonesystem_preview_draw), self); gtk_widget_add_events(GTK_WIDGET(g->preview), GDK_POINTER_MOTION_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_LEAVE_NOTIFY_MASK); /* create the zonesystem bar widget */ g->zones = gtk_drawing_area_new(); gtk_widget_set_tooltip_text(g->zones, _("lightness zones\nuse mouse scrollwheel to change the number of zones\n" "left-click on a border to create a marker\n" "right-click on a marker to delete it")); g_signal_connect(G_OBJECT(g->zones), "draw", G_CALLBACK(dt_iop_zonesystem_bar_draw), self); gtk_widget_add_events(GTK_WIDGET(g->zones), GDK_POINTER_MOTION_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_LEAVE_NOTIFY_MASK | darktable.gui->scroll_mask); dt_gui_connect_click_all(g->zones, dt_iop_zonesystem_bar_button_press, dt_iop_zonesystem_bar_button_release, self); dt_gui_connect_motion(g->zones, dt_iop_zonesystem_bar_motion_notify, NULL, dt_iop_zonesystem_bar_leave_notify, self); dt_gui_connect_scroll(g->zones, GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES | GTK_EVENT_CONTROLLER_SCROLL_DISCRETE, dt_iop_zonesystem_bar_scrolled, self); gtk_widget_set_size_request(g->zones, -1, DT_PIXEL_APPLY_DPI(40)); self->widget = dt_gui_vbox(g->preview, g->zones); /* add signal handler for preview pipe finish to redraw the preview */ DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_DEVELOP_PREVIEW_PIPE_FINISHED, _iop_zonesystem_redraw_preview_callback); g->image = NULL; g->image_buffer = NULL; g->image_width = 0; g->image_height = 0; } void gui_cleanup(dt_iop_module_t *self) { dt_iop_zonesystem_gui_data_t *g = self->gui_data; g_free(g->in_preview_buffer); g_free(g->out_preview_buffer); if(g->image) cairo_surface_destroy(g->image); free(g->image_buffer); } #define DT_ZONESYSTEM_INSET DT_PIXEL_APPLY_DPI(5) #define DT_ZONESYSTEM_BAR_SPLIT_WIDTH 0.0 #define DT_ZONESYSTEM_REFERENCE_SPLIT 0.30 static gboolean dt_iop_zonesystem_bar_draw(GtkWidget *widget, cairo_t *crf, dt_iop_module_t *self) { dt_iop_zonesystem_gui_data_t *g = self->gui_data; dt_iop_zonesystem_params_t *p = self->params; const int inset = DT_ZONESYSTEM_INSET; GtkAllocation allocation; gtk_widget_get_allocation(widget, &allocation); int width = allocation.width, height = allocation.height; cairo_surface_t *cst = dt_cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); cairo_t *cr = cairo_create(cst); /* clear background */ cairo_set_source_rgb(cr, .15, .15, .15); cairo_paint(cr); /* translate and scale */ width -= 2 * inset; height -= 2 * inset; cairo_save(cr); cairo_translate(cr, inset, inset); cairo_scale(cr, width, height); /* render the bars */ float zonemap[MAX_ZONE_SYSTEM_SIZE] = { 0 }; _iop_zonesystem_calculate_zonemap(p, zonemap); float s = (1. / (p->size - 2)); cairo_set_antialias(cr, CAIRO_ANTIALIAS_NONE); for(int i = 0; i < p->size - 1; i++) { /* draw the reference zone */ float z = s * i; cairo_rectangle(cr, (1. / (p->size - 1)) * i, 0, (1. / (p->size - 1)), DT_ZONESYSTEM_REFERENCE_SPLIT - DT_ZONESYSTEM_BAR_SPLIT_WIDTH); cairo_set_source_rgb(cr, z, z, z); cairo_fill(cr); /* draw zone mappings */ cairo_rectangle(cr, zonemap[i], DT_ZONESYSTEM_REFERENCE_SPLIT + DT_ZONESYSTEM_BAR_SPLIT_WIDTH, (zonemap[i + 1] - zonemap[i]), 1.0 - DT_ZONESYSTEM_REFERENCE_SPLIT); cairo_set_source_rgb(cr, z, z, z); cairo_fill(cr); } cairo_set_antialias(cr, CAIRO_ANTIALIAS_DEFAULT); cairo_restore(cr); /* render zonebar control lines */ cairo_set_antialias(cr, CAIRO_ANTIALIAS_NONE); cairo_set_line_width(cr, 1.); cairo_rectangle(cr, inset, inset, width, height); cairo_set_source_rgb(cr, .1, .1, .1); cairo_stroke(cr); cairo_set_antialias(cr, CAIRO_ANTIALIAS_DEFAULT); /* render control points handles */ cairo_set_source_rgb(cr, 0.6, 0.6, 0.6); cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(1.)); const float arrw = DT_PIXEL_APPLY_DPI(7.0f); for(int k = 1; k < p->size - 1; k++) { float nzw = zonemap[k + 1] - zonemap[k]; float pzw = zonemap[k] - zonemap[k - 1]; if((((g->mouse_x / width) > (zonemap[k] - (pzw / 2.0))) && ((g->mouse_x / width) < (zonemap[k] + (nzw / 2.0)))) || p->zone[k] != -1) { gboolean is_under_mouse = ((width * zonemap[k]) - arrw * .5f < g->mouse_x && (width * zonemap[k]) + arrw * .5f > g->mouse_x); cairo_move_to(cr, inset + (width * zonemap[k]), height + (2 * inset) - 1); cairo_rel_line_to(cr, -arrw * .5f, 0); cairo_rel_line_to(cr, arrw * .5f, -arrw); cairo_rel_line_to(cr, arrw * .5f, arrw); cairo_close_path(cr); if(is_under_mouse) cairo_fill(cr); else cairo_stroke(cr); } } /* push mem surface into widget */ cairo_destroy(cr); cairo_set_source_surface(crf, cst, 0, 0); cairo_paint(crf); cairo_surface_destroy(cst); return TRUE; } static void dt_iop_zonesystem_bar_button_press(GtkGestureSingle *gesture, gint n_press, gdouble x, gdouble y, dt_iop_module_t *self) { dt_iop_zonesystem_params_t *p = self->params; dt_iop_zonesystem_gui_data_t *g = self->gui_data; GtkWidget *widget = dt_gui_get_widget(gesture); const int inset = DT_ZONESYSTEM_INSET; GtkAllocation allocation; gtk_widget_get_allocation(widget, &allocation); int width = allocation.width - 2 * inset; /* calculate zonemap */ float zonemap[MAX_ZONE_SYSTEM_SIZE] = { -1 }; _iop_zonesystem_calculate_zonemap(p, zonemap); /* translate mouse into zone index */ int k = _iop_zonesystem_zone_index_from_lightness(g->mouse_x / width, zonemap, p->size); float zw = zonemap[k + 1] - zonemap[k]; if((g->mouse_x / width) > zonemap[k] + (zw / 2)) k++; const guint button = gtk_gesture_single_get_current_button(gesture); if(button == GDK_BUTTON_PRIMARY) { if(p->zone[k] == -1) { p->zone[k] = zonemap[k]; dt_dev_add_history_item(darktable.develop, self, TRUE); } g->is_dragging = TRUE; g->current_zone = k; } else if(button == GDK_BUTTON_SECONDARY) { /* clear the controlpoint */ p->zone[k] = -1; dt_dev_add_history_item(darktable.develop, self, TRUE); } } static void dt_iop_zonesystem_bar_button_release(GtkGestureSingle *gesture, gint n_press, gdouble x, gdouble y, dt_iop_module_t *self) { dt_iop_zonesystem_gui_data_t *g = self->gui_data; if(gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_PRIMARY) g->is_dragging = FALSE; } static void dt_iop_zonesystem_bar_scrolled(GtkEventControllerScroll *controller, gdouble dx, gdouble dy, dt_iop_module_t *self) { dt_iop_zonesystem_params_t *p = self->params; int cs = CLAMP(p->size, 4, MAX_ZONE_SYSTEM_SIZE); if(dy != 0.0) { p->size = CLAMP(p->size - dy, 4, MAX_ZONE_SYSTEM_SIZE); p->zone[cs] = -1; dt_dev_add_history_item(darktable.develop, self, TRUE); gtk_widget_queue_draw(dt_gui_get_widget(controller)); } } static void dt_iop_zonesystem_bar_leave_notify(GtkEventControllerMotion *controller, dt_iop_module_t *self) { dt_iop_zonesystem_gui_data_t *g = self->gui_data; g->hilite_zone = FALSE; gtk_widget_queue_draw(g->preview); } static void dt_iop_zonesystem_bar_motion_notify(GtkEventControllerMotion *controller, gdouble x, gdouble y, dt_iop_module_t *self) { dt_iop_zonesystem_params_t *p = self->params; dt_iop_zonesystem_gui_data_t *g = self->gui_data; GtkWidget *widget = dt_gui_get_widget(controller); const int inset = DT_ZONESYSTEM_INSET; GtkAllocation allocation; gtk_widget_get_allocation(widget, &allocation); int width = allocation.width - 2 * inset, height = allocation.height - 2 * inset; /* calculate zonemap */ float zonemap[MAX_ZONE_SYSTEM_SIZE] = { -1 }; _iop_zonesystem_calculate_zonemap(p, zonemap); /* record mouse position within control */ g->mouse_x = CLAMP(x - inset, 0, width); g->mouse_y = CLAMP(height - 1 - y + inset, 0, height); if(g->is_dragging) { if((g->mouse_x / width) > zonemap[g->current_zone - 1] && (g->mouse_x / width) < zonemap[g->current_zone + 1]) { p->zone[g->current_zone] = (g->mouse_x / width); dt_dev_add_history_item(darktable.develop, self, TRUE); } } else { /* decide which zone the mouse is over */ if(g->mouse_y >= height * (1.0 - DT_ZONESYSTEM_REFERENCE_SPLIT)) { g->zone_under_mouse = (g->mouse_x / width) / (1.0 / (p->size - 1)); g->mouse_over_output_zones = TRUE; } else { float xpos = g->mouse_x / width; for(int z = 0; z < p->size - 1; z++) { if(xpos >= zonemap[z] && xpos < zonemap[z + 1]) { g->zone_under_mouse = z; break; } } g->mouse_over_output_zones = FALSE; } g->hilite_zone = (g->mouse_y < height) ? TRUE : FALSE; } gtk_widget_queue_draw(self->widget); gtk_widget_queue_draw(g->preview); } static gboolean dt_iop_zonesystem_preview_draw(GtkWidget *widget, cairo_t *crf, dt_iop_module_t *self) { const int inset = DT_PIXEL_APPLY_DPI(2); GtkAllocation allocation; gtk_widget_get_allocation(widget, &allocation); int width = allocation.width, height = allocation.height; dt_iop_zonesystem_gui_data_t *g = self->gui_data; dt_iop_zonesystem_params_t *p = self->params; cairo_surface_t *cst = dt_cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); cairo_t *cr = cairo_create(cst); /* clear background */ GtkStyleContext *context = gtk_widget_get_style_context(self->expander); gtk_render_background(context, cr, 0, 0, allocation.width, allocation.height); width -= 2 * inset; height -= 2 * inset; cairo_translate(cr, inset, inset); dt_iop_gui_enter_critical_section(self); if(g->in_preview_buffer && g->out_preview_buffer && self->enabled) { /* calculate the zonemap */ float zonemap[MAX_ZONE_SYSTEM_SIZE] = { -1 }; _iop_zonesystem_calculate_zonemap(p, zonemap); /* let's generate a pixbuf from pixel zone buffer */ guchar *image = g_malloc_n((size_t)4 * g->preview_width * g->preview_height, sizeof(guchar)); guchar *buffer = g->mouse_over_output_zones ? g->out_preview_buffer : g->in_preview_buffer; for(int k = 0; k < g->preview_width * g->preview_height; k++) { int zone = 255 * CLIP(((1.0 / (p->size - 1)) * buffer[k])); image[4 * k + 2] = (g->hilite_zone && buffer[k] == g->zone_under_mouse) ? 255 : zone; image[4 * k + 1] = (g->hilite_zone && buffer[k] == g->zone_under_mouse) ? 255 : zone; image[4 * k + 0] = (g->hilite_zone && buffer[k] == g->zone_under_mouse) ? 0 : zone; } dt_iop_gui_leave_critical_section(self); const int wd = g->preview_width, ht = g->preview_height; const float scale = fminf(width / (float)wd, height / (float)ht); const int stride = cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, wd); cairo_surface_t *surface = cairo_image_surface_create_for_data(image, CAIRO_FORMAT_RGB24, wd, ht, stride); cairo_translate(cr, width / 2.0, height / 2.0f); cairo_scale(cr, scale, scale); cairo_translate(cr, -.5f * wd, -.5f * ht); cairo_rectangle(cr, DT_PIXEL_APPLY_DPI(1), DT_PIXEL_APPLY_DPI(1), wd - DT_PIXEL_APPLY_DPI(2), ht - DT_PIXEL_APPLY_DPI(2)); cairo_set_source_surface(cr, surface, 0, 0); cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_GOOD); cairo_fill_preserve(cr); cairo_surface_destroy(surface); cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(1.0)); cairo_set_source_rgb(cr, .1, .1, .1); cairo_stroke(cr); g_free(image); } else { dt_iop_gui_leave_critical_section(self); // draw a big, subdued dt logo if(g->image) { GdkRGBA *color; gtk_style_context_get(context, gtk_widget_get_state_flags(self->expander), "background-color", &color, NULL); cairo_set_source_surface(cr, g->image, (width - g->image_width) * 0.5, (height - g->image_height) * 0.5); cairo_rectangle(cr, 0, 0, width, height); cairo_set_operator(cr, CAIRO_OPERATOR_HSL_LUMINOSITY); cairo_fill_preserve(cr); cairo_set_operator(cr, CAIRO_OPERATOR_DARKEN); cairo_set_source_rgb(cr, color->red + 0.02, color->green + 0.02, color->blue + 0.02); cairo_fill_preserve(cr); cairo_set_operator(cr, CAIRO_OPERATOR_LIGHTEN); cairo_set_source_rgb(cr, color->red - 0.02, color->green - 0.02, color->blue - 0.02); cairo_fill(cr); gdk_rgba_free(color); } } cairo_destroy(cr); cairo_set_source_surface(crf, cst, 0, 0); cairo_paint(crf); cairo_surface_destroy(cst); return TRUE; } static void _iop_zonesystem_redraw_preview_callback(gpointer instance, dt_iop_module_t *self) { dt_iop_zonesystem_gui_data_t *g = self->gui_data; dt_control_queue_redraw_widget(g->preview); } // clang-format off // modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py // vim: shiftwidth=2 expandtab tabstop=2 cindent // kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified; // clang-format on