diff --git a/README.md b/README.md index 983a15e..8f6e2a6 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ A fast, ncview‑inspired visualization tool for structured and unstructured geo - **Multiple formats**: Supports netCDF, zarr, GRIB, and MITgcm binary (MDS) - **Unified data handling**: Treats all data as collections of points with lon/lat coordinates - **Fast visualization**: KDTree-based nearest-neighbor interpolation to regular grid -- **X11/Xaw interface**: Works over SSH with X forwarding +- **X11/Xaw interface**: Works over SSH with X forwarding, dark/light theme - **Terminal quick-look mode**: Separate `uterm` binary with raw terminal interaction (no X is needed) - **Animation support**: Step through time dimensions - **Multiple colormaps**: viridis, hot, grayscale, plus the full cmocean set @@ -256,6 +256,7 @@ Options: --box W,E,S,N Regional box (e.g. --box -10,30,35,70 for Europe) --polar Polar LAEA projection (north or south) --cutoff Cutoff latitude for polar view (default: 60) + --light Use light theme (default: dark) --yac Use YAC interpolation with default method (avg_arith) --yac-method Use YAC interpolation with specific method; click the method button in the GUI to cycle methods at runtime diff --git a/src/interface/range_popup.c b/src/interface/range_popup.c index c34ac7b..8424623 100644 --- a/src/interface/range_popup.c +++ b/src/interface/range_popup.c @@ -20,7 +20,7 @@ #include #include -#define MINMAX_TEXT_WIDTH 120 +#define MINMAX_TEXT_WIDTH 160 /* X11 handles (passed during init) */ static Display *popup_display = NULL; @@ -115,7 +115,7 @@ void range_popup_init(Widget parent, Display *dpy, XtAppContext app_ctx) { range_min_label = XtVaCreateManagedWidget( "range_min_label", labelWidgetClass, range_form, XtNlabel, "Minimum:", - XtNwidth, 80, + XtNwidth, 90, XtNborderWidth, 0, NULL); @@ -130,7 +130,7 @@ void range_popup_init(Widget parent, Display *dpy, XtAppContext app_ctx) { range_max_label = XtVaCreateManagedWidget( "range_max_label", labelWidgetClass, range_form, XtNlabel, "Maximum:", - XtNwidth, 80, + XtNwidth, 90, XtNborderWidth, 0, XtNfromVert, range_min_label, NULL); @@ -147,6 +147,7 @@ void range_popup_init(Widget parent, Display *dpy, XtAppContext app_ctx) { range_symmetric_btn = XtVaCreateManagedWidget( "Symmetric about Zero", commandWidgetClass, range_form, XtNfromVert, range_max_label, + XtNvertDistance, 8, NULL); XtAddCallback(range_symmetric_btn, XtNcallback, symmetric_callback, NULL); @@ -154,6 +155,7 @@ void range_popup_init(Widget parent, Display *dpy, XtAppContext app_ctx) { range_reset_btn = XtVaCreateManagedWidget( "Reset to Global Values", commandWidgetClass, range_form, XtNfromVert, range_symmetric_btn, + XtNvertDistance, 8, NULL); XtAddCallback(range_reset_btn, XtNcallback, reset_global_callback, NULL); @@ -170,6 +172,8 @@ void range_popup_init(Widget parent, Display *dpy, XtAppContext app_ctx) { range_ok_btn = XtVaCreateManagedWidget( "OK", commandWidgetClass, range_form, XtNfromVert, range_reset_btn, + XtNvertDistance, 12, + XtNwidth, 80, NULL); XtAddCallback(range_ok_btn, XtNcallback, ok_callback, NULL); @@ -177,6 +181,9 @@ void range_popup_init(Widget parent, Display *dpy, XtAppContext app_ctx) { "Cancel", commandWidgetClass, range_form, XtNfromHoriz, range_ok_btn, XtNfromVert, range_reset_btn, + XtNvertDistance, 12, + XtNhorizDistance, 8, + XtNwidth, 80, NULL); XtAddCallback(range_cancel_btn, XtNcallback, cancel_callback, NULL); } diff --git a/src/interface/timeseries_popup.c b/src/interface/timeseries_popup.c index 9bfcf11..7420a46 100644 --- a/src/interface/timeseries_popup.c +++ b/src/interface/timeseries_popup.c @@ -6,6 +6,7 @@ */ #include "timeseries_popup.h" +#include "x_interface.h" #include #include #include @@ -38,6 +39,9 @@ static GC ts_gc = None; /* Colors */ static unsigned long color_blue = 0; static unsigned long color_gray = 0; +static unsigned long color_bg = 0; +static unsigned long color_axis = 0; +static unsigned long color_label = 0; static int colors_allocated = 0; /* Cached data (deep copy) */ @@ -177,25 +181,68 @@ static void allocate_colors(void) { int screen = DefaultScreen(ts_display); Colormap cmap = DefaultColormap(ts_display, screen); XColor xc; + int is_light = x_is_light_theme(); /* Blue for data line */ - xc.red = 0x3333; xc.green = 0x6666; xc.blue = 0xFFFF; + if (is_light) { + xc.red = 0x2222; xc.green = 0x5555; xc.blue = 0xCCCC; + } else { + xc.red = 0x5555; xc.green = 0x9999; xc.blue = 0xFFFF; + } xc.flags = DoRed | DoGreen | DoBlue; - if (XAllocColor(ts_display, cmap, &xc)) { + if (XAllocColor(ts_display, cmap, &xc)) color_blue = xc.pixel; + else + color_blue = is_light ? BlackPixel(ts_display, screen) : WhitePixel(ts_display, screen); + + /* Grid lines */ + if (is_light) { + xc.red = 0xCCCC; xc.green = 0xCCCC; xc.blue = 0xCCCC; } else { - color_blue = BlackPixel(ts_display, screen); + xc.red = 0x4444; xc.green = 0x4444; xc.blue = 0x4444; } - - /* Light gray for grid */ - xc.red = 0xCCCC; xc.green = 0xCCCC; xc.blue = 0xCCCC; xc.flags = DoRed | DoGreen | DoBlue; - if (XAllocColor(ts_display, cmap, &xc)) { + if (XAllocColor(ts_display, cmap, &xc)) color_gray = xc.pixel; + else + color_gray = is_light ? WhitePixel(ts_display, screen) : BlackPixel(ts_display, screen); + + /* Background */ + if (is_light) { + color_bg = WhitePixel(ts_display, screen); } else { - color_gray = WhitePixel(ts_display, screen); + xc.red = 0x1E1E; xc.green = 0x1E1E; xc.blue = 0x1E1E; + xc.flags = DoRed | DoGreen | DoBlue; + if (XAllocColor(ts_display, cmap, &xc)) + color_bg = xc.pixel; + else + color_bg = BlackPixel(ts_display, screen); } + /* Axis color */ + if (is_light) { + xc.red = 0x4444; xc.green = 0x4444; xc.blue = 0x4444; + } else { + xc.red = 0x8888; xc.green = 0x8888; xc.blue = 0x8888; + } + xc.flags = DoRed | DoGreen | DoBlue; + if (XAllocColor(ts_display, cmap, &xc)) + color_axis = xc.pixel; + else + color_axis = is_light ? BlackPixel(ts_display, screen) : WhitePixel(ts_display, screen); + + /* Label color */ + if (is_light) { + xc.red = 0x2222; xc.green = 0x2222; xc.blue = 0x2222; + } else { + xc.red = 0xCCCC; xc.green = 0xCCCC; xc.blue = 0xCCCC; + } + xc.flags = DoRed | DoGreen | DoBlue; + if (XAllocColor(ts_display, cmap, &xc)) + color_label = xc.pixel; + else + color_label = is_light ? BlackPixel(ts_display, screen) : WhitePixel(ts_display, screen); + colors_allocated = 1; } @@ -206,9 +253,6 @@ static void draw_plot(Widget w) { if (!XtIsRealized(w)) return; Window win = XtWindow(w); - int screen = DefaultScreen(ts_display); - unsigned long black = BlackPixel(ts_display, screen); - unsigned long white = WhitePixel(ts_display, screen); allocate_colors(); @@ -220,8 +264,8 @@ static void draw_plot(Widget w) { int plot_w = plot_x1 - plot_x0; int plot_h = plot_y1 - plot_y0; - /* White background */ - XSetForeground(ts_display, ts_gc, white); + /* Dark background */ + XSetForeground(ts_display, ts_gc, color_bg); XFillRectangle(ts_display, win, ts_gc, 0, 0, PLOT_WIDTH, PLOT_HEIGHT); /* Compute data range for Y axis (valid values only) */ @@ -287,11 +331,12 @@ static void draw_plot(Widget w) { } } - /* Draw axes (black) */ - XSetForeground(ts_display, ts_gc, black); + /* Draw axes (gray) */ + XSetForeground(ts_display, ts_gc, color_axis); XDrawRectangle(ts_display, win, ts_gc, plot_x0, plot_y0, plot_w, plot_h); /* Y-axis tick labels */ + XSetForeground(ts_display, ts_gc, color_label); XFontStruct *font = XQueryFont(ts_display, XGContextFromGC(ts_gc)); int font_ascent = font ? font->ascent : 10; @@ -302,9 +347,11 @@ static void draw_plot(Widget w) { if (py < plot_y0 || py > plot_y1) continue; /* Tick mark */ + XSetForeground(ts_display, ts_gc, color_axis); XDrawLine(ts_display, win, ts_gc, plot_x0 - TICK_LEN, py, plot_x0, py); /* Label */ + XSetForeground(ts_display, ts_gc, color_label); char buf[32]; snprintf(buf, sizeof(buf), "%.4g", val); int tw = font ? XTextWidth(font, buf, (int)strlen(buf)) : 40; @@ -321,9 +368,11 @@ static void draw_plot(Widget w) { if (px < plot_x0 || px > plot_x1) continue; /* Tick mark */ + XSetForeground(ts_display, ts_gc, color_axis); XDrawLine(ts_display, win, ts_gc, px, plot_y1, px, plot_y1 + TICK_LEN); /* Label */ + XSetForeground(ts_display, ts_gc, color_label); char buf[32]; if (use_cf_time) { if (!ts_format_time(buf, sizeof(buf), val, ts_cache.x_label)) @@ -405,7 +454,7 @@ static void draw_plot(Widget w) { /* Reset line width */ XSetLineAttributes(ts_display, ts_gc, 0, LineSolid, CapButt, JoinMiter); - XSetForeground(ts_display, ts_gc, black); + XSetForeground(ts_display, ts_gc, color_label); if (font) { XFreeFontInfo(NULL, font, 1); diff --git a/src/interface/x_interface.c b/src/interface/x_interface.c index 20f22ad..f3316e5 100644 --- a/src/interface/x_interface.c +++ b/src/interface/x_interface.c @@ -29,16 +29,16 @@ #include /* Layout constants (like ncview's app_data) */ -#define LABEL_WIDTH 580 -#define BUTTON_WIDTH 50 -#define CBAR_WIDTH 480 -#define CBAR_HEIGHT 16 -#define CBAR_LABEL_HEIGHT 14 -#define CBAR_PAD 2 +#define LABEL_WIDTH 640 +#define BUTTON_WIDTH 58 +#define CBAR_WIDTH 540 +#define CBAR_HEIGHT 24 +#define CBAR_LABEL_HEIGHT 16 +#define CBAR_PAD 3 #define DIM_COL_WIDTH 55 -#define NAME_COL_WIDTH 100 -#define VALUE_COL_WIDTH 95 -#define UNITS_COL_WIDTH 95 +#define NAME_COL_WIDTH 110 +#define VALUE_COL_WIDTH 105 +#define UNITS_COL_WIDTH 105 /* Global X11 resources */ static Display *display = NULL; @@ -75,8 +75,8 @@ static Widget *varsel_boxes = NULL; /* Array of box widgets for variable rows static int n_varsel_boxes = 0; static Widget *var_toggles = NULL; static int n_var_toggles = 0; -#define VARS_PER_ROW 5 -#define VAR_BUTTON_WIDTH 65 +#define VARS_PER_ROW 7 +#define VAR_BUTTON_WIDTH 72 /* Dimension info panel */ static Widget diminfo_form = NULL; @@ -148,6 +148,15 @@ static size_t current_n_times = 1; static size_t current_n_depths = 1; static int current_var_index = -1; +/* Theme flag (0=dark, 1=light) — set before x_init via x_set_light_theme() */ +static int light_theme = 0; + +/* Toggle highlight colors (forward declarations) */ +static Pixel toggle_highlight_pixel; +static Pixel toggle_normal_pixel; +static int toggle_colors_allocated; +static void allocate_toggle_colors(void); + /* ========== Button Callbacks ========== */ static void var_toggle_callback(Widget w, XtPointer client_data, XtPointer call_data) { @@ -156,6 +165,16 @@ static void var_toggle_callback(Widget w, XtPointer client_data, XtPointer call_ Boolean state; XtVaGetValues(w, XtNstate, &state, NULL); if (!state) return; + + /* Reset old toggle, highlight new one */ + allocate_toggle_colors(); + if (toggle_colors_allocated && var_toggles) { + if (current_var_index >= 0 && current_var_index < n_var_toggles) + XtVaSetValues(var_toggles[current_var_index], + XtNbackground, toggle_normal_pixel, NULL); + XtVaSetValues(w, XtNbackground, toggle_highlight_pixel, NULL); + } + current_var_index = idx; if (var_select_cb) var_select_cb(idx); } @@ -309,20 +328,64 @@ static void draw_colorbar(Widget w) { Dimension widget_w = 0, widget_h = 0; XtVaGetValues(w, XtNwidth, &widget_w, XtNheight, &widget_h, NULL); - /* Black background */ - XSetForeground(display, cbar_gc, BlackPixel(display, DefaultScreen(display))); + int screen = DefaultScreen(display); + Colormap cmap = DefaultColormap(display, screen); + XColor xc; + + /* Background */ + if (light_theme) { + xc.red = 0xF0F0; xc.green = 0xF0F0; xc.blue = 0xF0F0; + } else { + xc.red = 0x2B2B; xc.green = 0x2B2B; xc.blue = 0x2B2B; + } + xc.flags = DoRed | DoGreen | DoBlue; + unsigned long bg_pixel; + if (XAllocColor(display, cmap, &xc)) + bg_pixel = xc.pixel; + else + bg_pixel = light_theme ? WhitePixel(display, screen) : BlackPixel(display, screen); + + XSetForeground(display, cbar_gc, bg_pixel); XFillRectangle(display, XtWindow(w), cbar_gc, 0, 0, widget_w, widget_h); - /* Draw colorbar at top */ + /* Draw colorbar strip */ XPutImage(display, XtWindow(w), cbar_gc, cbar_ximage, 0, 0, 0, 0, cbar_width, cbar_height); - /* Draw labels below the colorbar in white */ - XSetForeground(display, cbar_gc, WhitePixel(display, DefaultScreen(display))); + /* Thin border around colorbar strip */ + if (light_theme) { + xc.red = 0xB0B0; xc.green = 0xB0B0; xc.blue = 0xB0B0; + } else { + xc.red = 0x5555; xc.green = 0x5555; xc.blue = 0x5555; + } + xc.flags = DoRed | DoGreen | DoBlue; + unsigned long border_pixel; + if (XAllocColor(display, cmap, &xc)) + border_pixel = xc.pixel; + else + border_pixel = light_theme ? BlackPixel(display, screen) : WhitePixel(display, screen); + XSetForeground(display, cbar_gc, border_pixel); + XDrawRectangle(display, XtWindow(w), cbar_gc, 0, 0, + (unsigned int)cbar_width - 1, (unsigned int)cbar_height - 1); + + /* Label color */ + if (light_theme) { + xc.red = 0x2222; xc.green = 0x2222; xc.blue = 0x2222; + } else { + xc.red = 0xD4D4; xc.green = 0xD4D4; xc.blue = 0xD4D4; + } + xc.flags = DoRed | DoGreen | DoBlue; + unsigned long label_pixel; + if (XAllocColor(display, cmap, &xc)) + label_pixel = xc.pixel; + else + label_pixel = light_theme ? BlackPixel(display, screen) : WhitePixel(display, screen); + + XSetForeground(display, cbar_gc, label_pixel); XFontStruct *font = XQueryFont(display, XGContextFromGC(cbar_gc)); int ascent = font ? font->ascent : 10; - const int n_labels = 5; + const int n_labels = 7; for (int i = 0; i < n_labels; i++) { float t = (float)i / (float)(n_labels - 1); float val = cbar_min_val + t * (cbar_max_val - cbar_min_val); @@ -334,6 +397,14 @@ static void draw_colorbar(Widget w) { if (x < 2) x = 2; if (x > (int)cbar_width - text_w - 2) x = (int)cbar_width - text_w - 2; + /* Tick mark from bar bottom to label area */ + int tick_x = (int)(t * (float)(cbar_width - 1)); + XSetForeground(display, cbar_gc, border_pixel); + XDrawLine(display, XtWindow(w), cbar_gc, + tick_x, (int)cbar_height, tick_x, (int)cbar_height + CBAR_PAD); + + /* Label text */ + XSetForeground(display, cbar_gc, label_pixel); int y = (int)cbar_height + CBAR_PAD + ascent; XDrawString(display, XtWindow(w), cbar_gc, x, y, buf, (int)strlen(buf)); } @@ -372,6 +443,138 @@ static XtActionsRec actions[] = { {"cmap_back", cmap_back_callback}, }; +void x_set_light_theme(void) { light_theme = 1; } +int x_is_light_theme(void) { return light_theme; } + +/* Dark theme fallback resources */ +static String dark_resources[] = { + "*background: #2B2B2B", + "*foreground: #D4D4D4", + "*borderColor: #555555", + "*borderWidth: 0", + "*font: -misc-fixed-medium-r-normal--13-120-75-75-c-80-iso10646-1", + "*labelTitle.font: -misc-fixed-bold-r-normal--15-140-75-75-c-90-iso10646-1", + "*labelVarname.font: -misc-fixed-bold-r-normal--13-120-75-75-c-80-iso10646-1", + "*Command.background: #3C3C3C", + "*Command.foreground: #E0E0E0", + "*Command.borderWidth: 1", + "*Command.borderColor: #555555", + "*Command.internalWidth: 6", + "*Command.internalHeight: 3", + "*Toggle.background: #3C3C3C", + "*Toggle.foreground: #D4D4D4", + "*Toggle.borderWidth: 1", + "*Toggle.borderColor: #555555", + "*Toggle.internalWidth: 4", + "*Toggle.internalHeight: 2", + "*Label.borderWidth: 0", + "*Label.internalWidth: 4", + "*Label.internalHeight: 2", + "*Scrollbar.background: #3C3C3C", + "*Scrollbar.foreground: #606060", + "*Scrollbar.borderWidth: 1", + "*Scrollbar.borderColor: #555555", + "*Scrollbar.thickness: 16", + "*Text.background: #1E1E1E", + "*Text.foreground: #E0E0E0", + "*Text.borderWidth: 1", + "*Text.borderColor: #606060", + "*Box.borderWidth: 0", + "*Box.hSpace: 4", + "*Box.vSpace: 4", + "*Form.borderWidth: 0", + "*Form.defaultDistance: 4", + "*separator.background: #444444", + "*sectionLabel.font: -misc-fixed-bold-r-normal--13-120-75-75-c-80-iso10646-1", + "*sectionLabel.foreground: #888888", + NULL +}; + +/* Light theme fallback resources */ +static String light_resources[] = { + "*background: #F0F0F0", + "*foreground: #1A1A1A", + "*borderColor: #B0B0B0", + "*borderWidth: 0", + "*font: -misc-fixed-medium-r-normal--13-120-75-75-c-80-iso10646-1", + "*labelTitle.font: -misc-fixed-bold-r-normal--15-140-75-75-c-90-iso10646-1", + "*labelVarname.font: -misc-fixed-bold-r-normal--13-120-75-75-c-80-iso10646-1", + "*Command.background: #E0E0E0", + "*Command.foreground: #1A1A1A", + "*Command.borderWidth: 1", + "*Command.borderColor: #B0B0B0", + "*Command.internalWidth: 6", + "*Command.internalHeight: 3", + "*Toggle.background: #E0E0E0", + "*Toggle.foreground: #1A1A1A", + "*Toggle.borderWidth: 1", + "*Toggle.borderColor: #B0B0B0", + "*Toggle.internalWidth: 4", + "*Toggle.internalHeight: 2", + "*Label.borderWidth: 0", + "*Label.internalWidth: 4", + "*Label.internalHeight: 2", + "*Scrollbar.background: #D0D0D0", + "*Scrollbar.foreground: #A0A0A0", + "*Scrollbar.borderWidth: 1", + "*Scrollbar.borderColor: #B0B0B0", + "*Scrollbar.thickness: 16", + "*Text.background: #FFFFFF", + "*Text.foreground: #1A1A1A", + "*Text.borderWidth: 1", + "*Text.borderColor: #B0B0B0", + "*Box.borderWidth: 0", + "*Box.hSpace: 4", + "*Box.vSpace: 4", + "*Form.borderWidth: 0", + "*Form.defaultDistance: 4", + "*separator.background: #C0C0C0", + "*sectionLabel.font: -misc-fixed-bold-r-normal--13-120-75-75-c-80-iso10646-1", + "*sectionLabel.foreground: #777777", + NULL +}; + +static void allocate_toggle_colors(void) { + if (toggle_colors_allocated || !display) return; + int screen = DefaultScreen(display); + Colormap cmap = DefaultColormap(display, screen); + XColor xc; + + if (light_theme) { + /* Light theme: blue highlight, light gray normal */ + xc.red = 0x4444; xc.green = 0x8888; xc.blue = 0xCCCC; + xc.flags = DoRed | DoGreen | DoBlue; + if (XAllocColor(display, cmap, &xc)) + toggle_highlight_pixel = xc.pixel; + else + toggle_highlight_pixel = WhitePixel(display, screen); + + xc.red = 0xE0E0; xc.green = 0xE0E0; xc.blue = 0xE0E0; + xc.flags = DoRed | DoGreen | DoBlue; + if (XAllocColor(display, cmap, &xc)) + toggle_normal_pixel = xc.pixel; + else + toggle_normal_pixel = WhitePixel(display, screen); + } else { + /* Dark theme: teal highlight, dark gray normal */ + xc.red = 0x2222; xc.green = 0x6666; xc.blue = 0x8888; + xc.flags = DoRed | DoGreen | DoBlue; + if (XAllocColor(display, cmap, &xc)) + toggle_highlight_pixel = xc.pixel; + else + toggle_highlight_pixel = BlackPixel(display, screen); + + xc.red = 0x3C3C; xc.green = 0x3C3C; xc.blue = 0x3C3C; + xc.flags = DoRed | DoGreen | DoBlue; + if (XAllocColor(display, cmap, &xc)) + toggle_normal_pixel = xc.pixel; + else + toggle_normal_pixel = BlackPixel(display, screen); + } + + toggle_colors_allocated = 1; +} + int x_init(int *argc, char **argv, const char **var_names, int n_vars, const USDimInfo *dims, int n_dims) { Widget btn; @@ -382,7 +585,7 @@ int x_init(int *argc, char **argv, const char **var_names, int n_vars, "Ushow", NULL, 0, argc, argv, - NULL, + light_theme ? light_resources : dark_resources, XtNtitle, "ushow", NULL ); @@ -448,12 +651,26 @@ int x_init(int *argc, char **argv, const char **var_names, int n_vars, NULL ); + /* ===== Separator: info | animation ===== */ + Widget sep1 = XtVaCreateManagedWidget( + "separator", labelWidgetClass, main_form, + XtNlabel, "", + XtNwidth, LABEL_WIDTH, + XtNheight, 2, + XtNfromVert, label_value, + XtNborderWidth, 0, + XtNinternalHeight, 0, + XtNinternalWidth, 0, + XtNresize, False, + NULL + ); + /* ===== Button Box 1: Animation controls ===== */ buttonbox = XtVaCreateManagedWidget( "buttonbox", boxWidgetClass, main_form, XtNorientation, XtorientHorizontal, - XtNfromVert, label_value, - XtNwidth, 580, + XtNfromVert, sep1, + XtNwidth, LABEL_WIDTH, NULL ); @@ -509,18 +726,32 @@ int x_init(int *argc, char **argv, const char **var_names, int n_vars, ); XtAddCallback(depth_scrollbar, XtNjumpProc, depth_scroll_callback, NULL); + /* ===== Separator: animation | options ===== */ + Widget sep2 = XtVaCreateManagedWidget( + "separator", labelWidgetClass, main_form, + XtNlabel, "", + XtNwidth, LABEL_WIDTH, + XtNheight, 2, + XtNfromVert, buttonbox, + XtNborderWidth, 0, + XtNinternalHeight, 0, + XtNinternalWidth, 0, + XtNresize, False, + NULL + ); + /* ===== Button Box 2: Options ===== */ optionbox = XtVaCreateManagedWidget( "optionbox", boxWidgetClass, main_form, XtNorientation, XtorientHorizontal, - XtNfromVert, buttonbox, - XtNwidth, 580, + XtNfromVert, sep2, + XtNwidth, LABEL_WIDTH, NULL ); cmap_button = XtVaCreateManagedWidget( "Cmap", commandWidgetClass, optionbox, - XtNwidth, 90, + XtNwidth, 100, XtNresize, False, NULL ); @@ -557,16 +788,30 @@ int x_init(int *argc, char **argv, const char **var_names, int n_vars, XtAddCallback(btn, XtNcallback, save_callback_fn, NULL); render_mode_button = XtVaCreateManagedWidget("Interp", commandWidgetClass, optionbox, - XtNwidth, BUTTON_WIDTH + 10, + XtNwidth, 70, XtNresize, False, NULL); XtAddCallback(render_mode_button, XtNcallback, render_mode_callback_fn, NULL); + /* ===== Separator: options | colorbar ===== */ + Widget sep3 = XtVaCreateManagedWidget( + "separator", labelWidgetClass, main_form, + XtNlabel, "", + XtNwidth, LABEL_WIDTH, + XtNheight, 2, + XtNfromVert, optionbox, + XtNborderWidth, 0, + XtNinternalHeight, 0, + XtNinternalWidth, 0, + XtNresize, False, + NULL + ); + /* ===== Colorbar ===== */ colorbar_form = XtVaCreateManagedWidget( "colorbarForm", boxWidgetClass, main_form, XtNorientation, XtorientHorizontal, - XtNfromVert, optionbox, + XtNfromVert, sep3, XtNwidth, LABEL_WIDTH, NULL ); @@ -580,14 +825,28 @@ int x_init(int *argc, char **argv, const char **var_names, int n_vars, ); btn = XtVaCreateManagedWidget("Range", commandWidgetClass, colorbar_form, - XtNwidth, BUTTON_WIDTH + 10, + XtNwidth, 70, NULL); XtAddCallback(btn, XtNcallback, range_button_callback_fn, NULL); + /* ===== Separator: colorbar | variables ===== */ + Widget sep4 = XtVaCreateManagedWidget( + "separator", labelWidgetClass, main_form, + XtNlabel, "", + XtNwidth, LABEL_WIDTH, + XtNheight, 2, + XtNfromVert, colorbar_form, + XtNborderWidth, 0, + XtNinternalHeight, 0, + XtNinternalWidth, 0, + XtNresize, False, + NULL + ); + /* ===== Variable Selector ===== */ varsel_form = XtVaCreateManagedWidget( "varselForm", boxWidgetClass, main_form, - XtNfromVert, colorbar_form, + XtNfromVert, sep4, XtNorientation, XtorientVertical, XtNwidth, LABEL_WIDTH, XtNresizable, True, @@ -596,10 +855,24 @@ int x_init(int *argc, char **argv, const char **var_names, int n_vars, /* Variable toggle buttons are created before realization */ x_setup_var_selector(var_names, n_vars); + /* ===== Separator: variables | dimensions ===== */ + Widget sep5 = XtVaCreateManagedWidget( + "separator", labelWidgetClass, main_form, + XtNlabel, "", + XtNwidth, LABEL_WIDTH, + XtNheight, 2, + XtNfromVert, varsel_form, + XtNborderWidth, 0, + XtNinternalHeight, 0, + XtNinternalWidth, 0, + XtNresize, False, + NULL + ); + /* ===== Dimension Info Panel ===== */ diminfo_form = XtVaCreateManagedWidget( "diminfoForm", boxWidgetClass, main_form, - XtNfromVert, varsel_form, + XtNfromVert, sep5, XtNorientation, XtorientVertical, XtNwidth, LABEL_WIDTH, XtNresizable, True, @@ -831,6 +1104,12 @@ void x_setup_var_selector(const char **var_names, int n_vars) { XtVaSetValues(var_toggles[0], XtNradioGroup, var_toggles[0], NULL); } + /* Highlight initial active variable */ + allocate_toggle_colors(); + if (toggle_colors_allocated && n_vars > 0) { + XtVaSetValues(var_toggles[0], XtNbackground, toggle_highlight_pixel, NULL); + } + current_var_index = 0; } diff --git a/src/interface/x_interface.h b/src/interface/x_interface.h index 584e263..a0dd6ba 100644 --- a/src/interface/x_interface.h +++ b/src/interface/x_interface.h @@ -7,6 +7,17 @@ #include "../ushow.defines.h" +/* + * Set light theme (call before x_init). Default is dark theme. + */ +void x_set_light_theme(void); + +/* + * Query current theme (for use by sub-components like timeseries popup). + * Returns 1 if light theme, 0 if dark theme. + */ +int x_is_light_theme(void); + /* * Initialize X11 display. * Returns 0 on success, -1 on failure. diff --git a/src/ushow.c b/src/ushow.c index 4ca738e..a2c2604 100644 --- a/src/ushow.c +++ b/src/ushow.c @@ -872,6 +872,7 @@ static void print_usage(const char *prog) { fprintf(stderr, " conserv1, conserv2\n"); fprintf(stderr, " --yac-3d Per-depth masked interpolation for 3D variables\n"); #endif + fprintf(stderr, " --light Use light theme (default: dark)\n"); fprintf(stderr, " -h, --help Show this help\n"); fprintf(stderr, "\nExamples:\n"); fprintf(stderr, " %s data.nc # Single file\n", prog); @@ -900,6 +901,7 @@ int main(int argc, char *argv[]) { {"yac-method", required_argument, 0, 1100}, {"yac-3d", no_argument, 0, 1101}, #endif + {"light", no_argument, 0, 1300}, {"help", no_argument, 0, 'h'}, {0, 0, 0, 0} }; @@ -969,6 +971,9 @@ int main(int argc, char *argv[]) { case 1202: options.target_config.cutoff_lat = atof(optarg); break; + case 1300: + x_set_light_theme(); + break; case 'h': default: print_usage(argv[0]);