Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,9 @@ COMMON_SRCS = $(SRCDIR)/kdtree.c \
USHOW_SRCS = $(SRCDIR)/ushow.c \
$(COMMON_SRCS) \
$(SRCDIR)/interface/x_interface.c \
$(SRCDIR)/interface/colorbar.c
$(SRCDIR)/interface/colorbar.c \
$(SRCDIR)/interface/range_popup.c \
$(SRCDIR)/interface/range_utils.c

UTERM_SRCS = $(SRCDIR)/uterm.c \
$(SRCDIR)/term_render_mode.c \
Expand Down Expand Up @@ -210,6 +212,11 @@ $(OBJDIR)/interface/x_interface.o: $(SRCDIR)/interface/x_interface.c \
$(SRCDIR)/interface/colorbar.h $(SRCDIR)/ushow.defines.h
$(OBJDIR)/interface/colorbar.o: $(SRCDIR)/interface/colorbar.c \
$(SRCDIR)/interface/colorbar.h $(SRCDIR)/colormaps.h
$(OBJDIR)/interface/range_popup.o: $(SRCDIR)/interface/range_popup.c \
$(SRCDIR)/interface/range_popup.h \
$(SRCDIR)/interface/range_utils.h
$(OBJDIR)/interface/range_utils.o: $(SRCDIR)/interface/range_utils.c \
$(SRCDIR)/interface/range_utils.h

# Zarr dependencies (when WITH_ZARR is set)
ifdef WITH_ZARR
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ The test suite includes:
- **test_regrid**: Interpolation to regular grids
- **test_colormaps**: Color mapping functions
- **test_term_render_mode**: Terminal render mode parsing/cycling helpers
- **test_range_popup**: Range popup logic (symmetric computation, value parsing)
- **test_file_netcdf**: NetCDF file I/O
- **test_file_zarr**: Zarr file I/O (when built with `WITH_ZARR=1`)
- **test_integration**: End-to-end workflow tests
Expand All @@ -205,6 +206,11 @@ The test suite includes:
- `Fwd >`: Start forward animation
- **Time/Depth sliders**: Navigate through dimensions
- **Colormap button**: Click to cycle through colormaps
- **Min-/Min+/Max-/Max+**: Adjust display range in 10% steps
- **Range button**: Opens a popup dialog to set the display range explicitly:
- **Minimum/Maximum**: Editable text fields for exact values
- **Symmetric about Zero**: Sets range to [-max(|min|,|max|), max(|min|,|max|)]
- **Reset to Global Values**: Restores the variable's full data range
- **Dimension panel**: Shows dimension names, ranges, current values
- **Colorbar**: Min/max and intermediate labels update as you adjust range

Expand Down
237 changes: 237 additions & 0 deletions src/interface/range_popup.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
/*
* range_popup.c - Range popup dialog for setting min/max values
*
* Inspired by ncview's range.c popup dialog.
* Modal popup with editable min/max text fields,
* "Symmetric about Zero" and "Reset to Global Values" buttons.
*/

#include "range_popup.h"
#include "range_utils.h"
#include <X11/Xlib.h>
#include <X11/StringDefs.h>
#include <X11/Shell.h>
#include <X11/Xaw/Form.h>
#include <X11/Xaw/Label.h>
#include <X11/Xaw/Command.h>
#include <X11/Xaw/AsciiText.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

#define MINMAX_TEXT_WIDTH 120

/* X11 handles (passed during init) */
static Display *popup_display = NULL;
static XtAppContext popup_app_context;

/* Popup widgets */
static Widget range_popup_shell = NULL;
static Widget range_form = NULL;
static Widget range_min_label = NULL;
static Widget range_min_text = NULL;
static Widget range_max_label = NULL;
static Widget range_max_text = NULL;
static Widget range_symmetric_btn = NULL;
static Widget range_reset_btn = NULL;
static Widget range_global_label = NULL;
static Widget range_ok_btn = NULL;
static Widget range_cancel_btn = NULL;

/* Modal loop state */
static int popup_done = 0;
static int popup_result = RANGE_POPUP_CANCEL;

/* Global range for "Reset to Global Values" */
static float stored_global_min = 0.0f;
static float stored_global_max = 1.0f;

/* ========== Callbacks ========== */

static void ok_callback(Widget w, XtPointer client_data, XtPointer call_data) {
(void)w; (void)client_data; (void)call_data;
popup_result = RANGE_POPUP_OK;
popup_done = 1;
}

