From 43d94aba63b877fbeaad407feec33b8871efb3a9 Mon Sep 17 00:00:00 2001 From: Dan Torop Date: Fri, 27 Mar 2026 23:11:23 -0400 Subject: [PATCH 1/3] waveform scope: add option for luma display In waveform scope can now either show RGB channels or luma. When luma option is on, make the RGB channel buttons insensitive. Note that this is luma (Y') not relative luminance (Y). Until add an option to display linearized RGB data, it doesn't make sense to show linearized Y, not least as the jump from seeing non-linear RGB channels to linearized relative luminance is disconcerting. Calculate either RGB channels or luma waveform, not both. This makes sure that RGB waveform calc is as fast as it was before luminance calculation added. Luma calculation is ~15% faster than RGB calculation. This does require reprocessing the pixelpipe when turn on/off the luma option. Future improvements could be: - Add option to show linearized waveform (and rgb parade) data - When linearized option is on, show luminance rather than luma - Add option to show luma/luminance in addition to RGB channels in RGB parade mode - Show luma/luminance in histogram Also: - Rename vs_prof parameter to profile in non-vectorscope _process() functions. The vs_prof name was a result of copying the name from vectorscope process. - Update some comments and general tidying. --- data/darktableconfig.xml.in | 7 ++ data/themes/darktable.css | 31 +++++++++ src/libs/histogram.c | 63 +++++++++++------- src/libs/scopes.h | 25 ++++---- src/libs/scopes/histogram.c | 6 +- src/libs/scopes/split.c | 18 +++--- src/libs/scopes/waveform.c | 125 ++++++++++++++++++++++++++---------- 7 files changed, 193 insertions(+), 82 deletions(-) diff --git a/data/darktableconfig.xml.in b/data/darktableconfig.xml.in index 233c91d70feb..0e2f2bdbb4ff 100644 --- a/data/darktableconfig.xml.in +++ b/data/darktableconfig.xml.in @@ -3362,6 +3362,13 @@ + + plugins/darkroom/histogram/show_luma + bool + false + + + plugins/darkroom/histogram/vectorscope/harmony_type diff --git a/data/themes/darktable.css b/data/themes/darktable.css index f1ab12955fe6..2b7e947d43e8 100644 --- a/data/themes/darktable.css +++ b/data/themes/darktable.css @@ -745,6 +745,11 @@ dialog .sidebar row:selected:hover label, background-color: alpha(@graph_blue, 0.2); } +#luma-channel-button +{ + background-color: alpha(white, 0.2); +} + /* set now them active state */ #red-channel-button:checked { @@ -761,6 +766,11 @@ dialog .sidebar row:selected:hover label, background-color: alpha(@graph_blue, 0.66); } +#luma-channel-button:checked +{ + background-color: alpha(white, 0.66); +} + /* set now hover state */ #red-channel-button:hover { @@ -777,6 +787,27 @@ dialog .sidebar row:selected:hover label, background-color: alpha(@graph_blue, 0.5); } +#luma-channel-button:hover +{ + background-color: alpha(white, 0.5); +} + +/* inactive state */ +#red-channel-button:disabled +{ + background-color: alpha(@graph_red, 0.1); +} + +#green-channel-button:disabled +{ + background-color: alpha(@graph_green, 0.1); +} + +#blue-channel-button:disabled +{ + background-color: alpha(@graph_blue, 0.1); +} + /*------------------ - Dialog windows - ------------------*/ diff --git a/src/libs/histogram.c b/src/libs/histogram.c index fed945e8c177..87f88785ab23 100644 --- a/src/libs/histogram.c +++ b/src/libs/histogram.c @@ -41,10 +41,11 @@ static void _lib_histogram_get_sector_angles(dt_lib_module_t *self, float *angles, int *n); -static const gchar *rgb_names[DT_SCOPES_RGB_N] = +static const gchar *channel_names[DT_SCOPES_CHANNEL_N] = { N_("red"), N_("green"), - N_("blue") + N_("blue"), + N_("luma") }; const char *name(dt_lib_module_t *self) @@ -442,7 +443,7 @@ static void _mode_toggle(GtkWidget *button, dt_scopes_t *s) lib_histogram_update_tooltip(s); dt_scopes_call(prior_mode, mode_leave); - gtk_widget_set_visible(s->button_box_rgb, + gtk_widget_set_visible(s->button_box_channels, dt_scopes_func_exists(s->cur_mode, draw_scope_channels)); dt_scopes_call(s->cur_mode, update_buttons); dt_scopes_call(s->cur_mode, mode_enter); @@ -457,16 +458,23 @@ static void _mode_toggle(GtkWidget *button, dt_scopes_t *s) static void _channel_toggle(GtkWidget *button, dt_scopes_t *s) { - for(int i = 0; i < DT_SCOPES_RGB_N; i++) - if(s->channel_buttons[i] == button) + for(int i = 0; i < DT_SCOPES_CHANNEL_N; i++) + if(s->channel_btns[i] == button) { char conf[48]; g_snprintf(conf, sizeof(conf), - "plugins/darkroom/histogram/show_%s", rgb_names[i]); + "plugins/darkroom/histogram/show_%s", channel_names[i]); s->channels[i] = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)); dt_conf_set_bool(conf, s->channels[i]); - dt_scopes_refresh(s); + if(i == DT_SCOPES_CHANNEL_LUMA) + { + for(int ch = 0; ch <= DT_SCOPES_CHANNEL_BLUE; ch++) + gtk_widget_set_sensitive(s->channel_btns[ch], !s->channels[i]); + dt_scopes_reprocess(); + } + else + dt_scopes_refresh(s); } } @@ -537,7 +545,7 @@ static void _eventbox_enter_notify_callback(GtkEventControllerMotion *controller if(s->cur_mode != &s->modes[i]) dt_scopes_call(&s->modes[i], mode_leave); dt_scopes_call(s->cur_mode, mode_enter); - gtk_widget_set_visible(s->button_box_rgb, + gtk_widget_set_visible(s->button_box_channels, dt_scopes_func_exists(s->cur_mode, draw_scope_channels)); gtk_widget_show(s->button_box_left); gtk_widget_show(s->button_box_right); @@ -675,12 +683,14 @@ void gui_init(dt_lib_module_t *self) dt_pthread_mutex_init(&s->lock, NULL); - s->channels[DT_SCOPES_RGB_RED] + s->channels[DT_SCOPES_CHANNEL_RED] = dt_conf_get_bool("plugins/darkroom/histogram/show_red"); - s->channels[DT_SCOPES_RGB_GREEN] + s->channels[DT_SCOPES_CHANNEL_GREEN] = dt_conf_get_bool("plugins/darkroom/histogram/show_green"); - s->channels[DT_SCOPES_RGB_BLUE] + s->channels[DT_SCOPES_CHANNEL_BLUE] = dt_conf_get_bool("plugins/darkroom/histogram/show_blue"); + s->channels[DT_SCOPES_CHANNEL_LUMA] + = dt_conf_get_bool("plugins/darkroom/histogram/show_luma"); // proxy functions and data so that pixelpipe or tether can // provide data for a histogram @@ -804,14 +814,14 @@ void gui_init(dt_lib_module_t *self) GDK_KEY_H, GDK_CONTROL_MASK | GDK_SHIFT_MASK); // RGB channel buttons - s->button_box_rgb = dt_gui_hbox(); - gtk_widget_set_valign(s->button_box_rgb, GTK_ALIGN_CENTER); - gtk_widget_set_halign(s->button_box_rgb, GTK_ALIGN_CENTER); - // red/green/blue channel on/off - for(int i=DT_SCOPES_RGB_RED; i < DT_SCOPES_RGB_N; i++) + s->button_box_channels = dt_gui_hbox(); + gtk_widget_set_valign(s->button_box_channels, GTK_ALIGN_CENTER); + gtk_widget_set_halign(s->button_box_channels, GTK_ALIGN_CENTER); + // red/green/blue/luma on/off + for(int i=DT_SCOPES_CHANNEL_RED; i < DT_SCOPES_CHANNEL_N; i++) { - g_autofree char *name = g_strdup_printf("%s-channel-button", rgb_names[i]); - g_autofree char *tip = g_strdup_printf(_("toggle %s channel"), _(rgb_names[i])); + g_autofree char *name = g_strdup_printf("%s-channel-button", channel_names[i]); + g_autofree char *tip = g_strdup_printf(_("toggle %s channel"), _(channel_names[i])); GtkWidget *btn = dtgtk_togglebutton_new(dtgtk_cairo_paint_color, CPF_NONE, NULL); dt_gui_add_class(btn, "rgb_toggle"); @@ -819,13 +829,20 @@ void gui_init(dt_lib_module_t *self) gtk_widget_set_tooltip_text(btn, tip); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(btn), s->channels[i]); - dt_action_define(dark, N_("toggle colors"), rgb_names[i], btn, &dt_action_def_toggle); - dt_gui_box_add(s->button_box_rgb, btn); + if(i <= DT_SCOPES_CHANNEL_BLUE) + gtk_widget_set_sensitive(btn, !s->channels[DT_SCOPES_CHANNEL_LUMA]); + dt_action_define(dark, N_("toggle colors"), channel_names[i], btn, &dt_action_def_toggle); g_signal_connect(G_OBJECT(btn), "toggled", G_CALLBACK(_channel_toggle), s); - s->channel_buttons[i] = btn; + s->channel_btns[i] = btn; } - // RGB channels are always rightmost - dt_gui_box_add(s->button_box_right, s->button_box_rgb); + // luma is always leftmost + dt_gui_box_add(s->button_box_channels, + s->channel_btns[DT_SCOPES_CHANNEL_LUMA], + s->channel_btns[DT_SCOPES_CHANNEL_RED], + s->channel_btns[DT_SCOPES_CHANNEL_GREEN], + s->channel_btns[DT_SCOPES_CHANNEL_BLUE]); + // channels are always rightmost + dt_gui_box_add(s->button_box_right, s->button_box_channels); for(dt_scopes_mode_type_t i = 0; i < DT_SCOPES_MODE_N; i++) { diff --git a/src/libs/scopes.h b/src/libs/scopes.h index 3c83edd56687..c6fd0172d730 100644 --- a/src/libs/scopes.h +++ b/src/libs/scopes.h @@ -43,15 +43,16 @@ typedef enum dt_scopes_highlight_t DT_SCOPES_HIGHLIGHT_EXPOSURE } dt_scopes_highlight_t; -typedef enum dt_scopes_rgb_t +typedef enum dt_scopes_channels_t { - DT_SCOPES_RGB_RED = 0, - DT_SCOPES_RGB_GREEN, - DT_SCOPES_RGB_BLUE, - DT_SCOPES_RGB_N // needs to be the last one -} dt_scopes_rgb_t; + DT_SCOPES_CHANNEL_RED = 0, + DT_SCOPES_CHANNEL_GREEN, + DT_SCOPES_CHANNEL_BLUE, + DT_SCOPES_CHANNEL_LUMA, + DT_SCOPES_CHANNEL_N // needs to be the last one +} dt_scopes_channels_t; -typedef gboolean scopes_channels_t[DT_SCOPES_RGB_N]; +typedef gboolean dt_scopes_channels_list_t[DT_SCOPES_CHANNEL_N]; struct dt_scopes_t; struct dt_scopes_mode_t; @@ -66,7 +67,7 @@ typedef struct dt_scopes_functions_t const float *const input, // FIXME: should ROI by dt_histogram_roi_t or another type? dt_histogram_roi_t *const roi, - const dt_iop_order_iccprofile_info_t *vs_prof); + const dt_iop_order_iccprofile_info_t *profile); // FIXME: do want a proper clear function or just tag as not up to date? void (*clear)(struct dt_scopes_mode_t *const self); void (*draw_bkgd)(const struct dt_scopes_mode_t *const self, @@ -90,7 +91,7 @@ typedef struct dt_scopes_functions_t cairo_t *cr, const int width, const int height, - const scopes_channels_t channels); + const dt_scopes_channels_list_t channels); // FIXME: rename to something more sensible dt_scopes_highlight_t (*get_highlight)(const struct dt_scopes_mode_t *const self, const double posx, @@ -140,7 +141,7 @@ typedef struct dt_scopes_t dt_scopes_mode_t modes[DT_SCOPES_MODE_N]; // all available modes int update_counter; // most recent pixelpipe vs mode data dt_scopes_highlight_t highlight; // depends on mouse position - scopes_channels_t channels; // display state chosen by RGB buttons + dt_scopes_channels_list_t channels; // RGB & luma display state gboolean dragging; // to block motion handling during drag gdouble last_offset_x, last_offset_y; // for drag handling // UI elements @@ -148,8 +149,8 @@ typedef struct dt_scopes_t GtkWidget *button_box_left; // GtkBox -- scope mode buttons GtkWidget *button_box_split; // GtkBox -- option buttons for left scope GtkWidget *button_box_right; // GtkBox -- option buttons for main scope - GtkWidget *button_box_rgb; // GtkBox -- RGB channels buttons - GtkWidget *channel_buttons[DT_SCOPES_RGB_N]; // Array of GtkToggleButton -- RGB channels + GtkWidget *button_box_channels; // GtkBox -- RGB & luma buttons + GtkWidget *channel_btns[DT_SCOPES_CHANNEL_N]; // Array of GtkToggleButton -- channels GtkWidget *scope_draw; // GtkDrawingArea -- scope & resize // for access to data during process/draw dt_pthread_mutex_t lock; diff --git a/src/libs/scopes/histogram.c b/src/libs/scopes/histogram.c index 3aa32a60a212..90970ebbdba3 100644 --- a/src/libs/scopes/histogram.c +++ b/src/libs/scopes/histogram.c @@ -53,7 +53,7 @@ const char* _hist_name(const dt_scopes_mode_t *const self) static void _hist_process(dt_scopes_mode_t *const self, const float *const input, dt_histogram_roi_t *const roi, - const dt_iop_order_iccprofile_info_t *vs_prof) + const dt_iop_order_iccprofile_info_t *profile) { dt_scopes_hist_t *const d = self->data; dt_dev_histogram_collection_params_t histogram_params = { 0 }; @@ -126,7 +126,7 @@ static void _hist_draw(const dt_scopes_mode_t *const self, cairo_t *cr, const int width, const int height, - const scopes_channels_t channels) + const dt_scopes_channels_list_t channels) { const dt_scopes_hist_t *const d = self->data; @@ -146,7 +146,7 @@ static void _hist_draw(const dt_scopes_mode_t *const self, cairo_scale(cr, width / 255.0, -(height - 10) / hist_max); cairo_set_operator(cr, CAIRO_OPERATOR_ADD); cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(1.)); - for(int k = 0; k < DT_SCOPES_RGB_N; k++) + for(int k = 0; k <= DT_SCOPES_CHANNEL_BLUE; k++) if(channels[k]) { // FIXME: this is the last place in dt these are used -- if can diff --git a/src/libs/scopes/split.c b/src/libs/scopes/split.c index 08b86dfade27..5a5cc484f8c8 100644 --- a/src/libs/scopes/split.c +++ b/src/libs/scopes/split.c @@ -33,11 +33,11 @@ const char* _split_name(const dt_scopes_mode_t *const self) static void _split_process(dt_scopes_mode_t *const self, const float *const input, dt_histogram_roi_t *const roi, - const dt_iop_order_iccprofile_info_t *vs_prof) + const dt_iop_order_iccprofile_info_t *profile) { dt_scopes_split_t *const d = self->data; - dt_scopes_call(d->left, process, input, roi, vs_prof); - dt_scopes_call(d->right, process, input, roi, vs_prof); + dt_scopes_call(d->left, process, input, roi, profile); + dt_scopes_call(d->right, process, input, roi, profile); self->update_counter = self->scopes->update_counter; } @@ -90,7 +90,7 @@ static void _split_draw(const dt_scopes_mode_t *const self, cairo_t *cr, const int width, const int height, - const scopes_channels_t channels) + const dt_scopes_channels_list_t channels) { const dt_scopes_split_t *const d = self->data; const int half_width = width / 2; @@ -225,7 +225,7 @@ static void _responsive_buttons(dt_scopes_t *const s) const int mode_btns_hori = DT_SCOPES_MODE_N; const int opt_btns_wave = 1; // FIXME: change this if there are more - const int opt_btns_hori = DT_SCOPES_RGB_N + opt_btns_wave; + const int opt_btns_hori = DT_SCOPES_CHANNEL_N + opt_btns_wave; const int opt_btns_vert = 1 + opt_btns_wave; const int estd_margin = 6; // for both boxes and buttons const double estd_btn_width = min_w + estd_margin; @@ -247,7 +247,7 @@ static void _responsive_buttons(dt_scopes_t *const s) // compact layout should show all buttons without overlap on the // smallest panel width and scope height gtk_orientable_set_orientation(GTK_ORIENTABLE(s->button_box_left), orient_btn_left); - gtk_orientable_set_orientation(GTK_ORIENTABLE(s->button_box_rgb), orient_btn_rgb); + gtk_orientable_set_orientation(GTK_ORIENTABLE(s->button_box_channels), orient_btn_rgb); } static void _reparent(GtkWidget *src, GtkWidget *dest, GtkWidget *child) @@ -279,7 +279,7 @@ static void _split_mode_enter(dt_scopes_mode_t *const self) d->left->options_box); if(d->left->functions->draw_scope_channels) _reparent(self->scopes->button_box_right, self->scopes->button_box_split, - self->scopes->button_box_rgb); + self->scopes->button_box_channels); gtk_widget_show_all(self->scopes->button_box_split); } @@ -293,11 +293,11 @@ static void _split_mode_leave(const dt_scopes_mode_t *const self) d->left->options_box); if(d->left->functions->draw_scope_channels) _reparent(self->scopes->button_box_split, self->scopes->button_box_right, - self->scopes->button_box_rgb); + self->scopes->button_box_channels); gtk_widget_hide(self->scopes->button_box_split); gtk_orientable_set_orientation(GTK_ORIENTABLE(self->scopes->button_box_left), GTK_ORIENTATION_HORIZONTAL); - gtk_orientable_set_orientation(GTK_ORIENTABLE(self->scopes->button_box_rgb), + gtk_orientable_set_orientation(GTK_ORIENTABLE(self->scopes->button_box_channels), GTK_ORIENTATION_HORIZONTAL); } diff --git a/src/libs/scopes/waveform.c b/src/libs/scopes/waveform.c index 7091365838c4..3214cf481390 100644 --- a/src/libs/scopes/waveform.c +++ b/src/libs/scopes/waveform.c @@ -42,9 +42,11 @@ static const gchar *dt_wave_orient_names[DT_WAVE_ORIENT_N] = typedef struct dt_scopes_wave_t { - uint8_t *waveform_img[3]; // image per RGB channel - int waveform_bins, waveform_tones, waveform_max_bins; - GtkWidget *orient_button; // GtkButton -- horizontal or vertical + uint8_t *waveform_img[3]; // processed data per channel + int waveform_max_bins; // for spatial sampling of image + int waveform_bins; // spatial sampling of currently processed data + int waveform_tones; // # tones/bin + GtkWidget *orient_button; // GtkButton -- horizontal or vertical // state set by buttons dt_wave_orient_t orient; } dt_scopes_wave_t; @@ -58,7 +60,7 @@ const char* _wave_name(const dt_scopes_mode_t *const self) static void _wave_process(dt_scopes_mode_t *const self, const float *const input, dt_histogram_roi_t *const roi, - const dt_iop_order_iccprofile_info_t *vs_prof) + const dt_iop_order_iccprofile_info_t *img_prof) { dt_scopes_wave_t *const d = self->data; // FIXME: for point sample, calculate whole graph and the point @@ -72,6 +74,8 @@ static void _wave_process(dt_scopes_mode_t *const self, // orientation, bin_width will generally be 4, for "portrait" it // will generally be 3. Note that waveform_bins varies, depending on // preview image width and # of bins. + // FIXME: check these #s, waveform_max_bins, waveform_tones, and #s + // below now that mipf is mip3 resolution and CPUs are a bit faster const dt_wave_orient_t orient = d->orient; const int to_bin = orient == DT_WAVE_ORIENT_HORI ? sample_width : sample_height; const size_t samples_per_bin = ceilf(to_bin / (float)d->waveform_max_bins); @@ -81,11 +85,14 @@ static void _wave_process(dt_scopes_mode_t *const self, // Note that, with current constants, the input buffer is from the // preview pixelpipe and should be <= 1440x900x4. The output buffer - // will be <= 360x160x3. Hence process works with a relatively small + // will be <= 720x160x4. Hence process works with a relatively small // quantity of data. size_t bin_pad; + const gboolean is_rgb = !self->scopes->channels[DT_SCOPES_CHANNEL_LUMA]; + const size_t num_channels = (is_rgb ? 3U : 1U); uint32_t *const restrict partial_binned = - dt_calloc_perthread(3U * num_bins * num_tones, sizeof(uint32_t), &bin_pad); + dt_calloc_perthread(num_channels * num_bins * num_tones, + sizeof(uint32_t), &bin_pad); DT_OMP_FOR() for(size_t y=0; ycrop_x) + ch]; - // Using ceilf brings everything <= 0 to bottom tone, - // everything > 1.0f/(num_tones-1) to top tone. - tone[ch] = ceilf(CLAMPS(v, 0.0f, 1.0f) * (num_tones-1)); + if(is_rgb) + { // RGB waveform + size_t tone[4] DT_ALIGNED_PIXEL; + for_each_channel(ch, aligned(px,tone:16)) + { + // 1.0 is at 8/9 of the height! + const float v = (8.0f / 9.0f) * px[4U * (x + roi->crop_x) + ch]; + // Using ceilf brings everything <= 0 to bottom tone, + // everything > 1.0f/(num_tones-1) to top tone. + tone[ch] = ceilf(CLAMPS(v, 0.0f, 1.0f) * (num_tones-1)); + } + // must be for_three_ch as binned[3] does not exist + for_three_channels(ch, aligned(tone,binned:16)) + binned[num_tones * (ch * num_bins + bin) + tone[ch]]++; + } + else + { // luma/luminance waveform + dt_aligned_pixel_t RGB; + for_each_channel(ch, aligned(px,RGB:16)) + RGB[ch] = px[4U * (x + roi->crop_x) + ch]; + // luma (Y') from linearized data by pretending this is a + // linear colorspace + const int nonlinearlut_fake = FALSE; + // FIXME: also have option for showing a linear waveform, then + // calculate relative luminance (Y) from linearized data + const float luma = + dt_ioppr_get_rgb_matrix_luminance(RGB, + img_prof->matrix_in, + img_prof->lut_in, + img_prof->unbounded_coeffs_in, + img_prof->lutsize, + nonlinearlut_fake); + const float v = (8.0f / 9.0f) * luma; + const size_t tone = ceilf(CLAMPS(v, 0.0f, 1.0f) * (num_tones-1)); + binned[num_tones * bin + tone]++; } - for(size_t ch = 0; ch < 3; ch++) - binned[num_tones * (ch * num_bins + bin) + tone[ch]]++; } } @@ -127,7 +159,7 @@ static void _wave_process(dt_scopes_mode_t *const self, // NOTE: if constant is decreased, will brighten output // FIXME: instead of using an area-beased scale, figure out max bin - // count and scale to that? + // count and scale to that? const float brightness = num_tones / 40.0f; const float scale = brightness / ((orient == DT_WAVE_ORIENT_HORI @@ -136,13 +168,14 @@ static void _wave_process(dt_scopes_mode_t *const self, const size_t nthreads = dt_get_num_threads(); DT_OMP_FOR(collapse(3)) - for(size_t ch = 0; ch < 3; ch++) + for(size_t ch = 0; ch < num_channels; ch++) for(size_t bin = 0; bin < num_bins; bin++) for(size_t tone = 0; tone < num_tones; tone++) { uint8_t *const restrict wf_img = DT_IS_ALIGNED((uint8_t *const restrict)d->waveform_img[ch]); uint32_t acc = 0; + // FIXME: can use openmp accumlate method? for(size_t n = 0; n < nthreads; n++) { uint32_t *const restrict binned = dt_get_bythread(partial_binned, bin_pad, n); @@ -208,7 +241,7 @@ static void _wave_draw(const dt_scopes_mode_t *const self, cairo_t *cr, const int width, const int height, - const scopes_channels_t channels) + const dt_scopes_channels_list_t channels) { const dt_scopes_wave_t *const d = self->data; @@ -217,34 +250,47 @@ static void _wave_draw(const dt_scopes_mode_t *const self, // composite before scaling to screen dimensions, as scaling each // layer on draw causes a >2x slowdown - const double alpha_chroma = 0.75, desat_over = 0.75, alpha_over = 0.35; + const double alpha_chroma = 0.75; + const double desat_over = 0.75; + const double alpha_over = channels[DT_SCOPES_CHANNEL_LUMA] ? 0.65 : 0.35; const int img_width = d->orient == DT_WAVE_ORIENT_HORI ? d->waveform_bins : d->waveform_tones; const int img_height = d->orient == DT_WAVE_ORIENT_HORI ? d->waveform_tones : d->waveform_bins; const size_t img_stride = cairo_format_stride_for_width(CAIRO_FORMAT_A8, img_width); + const gboolean is_rgb = !self->scopes->channels[DT_SCOPES_CHANNEL_LUMA]; + const int num_channels = (is_rgb ? 3 : 1); cairo_surface_t *cs[3] = { NULL, NULL, NULL }; cairo_surface_t *cst = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, img_width, img_height); cairo_t *crt = cairo_create(cst); cairo_set_operator(crt, CAIRO_OPERATOR_ADD); - for(int ch = 0; ch < 3; ch++) - if(channels[ch]) + for(int ch = 0; ch < num_channels; ch++) + if((is_rgb && channels[ch]) || !is_rgb) { cs[ch] = cairo_image_surface_create_for_data(d->waveform_img[ch], CAIRO_FORMAT_A8, img_width, img_height, img_stride); - cairo_set_source_rgba(crt, ch==0 ? 1.:0., ch==1 ? 1.:0., ch==2 ? 1.:0., alpha_chroma); + if(is_rgb) + cairo_set_source_rgba(crt, + ch==0 ? 1.:0., + ch==1 ? 1.:0., + ch==2 ? 1.:0., alpha_chroma); + else + cairo_set_source_rgba(crt, 1., 1., 1., alpha_chroma); cairo_mask_surface(crt, cs[ch], 0., 0.); } cairo_set_operator(crt, CAIRO_OPERATOR_HARD_LIGHT); - for(int ch = 0; ch < 3; ch++) + for(int ch = 0; ch < num_channels; ch++) if(cs[ch]) { - cairo_set_source_rgba(crt, - ch==0 ? 1.:desat_over, - ch==1 ? 1.:desat_over, - ch==2 ? 1.:desat_over, alpha_over); + if(is_rgb) + cairo_set_source_rgba(crt, + ch==0 ? 1.:desat_over, + ch==1 ? 1.:desat_over, + ch==2 ? 1.:desat_over, alpha_over); + else + cairo_set_source_rgba(crt, 1., 1., 1., alpha_over); cairo_mask_surface(crt, cs[ch], 0., 0.); cairo_surface_destroy(cs[ch]); } @@ -323,12 +369,24 @@ static void _wave_mode_enter(dt_scopes_mode_t *const self) { const dt_scopes_wave_t *const d = self->data; gtk_widget_show(d->orient_button); + // luma button currently only works in waveform + if(gtk_toggle_button_get_active + (GTK_TOGGLE_BUTTON(self->scopes->channel_btns[DT_SCOPES_CHANNEL_LUMA]))) + for(int ch=0; ch <= DT_SCOPES_CHANNEL_BLUE; ch++) + gtk_widget_set_sensitive(self->scopes->channel_btns[ch], FALSE); + gtk_widget_show(self->scopes->channel_btns[DT_SCOPES_CHANNEL_LUMA]); } static void _wave_mode_leave(const dt_scopes_mode_t *const self) { const dt_scopes_wave_t *const d = self->data; gtk_widget_hide(d->orient_button); + // for now luma option only works in waveform + gtk_widget_hide(self->scopes->channel_btns[DT_SCOPES_CHANNEL_LUMA]); + if(gtk_toggle_button_get_active + (GTK_TOGGLE_BUTTON(self->scopes->channel_btns[DT_SCOPES_CHANNEL_LUMA]))) + for(int ch=0; ch <= DT_SCOPES_CHANNEL_BLUE; ch++) + gtk_widget_set_sensitive(self->scopes->channel_btns[ch], TRUE); } static void _wave_gui_init(dt_scopes_mode_t *const self, @@ -347,7 +405,7 @@ static void _wave_gui_init(dt_scopes_mode_t *const self, // drawn. Choose buffer dimensions which produces workable detail, // don't use too much CPU/memory, and allow reasonable gradations // of tone. - + // // Don't use absurd amounts of memory, exceed width of DT_MIPMAP_F // (which will be darktable.mipmap_cache->max_width[DT_MIPMAP_F]*2 // for mosaiced images), nor make it too slow to calculate @@ -356,7 +414,7 @@ static void _wave_gui_init(dt_scopes_mode_t *const self, // width will vary with integral binning of image. // // FIXME: increasing waveform_max_bins increases processing speed - // less than increasing waveform_tones -- tune these better? + // less than increasing waveform_tones -- tune these better? d->waveform_max_bins = darktable.mipmap_cache->max_width[DT_MIPMAP_F]/2; // initially no waveform to draw d->waveform_bins = 0; @@ -369,18 +427,15 @@ static void _wave_gui_init(dt_scopes_mode_t *const self, // of tonal gradation. 256 would match the # of bins in a regular // histogram. d->waveform_tones = 160; - // FIXME: combine waveform_8bit and vectorscope_graph, as only ever - // use one or the other - // // FIXME: keep alignment instead via single alloc via - // dt_alloc_perthread()? + // dt_alloc_perthread()? const size_t bytes_hori = d->waveform_tones * cairo_format_stride_for_width(CAIRO_FORMAT_A8, d->waveform_max_bins); const size_t bytes_vert = d->waveform_max_bins * cairo_format_stride_for_width(CAIRO_FORMAT_A8, d->waveform_tones); - for(int ch=0; ch<3; ch++) + for(int ch=0; ch <= DT_SCOPES_CHANNEL_BLUE; ch++) d->waveform_img[ch] = dt_alloc_align_uint8(MAX(bytes_hori, bytes_vert)); } From 6af68dd96930579a47fbc55eee2c60751c26ef3e Mon Sep 17 00:00:00 2001 From: Dan Torop Date: Thu, 16 Apr 2026 10:28:38 -0400 Subject: [PATCH 2/3] histogram scope: add option for luma display Make the "luma" channel work for histogram scope to match the behavior of waveform scope. The same toggle button affects both scopes. Fix bug in prior commit where switching between rgb parade to waveform could show junk data when luma toggle is on. This is because rgb parade and waveform so far have shared the same data. Fix this by: - Not marking parade as up to date when update luma waveform. This forces a reprocess when switching from luma waveform to parade. - Store if current waveform data is rgb or luma. - Force a reprocess if switching from parade to luma waveform. Also: - fix (inconsequential) bug in dt_histogram_helper() where would always calculate max of histogram - shorten the DT_SCOPES_CHANNEL_* constants to DT_SCOPES_CH_* - note why skip first bucket when count histogram max --- data/themes/darktable.css | 1 + src/bauhaus/bauhaus.c | 2 ++ src/bauhaus/bauhaus.h | 2 +- src/common/histogram.c | 60 +++++++++++++++++++++++++++++-------- src/develop/pixelpipe.h | 2 +- src/libs/histogram.c | 30 +++++++++---------- src/libs/scopes.h | 14 ++++----- src/libs/scopes/histogram.c | 28 ++++++++++------- src/libs/scopes/split.c | 2 +- src/libs/scopes/waveform.c | 40 ++++++++++++------------- 10 files changed, 113 insertions(+), 68 deletions(-) diff --git a/data/themes/darktable.css b/data/themes/darktable.css index 2b7e947d43e8..57dc99ad4934 100644 --- a/data/themes/darktable.css +++ b/data/themes/darktable.css @@ -224,6 +224,7 @@ Why that? Just because it's a developer choice and most classes use underscore i @define-color graph_red rgb(237,30,20); @define-color graph_green rgb(28,235,26); @define-color graph_blue rgb(14,14,233); +@define-color graph_white @grey_95; @define-color colorlabel_red rgb(230,0,0); @define-color colorlabel_green rgb(0,230,0); diff --git a/src/bauhaus/bauhaus.c b/src/bauhaus/bauhaus.c index 28bc786ecc6c..adb95386de7b 100644 --- a/src/bauhaus/bauhaus.c +++ b/src/bauhaus/bauhaus.c @@ -811,6 +811,8 @@ void dt_bauhaus_load_theme() &bh->graph_colors[1]); gtk_style_context_lookup_color(ctx, "graph_blue", &bh->graph_colors[2]); + gtk_style_context_lookup_color(ctx, "graph_white", + &bh->graph_colors[3]); gtk_style_context_lookup_color(ctx, "colorlabel_red", &bh->colorlabels[DT_COLORLABELS_RED]); gtk_style_context_lookup_color(ctx, "colorlabel_yellow", diff --git a/src/bauhaus/bauhaus.h b/src/bauhaus/bauhaus.h index e9ddd0943839..ed7e96e7de34 100644 --- a/src/bauhaus/bauhaus.h +++ b/src/bauhaus/bauhaus.h @@ -152,7 +152,7 @@ typedef struct dt_bauhaus_t // colors for graphs GdkRGBA graph_bg, graph_exterior, graph_border, graph_fg, graph_grid, graph_fg_active, graph_overlay, inset_histogram; - GdkRGBA graph_colors[3]; // primaries + GdkRGBA graph_colors[4]; // primaries and white GdkRGBA colorlabels[DT_COLORLABELS_LAST]; } dt_bauhaus_t; diff --git a/src/common/histogram.c b/src/common/histogram.c index 464b53419280..4a883c59c861 100644 --- a/src/common/histogram.c +++ b/src/common/histogram.c @@ -1,6 +1,6 @@ /* This file is part of darktable, - Copyright (C) 2014-2023 darktable developers. + Copyright (C) 2014-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 @@ -70,6 +70,28 @@ static inline void _bin_raw(const dt_dev_histogram_collection_params_t *const pa //------------------------------------------------------------------------------ +static inline void _bin_luma(const dt_dev_histogram_collection_params_t *const params, + const void *const restrict pixel, + uint32_t *const restrict histogram, + const int j, + const dt_iop_order_iccprofile_info_t *const profile) +{ + const dt_histogram_roi_t *roi = params->roi; + float *in = (float *)pixel + 4 * (roi->width * j + roi->crop_x); + const float max_bin = params->bins_count - 1; + + for(int i = 0; i < roi->width - roi->crop_right - roi->crop_x; i++) + { + const float luma = profile->matrix_in[1][0] * in[i*4] + + profile->matrix_in[1][1] * in[i*4+1] + + profile->matrix_in[1][2] * in[i*4+2]; + const size_t bin = CLAMP(max_bin * luma, 0.0f, max_bin); + histogram[bin]++; + } +} + +//------------------------------------------------------------------------------ + static inline void _bin_rgb(const dt_dev_histogram_collection_params_t *const params, const void *const restrict pixel, uint32_t *const restrict histogram, @@ -225,15 +247,23 @@ void dt_histogram_helper(dt_dev_histogram_collection_params_t *histogram_params, break; case IOP_CS_RGB: - histogram_stats->ch = 3u; - if(compensate_middle_grey && profile_info) - // for rgbcurve (compensated) + if(histogram_stats->ch == 1u) + // for histogram utility module _hist_worker(histogram_params, histogram_stats, pixel, histogram, - _bin_rgb_compensated, profile_info); + _bin_luma, profile_info); else - // used by levels, rgbcurve (uncompensated), rgblevels - _hist_worker(histogram_params, histogram_stats, pixel, histogram, - _bin_rgb, profile_info); + { + histogram_stats->ch = 3u; + if(compensate_middle_grey && profile_info) + // for rgbcurve (compensated) + _hist_worker(histogram_params, histogram_stats, pixel, histogram, + _bin_rgb_compensated, profile_info); + else + // used by levels, rgbcurve (uncompensated), rgblevels, and + // histogram utility module + _hist_worker(histogram_params, histogram_stats, pixel, histogram, + _bin_rgb, profile_info); + } break; case IOP_CS_LAB: @@ -257,7 +287,7 @@ void dt_histogram_helper(dt_dev_histogram_collection_params_t *histogram_params, if(*histogram && histogram_max) { // RGB, Lab, and LCh - if(cst == IOP_CS_RGB || IOP_CS_LAB) + if(cst == IOP_CS_RGB || cst == IOP_CS_LAB) { uint32_t *hist = *histogram; @@ -271,9 +301,15 @@ void dt_histogram_helper(dt_dev_histogram_collection_params_t *histogram_params, m[2] = hist[2]; } - for(int k = 4; k < 4 * histogram_stats->bins_count; k += 4) - for_each_channel(ch,aligned(hist:64) aligned(m:16)) - m[ch] = MAX(m[ch], hist[k+ch]); + // intentionally skip first bucket to show bit more y-axis range + // for underexposed images + if(histogram_stats->ch == 1u) + for(int k = 1; k < histogram_stats->bins_count; k++) + m[0] = MAX(m[0], hist[k]); + else + for(int k = 4; k < 4 * histogram_stats->bins_count; k += 4) + for_each_channel(ch,aligned(hist:64) aligned(m:16)) + m[ch] = MAX(m[ch], hist[k+ch]); } else // raw max not implemented, as is only seen in exposure diff --git a/src/develop/pixelpipe.h b/src/develop/pixelpipe.h index 0dbe3c3ab2ca..ac5c27ba8635 100644 --- a/src/develop/pixelpipe.h +++ b/src/develop/pixelpipe.h @@ -110,7 +110,7 @@ typedef struct dt_dev_histogram_stats_t size_t buf_size; /** count of pixels sampled during histogram capture. */ uint32_t pixels; - /** count of channels: 1 for RAW, 3 for rgb/Lab. */ + /** count of channels: 1 for RAW/luma, 3 for rgb/Lab. */ uint32_t ch; } dt_dev_histogram_stats_t; diff --git a/src/libs/histogram.c b/src/libs/histogram.c index 87f88785ab23..2ffb2282d9d4 100644 --- a/src/libs/histogram.c +++ b/src/libs/histogram.c @@ -41,7 +41,7 @@ static void _lib_histogram_get_sector_angles(dt_lib_module_t *self, float *angles, int *n); -static const gchar *channel_names[DT_SCOPES_CHANNEL_N] = +static const gchar *channel_names[DT_SCOPES_CH_N] = { N_("red"), N_("green"), N_("blue"), @@ -458,7 +458,7 @@ static void _mode_toggle(GtkWidget *button, dt_scopes_t *s) static void _channel_toggle(GtkWidget *button, dt_scopes_t *s) { - for(int i = 0; i < DT_SCOPES_CHANNEL_N; i++) + for(int i = 0; i < DT_SCOPES_CH_N; i++) if(s->channel_btns[i] == button) { char conf[48]; @@ -467,9 +467,9 @@ static void _channel_toggle(GtkWidget *button, dt_scopes_t *s) s->channels[i] = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)); dt_conf_set_bool(conf, s->channels[i]); - if(i == DT_SCOPES_CHANNEL_LUMA) + if(i == DT_SCOPES_CH_LUMA) { - for(int ch = 0; ch <= DT_SCOPES_CHANNEL_BLUE; ch++) + for(int ch = 0; ch <= DT_SCOPES_CH_BLUE; ch++) gtk_widget_set_sensitive(s->channel_btns[ch], !s->channels[i]); dt_scopes_reprocess(); } @@ -683,13 +683,13 @@ void gui_init(dt_lib_module_t *self) dt_pthread_mutex_init(&s->lock, NULL); - s->channels[DT_SCOPES_CHANNEL_RED] + s->channels[DT_SCOPES_CH_RED] = dt_conf_get_bool("plugins/darkroom/histogram/show_red"); - s->channels[DT_SCOPES_CHANNEL_GREEN] + s->channels[DT_SCOPES_CH_GREEN] = dt_conf_get_bool("plugins/darkroom/histogram/show_green"); - s->channels[DT_SCOPES_CHANNEL_BLUE] + s->channels[DT_SCOPES_CH_BLUE] = dt_conf_get_bool("plugins/darkroom/histogram/show_blue"); - s->channels[DT_SCOPES_CHANNEL_LUMA] + s->channels[DT_SCOPES_CH_LUMA] = dt_conf_get_bool("plugins/darkroom/histogram/show_luma"); // proxy functions and data so that pixelpipe or tether can @@ -818,7 +818,7 @@ void gui_init(dt_lib_module_t *self) gtk_widget_set_valign(s->button_box_channels, GTK_ALIGN_CENTER); gtk_widget_set_halign(s->button_box_channels, GTK_ALIGN_CENTER); // red/green/blue/luma on/off - for(int i=DT_SCOPES_CHANNEL_RED; i < DT_SCOPES_CHANNEL_N; i++) + for(int i=DT_SCOPES_CH_RED; i < DT_SCOPES_CH_N; i++) { g_autofree char *name = g_strdup_printf("%s-channel-button", channel_names[i]); g_autofree char *tip = g_strdup_printf(_("toggle %s channel"), _(channel_names[i])); @@ -829,18 +829,18 @@ void gui_init(dt_lib_module_t *self) gtk_widget_set_tooltip_text(btn, tip); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(btn), s->channels[i]); - if(i <= DT_SCOPES_CHANNEL_BLUE) - gtk_widget_set_sensitive(btn, !s->channels[DT_SCOPES_CHANNEL_LUMA]); + if(i <= DT_SCOPES_CH_BLUE) + gtk_widget_set_sensitive(btn, !s->channels[DT_SCOPES_CH_LUMA]); dt_action_define(dark, N_("toggle colors"), channel_names[i], btn, &dt_action_def_toggle); g_signal_connect(G_OBJECT(btn), "toggled", G_CALLBACK(_channel_toggle), s); s->channel_btns[i] = btn; } // luma is always leftmost dt_gui_box_add(s->button_box_channels, - s->channel_btns[DT_SCOPES_CHANNEL_LUMA], - s->channel_btns[DT_SCOPES_CHANNEL_RED], - s->channel_btns[DT_SCOPES_CHANNEL_GREEN], - s->channel_btns[DT_SCOPES_CHANNEL_BLUE]); + s->channel_btns[DT_SCOPES_CH_LUMA], + s->channel_btns[DT_SCOPES_CH_RED], + s->channel_btns[DT_SCOPES_CH_GREEN], + s->channel_btns[DT_SCOPES_CH_BLUE]); // channels are always rightmost dt_gui_box_add(s->button_box_right, s->button_box_channels); diff --git a/src/libs/scopes.h b/src/libs/scopes.h index c6fd0172d730..c7fc6f3a46dc 100644 --- a/src/libs/scopes.h +++ b/src/libs/scopes.h @@ -45,14 +45,14 @@ typedef enum dt_scopes_highlight_t typedef enum dt_scopes_channels_t { - DT_SCOPES_CHANNEL_RED = 0, - DT_SCOPES_CHANNEL_GREEN, - DT_SCOPES_CHANNEL_BLUE, - DT_SCOPES_CHANNEL_LUMA, - DT_SCOPES_CHANNEL_N // needs to be the last one + DT_SCOPES_CH_RED = 0, + DT_SCOPES_CH_GREEN, + DT_SCOPES_CH_BLUE, + DT_SCOPES_CH_LUMA, + DT_SCOPES_CH_N // needs to be the last one } dt_scopes_channels_t; -typedef gboolean dt_scopes_channels_list_t[DT_SCOPES_CHANNEL_N]; +typedef gboolean dt_scopes_channels_list_t[DT_SCOPES_CH_N]; struct dt_scopes_t; struct dt_scopes_mode_t; @@ -150,7 +150,7 @@ typedef struct dt_scopes_t GtkWidget *button_box_split; // GtkBox -- option buttons for left scope GtkWidget *button_box_right; // GtkBox -- option buttons for main scope GtkWidget *button_box_channels; // GtkBox -- RGB & luma buttons - GtkWidget *channel_btns[DT_SCOPES_CHANNEL_N]; // Array of GtkToggleButton -- channels + GtkWidget *channel_btns[DT_SCOPES_CH_N]; // Array of GtkToggleButton -- channels GtkWidget *scope_draw; // GtkDrawingArea -- scope & resize // for access to data during process/draw dt_pthread_mutex_t lock; diff --git a/src/libs/scopes/histogram.c b/src/libs/scopes/histogram.c index 90970ebbdba3..0551704c8206 100644 --- a/src/libs/scopes/histogram.c +++ b/src/libs/scopes/histogram.c @@ -56,17 +56,19 @@ static void _hist_process(dt_scopes_mode_t *const self, const dt_iop_order_iccprofile_info_t *profile) { dt_scopes_hist_t *const d = self->data; + const gboolean is_rgb = !self->scopes->channels[DT_SCOPES_CH_LUMA]; + const uint32_t channels = is_rgb ? 4u : 1u; dt_dev_histogram_collection_params_t histogram_params = { 0 }; const dt_iop_colorspace_type_t cst = IOP_CS_RGB; dt_dev_histogram_stats_t histogram_stats = { .bins_count = HISTOGRAM_BINS, - .ch = 4, + .ch = channels, .pixels = 0, - .buf_size = sizeof(uint32_t) * 4 * HISTOGRAM_BINS }; + .buf_size = sizeof(uint32_t) * channels * HISTOGRAM_BINS }; uint32_t histogram_max[4] = { 0 }; d->histogram_max = 0; - memset(d->histogram, 0, sizeof(uint32_t) * 4 * HISTOGRAM_BINS); + memset(d->histogram, 0, sizeof(uint32_t) * channels * HISTOGRAM_BINS); histogram_params.roi = roi; histogram_params.bins_count = HISTOGRAM_BINS; @@ -77,8 +79,11 @@ static void _hist_process(dt_scopes_mode_t *const self, // FIXME: set up "custom" histogram worker which can do colorspace // conversion on fly -- in cases that we need to do that -- may need // to add from colorspace to dt_dev_histogram_collection_params_t - dt_histogram_helper(&histogram_params, &histogram_stats, cst, IOP_CS_NONE, - input, &d->histogram, histogram_max, FALSE, NULL); + dt_histogram_helper(&histogram_params, &histogram_stats, + cst, IOP_CS_NONE, + input, + &d->histogram, histogram_max, + FALSE, profile); d->histogram_max = MAX(MAX(histogram_max[0], histogram_max[1]), histogram_max[2]); self->update_counter = self->scopes->update_counter; } @@ -146,14 +151,15 @@ static void _hist_draw(const dt_scopes_mode_t *const self, cairo_scale(cr, width / 255.0, -(height - 10) / hist_max); cairo_set_operator(cr, CAIRO_OPERATOR_ADD); cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(1.)); - for(int k = 0; k <= DT_SCOPES_CHANNEL_BLUE; k++) - if(channels[k]) + for(int k = 0; k < DT_SCOPES_CH_N; k++) + if((channels[k] + && ((k <= DT_SCOPES_CH_BLUE && !channels[DT_SCOPES_CH_LUMA]) + || k == DT_SCOPES_CH_LUMA))) { - // FIXME: this is the last place in dt these are used -- if can - // eliminate, then can directly set button colors in CSS and - // simplify things set_color(cr, darktable.bauhaus->graph_colors[k]); - dt_draw_histogram_8(cr, d->histogram, 4, k, + dt_draw_histogram_8(cr, d->histogram, + k == DT_SCOPES_CH_LUMA ? 1 : 4, + k == DT_SCOPES_CH_LUMA ? 0 : k, d->scale == DT_HIST_SCALE_LINEAR); } cairo_pop_group_to_source(cr); diff --git a/src/libs/scopes/split.c b/src/libs/scopes/split.c index 5a5cc484f8c8..04a07d0737f3 100644 --- a/src/libs/scopes/split.c +++ b/src/libs/scopes/split.c @@ -225,7 +225,7 @@ static void _responsive_buttons(dt_scopes_t *const s) const int mode_btns_hori = DT_SCOPES_MODE_N; const int opt_btns_wave = 1; // FIXME: change this if there are more - const int opt_btns_hori = DT_SCOPES_CHANNEL_N + opt_btns_wave; + const int opt_btns_hori = DT_SCOPES_CH_N + opt_btns_wave; const int opt_btns_vert = 1 + opt_btns_wave; const int estd_margin = 6; // for both boxes and buttons const double estd_btn_width = min_w + estd_margin; diff --git a/src/libs/scopes/waveform.c b/src/libs/scopes/waveform.c index 3214cf481390..f3f36eb119a1 100644 --- a/src/libs/scopes/waveform.c +++ b/src/libs/scopes/waveform.c @@ -49,6 +49,7 @@ typedef struct dt_scopes_wave_t GtkWidget *orient_button; // GtkButton -- horizontal or vertical // state set by buttons dt_wave_orient_t orient; + gboolean is_rgb; } dt_scopes_wave_t; @@ -88,7 +89,8 @@ static void _wave_process(dt_scopes_mode_t *const self, // will be <= 720x160x4. Hence process works with a relatively small // quantity of data. size_t bin_pad; - const gboolean is_rgb = !self->scopes->channels[DT_SCOPES_CHANNEL_LUMA]; + const gboolean is_rgb = (self == &self->scopes->modes[DT_SCOPES_MODE_PARADE]) + || !self->scopes->channels[DT_SCOPES_CH_LUMA]; const size_t num_channels = (is_rgb ? 3U : 1U); uint32_t *const restrict partial_binned = dt_calloc_perthread(num_channels * num_bins * num_tones, @@ -191,10 +193,15 @@ static void _wave_process(dt_scopes_mode_t *const self, dt_free_align(partial_binned); - // waveform and rgb parade share underlying data, so updates to one update both - self->scopes->modes[DT_SCOPES_MODE_WAVEFORM].update_counter = - self->scopes->modes[DT_SCOPES_MODE_PARADE].update_counter = - self->scopes->update_counter; + // waveform and rgb parade share underlying RGB (but not luminosity) + // data, so some updates will update both + if(is_rgb) + self->scopes->modes[DT_SCOPES_MODE_WAVEFORM].update_counter = + self->scopes->modes[DT_SCOPES_MODE_PARADE].update_counter = + self->scopes->update_counter; + else + self->update_counter = self->scopes->update_counter; + d->is_rgb = is_rgb; } static void _wave_draw_grid(const dt_scopes_mode_t *const self, @@ -252,13 +259,13 @@ static void _wave_draw(const dt_scopes_mode_t *const self, // layer on draw causes a >2x slowdown const double alpha_chroma = 0.75; const double desat_over = 0.75; - const double alpha_over = channels[DT_SCOPES_CHANNEL_LUMA] ? 0.65 : 0.35; + const double alpha_over = channels[DT_SCOPES_CH_LUMA] ? 0.65 : 0.35; const int img_width = d->orient == DT_WAVE_ORIENT_HORI ? d->waveform_bins : d->waveform_tones; const int img_height = d->orient == DT_WAVE_ORIENT_HORI ? d->waveform_tones : d->waveform_bins; const size_t img_stride = cairo_format_stride_for_width(CAIRO_FORMAT_A8, img_width); - const gboolean is_rgb = !self->scopes->channels[DT_SCOPES_CHANNEL_LUMA]; + const gboolean is_rgb = !self->scopes->channels[DT_SCOPES_CH_LUMA]; const int num_channels = (is_rgb ? 3 : 1); cairo_surface_t *cs[3] = { NULL, NULL, NULL }; cairo_surface_t *cst = cairo_image_surface_create @@ -369,24 +376,17 @@ static void _wave_mode_enter(dt_scopes_mode_t *const self) { const dt_scopes_wave_t *const d = self->data; gtk_widget_show(d->orient_button); - // luma button currently only works in waveform - if(gtk_toggle_button_get_active - (GTK_TOGGLE_BUTTON(self->scopes->channel_btns[DT_SCOPES_CHANNEL_LUMA]))) - for(int ch=0; ch <= DT_SCOPES_CHANNEL_BLUE; ch++) - gtk_widget_set_sensitive(self->scopes->channel_btns[ch], FALSE); - gtk_widget_show(self->scopes->channel_btns[DT_SCOPES_CHANNEL_LUMA]); + // force reprocess if have valid RGB parade data but are switching + // to a luma waveform + if(self == &self->scopes->modes[DT_SCOPES_MODE_WAVEFORM] + && d->is_rgb && self->scopes->channels[DT_SCOPES_CH_LUMA]) + self->update_counter--; } static void _wave_mode_leave(const dt_scopes_mode_t *const self) { const dt_scopes_wave_t *const d = self->data; gtk_widget_hide(d->orient_button); - // for now luma option only works in waveform - gtk_widget_hide(self->scopes->channel_btns[DT_SCOPES_CHANNEL_LUMA]); - if(gtk_toggle_button_get_active - (GTK_TOGGLE_BUTTON(self->scopes->channel_btns[DT_SCOPES_CHANNEL_LUMA]))) - for(int ch=0; ch <= DT_SCOPES_CHANNEL_BLUE; ch++) - gtk_widget_set_sensitive(self->scopes->channel_btns[ch], TRUE); } static void _wave_gui_init(dt_scopes_mode_t *const self, @@ -435,7 +435,7 @@ static void _wave_gui_init(dt_scopes_mode_t *const self, const size_t bytes_vert = d->waveform_max_bins * cairo_format_stride_for_width(CAIRO_FORMAT_A8, d->waveform_tones); - for(int ch=0; ch <= DT_SCOPES_CHANNEL_BLUE; ch++) + for(int ch=0; ch <= DT_SCOPES_CH_BLUE; ch++) d->waveform_img[ch] = dt_alloc_align_uint8(MAX(bytes_hori, bytes_vert)); } From c759f1fa98e05a90d57b9d3810759ef34e07e0d4 Mon Sep 17 00:00:00 2001 From: Dan Torop Date: Fri, 17 Apr 2026 00:25:09 -0400 Subject: [PATCH 3/3] scopes: separate luma button for histogram/waveform As with linear/logarithmic (where histogram and vectorscope have separate states), the luma display can be different in histogram of waveform. --- data/themes/darktable.css | 6 +- src/bauhaus/bauhaus.h | 2 +- src/libs/histogram.c | 18 +---- src/libs/scopes.h | 7 +- src/libs/scopes/histogram.c | 68 ++++++++++++++----- src/libs/scopes/split.c | 2 +- src/libs/scopes/waveform.c | 130 +++++++++++++++++++++++++----------- 7 files changed, 155 insertions(+), 78 deletions(-) diff --git a/data/themes/darktable.css b/data/themes/darktable.css index 57dc99ad4934..ae832ad977e3 100644 --- a/data/themes/darktable.css +++ b/data/themes/darktable.css @@ -746,7 +746,7 @@ dialog .sidebar row:selected:hover label, background-color: alpha(@graph_blue, 0.2); } -#luma-channel-button +#luma-toggle-button { background-color: alpha(white, 0.2); } @@ -767,7 +767,7 @@ dialog .sidebar row:selected:hover label, background-color: alpha(@graph_blue, 0.66); } -#luma-channel-button:checked +#luma-toggle-button:checked { background-color: alpha(white, 0.66); } @@ -788,7 +788,7 @@ dialog .sidebar row:selected:hover label, background-color: alpha(@graph_blue, 0.5); } -#luma-channel-button:hover +#luma-toggle-button:hover { background-color: alpha(white, 0.5); } diff --git a/src/bauhaus/bauhaus.h b/src/bauhaus/bauhaus.h index ed7e96e7de34..1279f08ec551 100644 --- a/src/bauhaus/bauhaus.h +++ b/src/bauhaus/bauhaus.h @@ -152,7 +152,7 @@ typedef struct dt_bauhaus_t // colors for graphs GdkRGBA graph_bg, graph_exterior, graph_border, graph_fg, graph_grid, graph_fg_active, graph_overlay, inset_histogram; - GdkRGBA graph_colors[4]; // primaries and white + GdkRGBA graph_colors[4]; // additive primaries and luma white GdkRGBA colorlabels[DT_COLORLABELS_LAST]; } dt_bauhaus_t; diff --git a/src/libs/histogram.c b/src/libs/histogram.c index 2ffb2282d9d4..3d7fcf24578d 100644 --- a/src/libs/histogram.c +++ b/src/libs/histogram.c @@ -45,7 +45,6 @@ static const gchar *channel_names[DT_SCOPES_CH_N] = { N_("red"), N_("green"), N_("blue"), - N_("luma") }; const char *name(dt_lib_module_t *self) @@ -467,14 +466,7 @@ static void _channel_toggle(GtkWidget *button, dt_scopes_t *s) s->channels[i] = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)); dt_conf_set_bool(conf, s->channels[i]); - if(i == DT_SCOPES_CH_LUMA) - { - for(int ch = 0; ch <= DT_SCOPES_CH_BLUE; ch++) - gtk_widget_set_sensitive(s->channel_btns[ch], !s->channels[i]); - dt_scopes_reprocess(); - } - else - dt_scopes_refresh(s); + dt_scopes_refresh(s); } } @@ -689,8 +681,6 @@ void gui_init(dt_lib_module_t *self) = dt_conf_get_bool("plugins/darkroom/histogram/show_green"); s->channels[DT_SCOPES_CH_BLUE] = dt_conf_get_bool("plugins/darkroom/histogram/show_blue"); - s->channels[DT_SCOPES_CH_LUMA] - = dt_conf_get_bool("plugins/darkroom/histogram/show_luma"); // proxy functions and data so that pixelpipe or tether can // provide data for a histogram @@ -817,7 +807,7 @@ void gui_init(dt_lib_module_t *self) s->button_box_channels = dt_gui_hbox(); gtk_widget_set_valign(s->button_box_channels, GTK_ALIGN_CENTER); gtk_widget_set_halign(s->button_box_channels, GTK_ALIGN_CENTER); - // red/green/blue/luma on/off + // red/green/blue on/off for(int i=DT_SCOPES_CH_RED; i < DT_SCOPES_CH_N; i++) { g_autofree char *name = g_strdup_printf("%s-channel-button", channel_names[i]); @@ -829,15 +819,11 @@ void gui_init(dt_lib_module_t *self) gtk_widget_set_tooltip_text(btn, tip); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(btn), s->channels[i]); - if(i <= DT_SCOPES_CH_BLUE) - gtk_widget_set_sensitive(btn, !s->channels[DT_SCOPES_CH_LUMA]); dt_action_define(dark, N_("toggle colors"), channel_names[i], btn, &dt_action_def_toggle); g_signal_connect(G_OBJECT(btn), "toggled", G_CALLBACK(_channel_toggle), s); s->channel_btns[i] = btn; } - // luma is always leftmost dt_gui_box_add(s->button_box_channels, - s->channel_btns[DT_SCOPES_CH_LUMA], s->channel_btns[DT_SCOPES_CH_RED], s->channel_btns[DT_SCOPES_CH_GREEN], s->channel_btns[DT_SCOPES_CH_BLUE]); diff --git a/src/libs/scopes.h b/src/libs/scopes.h index c7fc6f3a46dc..ebab476c5f6f 100644 --- a/src/libs/scopes.h +++ b/src/libs/scopes.h @@ -48,7 +48,6 @@ typedef enum dt_scopes_channels_t DT_SCOPES_CH_RED = 0, DT_SCOPES_CH_GREEN, DT_SCOPES_CH_BLUE, - DT_SCOPES_CH_LUMA, DT_SCOPES_CH_N // needs to be the last one } dt_scopes_channels_t; @@ -141,7 +140,7 @@ typedef struct dt_scopes_t dt_scopes_mode_t modes[DT_SCOPES_MODE_N]; // all available modes int update_counter; // most recent pixelpipe vs mode data dt_scopes_highlight_t highlight; // depends on mouse position - dt_scopes_channels_list_t channels; // RGB & luma display state + dt_scopes_channels_list_t channels; // RGB channels to display gboolean dragging; // to block motion handling during drag gdouble last_offset_x, last_offset_y; // for drag handling // UI elements @@ -149,8 +148,8 @@ typedef struct dt_scopes_t GtkWidget *button_box_left; // GtkBox -- scope mode buttons GtkWidget *button_box_split; // GtkBox -- option buttons for left scope GtkWidget *button_box_right; // GtkBox -- option buttons for main scope - GtkWidget *button_box_channels; // GtkBox -- RGB & luma buttons - GtkWidget *channel_btns[DT_SCOPES_CH_N]; // Array of GtkToggleButton -- channels + GtkWidget *button_box_channels; // GtkBox -- RGB channel buttons + GtkWidget *channel_btns[DT_SCOPES_CH_N]; // Array of GtkToggleButton -- channels GtkWidget *scope_draw; // GtkDrawingArea -- scope & resize // for access to data during process/draw dt_pthread_mutex_t lock; diff --git a/src/libs/scopes/histogram.c b/src/libs/scopes/histogram.c index 0551704c8206..1f119b5dc4e8 100644 --- a/src/libs/scopes/histogram.c +++ b/src/libs/scopes/histogram.c @@ -38,10 +38,15 @@ static const gchar *dt_hist_scale_names[DT_HIST_SCALE_N] = typedef struct dt_scopes_hist_t { + // buffers/data uint32_t *histogram; uint32_t histogram_max; - dt_hist_scale_t scale; + // buttons GtkWidget *scale_button; // GtkButton -- linear or logarithmic histogram + GtkWidget *luma_button; // GtkButton -- luma/rgb toggle + // state set by buttons + dt_hist_scale_t scale; + gboolean is_rgb; } dt_scopes_hist_t; @@ -56,8 +61,7 @@ static void _hist_process(dt_scopes_mode_t *const self, const dt_iop_order_iccprofile_info_t *profile) { dt_scopes_hist_t *const d = self->data; - const gboolean is_rgb = !self->scopes->channels[DT_SCOPES_CH_LUMA]; - const uint32_t channels = is_rgb ? 4u : 1u; + const uint32_t channels = d->is_rgb ? 4u : 1u; dt_dev_histogram_collection_params_t histogram_params = { 0 }; const dt_iop_colorspace_type_t cst = IOP_CS_RGB; dt_dev_histogram_stats_t histogram_stats = @@ -151,17 +155,22 @@ static void _hist_draw(const dt_scopes_mode_t *const self, cairo_scale(cr, width / 255.0, -(height - 10) / hist_max); cairo_set_operator(cr, CAIRO_OPERATOR_ADD); cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(1.)); - for(int k = 0; k < DT_SCOPES_CH_N; k++) - if((channels[k] - && ((k <= DT_SCOPES_CH_BLUE && !channels[DT_SCOPES_CH_LUMA]) - || k == DT_SCOPES_CH_LUMA))) - { - set_color(cr, darktable.bauhaus->graph_colors[k]); - dt_draw_histogram_8(cr, d->histogram, - k == DT_SCOPES_CH_LUMA ? 1 : 4, - k == DT_SCOPES_CH_LUMA ? 0 : k, - d->scale == DT_HIST_SCALE_LINEAR); - } + if(d->is_rgb) + { + for(int k = 0; k < DT_SCOPES_CH_N; k++) + if(channels[k]) + { + set_color(cr, darktable.bauhaus->graph_colors[k]); + dt_draw_histogram_8(cr, d->histogram, 4, k, + d->scale == DT_HIST_SCALE_LINEAR); + } + } + else + { + set_color(cr, darktable.bauhaus->graph_colors[3]); + dt_draw_histogram_8(cr, d->histogram, 1, 0, + d->scale == DT_HIST_SCALE_LINEAR); + } cairo_pop_group_to_source(cr); cairo_set_operator(cr, CAIRO_OPERATOR_ADD); cairo_paint_with_alpha(cr, 0.5); @@ -178,6 +187,7 @@ static void _hist_mode_enter(dt_scopes_mode_t *const self) { dt_scopes_hist_t *d = self->data; gtk_widget_show(d->scale_button); + gtk_widget_show(d->luma_button); // FIXME: can call _hist_scale_update() here instead of in gui_init_options? } @@ -185,6 +195,7 @@ static void _hist_mode_leave(const dt_scopes_mode_t *const self) { dt_scopes_hist_t *d = self->data; gtk_widget_hide(d->scale_button); + gtk_widget_hide(d->luma_button); } static void _hist_gui_init(dt_scopes_mode_t *const self, @@ -207,6 +218,7 @@ static void _hist_gui_init(dt_scopes_mode_t *const self, static void _hist_update_buttons(const dt_scopes_mode_t *const self) { dt_scopes_hist_t *d = self->data; + switch(d->scale) { case DT_HIST_SCALE_LOGARITHMIC: @@ -222,6 +234,10 @@ static void _hist_update_buttons(const dt_scopes_mode_t *const self) case DT_HIST_SCALE_N: dt_unreachable_codepath(); } + + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->luma_button), !d->is_rgb); + gtk_widget_set_sensitive(self->scopes->button_box_channels, d->is_rgb); + // FIXME: this should really redraw current iop if its background is // a histogram (check request_histogram) darktable.lib->proxy.histogram.is_linear = @@ -241,17 +257,39 @@ static void _hist_scale_clicked(GtkWidget *button, dt_scopes_mode_t *self) dt_scopes_refresh(self->scopes); } +static void _hist_luma_toggle(GtkWidget *button, dt_scopes_mode_t *self) +{ + dt_scopes_hist_t *d = self->data; + d->is_rgb = !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)); + dt_conf_set_bool("plugins/darkroom/histogram/show_luma", !d->is_rgb); + d->histogram_max = 0; + _hist_update_buttons(self); + dt_scopes_reprocess(); +} + static void _hist_add_options(dt_scopes_mode_t *const self, dt_action_t *dark) { dt_scopes_hist_t *d = self->data; + d->scale_button = dtgtk_button_new(dtgtk_cairo_paint_empty, CPF_NONE, NULL); gtk_widget_set_valign(d->scale_button, GTK_ALIGN_START); dt_action_define(dark, NULL, N_("switch histogram scale"), d->scale_button, &dt_action_def_button); - dt_gui_box_add(self->options_box, d->scale_button); g_signal_connect(G_OBJECT(d->scale_button), "clicked", G_CALLBACK(_hist_scale_clicked), self); + + d->luma_button = dtgtk_togglebutton_new(dtgtk_cairo_paint_color, + CPF_NONE, NULL); + gtk_widget_set_valign(d->luma_button, GTK_ALIGN_START); + // FIXME: s/rgb_toggle/color_toggle/? + dt_gui_add_class(d->luma_button, "rgb_toggle"); + gtk_widget_set_name(d->luma_button, "luma-toggle-button"); + gtk_widget_set_tooltip_text(d->luma_button, "toggle luma/rgb"); + dt_action_define(dark, N_("toggle colors"), N_("histogram luma/rgb"), d->luma_button, &dt_action_def_toggle); + g_signal_connect(G_OBJECT(d->luma_button), "toggled", G_CALLBACK(_hist_luma_toggle), self); + + dt_gui_box_add(self->options_box, d->scale_button, d->luma_button); } static void _hist_gui_cleanup(dt_scopes_mode_t *const self) diff --git a/src/libs/scopes/split.c b/src/libs/scopes/split.c index 04a07d0737f3..b53f56c30b6f 100644 --- a/src/libs/scopes/split.c +++ b/src/libs/scopes/split.c @@ -224,7 +224,7 @@ static void _responsive_buttons(dt_scopes_t *const s) if(min_w == 0.0) return; const int mode_btns_hori = DT_SCOPES_MODE_N; - const int opt_btns_wave = 1; // FIXME: change this if there are more + const int opt_btns_wave = 2; // FIXME: change this if there are more const int opt_btns_hori = DT_SCOPES_CH_N + opt_btns_wave; const int opt_btns_vert = 1 + opt_btns_wave; const int estd_margin = 6; // for both boxes and buttons diff --git a/src/libs/scopes/waveform.c b/src/libs/scopes/waveform.c index f3f36eb119a1..7f2b75fcd271 100644 --- a/src/libs/scopes/waveform.c +++ b/src/libs/scopes/waveform.c @@ -42,14 +42,17 @@ static const gchar *dt_wave_orient_names[DT_WAVE_ORIENT_N] = typedef struct dt_scopes_wave_t { + // buffers/data uint8_t *waveform_img[3]; // processed data per channel int waveform_max_bins; // for spatial sampling of image int waveform_bins; // spatial sampling of currently processed data int waveform_tones; // # tones/bin + // buttons GtkWidget *orient_button; // GtkButton -- horizontal or vertical + GtkWidget *luma_button; // GtkButton -- luma/rgb toggle // state set by buttons dt_wave_orient_t orient; - gboolean is_rgb; + gboolean is_rgb; // is waveform buffer data RGB or luma } dt_scopes_wave_t; @@ -89,8 +92,8 @@ static void _wave_process(dt_scopes_mode_t *const self, // will be <= 720x160x4. Hence process works with a relatively small // quantity of data. size_t bin_pad; - const gboolean is_rgb = (self == &self->scopes->modes[DT_SCOPES_MODE_PARADE]) - || !self->scopes->channels[DT_SCOPES_CH_LUMA]; + const gboolean is_rgb = d->is_rgb + || self == &self->scopes->modes[DT_SCOPES_MODE_PARADE]; const size_t num_channels = (is_rgb ? 3U : 1U); uint32_t *const restrict partial_binned = dt_calloc_perthread(num_channels * num_bins * num_tones, @@ -201,7 +204,6 @@ static void _wave_process(dt_scopes_mode_t *const self, self->scopes->update_counter; else self->update_counter = self->scopes->update_counter; - d->is_rgb = is_rgb; } static void _wave_draw_grid(const dt_scopes_mode_t *const self, @@ -259,48 +261,56 @@ static void _wave_draw(const dt_scopes_mode_t *const self, // layer on draw causes a >2x slowdown const double alpha_chroma = 0.75; const double desat_over = 0.75; - const double alpha_over = channels[DT_SCOPES_CH_LUMA] ? 0.65 : 0.35; + const double alpha_over = d->is_rgb ? 0.35 : 0.65; const int img_width = d->orient == DT_WAVE_ORIENT_HORI - ? d->waveform_bins : d->waveform_tones; + ? d->waveform_bins : d->waveform_tones; const int img_height = d->orient == DT_WAVE_ORIENT_HORI - ? d->waveform_tones : d->waveform_bins; + ? d->waveform_tones : d->waveform_bins; const size_t img_stride = cairo_format_stride_for_width(CAIRO_FORMAT_A8, img_width); - const gboolean is_rgb = !self->scopes->channels[DT_SCOPES_CH_LUMA]; - const int num_channels = (is_rgb ? 3 : 1); - cairo_surface_t *cs[3] = { NULL, NULL, NULL }; - cairo_surface_t *cst = cairo_image_surface_create - (CAIRO_FORMAT_ARGB32, img_width, img_height); + cairo_surface_t *cst = + cairo_image_surface_create(CAIRO_FORMAT_ARGB32, img_width, img_height); cairo_t *crt = cairo_create(cst); cairo_set_operator(crt, CAIRO_OPERATOR_ADD); - for(int ch = 0; ch < num_channels; ch++) - if((is_rgb && channels[ch]) || !is_rgb) - { - cs[ch] = cairo_image_surface_create_for_data(d->waveform_img[ch], CAIRO_FORMAT_A8, - img_width, img_height, img_stride); - if(is_rgb) + if(d->is_rgb) + { + cairo_surface_t *cs[3] = { NULL, NULL, NULL }; + for(int ch = 0; ch < DT_SCOPES_CH_N; ch++) + if(channels[ch]) + { + cs[ch] = + cairo_image_surface_create_for_data(d->waveform_img[ch], CAIRO_FORMAT_A8, + img_width, img_height, img_stride); cairo_set_source_rgba(crt, ch==0 ? 1.:0., ch==1 ? 1.:0., ch==2 ? 1.:0., alpha_chroma); - else - cairo_set_source_rgba(crt, 1., 1., 1., alpha_chroma); - cairo_mask_surface(crt, cs[ch], 0., 0.); - } - cairo_set_operator(crt, CAIRO_OPERATOR_HARD_LIGHT); - for(int ch = 0; ch < num_channels; ch++) - if(cs[ch]) - { - if(is_rgb) + cairo_mask_surface(crt, cs[ch], 0., 0.); + } + cairo_set_operator(crt, CAIRO_OPERATOR_HARD_LIGHT); + for(int ch = 0; ch < DT_SCOPES_CH_N; ch++) + if(cs[ch]) + { cairo_set_source_rgba(crt, ch==0 ? 1.:desat_over, ch==1 ? 1.:desat_over, ch==2 ? 1.:desat_over, alpha_over); - else - cairo_set_source_rgba(crt, 1., 1., 1., alpha_over); - cairo_mask_surface(crt, cs[ch], 0., 0.); - cairo_surface_destroy(cs[ch]); - } + cairo_mask_surface(crt, cs[ch], 0., 0.); + cairo_surface_destroy(cs[ch]); + } + } + else + { + cairo_surface_t *cs = + cairo_image_surface_create_for_data(d->waveform_img[0], CAIRO_FORMAT_A8, + img_width, img_height, img_stride); + cairo_set_source_rgba(crt, 1., 1., 1., alpha_chroma); + cairo_mask_surface(crt, cs, 0., 0.); + cairo_set_operator(crt, CAIRO_OPERATOR_HARD_LIGHT); + cairo_set_source_rgba(crt, 1., 1., 1., alpha_over); + cairo_mask_surface(crt, cs, 0., 0.); + cairo_surface_destroy(cs); + } cairo_destroy(crt); // scale and write to output buffer @@ -355,6 +365,7 @@ static void _wave_clear(dt_scopes_mode_t *const self) static void _wave_update_buttons(const dt_scopes_mode_t *const self) { dt_scopes_wave_t *d = self->data; + switch(d->orient) { case DT_WAVE_ORIENT_HORI: @@ -370,16 +381,24 @@ static void _wave_update_buttons(const dt_scopes_mode_t *const self) case DT_WAVE_ORIENT_N: dt_unreachable_codepath(); } + + if(self == &self->scopes->modes[DT_SCOPES_MODE_WAVEFORM]) + { + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->luma_button), !d->is_rgb); + gtk_widget_set_sensitive(self->scopes->button_box_channels, d->is_rgb); + } } static void _wave_mode_enter(dt_scopes_mode_t *const self) { - const dt_scopes_wave_t *const d = self->data; + dt_scopes_wave_t *const d = self->data; gtk_widget_show(d->orient_button); + gtk_widget_show(d->luma_button); // force reprocess if have valid RGB parade data but are switching // to a luma waveform - if(self == &self->scopes->modes[DT_SCOPES_MODE_WAVEFORM] - && d->is_rgb && self->scopes->channels[DT_SCOPES_CH_LUMA]) + const gboolean cur_buffer_is_rgb = d->is_rgb; + d->is_rgb = !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(d->luma_button)); + if(cur_buffer_is_rgb && !d->is_rgb) self->update_counter--; } @@ -387,6 +406,7 @@ static void _wave_mode_leave(const dt_scopes_mode_t *const self) { const dt_scopes_wave_t *const d = self->data; gtk_widget_hide(d->orient_button); + gtk_widget_hide(d->luma_button); } static void _wave_gui_init(dt_scopes_mode_t *const self, @@ -399,6 +419,7 @@ static void _wave_gui_init(dt_scopes_mode_t *const self, for(dt_wave_orient_t i=0; iorient = i; + d->is_rgb = !dt_conf_get_bool("plugins/darkroom/waveform/show_luma"); // Waveform buffer doesn't need to be coupled with the scopes // widget size. The waveform is almost always scaled when @@ -450,17 +471,38 @@ static void _wave_orient_clicked(GtkWidget *button, dt_scopes_mode_t *const self dt_scopes_reprocess(); } +static void _wave_luma_toggle(GtkWidget *button, dt_scopes_mode_t *self) +{ + dt_scopes_wave_t *d = self->data; + d->is_rgb = !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)); + dt_conf_set_bool("plugins/darkroom/waveform/show_luma", !d->is_rgb); + d->waveform_bins = 0; + _wave_update_buttons(self); + dt_scopes_reprocess(); +} + static void _wave_add_options(dt_scopes_mode_t *const self, dt_action_t *dark) { dt_scopes_wave_t *const d = self->data; + d->orient_button = dtgtk_button_new(dtgtk_cairo_paint_empty, CPF_NONE, NULL); gtk_widget_set_valign(d->orient_button, GTK_ALIGN_START); dt_action_define(dark, NULL, N_("switch scope orientation"), d->orient_button, &dt_action_def_button); - dt_gui_box_add(self->options_box, d->orient_button); g_signal_connect(G_OBJECT(d->orient_button), "clicked", G_CALLBACK(_wave_orient_clicked), self); + + d->luma_button = dtgtk_togglebutton_new(dtgtk_cairo_paint_color, + CPF_NONE, NULL); + gtk_widget_set_valign(d->luma_button, GTK_ALIGN_START); + dt_gui_add_class(d->luma_button, "rgb_toggle"); + gtk_widget_set_name(d->luma_button, "luma-toggle-button"); + gtk_widget_set_tooltip_text(d->luma_button, "toggle luma/rgb"); + dt_action_define(dark, N_("toggle colors"), N_("waveform luma/rgb"), d->luma_button, &dt_action_def_toggle); + g_signal_connect(G_OBJECT(d->luma_button), "toggled", G_CALLBACK(_wave_luma_toggle), self); + + dt_gui_box_add(self->options_box, d->orient_button, d->luma_button); } static void _wave_gui_cleanup(dt_scopes_mode_t *const self) @@ -569,6 +611,18 @@ static void _parade_draw(const dt_scopes_mode_t *const self, cairo_restore(cr); } +static void _parade_mode_enter(dt_scopes_mode_t *const self) +{ + const dt_scopes_wave_t *const d = self->data; + gtk_widget_show(d->orient_button); +} + +static void _parade_mode_leave(const dt_scopes_mode_t *const self) +{ + const dt_scopes_wave_t *const d = self->data; + gtk_widget_hide(d->orient_button); +} + static void _parade_gui_init(dt_scopes_mode_t *const self, dt_scopes_t *const scopes) { @@ -600,8 +654,8 @@ const dt_scopes_functions_t dt_scopes_functions_parade = { .append_to_tooltip = NULL, .eventbox_scroll = NULL, .update_buttons = _wave_update_buttons, - .mode_enter = _wave_mode_enter, - .mode_leave = _wave_mode_leave, + .mode_enter = _parade_mode_enter, + .mode_leave = _parade_mode_leave, .gui_init = _parade_gui_init, .add_options = NULL, .gui_cleanup = _parade_gui_cleanup