/* * This file is part of firk's window manager for x11 (fwm) * Copyright (C) 2022 firk * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 * as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include #include #include #include #include #include "fwm-menu.h" #include "xdg_parser.h" #include "dirscan.h" #include "xkrap.h" #include "xsupp.h" #include "tick.h" Display * display; Screen * screen; char const * font_name; static winprops wp0; static GC gc; static struct { Window win; unsigned w, h; } root; static struct { Window win; int x0, y0; unsigned w, h; int hovered, clicked; } but; static unsigned long colors[16]; static void init_colors(void) { XColor c; unsigned int n; for(n=0;n<16;n++) { c.flags = DoRed|DoGreen|DoBlue; c.red = ((n&8)?0x5500:0) + ((n&4)?0xAA00:0); c.green = ((n&8)?0x5500:0) + ((n&2)?0xAA00:0); c.blue = ((n&8)?0x5500:0) + ((n&1)?0xAA00:0); if(n==6) c.green=0x5500; if(!XAllocColor(display, DefaultColormapOfScreen(screen), &c)) failprint("color[%d] alloc failed", n); colors[n] = c.pixel; } } static void free_colors(void) { XFreeColors(display, DefaultColormapOfScreen(screen), colors, 16, 0); } extern unsigned long base_color(unsigned char c) { return colors[c&15]; } extern void fgcolor(unsigned char c) { static int last_c = -1; if(last_c==c) return; last_c = c; XSetForeground(display, gc, base_color(c)); } extern void bgcolor(unsigned char c) { static int last_c = -1; if(last_c==c) return; last_c = c; XSetBackground(display, gc, base_color(c)); } static int update_screen_size(int w, int h) { if(w<0) w = 0; if(h<0) h = 0; if(w>30000) w = 30000; if(h>30000) h = 30000; if(root.w==(unsigned)w && root.h==(unsigned)h) return 0; root.w = w; root.h = h; return 1; } static int calc_sizes(void) { int x, y; if((x=wp0.x0)<0) { x += root.w; if(x<-1) x = -1; x = x - wp0.w + 1; } if((y=wp0.y0)<0) { y += root.h; if(y<-1) y = -1; y = y - wp0.h + 1; } if(x==but.x0 && y==but.y0 && wp0.w==(int)but.w && wp0.h==(int)but.h) return 0; but.w = wp0.w; but.h = wp0.h; but.x0 = x; but.y0 = y; return 1; } extern void x11_init(char const * display_name, winprops *wp) { XSetWindowAttributes wa; if(!(display = XOpenDisplay(display_name))) failprint("XOpenDisplay(%s) failed\n", display_name); if(!(screen = DefaultScreenOfDisplay(display))) failprint("DefaultScreenOfDisplay() failed\n"); wp0 = *wp; init_colors(); update_screen_size(WidthOfScreen(screen), HeightOfScreen(screen)); if(wp0.w<=0) wp0.w = 100; if(wp0.h<=0) wp0.h = 10; if(wp0.w>10000) wp0.w = 10000; if(wp0.h>10000) wp0.h = 10000; if(wp0.bw<0) wp0.bw = 0; if(wp0.bw>100) wp0.bw = 100; calc_sizes(); root.win = DefaultRootWindow(display); wa.override_redirect = wp0.o?True:False; wa.event_mask = ExposureMask|ButtonPressMask|ButtonReleaseMask|PointerMotionMask|LeaveWindowMask; wa.background_pixel = base_color(0); wa.border_pixel = base_color(15); but.win = XCreateWindow(display, root.win, but.x0, but.y0, but.w, but.h, wp0.bw, CopyFromParent, CopyFromParent, CopyFromParent, CWOverrideRedirect|CWEventMask|CWBackPixel|CWBorderPixel, &wa); XSetStandardProperties(display, but.win, "MENU", "MENU", None, NULL, 0, NULL); XSelectInput(display, root.win, StructureNotifyMask); gc = XCreateGC(display, but.win, 0, NULL); fgcolor(15); bgcolor(0); if(font_name && *font_name) XSetFont(display, gc, XLoadFont(display, font_name)); XClearWindow(display, but.win); XMapRaised(display, but.win); XSync(display, False); } static void redraw_button(void) { if(but.w<3 || but.h<3) return; fgcolor(but.clicked?3:(but.hovered?1:0)); XFillRectangle(display, but.win, gc, 0, 0, but.w, but.h); fgcolor(15); XDrawString(display, but.win, gc, 5, 14, "< MENU", 6); } static void redraw_menu_add_zone(t_menu_list *ml, int x, int y, int w, int h) { unsigned x1, y1, x2, y2, yy; if(w<=0 || h<=0) return; if(x<0) { w+=x; x=0; if(w<=0) return; } if(y<0) { h+=y; y=0; if(h<=0) return; } x1 = x; y1 = y; x2 = x+w; y2 = y+h; if(x2>ml->gui.w) x2 = ml->gui.w; if(y2>ml->gui.h) y2 = ml->gui.h; if(x1>=x2 || y1>=y2) return; if(y1gui.h-y2MENU_VPAD+ml->e.display_count*MENU_CELL_HEIGHT) ml->gui.vpad_need_redraw = 1; if(x1gui.w-x2gui.hpad_need_redraw = 1; if(y1e.display_start+y1; if(yy>=ml->e.n) break; ml->e.list[yy].need_redraw = 1; } } static unsigned long sel_ticks; static int sel_pending; static int press_pending; static size_t press_JM, press_sel; static int press_x, press_y; static void redraw_menu(t_menu_list *ml, int force); static void menu_list_click(size_t JM, int x, int y) { t_menu_list *ml; unsigned j; size_t idx; ml = menus+JM; if(x=ml->gui.w || y=ml->e.display_count) return; idx = ml->e.display_start+j; if(ml->e.sel!=idx) return; assert(idxe.n); if(ml->e.list[idx].isdir) return; run_menu_entry(JM, idx); } static void menu_list_set_sel(size_t JM, int x, int y) { t_menu_list *ml; unsigned j; size_t oldsel, newsel; sel_ticks = get_ticks(); sel_pending = 1; ml = menus+JM; if(x=ml->gui.w) { newsel = ml->e.sel_stable; goto found; } if(y>=MENU_VPAD) { j = (y-MENU_VPAD)/MENU_CELL_HEIGHT; if(je.display_count) { newsel = ml->e.display_start+j; assert(newsele.n); goto found; } } newsel = ml->e.n; found: oldsel = ml->e.sel; if(oldsel!=newsel) { if(oldsele.n) ml->e.list[oldsel].need_redraw = 1; if(newsele.n) ml->e.list[newsel].need_redraw = 1; ml->e.sel = newsel; redraw_menu(ml, 0); } return; } static void redraw_menu(t_menu_list *ml, int force) { size_t j, start, max, sel, eh; t_menu_entry *e; char const *title; unsigned w, h, y, hmax; XPoint points[3]; if(ml->gui.w<3 || ml->gui.h<3) return; w = ml->gui.w; h = ml->gui.h; if(w<=2*MENU_HPAD || h<=2*MENU_VPAD) { fgcolor(0); XFillRectangle(display, ml->gui.win, gc, 0, 0, ml->gui.w, ml->gui.h); return; } hmax = 2*MENU_VPAD+ml->e.n*MENU_CELL_HEIGHT; if(h>hmax) { if(force || ml->gui.vpad_need_redraw) { fgcolor(0); XFillRectangle(display, ml->gui.win, gc, 0, hmax, w, h-hmax); } h = hmax; } w -= 2*MENU_HPAD; h -= 2*MENU_VPAD; ml->e.display_count = eh = h/MENU_CELL_HEIGHT; ml->e.display_sstep = (eh>10)?5:((eh>5)?eh-5:1); h = eh*MENU_CELL_HEIGHT; start = ml->e.display_start; sel = ml->e.sel; max = ml->e.n; e = ml->e.list; assert(eh<=max); if((force || ml->gui.hpad_need_redraw) && MENU_HPAD) { fgcolor(0); XFillRectangle(display, ml->gui.win, gc, 0, MENU_VPAD, MENU_HPAD, h); XFillRectangle(display, ml->gui.win, gc, ml->gui.w-MENU_HPAD, MENU_VPAD, MENU_HPAD, h); } if(start>max-eh) { ml->e.display_start = start = max-eh; force = 1; } if((force || ml->gui.vpad_need_redraw) && MENU_VPAD) { fgcolor(0); XFillRectangle(display, ml->gui.win, gc, 0, 0, ml->gui.w, MENU_VPAD); XFillRectangle(display, ml->gui.win, gc, 0, MENU_VPAD+h, ml->gui.w, ml->gui.h-MENU_VPAD-h); if(start) { fgcolor(15); points[0].x = ml->gui.w/2; points[0].y = MENU_VPAD/4; points[1].x = ml->gui.w/2-5; points[2].x = ml->gui.w/2+5; points[2].y = points[1].y = MENU_VPAD/4*3; XFillPolygon(display, ml->gui.win, gc, points, 3, Convex, CoordModeOrigin); } if(startgui.w/2; points[0].y = MENU_VPAD + h + MENU_VPAD/4*3; points[1].x = ml->gui.w/2-5; points[2].x = ml->gui.w/2+5; points[2].y = points[1].y = MENU_VPAD + h + MENU_VPAD/4; XFillPolygon(display, ml->gui.win, gc, points, 3, Convex, CoordModeOrigin); } } ml->gui.hpad_need_redraw = ml->gui.vpad_need_redraw = 0; for(j=0; je.list + (start+j); if(!force && !e->need_redraw) continue; e->need_redraw = 0; y = MENU_VPAD+j*MENU_CELL_HEIGHT; fgcolor((start+j==sel)?((press_pending && menus+press_JM==ml)?3:1):0); XFillRectangle(display, ml->gui.win, gc, MENU_HPAD, y, w, MENU_CELL_HEIGHT); fgcolor(15); if(w>MENU_CELL_HPAD*2) { if(e->isdir) { if(w>MENU_CELL_HPAD*2+MENU_CELL_DIRW) { if(ml->gui.sub_dir) { points[0].x = MENU_HPAD+w-(MENU_CELL_HPAD/2+MENU_CELL_DIRW/4); points[1].x = MENU_HPAD+w-(MENU_CELL_HPAD/2+MENU_CELL_DIRW/4*3); points[2].x = MENU_HPAD+w-(MENU_CELL_HPAD/2+MENU_CELL_DIRW/4*3); } else { points[0].x = MENU_HPAD+MENU_CELL_HPAD/2+MENU_CELL_DIRW/4; points[1].x = MENU_HPAD+MENU_CELL_HPAD/2+MENU_CELL_DIRW/4*3; points[2].x = MENU_HPAD+MENU_CELL_HPAD/2+MENU_CELL_DIRW/4*3; } points[0].y = y+MENU_CELL_HEIGHT/2; points[1].y = y+MENU_CELL_HEIGHT/4; points[2].y = y+MENU_CELL_HEIGHT-MENU_CELL_HEIGHT/4; XFillPolygon(display, ml->gui.win, gc, points, 3, Convex, CoordModeOrigin); draw_string_limwidth_utf8(display, ml->gui.win, gc, MENU_HPAD+MENU_CELL_HPAD+(ml->gui.sub_dir?0:MENU_CELL_DIRW), y+MENU_CELL_OFFS, w-2*MENU_CELL_HPAD-MENU_CELL_DIRW, e->name, strlen(e->name)); } } else { if(!(title = e->xdg.title)) title = e->name; draw_string_limwidth_utf8(display, ml->gui.win, gc, MENU_HPAD+MENU_CELL_HPAD, y+MENU_CELL_OFFS, w-2*MENU_CELL_HPAD, title, strlen(title)); } } } } static unsigned int int_to_usize(int v) { if(v<=0) return 0; if(v>=10000) return 10000; return (unsigned int)v; } extern void x11_redraw(void) { size_t j; redraw_button(); for(j=0; j=1 && NM<=MAX_MENU_NESTING); ml = menus+(NM-1); maxw = 0; for(j=0,me=ml->e.list; je.n; j++,me++) { st = me->xdg.title?me->xdg.title:me->name; w = text_width_utf8(gc, st, strlen(st)); if(me->isdir) w += MENU_CELL_DIRW; if(w>0 && (unsigned)w>maxw) maxw = w; } if(maxw>MENU_WIDTH_MAX-MENU_HPAD*2-MENU_CELL_HPAD*2) mw = MENU_WIDTH_MAX; else mw = maxw+MENU_HPAD*2+MENU_CELL_HPAD*2; if(mw(unsigned)wp0.bw*2) maxh-=wp0.bw*2; else maxh = 0; if(maxhmaxh) mh = maxh; mh -= (mh-MENU_VPAD*2)%MENU_CELL_HEIGHT; ml->gui.w = mw; ml->gui.h = mh; if(NM>=2) { x0 = menus[NM-2].gui.x0 - mw + MENU_HPAD; jj = menus[NM-2].e.sel; if(jj>menus[NM-2].e.display_start) jj -= menus[NM-2].e.display_start; else jj = 0; y0 = menus[NM-2].gui.y0 + jj*MENU_CELL_HEIGHT; if(!ml->e.n) y0 += MENU_VPAD; } else { x0 = but.x0 - mw + MENU_HPAD; y0 = but.y0; } if(x0<0) x0 = 0; if(y0<0) y0 = 0; if(mh>=root.h) y0 = 0; else if(root.h-mh<(unsigned)y0) y0 = root.h-mh; ml->gui.x0 = x0; ml->gui.y0 = y0; wa.override_redirect = True; wa.event_mask = ExposureMask|ButtonPressMask|ButtonReleaseMask|PointerMotionMask|LeaveWindowMask; wa.background_pixel = base_color(0); wa.border_pixel = base_color(15); ml->gui.win = XCreateWindow(display, root.win, x0, y0, mw, mh, wp0.bw, CopyFromParent, CopyFromParent, CopyFromParent, CWOverrideRedirect|CWEventMask|CWBackPixel|CWBorderPixel, &wa); XSetStandardProperties(display, ml->gui.win, "MENU", "MENU", None, NULL, 0, NULL); XClearWindow(display, ml->gui.win); XMapRaised(display, ml->gui.win); redraw_menu(ml, 1); XSync(display, False); } static void x11_menu_list_close(void) { t_menu_list *ml; assert(NM>=1 && NM<=MAX_MENU_NESTING); ml = menus+(NM-1); XDestroyWindow(display, ml->gui.win); menu_list_close(); } static void x11_menu_list_fix(void) { size_t j, jj; sel_pending = 0; for(j=0; jj+1) x11_menu_list_close(); jj = menus[j].e.sel_stable = menus[j].e.sel; if(jj SEL_TIMEOUT_MS) { x11_menu_list_fix(); continue; } if(!XPending(display)) { usleep(10000); continue; } } XNextEvent(display, &ev); if(x11_handle_event(&ev)) continue; } } static void reset_press_pending(void) { t_menu_list *ml; if(!press_pending) return; press_pending = 0; if(press_JM>=NM) return; ml = menus+press_JM; if(ml->e.sel!=press_sel || press_sel>ml->e.n) return; ml->e.list[press_sel].need_redraw = 1; redraw_menu(ml, 0); } extern int x11_handle_event(XEvent *ev) { size_t j; int ym; t_menu_list *ml; switch(ev->type) { case ConfigureNotify: if(ev->xconfigure.window==root.win) { if(update_screen_size(ev->xconfigure.width, ev->xconfigure.height) && calc_sizes()) { while(NM) x11_menu_list_close(); XMoveWindow(display, but.win, but.x0, but.y0); redraw_button(); } return 1; } if(ev->xconfigure.window==but.win) { but.w = int_to_usize(ev->xconfigure.width); but.h = int_to_usize(ev->xconfigure.height); redraw_button(); return 1; } for(j=0,ml=menus; jxconfigure.window==ml->gui.win) { ml->gui.w = int_to_usize(ev->xconfigure.width); ml->gui.h = int_to_usize(ev->xconfigure.height); redraw_menu(ml, 1); return 1; } break; case Expose: if(ev->xexpose.window==but.win) { if(ev->xexpose.count==0) redraw_button(); return 1; } for(j=0,ml=menus; jxexpose.window==ml->gui.win) { redraw_menu_add_zone(menus+j, ev->xexpose.x, ev->xexpose.y, ev->xexpose.width, ev->xexpose.height); if(ev->xexpose.count==0) redraw_menu(menus+j, 0); return 1; } break; case ButtonPress: if(ev->xbutton.window==but.win) { if(ev->xbutton.button==Button1 && but.hovered) { but.clicked = 1; redraw_button(); } return 1; } for(j=0,ml=menus; jxbutton.window==ml->gui.win) { if(ev->xbutton.button==Button1) { menu_list_set_sel(j, ev->xbutton.x, ev->xbutton.y); if(sel_pending) x11_menu_list_fix(); press_pending = 1; press_JM = j; press_sel = ml->e.sel; press_x = ev->xbutton.x; press_y = ev->xbutton.y; if(ml->e.sele.n) { ml->e.list[ml->e.sel].need_redraw = 1; redraw_menu(ml, 0); } return 1; } if(ev->xbutton.button==Button4) { reset_press_pending(); if(ml->e.display_start > ml->e.display_sstep) { ml->e.display_start-=ml->e.display_sstep; redraw_menu(ml, 1); } else if(ml->e.display_start) { ml->e.display_start = 0; redraw_menu(ml, 1); } menu_list_set_sel(j, ev->xbutton.x, ev->xbutton.y); return 1; } if(ev->xbutton.button==Button5) { size_t sstep, start, count, total; reset_press_pending(); sstep = ml->e.display_sstep; count = ml->e.display_count; start = ml->e.display_start; total = ml->e.n; if(total<=count) start = 0; else if(start>total-count) start = total-count; else if(start+sstep>=total-count) start = total-count; else start += sstep; /* else if(start>sstep) start -= sstep; else start = 0;*/ // printf("total %u count %u start %u sstep %u\n", total, count, start, sstep); if(start!=ml->e.display_start) { ml->e.display_start = start; redraw_menu(ml, 1); } menu_list_set_sel(j, ev->xbutton.x, ev->xbutton.y); return 1; } } break; case ButtonRelease: if(ev->xbutton.window==but.win) { if(ev->xbutton.button==Button1 && but.clicked) { but.clicked = 0; if(NM) { while(NM) x11_menu_list_close(); } else x11_menu_list_open(NULL); redraw_button(); } return 1; } for(j=0,ml=menus; jxbutton.window==ml->gui.win) { if(ev->xbutton.button==Button1) { if(!press_pending) return 1; if(press_JM==j || press_x==ev->xbutton.x || press_y==ev->xbutton.y) menu_list_click(j, ev->xbutton.x, ev->xbutton.y); reset_press_pending(); return 1; } } reset_press_pending(); break; case MotionNotify: reset_press_pending(); if(ev->xmotion.window==but.win) { if(!but.hovered) { but.hovered = 1; redraw_button(); } return 1; } for(j=0,ml=menus; jxmotion.window==ml->gui.win) { menu_list_set_sel(j, ev->xmotion.x, ev->xmotion.y); return 1; } break; case LeaveNotify: reset_press_pending(); if(ev->xcrossing.window==but.win) { if(but.hovered) { but.hovered = 0; redraw_button(); } return 1; } for(j=0,ml=menus; jxcrossing.window==ml->gui.win) { menu_list_set_sel(j, -1, -1); return 1; } break; } return 0; }