static void cancel_callback(Widget w, XtPointer client_data, XtPointer call_data) {
(void)w; (void)client_data; (void)call_data;
popup_result = RANGE_POPUP_CANCEL;
popup_done = 1;
}

static void symmetric_callback(Widget w, XtPointer client_data, XtPointer call_data) {
(void)w; (void)client_data; (void)call_data;
char *sptr;
float cur_min, cur_max, new_min, new_max;
char tstr[128];

XtVaGetValues(range_min_text, XtNstring, &sptr, NULL);
if (sscanf(sptr, "%g", &cur_min) != 1) return;
XtVaGetValues(range_max_text, XtNstring, &sptr, NULL);
if (sscanf(sptr, "%g", &cur_max) != 1) return;

range_compute_symmetric(cur_min, cur_max, &new_min, &new_max);

snprintf(tstr, sizeof(tstr), "%g", new_min);
XtVaSetValues(range_min_text, XtNstring, tstr, NULL);

snprintf(tstr, sizeof(tstr), "%g", new_max);
XtVaSetValues(range_max_text, XtNstring, tstr, NULL);
}

static void reset_global_callback(Widget w, XtPointer client_data, XtPointer call_data) {
(void)w; (void)client_data; (void)call_data;
char tstr[128];

snprintf(tstr, sizeof(tstr), "%g", stored_global_min);
XtVaSetValues(range_min_text, XtNstring, tstr, NULL);

snprintf(tstr, sizeof(tstr), "%g", stored_global_max);
XtVaSetValues(range_max_text, XtNstring, tstr, NULL);
}

/* ========== Public API ========== */

void range_popup_init(Widget parent, Display *dpy, XtAppContext app_ctx) {
popup_display = dpy;
popup_app_context = app_ctx;

/* Popup shell */
range_popup_shell = XtVaCreatePopupShell(
"Set Range",
transientShellWidgetClass,
parent,
NULL);

/* Form container */
range_form = XtVaCreateManagedWidget(
"rangeForm", formWidgetClass, range_popup_shell,
XtNborderWidth, 0,
NULL);

/* Row 1: Minimum label + text */
range_min_label = XtVaCreateManagedWidget(
"range_min_label", labelWidgetClass, range_form,
XtNlabel, "Minimum:",
XtNwidth, 80,
XtNborderWidth, 0,
NULL);

range_min_text = XtVaCreateManagedWidget(
"range_min_text", asciiTextWidgetClass, range_form,
XtNeditType, XawtextEdit,
XtNfromHoriz, range_min_label,
XtNwidth, MINMAX_TEXT_WIDTH,
NULL);

/* Row 2: Maximum label + text */
range_max_label = XtVaCreateManagedWidget(
"range_max_label", labelWidgetClass, range_form,
XtNlabel, "Maximum:",
XtNwidth, 80,
XtNborderWidth, 0,
XtNfromVert, range_min_label,
NULL);

range_max_text = XtVaCreateManagedWidget(
"range_max_text", asciiTextWidgetClass, range_form,
XtNeditType, XawtextEdit,
XtNfromHoriz, range_max_label,
XtNfromVert, range_min_text,
XtNwidth, MINMAX_TEXT_WIDTH,
NULL);

/* Row 3: Symmetric about Zero */
range_symmetric_btn = XtVaCreateManagedWidget(
"Symmetric about Zero", commandWidgetClass, range_form,
XtNfromVert, range_max_label,
NULL);
XtAddCallback(range_symmetric_btn, XtNcallback, symmetric_callback, NULL);

/* Row 4: Reset to Global Values + label showing values */
range_reset_btn = XtVaCreateManagedWidget(
"Reset to Global Values", commandWidgetClass, range_form,
XtNfromVert, range_symmetric_btn,
NULL);
XtAddCallback(range_reset_btn, XtNcallback, reset_global_callback, NULL);

range_global_label = XtVaCreateManagedWidget(
"range_global_label", labelWidgetClass, range_form,
XtNborderWidth, 0,
XtNfromVert, range_symmetric_btn,
XtNfromHoriz, range_reset_btn,
XtNwidth, 180,
XtNlabel, "",
NULL);

/* Row 5: OK / Cancel */
range_ok_btn = XtVaCreateManagedWidget(
"OK", commandWidgetClass, range_form,
XtNfromVert, range_reset_btn,
NULL);
XtAddCallback(range_ok_btn, XtNcallback, ok_callback, NULL);

range_cancel_btn = XtVaCreateManagedWidget(
"Cancel", commandWidgetClass, range_form,
XtNfromHoriz, range_ok_btn,
XtNfromVert, range_reset_btn,
NULL);
XtAddCallback(range_cancel_btn, XtNcallback, cancel_callback, NULL);
}

