eco2d/code/game/source/editors/texed.c

503 lines
15 KiB
C
Raw Normal View History

2021-05-15 11:43:29 +00:00
#define ZPL_NO_WINDOWS_H
#include "zpl.h"
2021-05-14 05:59:33 +00:00
#include "editors/texed.h"
#include "raylib.h"
2021-05-15 11:43:29 +00:00
#include "utils/raylib_helpers.h"
#include "cwpack/cwpack.h"
2021-05-14 05:59:33 +00:00
#define RAYGUI_IMPLEMENTATION
#define RAYGUI_SUPPORT_ICONS
#include "raygui.h"
2021-05-15 11:43:29 +00:00
#define GUI_FILE_DIALOG_IMPLEMENTATION
#include "gui_file_dialog.h"
2021-05-15 16:41:30 +00:00
#define GUI_TEXTBOX_EXTENDED_IMPLEMENTATION
#include "gui_textbox_extended.h"
2021-05-16 11:36:01 +00:00
#define TD_DEFAULT_IMG_WIDTH 64
#define TD_DEFAULT_IMG_HEIGHT 64
#define TD_UI_PADDING 5.0f
#define TD_UI_PREVIEW_BORDER 4.0f
#define TD_UI_DEFAULT_ZOOM 4.0f
2021-05-17 05:53:10 +00:00
#define TD_IMAGES_MAX_STACK 128
2021-05-16 11:36:01 +00:00
2021-05-14 05:59:33 +00:00
static uint16_t screenWidth = 1280;
static uint16_t screenHeight = 720;
2021-05-16 11:36:01 +00:00
static float zoom = TD_UI_DEFAULT_ZOOM;
static float old_zoom = TD_UI_DEFAULT_ZOOM;
2021-05-15 18:00:57 +00:00
static Texture2D checker_tex;
static uint16_t old_screen_w;
static uint16_t old_screen_h;
2021-05-16 09:10:33 +00:00
static bool is_repaint_locked = false;
2021-05-17 19:27:56 +00:00
static int render_tiles = 0;
2021-05-15 11:43:29 +00:00
typedef enum {
TPARAM_FLOAT,
2021-05-15 16:41:30 +00:00
TPARAM_COORD,
2021-05-15 11:43:29 +00:00
TPARAM_INT,
TPARAM_COLOR,
TPARAM_STRING,
2021-05-16 14:11:29 +00:00
TPARAM_SLIDER,
2021-05-15 11:43:29 +00:00
TPARAM_FORCE_UINT8 = UINT8_MAX
} td_param_kind;
typedef struct {
td_param_kind kind;
2021-05-15 14:25:18 +00:00
char const *name;
char str[1000];
bool edit_mode;
2021-05-15 11:43:29 +00:00
union {
2021-05-16 14:11:29 +00:00
struct {
float flt, old_flt;
};
2021-05-15 15:19:50 +00:00
uint32_t u32;
int32_t i32;
2021-05-15 11:43:29 +00:00
Color color;
2021-05-15 14:25:18 +00:00
char copy[4];
2021-05-15 11:43:29 +00:00
};
} td_param;
typedef enum {
2021-05-15 17:17:47 +00:00
TOP_NEW_IMAGE,
2021-05-15 11:43:29 +00:00
TOP_DRAW_RECT,
TOP_DRAW_LINE,
2021-05-15 15:19:50 +00:00
TOP_DITHER,
2021-05-16 13:38:30 +00:00
TOP_DRAW_IMAGE,
2021-05-15 16:41:30 +00:00
TOP_DRAW_TEXT,
2021-05-15 17:40:27 +00:00
TOP_RESIZE_IMAGE,
2021-05-16 14:22:28 +00:00
TOP_COLOR_TWEAKS,
2021-05-16 14:58:08 +00:00
TOP_FLIP_IMAGE,
TOP_ROTATE_IMAGE,
2021-05-15 11:43:29 +00:00
2021-05-17 05:53:10 +00:00
TOP_PUSH_IMAGE,
TOP_POP_IMAGE,
2021-05-17 16:05:22 +00:00
TOP_IMAGE_GRAD_V,
TOP_IMAGE_GRAD_H,
TOP_IMAGE_GRAD_RAD,
TOP_IMAGE_CHECKED,
TOP_IMAGE_NOISE_WHITE,
TOP_IMAGE_NOISE_PERLIN,
TOP_IMAGE_CELLULAR,
TOP_COLOR_REPLACE,
TOP_IMAGE_ALPHA_MASK,
2021-05-17 19:10:12 +00:00
TOP_IMAGE_ALPHA_MASK_CLEAR,
2021-05-17 16:05:22 +00:00
2021-05-15 11:43:29 +00:00
TOP_FORCE_UINT8 = UINT8_MAX
} td_op_kind;
typedef struct {
td_op_kind kind;
char const *name;
bool is_hidden;
2021-05-16 10:14:37 +00:00
bool is_locked;
2021-05-15 11:43:29 +00:00
uint8_t num_params;
td_param *params;
} td_op;
#define OP(n) .kind = n, .name = #n
2021-05-16 12:07:28 +00:00
typedef struct {
bool visible;
char const *title;
char const *message;
char const *buttons;
int result;
} td_msgbox;
2021-05-15 11:43:29 +00:00
typedef struct {
char *filepath;
2021-05-17 05:53:10 +00:00
int32_t img_pos;
Image img[TD_IMAGES_MAX_STACK];
2021-05-15 11:43:29 +00:00
Texture2D tex;
GuiFileDialogState fileDialog;
2021-05-16 12:07:28 +00:00
td_msgbox msgbox;
2021-05-16 11:36:01 +00:00
bool is_saved;
2021-05-15 11:43:29 +00:00
td_op *ops; //< zpl_array
2021-05-15 14:25:18 +00:00
int selected_op;
2021-05-15 11:43:29 +00:00
} td_ctx;
static td_ctx ctx = {0};
static char filename[200];
2021-05-15 15:19:50 +00:00
#include "texed_ops_list.c"
2021-05-15 11:43:29 +00:00
2021-05-17 05:53:10 +00:00
void texed_new(int w, int h);
void texed_clear(void);
2021-05-15 11:43:29 +00:00
void texed_destroy(void);
void texed_load(void);
void texed_save(void);
2021-05-16 09:10:33 +00:00
void texed_export_cc(char const *path);
void texed_export_png(char const *path);
2021-05-15 11:43:29 +00:00
void texed_repaint_preview(void);
2021-05-16 09:10:33 +00:00
void texed_compose_image(void);
2021-05-16 12:07:28 +00:00
void texed_msgbox_init(char const *title, char const *message, char const *buttons);
2021-05-15 11:43:29 +00:00
void texed_process_ops(void);
2021-05-15 13:23:04 +00:00
void texed_process_params(void);
2021-05-16 13:38:30 +00:00
2021-05-17 05:53:10 +00:00
void texed_img_push(int w, int h, Color color);
void texed_img_pop(int x, int y, int w, int h, Color tint);
2021-05-16 13:38:30 +00:00
void texed_add_op(int kind);
2021-05-15 11:43:29 +00:00
void texed_rem_op(int idx);
void texed_swp_op(int idx, int idx2);
2021-05-16 13:38:30 +00:00
int texed_find_op(int kind);
2021-05-15 11:43:29 +00:00
void texed_draw_oplist_pane(zpl_aabb2 r);
2021-05-15 14:25:18 +00:00
void texed_draw_props_pane(zpl_aabb2 r);
2021-05-15 11:43:29 +00:00
void texed_draw_topbar(zpl_aabb2 r);
2021-05-16 12:07:28 +00:00
void texed_draw_msgbox(zpl_aabb2 r);
2021-05-15 11:43:29 +00:00
static inline
void DrawAABB(zpl_aabb2 rect, Color color) {
DrawRectangleEco(rect.min.x, rect.min.y,
rect.max.x-rect.min.x,
rect.max.y-rect.min.y,
color);
}
static inline
2021-05-15 15:19:50 +00:00
Rectangle aabb2_ray(zpl_aabb2 r) {
return (Rectangle) {
.x = r.min.x,
.y = r.min.y,
.width = r.max.x-r.min.x,
.height = r.max.y-r.min.y
};
}
#include "texed_ops.c"
#include "texed_prj.c"
#include "texed_widgets.c"
2021-05-14 05:59:33 +00:00
2021-05-16 09:10:33 +00:00
void texed_run(int argc, char **argv) {
zpl_opts opts={0};
zpl_opts_init(&opts, zpl_heap(), argv[0]);
zpl_opts_add(&opts, "td", "texed", "run texture editor", ZPL_OPTS_FLAG);
zpl_opts_add(&opts, "td-i", "texed-import", "convert an image to ecotex format", ZPL_OPTS_STRING);
zpl_opts_add(&opts, "td-ec", "texed-export-cc", "export ecotex image to C header file", ZPL_OPTS_STRING);
zpl_opts_add(&opts, "td-ep", "texed-export-png", "export ecotex image to PNG format", ZPL_OPTS_STRING);
uint32_t ok = zpl_opts_compile(&opts, argc, argv);
if (!ok) {
zpl_opts_print_errors(&opts);
zpl_opts_print_help(&opts);
return;
}
if (zpl_opts_has_arg(&opts, "texed-import")) {
zpl_string path = zpl_opts_string(&opts, "texed-import", "");
if (FileExists(zpl_bprintf("art/%s", path)) && IsFileExtension(path, ".png")) {
Image orig = LoadImage(zpl_bprintf("art/%s", path));
texed_new(orig.width, orig.height);
is_repaint_locked = true;
2021-05-16 13:38:30 +00:00
texed_add_op(TOP_DRAW_IMAGE);
2021-05-16 09:10:33 +00:00
td_param *params = ctx.ops[1].params;
zpl_strcpy(params[0].str, path);
is_repaint_locked = false;
texed_compose_image();
zpl_strcpy(filename, zpl_bprintf("%s.ecotex", GetFileNameWithoutExt(path)));
ctx.filepath = filename;
texed_save();
} else {
zpl_printf("%s\n", "provided file does not exist!");
}
return;
}
if (zpl_opts_has_arg(&opts, "texed-export-cc")) {
zpl_string path = zpl_opts_string(&opts, "texed-export-cc", "");
if (FileExists(zpl_bprintf("art/%s.ecotex", path))) {
zpl_array_init(ctx.ops, zpl_heap());
zpl_strcpy(filename, zpl_bprintf("%s.ecotex", path));
ctx.filepath = filename;
texed_load();
texed_export_cc(path);
} else {
zpl_printf("%s\n", "provided file does not exist!");
}
return;
}
if (zpl_opts_has_arg(&opts, "texed-export-png")) {
zpl_string path = zpl_opts_string(&opts, "texed-export-png", "");
if (FileExists(zpl_bprintf("art/%s.ecotex", path))) {
zpl_array_init(ctx.ops, zpl_heap());
zpl_strcpy(filename, zpl_bprintf("%s.ecotex", path));
ctx.filepath = filename;
texed_load();
texed_export_png(path);
} else {
zpl_printf("%s\n", "provided file does not exist!");
}
return;
}
2021-05-14 05:59:33 +00:00
InitWindow(screenWidth, screenHeight, "eco2d - texture editor");
2021-05-15 18:00:57 +00:00
SetWindowState(FLAG_WINDOW_RESIZABLE);
2021-05-14 05:59:33 +00:00
SetTargetFPS(60);
2021-05-15 11:43:29 +00:00
texed_new(TD_DEFAULT_IMG_WIDTH, TD_DEFAULT_IMG_HEIGHT);
2021-05-17 19:10:12 +00:00
{
GuiSetStyle(TEXTBOX, TEXT_COLOR_NORMAL, ColorToInt(RAYWHITE));
GuiSetStyle(DEFAULT, BACKGROUND_COLOR, 0x012e33ff);
GuiSetStyle(BUTTON, BASE, 0x202020ff);
GuiSetStyle(BUTTON, BASE + GUI_STATE_DISABLED*3, 0x303030ff);
GuiSetStyle(BUTTON, TEXT + GUI_STATE_FOCUSED*3, 0x303030ff);
GuiSetStyle(DEFAULT, TEXT_COLOR_NORMAL, 0xffffffff);
GuiSetStyle(LISTVIEW, SCROLLBAR_SIDE, SCROLLBAR_LEFT_SIDE);
}
2021-05-15 16:41:30 +00:00
2021-05-16 14:39:03 +00:00
while (1) {
2021-05-15 18:00:57 +00:00
zpl_aabb2 screen = {
.min = (zpl_vec2) {.x = 0.0f, .y = 0.0f},
.max = (zpl_vec2) {.x = GetScreenWidth(), .y = GetScreenHeight()},
};
2021-05-16 12:07:28 +00:00
zpl_aabb2 orig_screen = screen;
2021-05-15 18:00:57 +00:00
2021-05-16 13:15:44 +00:00
zpl_aabb2 topbar = zpl_aabb2_cut_top(&screen, 20.0f);
2021-05-15 18:00:57 +00:00
zpl_aabb2 oplist_pane = zpl_aabb2_cut_right(&screen, screenWidth / 2.0f);
2021-05-15 20:27:35 +00:00
zpl_aabb2 property_pane = zpl_aabb2_cut_bottom(&screen, screenHeight / 2.0f);
zpl_aabb2 preview_window = screen;
2021-05-15 18:00:57 +00:00
// NOTE(zaklaus): contract all panes for a clean UI separation
oplist_pane = zpl_aabb2_contract(&oplist_pane, TD_UI_PADDING);
preview_window = zpl_aabb2_contract(&preview_window, TD_UI_PADDING);
property_pane = zpl_aabb2_contract(&property_pane, TD_UI_PADDING);
Rectangle preview_rect = aabb2_ray(preview_window);
if (old_screen_w != GetScreenWidth() || old_screen_h != GetScreenHeight()) {
old_screen_w = GetScreenWidth();
old_screen_h = GetScreenHeight();
Image checkerboard = GenImageChecked(preview_rect.width, preview_rect.height, 16, 16, BLACK, ColorAlpha(GRAY, 0.2f));
checker_tex = LoadTextureFromImage(checkerboard);
UnloadImage(checkerboard);
2021-05-15 20:09:25 +00:00
ctx.fileDialog = InitGuiFileDialog(420, 310, zpl_bprintf("%s/art", GetWorkingDirectory()), false);
2021-05-15 18:00:57 +00:00
}
2021-05-14 05:59:33 +00:00
BeginDrawing();
2021-05-15 11:43:29 +00:00
ClearBackground(GetColor(0x222034));
2021-05-14 05:59:33 +00:00
{
2021-05-15 11:43:29 +00:00
if (ctx.fileDialog.fileDialogActive) GuiLock();
2021-05-16 12:07:28 +00:00
if (ctx.msgbox.visible) GuiLock();
2021-05-15 11:43:29 +00:00
DrawTextureEx(checker_tex, (Vector2){ preview_window.min.x, preview_window.min.y}, 0.0f, 1.0f, WHITE);
2021-05-16 14:22:28 +00:00
Rectangle tex_rect = aabb2_ray(preview_window);
2021-05-17 19:27:56 +00:00
float tile_x = tex_rect.x + zpl_max(0.0f, tex_rect.width/2.0f - (ctx.tex.width*zoom)/2.0f);
float tile_y = tex_rect.y + zpl_max(0.0f, tex_rect.height/2.0f - (ctx.tex.height*zoom)/2.0f);
2021-05-14 05:59:33 +00:00
2021-05-17 19:27:56 +00:00
for (int x = -render_tiles; x <= render_tiles; x++) {
for (int y = -render_tiles; y <= render_tiles; y++) {
DrawTextureEx(ctx.tex, (Vector2){tile_x + (ctx.tex.width*zoom) * x, tile_y + (ctx.tex.height*zoom)*y}, 0.0f, zoom, WHITE);
}
}
DrawAABB(topbar, BLACK);
2021-05-15 11:43:29 +00:00
DrawAABB(property_pane, GetColor(0x422060));
DrawAABB(oplist_pane, GetColor(0x425060));
2021-05-14 05:59:33 +00:00
2021-05-15 11:43:29 +00:00
texed_draw_topbar(topbar);
2021-05-15 14:25:18 +00:00
texed_draw_props_pane(property_pane);
2021-05-15 11:43:29 +00:00
texed_draw_oplist_pane(oplist_pane);
2021-05-14 05:59:33 +00:00
2021-05-15 11:43:29 +00:00
if (ctx.fileDialog.fileDialogActive) GuiUnlock();
2021-05-16 12:07:28 +00:00
if (ctx.msgbox.visible) GuiUnlock();
2021-05-15 11:43:29 +00:00
GuiFileDialog(&ctx.fileDialog);
2021-05-16 12:07:28 +00:00
texed_draw_msgbox(orig_screen);
2021-05-14 05:59:33 +00:00
}
EndDrawing();
2021-05-16 14:39:03 +00:00
static bool exit_pending = false;
if (WindowShouldClose()) {
if (!ctx.is_saved) {
texed_msgbox_init("Discard unsaved work?", "You have an unsaved work! Do you want to proceed?", "OK;Cancel");
exit_pending = true;
} else {
break;
}
}
if (exit_pending && ctx.msgbox.result != -1) {
exit_pending = false;
if (ctx.msgbox.result == 1) {
break;
}
ctx.msgbox.result = -1;
}
2021-05-14 05:59:33 +00:00
}
2021-05-15 11:43:29 +00:00
UnloadTexture(checker_tex);
2021-05-16 09:10:33 +00:00
zpl_opts_free(&opts);
2021-05-15 11:43:29 +00:00
texed_destroy();
}
void texed_new(int32_t w, int32_t h) {
2021-05-17 05:53:10 +00:00
ctx.img_pos = -1;
2021-05-17 16:05:22 +00:00
ctx.selected_op = -1;
2021-05-17 05:53:10 +00:00
zpl_memset(ctx.img, 0, sizeof(Image)*TD_IMAGES_MAX_STACK);
2021-05-15 11:43:29 +00:00
ctx.filepath = NULL;
2021-05-16 12:45:30 +00:00
ctx.msgbox.result = -1;
2021-05-15 11:43:29 +00:00
zpl_array_init(ctx.ops, zpl_heap());
2021-05-16 09:10:33 +00:00
is_repaint_locked = true;
2021-05-15 17:17:47 +00:00
texed_add_op(TOP_NEW_IMAGE);
2021-05-16 09:10:33 +00:00
zpl_i64_to_str(w, ctx.ops[0].params[0].str, 10);
zpl_i64_to_str(h, ctx.ops[0].params[1].str, 10);
is_repaint_locked = false;
2021-05-16 09:15:11 +00:00
texed_repaint_preview();
2021-05-15 11:43:29 +00:00
ctx.fileDialog = InitGuiFileDialog(420, 310, zpl_bprintf("%s/art", GetWorkingDirectory()), false);
2021-05-16 11:36:01 +00:00
ctx.is_saved = true;
2021-05-15 11:43:29 +00:00
}
2021-05-17 05:53:10 +00:00
void texed_clear(void) {
zpl_array_clear(ctx.ops);
for (int i = 0; i <= ctx.img_pos; i+=1)
UnloadImage(ctx.img[i]);
ctx.img_pos = -1;
2021-05-17 16:05:22 +00:00
ctx.selected_op = -1;
2021-05-17 05:53:10 +00:00
}
2021-05-15 11:43:29 +00:00
void texed_destroy(void) {
2021-05-17 05:53:10 +00:00
texed_clear();
2021-05-16 14:39:03 +00:00
CloseWindow();
2021-05-15 11:43:29 +00:00
}
2021-05-16 09:10:33 +00:00
void texed_export_cc(char const *path) {
2021-05-17 19:10:12 +00:00
zpl_printf("Building texture %s ...\n", zpl_bprintf("art/gen/%s.h", GetFileNameWithoutExt(path)));
2021-05-17 05:53:10 +00:00
ExportImageAsCode(ctx.img[ctx.img_pos], zpl_bprintf("art/gen/%s.h", GetFileNameWithoutExt(path)));
2021-05-16 09:10:33 +00:00
}
void texed_export_png(char const *path) {
2021-05-17 19:10:12 +00:00
zpl_printf("Exporting texture %s ...\n", zpl_bprintf("art/gen/%s.png", GetFileNameWithoutExt(path)));
2021-05-17 05:53:10 +00:00
ExportImage(ctx.img[ctx.img_pos], zpl_bprintf("art/gen/%s.png", GetFileNameWithoutExt(path)));
}
void texed_img_push(int w, int h, Color color) {
if (ctx.img_pos == TD_IMAGES_MAX_STACK)
return;
ctx.img_pos += 1;
ctx.img[ctx.img_pos] = GenImageColor(w, h, color);
}
void texed_img_pop(int x, int y, int w, int h, Color tint) {
if (ctx.img_pos == 0)
return;
Image *oi = &ctx.img[ctx.img_pos];
Image *di = &ctx.img[ctx.img_pos-1];
Rectangle src = {
0, 0,
oi->width, oi->height
};
w = (w == 0) ? di->width : w;
h = (h == 0) ? di->height : h;
Rectangle dst = {
x, y,
w, h,
};
ImageDraw(di, *oi, src, dst, tint);
UnloadImage(ctx.img[ctx.img_pos]);
ctx.img_pos -= 1;
2021-05-16 09:10:33 +00:00
}
2021-05-15 11:43:29 +00:00
void texed_repaint_preview(void) {
2021-05-16 09:10:33 +00:00
if (is_repaint_locked) return;
texed_compose_image();
if (!IsWindowReady()) return;
2021-05-15 11:43:29 +00:00
UnloadTexture(ctx.tex);
2021-05-17 05:53:10 +00:00
ctx.tex = LoadTextureFromImage(ctx.img[ctx.img_pos]);
2021-05-16 09:10:33 +00:00
}
void texed_compose_image(void) {
if (is_repaint_locked) return;
2021-05-16 11:36:01 +00:00
ctx.is_saved = false;
2021-05-15 13:23:04 +00:00
texed_process_params();
2021-05-15 11:43:29 +00:00
texed_process_ops();
2021-05-14 05:59:33 +00:00
}
2021-05-16 12:07:28 +00:00
void texed_msgbox_init(char const *title, char const *message, char const *buttons) {
ctx.msgbox.result = -1;
ctx.msgbox.visible = true;
ctx.msgbox.title = title;
ctx.msgbox.message = message;
ctx.msgbox.buttons = buttons;
}
2021-05-16 13:38:30 +00:00
int texed_find_op(int kind) {
for (int i = 0; i < DEF_OPS_LEN; i += 1) {
if (default_ops[i].kind == kind) {
return i;
}
}
return -1;
}
void texed_add_op(int kind) {
2021-05-17 05:53:10 +00:00
int idx = texed_find_op(kind);
assert(idx >= 0);
td_op *dop = &default_ops[idx];
2021-05-15 11:43:29 +00:00
td_op op = {
.kind = dop->kind,
.name = dop->name,
2021-05-16 10:24:08 +00:00
.is_locked = dop->is_locked,
2021-05-15 11:43:29 +00:00
.num_params = dop->num_params,
.params = (td_param*)zpl_malloc(sizeof(td_param)*dop->num_params)
};
zpl_memcopy(op.params, dop->params, sizeof(td_param)*dop->num_params);
2021-05-17 16:05:22 +00:00
//TODO(zaklaus): weird stuff down there
//zpl_array_append_at(ctx.ops, op, ctx.selected_op+1);
int ind = ctx.selected_op+1;
do {
if (ind >= zpl_array_count(ctx.ops)) { zpl_array_append(ctx.ops, op); break; }
if (zpl_array_capacity(ctx.ops) < zpl_array_count(ctx.ops) + 1) zpl_array_grow(ctx.ops, 0);
zpl_memmove(&(ctx.ops)[ind + 1], (ctx.ops + ind), zpl_size_of(td_op) * (zpl_array_count(ctx.ops) - ind));
ctx.ops[ind] = op;
zpl_array_count(ctx.ops)++;
} while (0);
ctx.selected_op++;
2021-05-15 11:43:29 +00:00
texed_repaint_preview();
}
void texed_swp_op(int idx, int idx2) {
assert(idx >= 0 && idx < (int)zpl_array_count(ctx.ops));
assert(idx2 >= 0 && idx2 < (int)zpl_array_count(ctx.ops));
td_op tmp = ctx.ops[idx2];
ctx.ops[idx2] = ctx.ops[idx];
ctx.ops[idx] = tmp;
2021-05-15 22:24:42 +00:00
if (idx == ctx.selected_op) ctx.selected_op = idx2;
2021-05-15 11:43:29 +00:00
texed_repaint_preview();
}
2021-05-15 15:19:50 +00:00
void texed_rem_op(int idx) {
assert(idx >= 0 && idx < (int)zpl_array_count(ctx.ops));
zpl_mfree(ctx.ops[idx].params);
zpl_array_remove_at(ctx.ops, idx);
2021-05-15 14:25:18 +00:00
2021-05-15 20:09:25 +00:00
if (zpl_array_count(ctx.ops) > 0 && idx <= ctx.selected_op) ctx.selected_op -= 1;
2021-05-15 12:51:44 +00:00
texed_repaint_preview();
2021-05-15 11:43:29 +00:00
}