/* * imgmover.c * * Simple C application using raylib to move images between directories. * * History: * 20250119 Creation. Menu bar. * 20250123 Load font. * 20250213 Support sticky drop-down menus. * 20250216 Modularize menu handling. * 20250222 Able to list files. * * Author: Dario Rodriguez dario@darionomono.com * (c) Dario Rodriguez 2025 * This program is licensed under the terms of GNU GPL v2.1+ */ #include #include #include #include #include #include #include #include "raylib.h" #include "roboto_regular.c" #define DEFAULTWIDTH 1280 #define DEFAULTHEIGHT 768 #define LEFTSIZE 720 #define RIGHTELEMHEIGHT 280 #define SEP "/" #define BLOCKLISTINGBUF 2048 #define BLOCKLISTINGELEMS 1024 #ifndef FILLXY #define FILLXY(xywh,valx,valy) (xywh).x=(valx),(xywh).y=(valy) #endif #ifndef FILLWH #define FILLWH(xywh,valw,valh) (xywh).w=(valw),(xywh).h=(valh) #endif #ifndef FILLXYWH #define FILLXYWH(xywh,valx,valy,valw,valh) (xywh).x=(valx),(xywh).y=(valy),(xywh).w=(valw),(xywh).h=(valh) #endif #ifndef UNROLLXYWH #define UNROLLXYWH(xywh) (xywh).x,(xywh).y,(xywh).w,(xywh).h #endif typedef struct xywh_t { int x; int y; int w; int h; } xywh_t; typedef struct font_t { Font font; int height; } font_t; typedef struct menudata_t { char *title; xywh_t xywh; int sizeoptions; char **options; xywh_t optionsxywh; int flag_open; int flag_stickyopen; int currentoption; } menudata_t; typedef struct menubar_t { int sizemenudata; menudata_t **menudata; } menubar_t; typedef struct listing_t { int sizeelems; int usedelems; char **elems; /* first byte in the elem is the type, next is the name, i.e. a directory is "dhome" and a file is "f.bashrc" */ int sizebuf; int usedbuf; char *buf; } listing_t; typedef struct dirdata_t { int height; char *dirname; listing_t listing; } dirdata_t; typedef struct im_t { int windowinit; int w; int h; int sizedirdata; dirdata_t **dirdata; int currentdirdata; int leftscrollpos; int rightscrollpos; menubar_t *menubar; font_t *font; } im_t; im_t *im_init(char *menus); void im_free(im_t *im); font_t *im_font_init(void); void im_font_free(font_t *font); menubar_t *im_menubar_init(char *menus); void im_menubar_free(menubar_t *menubar); int im_menubar_mouse(menubar_t *menubar, Vector2 mousepos, int lmbpressed, int lmbreleased, int lmbdown, int *click_avail, char **sel_menu, char **sel_submenu); int im_menubar_draw(menubar_t *menubar, font_t *font, int windowwidth, int windowheight); int listing_get(listing_t *listing, char *path, int flag_sort); void listing_freedata(listing_t *listing); int imutil_menu_count(char *menus); char *imutil_menu_get(char *menus, int targetn, int *len); int imutil_submenu_count(char *menus); char *imutil_submenu_get(char *menus, int targetn, int *len); char *imutil_strduplen(char *str, int len); int is_imutil_insidexywh(Vector2 pos, xywh_t *xywh); int menudata_pos2option(menudata_t *menudata, Vector2 pos); int *getcodepoints(int *sizecodepoints); int main(int argc, char *argv[]) { im_t *im; Vector2 mousepos; int flag_ignorelmb; int lmbpressed,lmbreleased,lmbdown; int click_avail; char *sel_menu,*sel_submenu; if((im=im_init("Fichero\nAjustes\nSalir\n\nEditar\nNuevo directorio\n\nAyuda\nInformación sobre el programa\n\n"))==NULL) { return(1); } flag_ignorelmb=0; while(!WindowShouldClose()) { mousepos=GetMousePosition(); lmbpressed=IsMouseButtonPressed(0); lmbreleased=IsMouseButtonReleased(0); lmbdown=IsMouseButtonDown(0); click_avail=1; /* process clicks on menus */ if(click_avail) { sel_menu=sel_submenu=NULL; im_menubar_mouse(im->menubar, mousepos, lmbpressed, lmbreleased, lmbdown, &click_avail, &sel_menu, &sel_submenu); if(sel_menu!=NULL && sel_submenu!=NULL) { #if 1 fprintf(stderr,"SELECTED: \"%s\"->\"%s\"\n",sel_menu,sel_submenu); #endif } } /* draw screen contents */ BeginDrawing(); ClearBackground(RAYWHITE); im_menubar_draw(im->menubar,im->font,im->w,im->h); #if 1 { int i,j; Vector2 v2; listing_t listing={0}; if(listing_get(&listing,".." SEP ".",1)==0) { i=2; v2.x=(float) (im->font->height/2); v2.y=(float) ((im->font->height+im->font->height/2)*i+(im->font->height/4)); DrawTextEx(im->font->font ,SEP ,v2 ,im->font->height ,0 ,((Color){ 45, 45, 45, 255 }) ); for(j=0;jfont->height/2); v2.y=(float) ((im->font->height+im->font->height/2)*i+(im->font->height/4)); DrawTextEx(im->font->font ,listing.elems[j] ,v2 ,im->font->height ,0 ,c ); } } } #endif EndDrawing(); } CloseWindow(); im_free(im),im=NULL; return(0); } im_t * im_init(char *menus) { im_t *im; if(menus==NULL) return(NULL); /* sanity check failed */ if((im=calloc(1,sizeof(im_t)))==NULL || (im->menubar=im_menubar_init(menus))==NULL ) { im_free(im),im=NULL; return(NULL); /* insuf. mem. */ } /* init window */ SetTraceLogLevel(LOG_ERROR); InitWindow((im->w=DEFAULTWIDTH),(im->h=DEFAULTHEIGHT),"Image Mover"); im->windowinit=1; SetTargetFPS(30); /* init font */ if((im->font=im_font_init())==NULL) { im_free(im),im=NULL; return(NULL); /* insuf. mem. */ } return(im); } void im_free(im_t *im) { int i; if(im==NULL) return; if(im->menubar!=NULL) im_menubar_free(im->menubar),im->menubar=NULL; if(im->dirdata!=NULL) { dirdata_t *dirdata; for(i=0;isizedirdata;i++) { if((dirdata=im->dirdata[i])==NULL) continue; if(dirdata->dirname!=NULL) free(dirdata->dirname),dirdata->dirname=NULL; listing_freedata(&(dirdata->listing)); } free(im->dirdata),im->dirdata=NULL,im->sizedirdata=0; } if(im->font!=NULL) im_font_free(im->font),im->font=NULL; #if 0 /* not working as intended */ if(im->windowinit) CloseWindow(),im->windowinit=0; #endif free(im),im=NULL; return; } font_t * im_font_init(void) { font_t *font; int sizecodepoints; int *codepoints; if((font=calloc(1,sizeof(font_t)))==NULL) return(NULL); /* insuf. mem. */ font->height=18; codepoints=getcodepoints(&sizecodepoints); font->font=LoadFontFromMemory(".ttf",(const unsigned char *)roboto_regular,sizeof(roboto_regular)-1,font->height,codepoints,sizecodepoints); return(font); } void im_font_free(font_t *font) { if(font==NULL) return; #if 0 /* not working as intended */ UnloadFont(font->font); #endif free(font),font=NULL; return; } menubar_t * im_menubar_init(char *menus) { int i,j; char *str,*substr; int len,sublen; menubar_t *menubar; menudata_t *menudata; if(menus==NULL) return(NULL); /* sanity check failed */ if((menubar=calloc(1,sizeof(menubar_t)))==NULL || (menubar->sizemenudata=imutil_menu_count(menus))<=0 || (menubar->menudata=calloc(menubar->sizemenudata,sizeof(menudata_t)))==NULL ) { im_menubar_free(menubar),menubar=NULL; return(NULL); /* insuf. mem. */ } /* init menus */ for(i=0;isizemenudata;i++) { if((menudata=menubar->menudata[i]=calloc(1,sizeof(menudata_t)))==NULL || (str=imutil_menu_get(menus,i,&len))==NULL || (menudata->title=imutil_strduplen(str,len))==NULL || (menudata->sizeoptions=imutil_submenu_count(str))<=0 || (menudata->options=calloc(menudata->sizeoptions,sizeof(char *)))==NULL ) { im_menubar_free(menubar),menubar=NULL; return(NULL); /* insuf. mem. */ } for(j=0;jsizeoptions;j++) { if((substr=imutil_submenu_get(str,j,&sublen))==NULL || (menudata->options[j]=imutil_strduplen(substr,sublen))==NULL ) { im_menubar_free(menubar),menubar=NULL; return(NULL); /* insuf. mem. */ } } } #if 0 /* test imutil_menu_xxx */ int n,m,l,ml,len,mlen; char *ptr,*mptr; for(n=0,l=imutil_menu_count(menus);n"); for(m=0,ml=imutil_submenu_count(ptr);msizemenudata;i++) { fprintf(stderr,"menu[%i]:\"%s\"->",i,menubar->menudata[i]->title); for(j=0;jmenudata[i]->sizeoptions;j++) fprintf(stderr,"|\"%s\"",menubar->menudata[i]->options[j]); fprintf(stderr,"\n"); } #endif return(menubar); } void im_menubar_free(menubar_t *menubar) { int i,j; menudata_t *menudata; if(menubar==NULL) return; if(menubar->menudata!=NULL) { for(i=0;isizemenudata;i++) { if((menudata=menubar->menudata[i])==NULL) continue; if(menudata->title!=NULL) free(menudata->title),menudata->title=NULL; if(menudata->options!=NULL) { for(j=0;jsizeoptions;j++) { if(menudata->options[j]!=NULL) free(menudata->options[j]),menudata->options[j]=NULL; } free(menudata->options),menudata->options=NULL,menudata->sizeoptions=0; } free(menubar->menudata[i]),menubar->menudata[i]=NULL,menudata=NULL; } free(menubar->menudata),menubar->menudata=NULL,menubar->sizemenudata=0; } free(menubar),menubar=NULL; return; } int im_menubar_mouse(menubar_t *menubar, Vector2 mousepos, int lmbpressed, int lmbreleased, int lmbdown, int *click_avail, char **sel_menu, char **sel_submenu) { int flag_outsideall; int i,j; if(menubar==NULL || click_avail==NULL || sel_menu==NULL || sel_submenu==NULL) return(-1); *click_avail=1; *sel_menu=NULL; *sel_submenu=NULL; flag_outsideall=1; for(i=0;isizemenudata;i++) { int insidetitle,currentoption; insidetitle=is_imutil_insidexywh(mousepos,&(menubar->menudata[i]->xywh)); currentoption=menudata_pos2option(menubar->menudata[i],mousepos); flag_outsideall=(currentoption!=-1 || insidetitle)?0:flag_outsideall; if(lmbreleased && insidetitle) { for(j=0;jsizemenudata;j++) { menubar->menudata[j]->flag_stickyopen=(j==i)?1:0; menubar->menudata[j]->flag_open=0; menubar->menudata[j]->currentoption=-1; } } else if((lmbpressed || lmbdown) && insidetitle) { for(j=0;jsizemenudata;j++) { menubar->menudata[j]->flag_open=(j==i)?1:0; menubar->menudata[j]->flag_stickyopen=0; menubar->menudata[j]->currentoption=-1; } } else if((lmbdown || menubar->menudata[i]->flag_stickyopen) && currentoption!=-1) { for(j=0;jsizemenudata;j++) { if(lmbreleased==0 || j!=i || menubar->menudata[i]->flag_stickyopen==0) { menubar->menudata[j]->flag_open=(j==i)?menubar->menudata[i]->flag_open:0; menubar->menudata[j]->flag_stickyopen=(j==i)?menubar->menudata[i]->flag_stickyopen:0; menubar->menudata[j]->currentoption=(j==i)?currentoption:-1; } else { menubar->menudata[j]->flag_open=0; menubar->menudata[j]->flag_stickyopen=0; menubar->menudata[j]->currentoption=-1; /* has selected this submenu */ *click_avail=0; *sel_menu=menubar->menudata[j]->title; *sel_submenu=menubar->menudata[j]->options[currentoption]; } } } else if(menubar->menudata[i]->flag_stickyopen && currentoption==-1) { if(lmbreleased) { menubar->menudata[i]->flag_open=0; menubar->menudata[i]->flag_stickyopen=0; } menubar->menudata[i]->currentoption=-1; } else if(lmbreleased && currentoption!=-1) { for(j=0;jsizemenudata;j++) { menubar->menudata[j]->flag_open=0; menubar->menudata[j]->flag_stickyopen=0; menubar->menudata[j]->currentoption=-1; } /* has selected this submenu */ *click_avail=0; *sel_menu=menubar->menudata[i]->title; *sel_submenu=menubar->menudata[i]->options[currentoption]; } else if(lmbdown==0) { menubar->menudata[i]->flag_open=0; menubar->menudata[i]->flag_stickyopen=0; menubar->menudata[i]->currentoption=-1; } } if(flag_outsideall) { for(j=0;jsizemenudata;j++) { menubar->menudata[j]->currentoption=-1; } } /* update click_avail */ for(j=0;jsizemenudata;j++) { if(menubar->menudata[j]->flag_open || menubar->menudata[j]->flag_stickyopen) { *click_avail=0; break; } } return(0); } int im_menubar_draw(menubar_t *menubar, font_t *font, int windowwidth, int windowheight) { int i,j,k,x; menudata_t *menudata; if(menubar==NULL || font==NULL) return(-1); /* sanity check failed */ DrawRectangle(0,0,windowwidth, font->height+font->height/2, (Color){ 235, 235, 235, 235 } ); for(i=0,x=0;isizemenudata;i++) { Vector2 v2; menudata=menubar->menudata[i]; v2=MeasureTextEx(font->font,menudata->title,font->height,0); FILLXYWH(menudata->xywh,x,0,((int)v2.x)+font->height,font->height+font->height/2); v2.x=(float) (menudata->xywh.x+font->height/2); v2.y=(float) (menudata->xywh.y+font->height/4); DrawTextEx(font->font ,menudata->title ,v2 ,font->height ,0 ,(Color){ 45, 45, 45, 255 } ); if(menudata->flag_open || menudata->flag_stickyopen) { int underline_height=3; int maxw; DrawRectangle(menudata->xywh.x,menudata->xywh.y+menudata->xywh.h-underline_height,menudata->xywh.w,underline_height, (Color){ 53,132,228,255 } ); for(j=0,maxw=0;jsizeoptions;j++) { v2=MeasureTextEx(font->font,menudata->options[j],font->height,0); maxw=(((int)(v2.x))>maxw)?((int)(v2.x)):maxw; } maxw=(maxw<(menudata->xywh.w+font->height))?(menudata->xywh.w+font->height):maxw; maxw+=font->height; FILLXYWH(menudata->optionsxywh,menudata->xywh.x+1,menudata->xywh.y+menudata->xywh.h+2,maxw,(font->height+font->height/2)*menudata->sizeoptions); DrawLine(menudata->optionsxywh.x-1,menudata->optionsxywh.y-2,menudata->optionsxywh.x+menudata->optionsxywh.w+2,menudata->optionsxywh.y-2,(Color){ 255,255,255,255 } ); DrawLine(menudata->optionsxywh.x-1,menudata->optionsxywh.y,menudata->optionsxywh.x-1,menudata->optionsxywh.y+menudata->optionsxywh.h+1,(Color){ 255,255,255,255 } ); DrawLine(menudata->optionsxywh.x+menudata->optionsxywh.w+2,menudata->optionsxywh.y,menudata->optionsxywh.x+menudata->optionsxywh.w+2,menudata->optionsxywh.y+menudata->optionsxywh.h+1,(Color){ 192,192,192,255 } ); DrawLine(menudata->optionsxywh.x-1,menudata->optionsxywh.y+menudata->optionsxywh.h+1,menudata->optionsxywh.x+menudata->optionsxywh.w+2,menudata->optionsxywh.y+menudata->optionsxywh.h+1,(Color){ 192,192,192,255 } ); DrawRectangle(menudata->optionsxywh.x,menudata->optionsxywh.y,menudata->optionsxywh.w,menudata->optionsxywh.h,(Color){ 235, 235, 235, 235 }); for(k=0;ksizeoptions;k++) { Color c; c=(k==menudata->currentoption)?((Color){ 255,255,255,255 }):((Color){ 45, 45, 45, 255 }); if(k==menudata->currentoption) DrawRectangle(menudata->optionsxywh.x+1,menudata->optionsxywh.y+(font->height+(font->height/2))*k,menudata->optionsxywh.w-2,font->height+font->height/2,(Color){ 53,132,228,255 }); v2.x=(float) (menudata->optionsxywh.x+font->height/2); v2.y=(float) (menudata->optionsxywh.y+(font->height/4)+(font->height+(font->height/2))*k); DrawTextEx(font->font ,menudata->options[k] ,v2 ,font->height ,0 ,c ); } } else { FILLXYWH(menudata->optionsxywh,0,0,0,0); } x=menudata->xywh.x+menudata->xywh.w; } return(0); } int imutil_menu_count(char *menus) { int n; char *ptr,*next; if(menus==NULL) return(0); /* sanity check error */ for(ptr=menus,n=0;(next=strchr(ptr,'\n'))!=NULL;ptr=next+1) { if(next[1]=='\n') { next++; n++; } } return(n); } char * imutil_menu_get(char *menus, int targetn, int *len) { int n; char *ptr,*next,*end; int is_title; if(len!=NULL) *len=0; if(menus==NULL || targetn<0) return(NULL); /* sanity check error */ end=menus+strlen(menus); for(ptr=menus,is_title=1,n=0;(next=strchr(ptr,'\n'))!=NULL;ptr=next+1) { if(targetn==n && is_title==1) { if(len!=NULL) *len=next-ptr; return(ptr); /* found, return start of menu string */ } is_title=0; if(next[1]=='\n') { n++; next++; is_title=1; } } return(end); /* not found, return end of menus */ } int imutil_submenu_count(char *menus) { int subn; char *ptr,*next; if(menus==NULL) return(0); /* sanity check error */ next=strchr(menus,'\n'); if(next==NULL) return(0); /* no title */ for(subn=0,ptr=next+1;(next=strchr(ptr,'\n'))!=NULL;ptr=next+1) { subn++; if(next[1]=='\n') break; } return(subn); } char * imutil_submenu_get(char *menus, int targetsubn, int *len) { char *ptr,*next,*end; int subn; if(len!=NULL) *len=0; if(menus==NULL || targetsubn<0) return(NULL); /* sanity check error */ end=menus+strlen(menus); next=strchr(menus,'\n'); if(next==NULL) return(end); /* no title */ for(ptr=next+1,subn=0;(next=strchr(ptr,'\n'))!=NULL;ptr=next+1,subn++) { if(targetsubn==subn) { if(len!=NULL) *len=next-ptr; return(ptr); } if(next[1]=='\n') break; /* "\n\n" marks the end of submenus */ } return(end); } char * imutil_strduplen(char *str, int len) { char *res; if(len<0 || (str==NULL && len!=0)) return(NULL); if((res=malloc(len+1))==NULL) return(NULL); memcpy(res,str,len); res[len]='\0'; return(res); } int is_imutil_insidexywh(Vector2 pos, xywh_t *xywh) { if(xywh==NULL) return(0); /* sanity check error */ if(pos.x>=(float)(xywh->x) && pos.x<=(float)(xywh->x+xywh->w) && pos.y>=(float)(xywh->y) && pos.y<=(float)(xywh->y+xywh->h) ) { return(1); } return(0); } int menudata_pos2option(menudata_t *menudata, Vector2 pos) { int n,h; if(menudata==NULL || (menudata->flag_open==0 && menudata->flag_stickyopen==0) || is_imutil_insidexywh(pos, &(menudata->optionsxywh))==0 ) { return(-1); } h=(menudata->sizeoptions==0)?0:menudata->optionsxywh.h/menudata->sizeoptions; n=(((int)pos.y)-menudata->optionsxywh.y)/h; return(n); } int * getcodepoints(int *sizecodepoints) { static int codepoints[]={ /* Basic Latin */ 32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127, /* Latin-1 Supplement */ 160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255, }; if(sizecodepoints!=NULL) *sizecodepoints=(int) (sizeof(codepoints)/sizeof(codepoints[0])); return(codepoints); } static int strptrcmp(void *a,void *b) { return(strcmp(*((char **)a),*((char **)b))); } int listing_get(listing_t *listing, char *path, int flag_sort) { int l; DIR *d; struct dirent *de; unsigned char dtype; if(listing==NULL) return(-1); /* sanity check failed */ listing->usedelems=listing->usedbuf=0; if(path==NULL) return(-1); /* nothing to fill */ if((d=opendir(".." SEP "."))==NULL) return(-1); /* dir not found */ while((de=readdir(d))!=NULL) { l=strlen(de->d_name); dtype='u'; #ifdef _DIRENT_HAVE_D_TYPE dtype=(de->d_type==DT_BLK)?'b' :(de->d_type==DT_CHR)?'c' :(de->d_type==DT_DIR)?'d' :(de->d_type==DT_FIFO)?'p' :(de->d_type==DT_LNK)?'l' :(de->d_type==DT_REG)?'f' :(de->d_type==DT_SOCK)?'s' :'u'; #endif if(dtype=='u') { char stpath[1024]; struct stat st; mode_t m; snprintf(stpath,sizeof(stpath),"%s%s%s",path,SEP,de->d_name); stpath[sizeof(stpath)-1]='\0'; if(stat(stpath,&st)==0 && (m=(st.st_mode&S_IFMT))!=0) { dtype=(m==S_IFBLK)?'b' :(m==S_IFCHR)?'c' :(m==S_IFDIR)?'d' :(m==S_IFIFO)?'p' #ifdef S_IFLNK :(m==S_IFLNK)?'l' #endif :(m==S_IFREG)?'f' #ifdef S_IFSOCK :(m==S_IFSOCK)?'s' #endif :'u'; } } /* check for space for this elem (and get space if necessary) */ if(listing->usedelems==listing->sizeelems) { char **newelems; if((newelems=realloc(listing->elems,sizeof(char *)*(listing->sizeelems+BLOCKLISTINGELEMS)))==NULL) { closedir(d),d=NULL; return(-1); /* insuf. mem. */ } listing->elems=newelems; listing->sizeelems+=BLOCKLISTINGELEMS; } /* check for space for this elem data (and get space if necessary) */ if((listing->sizebuf+l+2)>listing->sizebuf) { int i; char *newbuf; int newsize; int off; newsize=listing->sizebuf+l+2+BLOCKLISTINGBUF-1; newsize/=BLOCKLISTINGBUF; newsize*=BLOCKLISTINGBUF; if((newbuf=realloc(listing->buf,newsize))==NULL) { closedir(d),d=NULL; return(-1); /* insuf. mem. */ } /* the elem data buffer has a new pointer, fix previous entries */ for(i=0;iusedelems;i++) { off=(listing->elems[i])-listing->buf; listing->elems[i]=newbuf+off; } listing->buf=newbuf; listing->sizebuf+=BLOCKLISTINGELEMS; } /* store the data */ listing->elems[listing->usedelems++]=listing->buf+listing->usedbuf; listing->buf[listing->usedbuf++]=dtype; memcpy(listing->buf+listing->usedbuf,de->d_name,l+1); listing->usedbuf+=l+1; } closedir(d),d=NULL; if(flag_sort) qsort(listing->elems,listing->usedelems,sizeof(char *),(int (*)(const void *, const void *)) strptrcmp); return(0); } void listing_freedata(listing_t *listing) { if(listing==NULL) return; /* nothing to do */ if(listing->elems!=NULL) free(listing->elems),listing->elems=NULL,listing->sizeelems=listing->usedelems=0; if(listing->buf!=NULL) free(listing->buf),listing->buf=NULL,listing->sizebuf=listing->usedbuf=0; return; }