int range_popup_show(float old_min, float old_max,
float global_min, float global_max,
float *new_min, float *new_max) {
if (!range_popup_shell) return RANGE_POPUP_CANCEL;

char min_str[128], max_str[128], global_str[128];
XEvent event;

/* Store global range for reset button */
stored_global_min = global_min;
stored_global_max = global_max;

/* Set current values in text fields */
snprintf(min_str, sizeof(min_str), "%g", old_min);
snprintf(max_str, sizeof(max_str), "%g", old_max);
XtVaSetValues(range_min_text, XtNstring, min_str, NULL);
XtVaSetValues(range_max_text, XtNstring, max_str, NULL);

/* Update global values label */
snprintf(global_str, sizeof(global_str), "%g to %g", global_min, global_max);
XtVaSetValues(range_global_label, XtNlabel, global_str, NULL);

/* Initialize output values */
*new_min = old_min;
*new_max = old_max;

/* Show popup (modal) */
XtPopup(range_popup_shell, XtGrabExclusive);

/* Modal event loop */
popup_done = 0;
while (!popup_done) {
XtAppNextEvent(popup_app_context, &event);
XtDispatchEvent(&event);
}

/* Read values if OK was pressed */
if (popup_result == RANGE_POPUP_OK) {
char *sptr;
XtVaGetValues(range_min_text, XtNstring, &sptr, NULL);
sscanf(sptr, "%g", new_min);
XtVaGetValues(range_max_text, XtNstring, &sptr, NULL);
sscanf(sptr, "%g", new_max);
}

XtPopdown(range_popup_shell);

return popup_result;
}

void range_popup_cleanup(void) {
/* Widgets are destroyed as children of top_level */
range_popup_shell = NULL;
}
38 changes: 38 additions & 0 deletions src/interface/range_popup.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* range_popup.h - Range popup dialog for setting min/max values
*
* Modal popup with editable min/max text fields,
* "Symmetric about Zero" and "Reset to Global Values" buttons.
*/

#ifndef RANGE_POPUP_H
#define RANGE_POPUP_H

#include <X11/Intrinsic.h>
#include "range_utils.h"

/*
* Initialize the range popup widgets.
* Must be called after XtRealizeWidget(top_level).
*/
void range_popup_init(Widget parent, Display *dpy, XtAppContext app_ctx);

/*
* Show the range popup dialog (modal).
*
* old_min/old_max: current display range
* global_min/global_max: full data range for "Reset to Global Values"
* new_min/new_max: output values set by user
*
* Returns RANGE_POPUP_OK or RANGE_POPUP_CANCEL.
*/
int range_popup_show(float old_min, float old_max,
float global_min, float global_max,
float *new_min, float *new_max);

/*
* Cleanup range popup resources.
*/
void range_popup_cleanup(void);

#endif /* RANGE_POPUP_H */
24 changes: 24 additions & 0 deletions src/interface/range_utils.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* range_utils.c - Pure logic utilities for range manipulation
*
* No X11 dependency - can be used in tests and non-GUI code.
*/

#include "range_utils.h"
#include <math.h>
#include <stdio.h>

void range_compute_symmetric(float cur_min, float cur_max,
float *new_min, float *new_max) {
float biggest = fabsf(cur_min) > fabsf(cur_max) ? fabsf(cur_min) : fabsf(cur_max);
*new_min = -biggest;
*new_max = biggest;
}

int range_parse_value(const char *str, float *value) {
if (!str || !value) return 0;
float v;
if (sscanf(str, "%g", &v) != 1) return 0;
*value = v;
return 1;
}
26 changes: 26 additions & 0 deletions src/interface/range_utils.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* range_utils.h - Pure logic utilities for range manipulation
*
* No X11 dependency - can be used in tests and non-GUI code.
*/

#ifndef RANGE_UTILS_H
#define RANGE_UTILS_H

#define RANGE_POPUP_OK 1
#define RANGE_POPUP_CANCEL 0

/*
* Compute symmetric range about zero.
* Sets new_min = -max(|cur_min|, |cur_max|), new_max = max(|cur_min|, |cur_max|).
*/
void range_compute_symmetric(float cur_min, float cur_max,
float *new_min, float *new_max);

/*
* Parse a range value from a string.
* Returns 1 on success, 0 on failure. On failure, *value is unchanged.
*/
int range_parse_value(const char *str, float *value);

#endif /* RANGE_UTILS_H */
Loading