From c217c05fb0504045a7aefd1a86016a8d19ca19d9 Mon Sep 17 00:00:00 2001 From: ketmar Date: Sat, 20 Nov 2021 04:35:35 +0000 Subject: [PATCH] iv.egra: low-level gfx and high-level GUI library FossilOrigin-Name: b5609929c58fb7dd3a0dac2362a52f851a50346290738b44fed8ba82bf8187fe --- egra/gfx/backgl.d | 248 ++++++++++ egra/gfx/backx11.d | 214 ++++++++ egra/gfx/base.d | 1313 +++++++++++++++++++++++++++++++++++++++++++++++++ egra/gfx/config.d | 27 + egra/gfx/package.d | 28 ++ egra/gfx/text.d | 619 +++++++++++++++++++++++ egra/gui/dialogs.d | 215 ++++++++ egra/gui/editor.d | 1085 ++++++++++++++++++++++++++++++++++++++++ egra/gui/package.d | 25 + egra/gui/style.d | 848 ++++++++++++++++++++++++++++++++ egra/gui/subwindows.d | 1065 +++++++++++++++++++++++++++++++++++++++ egra/gui/widgets.d | 1155 +++++++++++++++++++++++++++++++++++++++++++ egra/package.d | 22 + egra/test.d | 638 ++++++++++++++++++++++++ 14 files changed, 7502 insertions(+) create mode 100644 egra/gfx/backgl.d create mode 100644 egra/gfx/backx11.d create mode 100644 egra/gfx/base.d create mode 100644 egra/gfx/config.d create mode 100644 egra/gfx/package.d create mode 100644 egra/gfx/text.d create mode 100644 egra/gui/dialogs.d create mode 100644 egra/gui/editor.d create mode 100644 egra/gui/package.d create mode 100644 egra/gui/style.d create mode 100644 egra/gui/subwindows.d create mode 100644 egra/gui/widgets.d create mode 100644 egra/package.d create mode 100644 egra/test.d diff --git a/egra/gfx/backgl.d b/egra/gfx/backgl.d new file mode 100644 index 0000000..d004877 --- /dev/null +++ b/egra/gfx/backgl.d @@ -0,0 +1,248 @@ +/* + * Simple Framebuffer Gfx/GUI lib + * + * coded by Ketmar // Invisible Vector + * Understanding is not required. Only obedience. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License ONLY. + * + * 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, see . + */ +module iv.egra.gfx.backgl /*is aliced*/; +private: + +import arsd.simpledisplay; + +import iv.alice; +import iv.cmdcon; +//import iv.glbinds : glTexParameterfv; // rdmd hack + +import iv.egra.gfx.config; +import iv.egra.gfx.base; + + +// ////////////////////////////////////////////////////////////////////////// // +enum GLTexType = GL_BGRA; // GL_RGBA; + + +// ////////////////////////////////////////////////////////////////////////// // +public __gshared uint vglTexId; // OpenGL texture id +public __gshared uint vArrowTextureId = 0; + + +// ////////////////////////////////////////////////////////////////////////// // +shared static this () { + import core.stdc.stdlib : malloc; + vglTexBuf = cast(uint*)malloc((VBufWidth*VBufHeight+4)*4); + if (vglTexBuf is null) { import core.exception : onOutOfMemoryErrorNoGC; onOutOfMemoryErrorNoGC(); } + vglTexBuf[0..VBufWidth*VBufHeight+4] = 0; +} + + +// ////////////////////////////////////////////////////////////////////////// // +public void vglCreateArrowTexture () { + //import iv.glbinds; + + enum wrapOpt = GL_REPEAT; + enum filterOpt = GL_NEAREST; //GL_LINEAR; + enum ttype = GL_UNSIGNED_BYTE; + + if (vArrowTextureId) glDeleteTextures(1, &vArrowTextureId); + vArrowTextureId = 0; + glGenTextures(1, &vArrowTextureId); + if (vArrowTextureId == 0) assert(0, "can't create arrow texture"); + + //GLint gltextbinding; + //glGetIntegerv(GL_TEXTURE_BINDING_2D, &gltextbinding); + //scope(exit) glBindTexture(GL_TEXTURE_2D, gltextbinding); + + glBindTexture(GL_TEXTURE_2D, vArrowTextureId); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapOpt); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapOpt); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filterOpt); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filterOpt); + //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); + //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); + + //GLfloat[4] bclr = 0.0; + //glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, bclr.ptr); + + uint[16*8] pmap = 0x00_000000U; + // sprite,sprite,mask,mask + static immutable ushort[$] spx = [ + 0b11111111_10000000, 0b00000000_01111111, + 0b01000000_10000000, 0b10000000_01111111, + 0b00100000_10000000, 0b11000000_01111111, + 0b00010000_01100000, 0b11100000_00011111, + 0b00001001_10011000, 0b11110000_00000111, + 0b00000110_01100110, 0b11111001_10000001, + 0b00000000_00011001, 0b11111111_11100000, + 0b00000000_00000110, 0b11111111_11111001, + ]; + + foreach (immutable dy; 0..8) { + ushort spr = spx[dy*2+0]; + ushort msk = spx[dy*2+1]; + foreach (immutable dx; 0..16) { + if ((msk&0x8000) == 0) { + pmap[dy*16+dx] = (spr&0x8000 ? 0xff_ffffffU : 0xff_000000U); + } + msk <<= 1; + spr <<= 1; + } + } + //pmap = 0xff_ff0000U; + //pmap[0] = 0; + + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 16, 8, 0, GLTexType, GL_UNSIGNED_BYTE, pmap.ptr); +} + + +// ////////////////////////////////////////////////////////////////////////// // +public void vglBlitArrow (int px, int py) { + if (vArrowTextureId != 0) { + glMatrixMode(GL_PROJECTION); // for ortho camera + glLoadIdentity(); + // left, right, bottom, top, near, far + //glViewport(0, 0, w*vbufEffScale, h*vbufEffScale); + //glOrtho(0, w, h, 0, -1, 1); // top-to-bottom + glViewport(0, 0, VBufWidth*vbufEffScale, VBufHeight*vbufEffScale); + glOrtho(0, VBufWidth, VBufHeight, 0, -1, 1); // top-to-bottom + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); + + glEnable(GL_TEXTURE_2D); + glDisable(GL_LIGHTING); + glDisable(GL_DITHER); + glDisable(GL_DEPTH_TEST); + + glEnable(GL_BLEND); + //glBlendFunc(GL_SRC_ALPHA, GL_ONE); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + //glColor4f(1, 1, 1, 1); + glBindTexture(GL_TEXTURE_2D, vArrowTextureId); + //scope(exit) glBindTexture(GL_TEXTURE_2D, 0); + glBegin(GL_QUADS); + glTexCoord2f(0.0f, 0.0f); glVertex2i(px, py); // top-left + glTexCoord2f(1.0f, 0.0f); glVertex2i(px+16*2, py); // top-right + glTexCoord2f(1.0f, 1.0f); glVertex2i(px+16*2, py+8*2); // bottom-right + glTexCoord2f(0.0f, 1.0f); glVertex2i(px, py+8*2); // bottom-left + glEnd(); + } +} + + +// ////////////////////////////////////////////////////////////////////////// // +// resize buffer, reinitialize OpenGL texture +public void vglResizeBuffer (int wdt, int hgt, int ascale=1) { + //import iv.glbinds; + + if (wdt < 1) wdt = 1; + if (hgt < 1) hgt = 1; + + if (wdt > 8192) wdt = 8192; + if (hgt > 8192) hgt = 8192; + + bool sizeChanged = (wdt != VBufWidth || hgt != VBufHeight); + VBufWidth = wdt; + VBufHeight = hgt; + + if (vglTexBuf is null || sizeChanged) { + import core.stdc.stdlib : realloc; + vglTexBuf = cast(uint*)realloc(vglTexBuf, (VBufWidth*VBufHeight+4)*vglTexBuf[0].sizeof); + if (vglTexBuf is null) { import core.exception : onOutOfMemoryErrorNoGC; onOutOfMemoryErrorNoGC(); } + vglTexBuf[0..VBufWidth*VBufHeight+4] = 0; + } + + if (vglTexId == 0 || sizeChanged) { + enum wrapOpt = GL_REPEAT; + enum filterOpt = GL_NEAREST; //GL_LINEAR; + enum ttype = GL_UNSIGNED_BYTE; + + if (vglTexId) glDeleteTextures(1, &vglTexId); + vglTexId = 0; + glGenTextures(1, &vglTexId); + if (vglTexId == 0) assert(0, "can't create OpenGL texture"); + + //GLint gltextbinding; + //glGetIntegerv(GL_TEXTURE_BINDING_2D, &gltextbinding); + //scope(exit) glBindTexture(GL_TEXTURE_2D, gltextbinding); + + glBindTexture(GL_TEXTURE_2D, vglTexId); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapOpt); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapOpt); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filterOpt); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filterOpt); + //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); + //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); + + //GLfloat[4] bclr = 0.0; + //glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, bclr.ptr); + + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, VBufWidth, VBufHeight, 0, GLTexType, GL_UNSIGNED_BYTE, vglTexBuf); + } + + if (ascale < 1) ascale = 1; + if (ascale > 32) ascale = 32; + vbufEffScale = cast(ubyte)ascale; +} + + +// ////////////////////////////////////////////////////////////////////////// // +public void vglUpdateTexture () { + if (vglTexId != 0) { + glBindTexture(GL_TEXTURE_2D, vglTexId); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0/*x*/, 0/*y*/, VBufWidth, VBufHeight, GLTexType, GL_UNSIGNED_BYTE, vglTexBuf); + //glBindTexture(GL_TEXTURE_2D, 0); + } +} + + +// ////////////////////////////////////////////////////////////////////////// // +public void vglBlitTexture () { + if (vglTexId != 0) { + glMatrixMode(GL_PROJECTION); // for ortho camera + glLoadIdentity(); + // left, right, bottom, top, near, far + //glViewport(0, 0, w*vbufEffScale, h*vbufEffScale); + //glOrtho(0, w, h, 0, -1, 1); // top-to-bottom + glViewport(0, 0, VBufWidth*vbufEffScale, VBufHeight*vbufEffScale); + glOrtho(0, VBufWidth, VBufHeight, 0, -1, 1); // top-to-bottom + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); + + glEnable(GL_TEXTURE_2D); + glDisable(GL_LIGHTING); + glDisable(GL_DITHER); + //glDisable(GL_BLEND); + glDisable(GL_DEPTH_TEST); + //glEnable(GL_BLEND); + //glBlendFunc(GL_SRC_ALPHA, GL_ONE); + //glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glDisable(GL_BLEND); + //glDisable(GL_STENCIL_TEST); + + if (vglTexId) { + immutable w = VBufWidth; + immutable h = VBufHeight; + + glColor4f(1, 1, 1, 1); + glBindTexture(GL_TEXTURE_2D, vglTexId); + //scope(exit) glBindTexture(GL_TEXTURE_2D, 0); + glBegin(GL_QUADS); + glTexCoord2f(0.0f, 0.0f); glVertex2i(0, 0); // top-left + glTexCoord2f(1.0f, 0.0f); glVertex2i(w, 0); // top-right + glTexCoord2f(1.0f, 1.0f); glVertex2i(w, h); // bottom-right + glTexCoord2f(0.0f, 1.0f); glVertex2i(0, h); // bottom-left + glEnd(); + } + } +} diff --git a/egra/gfx/backx11.d b/egra/gfx/backx11.d new file mode 100644 index 0000000..b7f26b6 --- /dev/null +++ b/egra/gfx/backx11.d @@ -0,0 +1,214 @@ +/* + * Simple Framebuffer Gfx/GUI lib + * + * coded by Ketmar // Invisible Vector + * Understanding is not required. Only obedience. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License ONLY. + * + * 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, see . + */ +module iv.egra.gfx.backx11 /*is aliced*/; +private: + +import iv.alice; +import iv.cmdcon; +import iv.cmdcongl; + +import iv.egra.gfx.config; +import iv.egra.gfx.base; + + +// ////////////////////////////////////////////////////////////////////////// // +public __gshared uint vArrowTextureId = 0; +//public __gshared Image egx11img; + + +// ////////////////////////////////////////////////////////////////////////// // +shared static this () { + import core.stdc.stdlib : malloc; + vglTexBuf = cast(uint*)malloc((VBufWidth*VBufHeight+4)*4); + if (vglTexBuf is null) { import core.exception : onOutOfMemoryErrorNoGC; onOutOfMemoryErrorNoGC(); } + vglTexBuf[0..VBufWidth*VBufHeight] = 0; +} + + +// ////////////////////////////////////////////////////////////////////////// // +/+ +private extern(C) nothrow @trusted @nogc { + import core.stdc.config : c_long, c_ulong; + + XImage* egfx_backx11_xxsimple_create_image (XDisplay* display, Visual* visual, uint depth, int format, int offset, ubyte* data, uint width, uint height, int bitmap_pad, int bytes_per_line) { + //return XCreateImage(display, visual, depth, format, offset, data, width, height, bitmap_pad, bytes_per_line); + return null; + } + + int egfx_backx11_xxsimple_destroy_image (XImage* ximg) { + ximg.data = null; + ximg.width = ximg.height = 0; + return 0; + } + + c_ulong egfx_backx11_xxsimple_get_pixel (XImage* ximg, int x, int y) { + if (ximg.data is null) return 0; + if (x < 0 || y < 0 || x >= ximg.width || y >= ximg.height) return 0; + auto buf = cast(const(uint)*)ximg.data; + //uint v = buf[y*ximg.width+x]; + //v = (v&0xff_00ff00u)|((v>>16)&0x00_0000ffu)|((v<<16)&0x00_ff0000u); + return buf[y*ximg.width+x]; + } + + int egfx_backx11_xxsimple_put_pixel (XImage* ximg, int x, int y, c_ulong clr) { + return 0; + } + + XImage* egfx_backx11_xxsimple_sub_image (XImage* ximg, int x, int y, uint wdt, uint hgt) { + return null; + } + + int egfx_backx11_xxsimple_add_pixel (XImage* ximg, c_long clr) { + return 0; + } + + // create "simple" XImage with allocated buffer + void egfx_backx11_ximageInitSimple (ref XImage handle, int width, int height, void* data) { + handle.width = width; + handle.height = height; + handle.xoffset = 0; + handle.format = ImageFormat.ZPixmap; + handle.data = data; + handle.byte_order = 0; + handle.bitmap_unit = 0; + handle.bitmap_bit_order = 0; + handle.bitmap_pad = 0; + handle.depth = 24; + handle.bytes_per_line = 0; + handle.bits_per_pixel = 0; // THIS MATTERS! + handle.red_mask = 0; + handle.green_mask = 0; + handle.blue_mask = 0; + + handle.obdata = null; + handle.f.create_image = &egfx_backx11_xxsimple_create_image; + handle.f.destroy_image = &egfx_backx11_xxsimple_destroy_image; + handle.f.get_pixel = &egfx_backx11_xxsimple_get_pixel; + handle.f.put_pixel = &egfx_backx11_xxsimple_put_pixel; + handle.f.sub_image = &egfx_backx11_xxsimple_sub_image; + handle.f.add_pixel = &egfx_backx11_xxsimple_add_pixel; + } +} ++/ + + +// ////////////////////////////////////////////////////////////////////////// // +public void vglCreateArrowTexture () { +} + + +// ////////////////////////////////////////////////////////////////////////// // +public void vglBlitArrow (int px, int py) { +} + + +// ////////////////////////////////////////////////////////////////////////// // +// resize buffer, reinitialize OpenGL texture +public void vglResizeBuffer (int wdt, int hgt, int ascale=1) { + if (wdt < 1) wdt = 1; + if (hgt < 1) hgt = 1; + + if (wdt > 8192) wdt = 8192; + if (hgt > 8192) hgt = 8192; + + bool sizeChanged = (wdt != VBufWidth || hgt != VBufHeight); + VBufWidth = wdt; + VBufHeight = hgt; + + if (vglTexBuf is null || sizeChanged) { + import core.stdc.stdlib : realloc; + vglTexBuf = cast(uint*)realloc(vglTexBuf, (VBufWidth*VBufHeight+4)*vglTexBuf[0].sizeof); + if (vglTexBuf is null) { import core.exception : onOutOfMemoryErrorNoGC; onOutOfMemoryErrorNoGC(); } + vglTexBuf[0..VBufWidth*VBufHeight] = 0; + } + + if (ascale < 1) ascale = 1; + if (ascale > 32) ascale = 32; + vbufEffScale = cast(ubyte)ascale; + + /+ + if (egx11img is null || egx11img.width != VBufWidth || egx11img.height != VBufHeight) { + egx11img = new Image(VBufWidth, VBufHeight); + //glconBackBuffer = egx11img; + } + +/ +} + + +// ////////////////////////////////////////////////////////////////////////// // +public void vglUpdateTexture () { + /* + if (vglTexBuf is null) return; + if (egx11img is null || egx11img.width != VBufWidth || egx11img.height != VBufHeight) { + egx11img = new Image(VBufWidth, VBufHeight); + glconBackBuffer = egx11img; + } + */ + /+ + if (egx11img !is null && vglTexBuf !is null) { + int bmpw = egx11img.width; + int bmph = egx11img.height; + int copyw = (bmpw < VBufWidth ? bmpw : VBufWidth); + if (bmph > VBufHeight) bmph = VBufHeight; + const(uint)* src = cast(const(uint)*)vglTexBuf; + uint* dest = cast(uint*)egx11img.getDataPointer; + while (bmph-- > 0) { + import core.stdc.string : memcpy; + memcpy(dest, src, copyw*4); + src += VBufWidth; + dest += bmpw; + } + } + +/ +} + + +// ////////////////////////////////////////////////////////////////////////// // +/* +extern(C) nothrow @trusted @nogc { + Status XInitImage (XImage* image); +} +*/ + + +public void vglBlitTexture (SimpleWindow w) { + if (w !is null && !w.closed) { + XImage ximg; + //egfx_backx11_ximageInitSimple(ximg, VBufWidth, VBufHeight, vglTexBuf); + + ximg.width = VBufWidth; + ximg.height = VBufHeight; + ximg.xoffset = 0; + ximg.format = ImageFormat.ZPixmap; + ximg.data = vglTexBuf; + ximg.byte_order = 0; + ximg.bitmap_unit = 32; + ximg.bitmap_bit_order = 0; + ximg.bitmap_pad = 8; + ximg.depth = 24; + ximg.bytes_per_line = 0; + ximg.bits_per_pixel = 32; // THIS MATTERS! + ximg.red_mask = 0x00ff0000; + ximg.green_mask = 0x0000ff00; + ximg.blue_mask = 0x000000ff; + XInitImage(&ximg); + + XPutImage(w.impl.display, cast(Drawable)w.impl.buffer, w.impl.gc, &ximg, 0, 0, 0/*destx*/, 0/*desty*/, VBufWidth, VBufHeight); + } +} diff --git a/egra/gfx/base.d b/egra/gfx/base.d new file mode 100644 index 0000000..f6aad29 --- /dev/null +++ b/egra/gfx/base.d @@ -0,0 +1,1313 @@ +/* + * Simple Framebuffer Gfx/GUI lib + * + * coded by Ketmar // Invisible Vector + * Understanding is not required. Only obedience. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License ONLY. + * + * 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, see . + */ +module iv.egra.gfx.base /*is aliced*/; +private: + +import iv.alice; +import iv.bclamp; +import iv.cmdcon; + +import iv.egra.gfx.config; + + +// ////////////////////////////////////////////////////////////////////////// // +package __gshared int VBufWidth = 740*2; +package __gshared int VBufHeight = 520*2; +package __gshared ubyte vbufEffScale = 1; // effective (current) window scale + +package __gshared uint* vglTexBuf; // OpenGL texture buffer + + +// ////////////////////////////////////////////////////////////////////////// // +public @property int screenEffScale () nothrow @trusted @nogc { pragma(inline, true); return vbufEffScale; } +public @property void screenEffScale (int scale) nothrow @trusted @nogc { pragma(inline, true); if (scale < 1) scale = 1; if (scale > 32) scale = 32; vbufEffScale = cast(ubyte)scale; } + +public @property int screenWidth () nothrow @trusted @nogc { pragma(inline, true); return VBufWidth; } +public @property int screenHeight () nothrow @trusted @nogc { pragma(inline, true); return VBufHeight; } + +public @property int screenWidthScaled () nothrow @trusted @nogc { pragma(inline, true); return VBufWidth*vbufEffScale; } +public @property int screenHeightScaled () nothrow @trusted @nogc { pragma(inline, true); return VBufHeight*vbufEffScale; } + + +// ////////////////////////////////////////////////////////////////////////// // +public enum GxDir { + Horiz, + Vert, +} + + +// ////////////////////////////////////////////////////////////////////////// // +public struct GxPoint { +public: + int x, y; + +nothrow @safe @nogc: + this() (in auto ref GxPoint p) pure { pragma(inline, true); x = p.x; y = p.y; } + this (in int ax, in int ay) pure { pragma(inline, true); x = ax; y = ay; } + + bool inside() (in auto ref GxRect rc) pure const { pragma(inline, true); return rc.inside(this); } + bool inside() (in auto ref GxSize sz) pure const { pragma(inline, true); return sz.inside(this); } + + bool opEquals() (in auto ref GxPoint p) pure const { pragma(inline, true); return ((p.x^x)|(p.y^y)); } + + int opCmp() (in auto ref GxPoint p) pure const { + pragma(inline, true); + return + y < p.y ? -1 : + y > p.y ? +1 : + x < p.x ? -1 : + x > p.x ? +1 : + 0; + } + + void opAssign() (in auto ref GxPoint p) { pragma(inline, true); x = p.x; y = p.y; } + + void opOpAssign(string op) (in int v) if (op == "+" || op == "-" || op == "*" || op == "/") { + pragma(inline, true); + mixin("x"~op~"=v; y"~op~"=v;"); + } + + void opOpAssign(string op) (in auto ref GxPoint pt) if (op == "+" || op == "-") { + pragma(inline, true); + mixin("x"~op~"=pt.x; y"~op~"=pt.y;"); + } + + GxPoint opBinary(string op) (in auto ref GxPoint pt) pure const if (op == "+" || op == "-") { + pragma(inline, true); + mixin("return GxPoint(x"~op~"pt.x, y"~op~"pt.y);"); + } + + GxPoint opBinary(string op) (in auto ref GxSize sz) pure const if (op == "+" || op == "-") { + pragma(inline, true); + mixin("return GxPoint(x"~op~"sz.w, y"~op~"sz.h);"); + } + + GxPoint opBinary(string op) (in int v) pure const if (op == "+" || op == "-") { + pragma(inline, true); + mixin("return GxPoint(x"~op~"v, y"~op~"v);"); + } + + int opIndex (in GxDir dir) pure const { pragma(inline, true); return (dir == GxDir.Horiz ? x : y); } + + void opIndexAssign (in int v, in GxDir dir) { pragma(inline, true); if (dir == GxDir.Horiz) x = v; else y = v; } + + void opIndexOpAssign(string op) (in int v, in GxDir dir) if (op == "+" || op == "-") { + pragma(inline, true); + if (dir == GxDir.Horiz) mixin("x "~op~"= v;"); else mixin("y "~op~"= v;"); + } +} + + +// ////////////////////////////////////////////////////////////////////////// // +public struct GxSize { +public: + int w, h; + +nothrow @safe @nogc: + @property bool valid () pure const { pragma(inline, true); return (w >= 0 && h >= 0); } + @property bool empty () pure const { pragma(inline, true); return (w < 1 || h < 1); } + + bool inside() (in auto ref GxPoint pt) pure const { pragma(inline, true); return (pt.x >= 0 && pt.y >= 0 && pt.x < w && pt.y < h); } + + void sanitize () { pragma(inline, true); if (w < 0) w = 0; if (h < 0) h = 0; } + + void opOpAssign(string op) (in int v) if (op == "+" || op == "-" || op == "*" || op == "/") { + pragma(inline, true); + mixin("w"~op~"=v; h"~op~"=v;"); + } + + void opOpAssign(string op) (in auto ref GxSize sz) if (op == "+" || op == "-") { + pragma(inline, true); + mixin("w"~op~"=sz.w; h"~op~"=sz.h;"); + } + + GxSize opBinary(string op) (in auto ref GxSize sz) pure const if (op == "+" || op == "-") { + pragma(inline, true); + mixin("return GxSize(w"~op~"sz.w, h"~op~"sz.h);"); + } + + int opIndex (in GxDir dir) pure const { pragma(inline, true); return (dir == GxDir.Horiz ? w : h); } + + void opIndexAssign (in int v, in GxDir dir) { pragma(inline, true); if (dir == GxDir.Horiz) w = v; else h = v; } + + //FIXME: overflow! + void opIndexOpAssign(string op) (in int v, in GxDir dir) if (op == "+" || op == "-") { + pragma(inline, true); + if (dir == GxDir.Horiz) { + mixin("w "~op~"= v;"); + if (w < 0) w = 0; + } else { + mixin("h "~op~"= v;"); + if (h < 0) h = 0; + } + } +} + + +// ////////////////////////////////////////////////////////////////////////// // +public struct GxRect { +public: + GxPoint pos; + GxSize size = GxSize(-1, -1); // <0: invalid rect + + string toString () const @trusted nothrow { + if (valid) { + import core.stdc.stdio : snprintf; + char[128] buf = void; + return buf[0..snprintf(buf.ptr, buf.length, "(%d,%d)-(%d,%d)", pos.x, pos.y, pos.x+size.w-1, pos.y+size.h-1)].idup; + } else { + return "(invalid-rect)"; + } + } + +nothrow @safe @nogc: + this() (in auto ref GxRect rc) pure { + pragma(inline, true); + pos = rc.pos; + size = rc.size; + } + + this (in int ax0, in int ay0, in int awidth, in int aheight) pure { + pragma(inline, true); + pos.x = ax0; + pos.y = ay0; + size.w = awidth; + size.h = aheight; + } + + this() (in int ax0, in int ay0, in auto ref GxSize asize) pure { + pragma(inline, true); + pos.x = ax0; + pos.y = ay0; + size = asize; + } + + this() (in auto ref GxPoint xy0, in int awidth, in int aheight) pure { + pragma(inline, true); + pos = xy0; + size.w = awidth; + size.h = aheight; + } + + this() (in auto ref GxPoint xy0, in auto ref GxSize asize) pure { + pragma(inline, true); + pos = xy0; + size = asize; + } + + this() (in auto ref GxPoint xy0, in auto ref GxPoint xy1) pure { + pragma(inline, true); + pos = xy0; + size.w = xy1.x-xy0.x+1; + size.h = xy1.y-xy0.y+1; + } + + this (in int awidth, in int aheight) pure { + pragma(inline, true); + pos.x = pos.y = 0; + size.w = awidth; + size.h = aheight; + } + + this() (in auto ref GxSize asize) pure { + pragma(inline, true); + pos.x = pos.y = 0; + size = asize; + } + + void setCoords(bool doSwap=true) (in int ax0, in int ay0, in int ax1, in int ay1) { + pragma(inline, true); + static if (doSwap) { + pos.x = (ax0 < ax1 ? ax0 : ax1); + pos.y = (ay0 < ay1 ? ay0 : ay1); + size.w = (ax0 < ax1 ? ax1 : ax0)-pos.x+1; + size.h = (ay0 < ay1 ? ay1 : ay0)-pos.y+1; + } else { + pos.x = ax0; + pos.y = ay0; + size.w = ax1-ax0+1; + size.h = ay1-ay0+1; + } + } + + void setCoords(bool doSwap=true) (in auto ref GxPoint p0, in int ax1, in int ay1) { pragma(inline, true); setCoords!doSwap(p0.x, p0.y, ax1, ay1); } + void setCoords(bool doSwap=true) (in int ax0, in int ay0, in auto ref GxPoint p1) { pragma(inline, true); setCoords!doSwap(ax0, ay0, p1.x, p1.y); } + void setCoords(bool doSwap=true) (in auto ref GxPoint p0, in auto ref GxPoint p1) { pragma(inline, true); setCoords!doSwap(p0.x, p0.y, p1.x, p1.y); } + + void opAssign() (in auto ref GxRect rc) { pragma(inline, true); pos = rc.pos; size = rc.size; } + + bool opEquals() (in auto ref GxRect rc) pure const { pragma(inline, true); return (pos == rc.pos && size == rc.size); } + + int opCmp() (in auto ref GxRect p) pure const { pragma(inline, true); return pos.opCmp(p.pos); } + + @property bool valid () pure const { pragma(inline, true); return size.valid; } + @property bool invalid () pure const { pragma(inline, true); return !size.valid; } + @property bool empty () pure const { pragma(inline, true); return size.empty; } // invalid rects are empty + + void invalidate () { pragma(inline, true); size.w = size.h = -1; } + + @property int left () pure const { pragma(inline, true); return pos.x; } + @property void left (in int v) { pragma(inline, true); pos.x = v; } + + @property int top () pure const { pragma(inline, true); return pos.y; } + @property void top (in int v) { pragma(inline, true); pos.y = v; } + + @property int right () pure const { pragma(inline, true); return pos.x+size.w-1; } + @property void right (in int v) { pragma(inline, true); size.w = v-pos.x+1; } + + @property int bottom () pure const { pragma(inline, true); return pos.y+size.h-1; } + @property void bottom (in int v) { pragma(inline, true); size.h = v-pos.y+1; } + + @property GxPoint lefttop () pure const { pragma(inline, true); return pos; } + @property GxPoint righttop () pure const { pragma(inline, true); return pos+GxSize(size.w-1, 0); } + @property GxPoint leftbottom () pure const { pragma(inline, true); return pos+GxSize(0, size.h-1); } + @property GxPoint rightbottom () pure const { pragma(inline, true); return pos+size-1; } + + @property void lefttop() (in auto ref GxPoint p) { pragma(inline, true); setCoords!false(p, rightbottom); } + @property void rightbottom() (in auto ref GxPoint p) { pragma(inline, true); setCoords!false(lefttop, p); } + + alias topleft = lefttop; + alias topright = righttop; + alias bottomleft = leftbottom; + alias bottomright = rightbottom; + + @property int x0 () pure const { pragma(inline, true); return pos.x; } + @property int y0 () pure const { pragma(inline, true); return pos.y; } + + @property void x0 (in int val) { pragma(inline, true); setCoords!false(val, pos.y, rightbottom); } + @property void y0 (in int val) { pragma(inline, true); setCoords!false(pos.x, val, rightbottom); } + + @property int x1 () pure const { pragma(inline, true); return (width > 0 ? x0+width-1 : x0-1); } + @property int y1 () pure const { pragma(inline, true); return (height > 0 ? y0+height-1 : y0-1); } + + @property void x1 (in int val) { pragma(inline, true); width = val-x0+1; } + @property void y1 (in int val) { pragma(inline, true); height = val-y0+1; } + + @property int width () pure const { pragma(inline, true); return size.w; } + @property int height () pure const { pragma(inline, true); return size.h; } + + @property void width (in int val) { pragma(inline, true); size.w = val; } + @property void height (in int val) { pragma(inline, true); size.h = val; } + + // is point inside this rect? + bool inside() (in auto ref GxPoint p) pure const { + pragma(inline, true); + return (width > 0 && height > 0 ? (p.x >= x0 && p.y >= y0 && p.x < x0+width && p.y < y0+height) : false); + } + + // is point inside this rect? + bool inside (in int ax, in int ay) pure const { + pragma(inline, true); + return (width > 0 && height > 0 ? (ax >= x0 && ay >= y0 && ax < x0+width && ay < y0+height) : false); + } + + // is `r` inside `this`? + bool contains() (in auto ref GxRect r) pure const { + pragma(inline, true); + return + width > 0 && height > 0 && + r.width > 0 && r.height > 0 && + r.x0 >= x0 && r.y0 >= y0 && + r.x0+r.width <= x0+width && r.y0+r.height <= y0+height; + } + + // does `r` and `this` overlap? + bool overlaps() (in auto ref GxRect r) pure const { + pragma(inline, true); + return + width > 0 && height > 0 && + r.width > 0 && r.height > 0 && + x0 < r.x0+r.width && r.x0 < x0+width && + y0 < r.y0+r.height && r.y0 < y0+height; + } + + // extend `this` so it will include `p` + void include() (in auto ref GxPoint p) { + pragma(inline, true); + if (empty) { + x0 = p.x; + y0 = p.y; + width = 1; + height = 1; + } else { + if (p.x < x0) x0 = p.x0; + if (p.y < y0) y0 = p.y0; + if (p.x1 > x1) x1 = p.x1; + if (p.y1 > y1) y1 = p.y1; + } + } + + // extend `this` so it will include `r` + void include() (in auto ref GxRect r) { + pragma(inline, true); + if (!r.empty) { + if (empty) { + x0 = r.x; + y0 = r.y; + width = r.width; + height = r.height; + } else { + if (r.x < x0) x0 = r.x0; + if (r.y < y0) y0 = r.y0; + if (r.x1 > x1) x1 = r.x1; + if (r.y1 > y1) y1 = r.y1; + } + } + } + + // clip `this` so it will not be larger than `r` + // returns `false` if the resulting rect (this) is empty or invalid + bool intersect (in int rx0, in int ry0, in int rwdt, in int rhgt) { + if (rwdt < 0 || rhgt < 0 || invalid) { size.w = size.h = -1; return false; } + if (rwdt == 0 || rhgt == 0 || empty) { size.w = size.h = 0; return false; } + immutable int rx1 = rx0+rwdt-1; + immutable int ry1 = ry0+rhgt-1; + if (ry1 < y0 || rx1 < x0 || rx0 > x1 || ry0 > y1) { size.w = size.h = 0; return false; } + // rc is at least partially inside this rect + if (x0 < rx0) x0 = rx0; + if (y0 < ry0) y0 = ry0; + if (x1 > rx1) x1 = rx1; + if (y1 > ry1) y1 = ry1; + assert(!empty); // yeah, always + return true; + } + + // clip `this` so it will not be larger than `r` + // returns `false` if the resulting rect (this) is empty or invalid + bool intersect (in int rwdt, in int rhgt) { + pragma(inline, true); + return intersect(0, 0, rwdt, rhgt); + } + + // clip `this` so it will not be larger than `r` + // returns `false` if the resulting rect (this) is empty or invalid + bool intersect() (in auto ref GxRect r) { + pragma(inline, true); + return intersect(r.x0, r.y0, r.width, r.height); + } + + void shrinkBy (in int dx, in int dy) { + pragma(inline, true); + if ((dx|dy) && valid) { + pos.x += dx; + pos.y += dy; + size.w -= dx<<1; + size.h -= dy<<1; + } + } + + void shrinkBy() (in auto ref GxSize sz) { pragma(inline, true); shrinkBy(sz.w, sz.h); } + + void growBy (in int dx, in int dy) { + pragma(inline, true); + if ((dx|dy) && valid) { + pos.x -= dx; + pos.y -= dy; + size.w += dx<<1; + size.h += dy<<1; + } + } + + void growBy() (in auto ref GxSize sz) { pragma(inline, true); growBy(sz.w, sz.h); } + + void set (in int ax0, in int ay0, in int awidth, in int aheight) { + pragma(inline, true); + x0 = ax0; + y0 = ay0; + width = awidth; + height = aheight; + } + + void set() (in auto ref GxPoint p0, in int awidth, in int aheight) { pragma(inline, true); set(p0.x, p0.y, awidth, aheight); } + void set() (in auto ref GxPoint p0, in auto ref GxSize asize) { pragma(inline, true); set(p0.x, p0.y, asize.w, asize.h); } + void set() (in int ax0, in int ay0, in auto ref GxSize asize) { pragma(inline, true); set(ax0, ay0, asize.w, asize.h); } + + void moveLeftTopBy (in int dx, in int dy) { + pragma(inline, true); + pos.x += dx; + pos.y += dy; + size.w -= dx; + size.h -= dy; + } + + void moveLeftTopBy() (in auto ref GxPoint p) { pragma(inline, true); moveLeftTopBy(p.x, p.y); } + + alias moveTopLeftBy = moveLeftTopBy; + + void moveRightBottomBy (in int dx, in int dy) { + pragma(inline, true); + size.w += dx; + size.h += dy; + } + + void moveRightBottomBy() (in auto ref GxPoint p) { pragma(inline, true); moveRightBottomBy(p.x, p.y); } + + alias moveBottomRightBy = moveRightBottomBy; + + void moveBy (in int dx, in int dy) { + pragma(inline, true); + pos.x += dx; + pos.y += dy; + } + + void moveBy() (in auto ref GxPoint p) { pragma(inline, true); moveBy(p.x, p.y); } + + void moveTo (in int nx, in int ny) { + pragma(inline, true); + x0 = nx; + y0 = ny; + } + + void moveTo() (in auto ref GxPoint p) { pragma(inline, true); moveTo(p.x, p.y); } + + /** + * clip (x,y,wdt) stripe to this rect + * + * Params: + * x = stripe start (not relative to rect) + * y = stripe start (not relative to rect) + * wdt = stripe length + * + * Returns: + * x = fixed x (invalid if result is false) + * wdt = fixed length (invalid if result is false) + * leftSkip = how much cells skipped at the left side (invalid if result is false) + * result = false if stripe is completely clipped out + */ + bool clipHStripe (ref int x, int y, ref int wdt, int* leftSkip=null) const @trusted { + if (empty) return false; + if (wdt <= 0 || y < y0 || y >= y0+height || x >= x0+width) return false; + if (x < x0) { + // left clip + immutable int dx = x0-x; + if (dx >= wdt) return false; // avoid overflow + if (leftSkip !is null) *leftSkip = dx; + wdt -= dx; + x = x0; + assert(wdt > 0); // yeah, always + } + if (wdt > width) wdt = width; // avoid overflow + if (x+wdt > x0+width) { + // right clip + wdt = x0+width-x; + assert(wdt > 0); // yeah, always + } + return true; + } + + bool clipHStripe (ref GxPoint p, ref int wdt, int* leftSkip=null) const @trusted { + pragma(inline, true); + return clipHStripe(ref p.x, p.y, ref wdt, leftSkip); + } + + /** + * clip (x,y,hgt) stripe to this rect + * + * Params: + * x = stripe start (not relative to rect) + * y = stripe start (not relative to rect) + * hgt = stripe length + * + * Returns: + * y = fixed y (invalid if result is false) + * hgt = fixed length (invalid if result is false) + * topSkip = how much cells skipped at the top side (invalid if result is false) + * result = false if stripe is completely clipped out + */ + bool clipVStripe (int x, ref int y, ref int hgt, int* topSkip=null) const @trusted { + if (empty) return false; + if (hgt <= 0 || x < x0 || x >= x0+width || y >= y0+height) return false; + if (y < y0) { + // top clip + immutable int dy = y0-y; + if (dy >= hgt) return false; // avoid overflow + if (topSkip !is null) *topSkip = dy; + hgt -= dy; + y = y0; + assert(hgt > 0); // yeah, always + } + if (hgt > height) hgt = height; // avoid overflow + if (y+hgt > y0+height) { + // bottom clip + hgt = y0+height-y; + assert(hgt > 0); // yeah, always + } + return true; + } + + bool clipVStripe (ref GxPoint p, ref int hgt, int* topSkip=null) const @trusted { + pragma(inline, true); + return clipVStripe(p.x, ref p.y, ref hgt, topSkip); + } + + bool clipHVStripes (ref int x, ref int y, ref int wdt, ref int hgt, int* leftSkip=null, int* topSkip=null) const @trusted { + if (empty || wdt <= 0 || hgt <= 0) return false; + if (y >= y0+height || x >= x0+width) return false; + // use dummy `x` and `y` for horizontal and vertical clippers, because they are only checked for validity there + if (!clipHStripe(ref x, y0, ref wdt, leftSkip)) return false; + return clipVStripe(x0, ref y, ref hgt, topSkip); + } + + bool clipHVStripes (ref GxPoint p, ref int wdt, ref int hgt, int* leftSkip=null, int* topSkip=null) const @trusted { + pragma(inline, true); + return clipHVStripes(ref p.x, ref p.y, ref wdt, ref hgt, leftSkip, topSkip); + } + + bool clipHVStripes (ref GxPoint p, ref GxSize sz, int* leftSkip=null, int* topSkip=null) const @trusted { + pragma(inline, true); + return clipHVStripes(ref p.x, ref p.y, ref sz.w, ref sz.h, leftSkip, topSkip); + } + + bool clipHVStripes (ref int x, ref int y, ref GxSize sz, int* leftSkip=null, int* topSkip=null) const @trusted { + pragma(inline, true); + return clipHVStripes(ref x, ref y, ref sz.w, ref sz.h, leftSkip, topSkip); + } +} + + +// ////////////////////////////////////////////////////////////////////////// // +public bool gxIsTransparent (in uint clr) pure nothrow @safe @nogc { pragma(inline, true); return ((clr&0xff000000u) == 0x00000000u); } +public bool gxIsSolid (in uint clr) pure nothrow @safe @nogc { pragma(inline, true); return ((clr&0xff000000u) == 0xff000000u); } + +public bool gxIsSolidBlack (in uint clr) pure nothrow @safe @nogc { pragma(inline, true); return (clr == 0xff000000u); } + +public ubyte gxGetRed (in uint clr) pure nothrow @safe @nogc { pragma(inline, true); return cast(ubyte)(clr>>16); } +public ubyte gxGetGreen (in uint clr) pure nothrow @safe @nogc { pragma(inline, true); return cast(ubyte)(clr>>8); } +public ubyte gxGetBlue (in uint clr) pure nothrow @safe @nogc { pragma(inline, true); return cast(ubyte)clr; } +public ubyte gxGetAlpha (in uint clr) pure nothrow @safe @nogc { pragma(inline, true); return cast(ubyte)(clr>>24); } + +public enum gxSolidBlack = 0xff000000u; +public enum gxSolidWhite = 0xffffffffu; + +public enum gxTransparent = 0x00000000u; +public enum gxColorMask = 0x00ffffffu; + +public enum gxUnknown = 0x00010203u; + + +// ////////////////////////////////////////////////////////////////////////// // +// mix dc with ARGB (or ABGR) clr; dc A is ignored (removed) +// main code never calls this with solid or transparent `colvar` +enum GxColMixMixin(string destvar, string dcvar, string colvar) = `{ + immutable uint col_ = `~colvar~`; + immutable uint dc_ = (`~dcvar~`)&0xffffff; + /*immutable uint a_ = 256-(col_>>24);*/ /* to not loose bits */ + immutable uint a_ = (col_>>24); + immutable uint srb_ = (col_&0xff00ff); + immutable uint sg_ = (col_&0x00ff00); + immutable uint drb_ = (dc_&0xff00ff); + immutable uint dg_ = (dc_&0x00ff00); + immutable uint orb_ = (drb_+(((srb_-drb_)*a_+0x800080)>>8))&0xff00ff; + immutable uint og_ = (dg_+(((sg_-dg_)*a_+0x008000)>>8))&0x00ff00; + (`~destvar~`) = orb_|og_; +}`; + +public uint gxColMix (in uint dc, in uint clr) pure nothrow @trusted @nogc { + pragma(inline, true); + if (gxIsSolid(clr)) return clr; + else if (gxIsTransparent(clr)) return dc; + else { + uint res = void; + mixin(GxColMixMixin!("res", "dc", "clr")); + return res; + } +} + + +// ////////////////////////////////////////////////////////////////////////// // +private template isGoodRGBInt(T) { + import std.traits : Unqual; + alias TT = Unqual!T; + enum isGoodRGBInt = + is(TT == ubyte) || + is(TT == short) || is(TT == ushort) || + is(TT == int) || is(TT == uint) || + is(TT == long) || is(TT == ulong); +} + + +// ////////////////////////////////////////////////////////////////////////// // +public uint gxrgb(T0, T1, T2) (T0 r, T1 g, T2 b) pure nothrow @trusted @nogc +if (isGoodRGBInt!T0 && isGoodRGBInt!T1 && isGoodRGBInt!T2) +{ + pragma(inline, true); + return (clampToByte(r)<<16)|(clampToByte(g)<<8)|clampToByte(b)|0xff000000u; +} + +public uint gxrgba(T0, T1, T2, T3) (T0 r, T1 g, T2 b, T3 a) pure nothrow @trusted @nogc +if (isGoodRGBInt!T0 && isGoodRGBInt!T1 && isGoodRGBInt!T2 && isGoodRGBInt!T3) +{ + pragma(inline, true); + return (clampToByte(a)<<24)|(clampToByte(r)<<16)|(clampToByte(g)<<8)|clampToByte(b); +} + + +public enum gxRGB(int r, int g, int b) = (clampToByte(r)<<16)|(clampToByte(g)<<8)|clampToByte(b)|0xff000000u; +public enum gxRGBA(int r, int g, int b, int a) = (clampToByte(a)<<24)|(clampToByte(r)<<16)|(clampToByte(g)<<8)|clampToByte(b); + + +// ////////////////////////////////////////////////////////////////////////// // +// current clip rect +public __gshared GxRect gxClipRect = GxRect(65535, 65535); + +public void gxWithSavedClip(DG) (scope DG dg) +if (is(typeof((inout int=0) { DG dg = void; dg(); }))) +{ + pragma(inline, true); + if (dg !is null) { + immutable rc = gxClipRect; + scope(exit) gxClipRect = rc; + dg(); + } +} + +public void gxClipReset () nothrow @trusted @nogc { + pragma(inline, true); + gxClipRect = GxRect(65535, 65535); +} + + +// ////////////////////////////////////////////////////////////////////////// // +public void gxClearScreen (uint clr) nothrow @trusted @nogc { + clr &= gxColorMask; // only solid color matters here anyway + if (clr == 0u) { + import core.stdc.string : memset; + memset(vglTexBuf, 0, (VBufWidth*VBufHeight)<<2); + } else { + vglTexBuf[0..VBufWidth*VBufHeight] = clr; + } +} + + +public void gxPutPixel (in int x, in int y, in uint c) nothrow @trusted @nogc { + pragma(inline, true); + if (x >= 0 && y >= 0 && x < VBufWidth && y < VBufHeight && !gxIsTransparent(c) && gxClipRect.inside(x, y)) { + uint* dp = cast(uint*)(cast(ubyte*)vglTexBuf)+y*VBufWidth+x; + *dp = gxColMix(*dp, c); + } +} + + +public void gxPutPixel() (in auto ref GxPoint p, in uint c) nothrow @trusted @nogc { + pragma(inline, true); + if (p.x >= 0 && p.y >= 0 && p.x < VBufWidth && p.y < VBufHeight && !gxIsTransparent(c) && gxClipRect.inside(p)) { + uint* dp = cast(uint*)(cast(ubyte*)vglTexBuf)+p.y*VBufWidth+p.x; + *dp = (gxIsSolid(c) ? (c&gxColorMask) : gxColMix(*dp, c)); + } +} + + +public void gxSetPixel (in int x, in int y, in uint c) nothrow @trusted @nogc { + pragma(inline, true); + if (x >= 0 && y >= 0 && x < VBufWidth && y < VBufHeight && !gxIsTransparent(c) && gxClipRect.inside(x, y)) { + *(cast(uint*)(cast(ubyte*)vglTexBuf)+y*VBufWidth+x) = c&gxColorMask; + } +} + + +public void gxSetPixel() (in auto ref GxPoint p, in uint c) nothrow @trusted @nogc { + pragma(inline, true); + if (p.x >= 0 && p.y >= 0 && p.x < VBufWidth && p.y < VBufHeight && !gxIsTransparent(c) && gxClipRect.inside(p)) { + *(cast(uint*)(cast(ubyte*)vglTexBuf)+p.y*VBufWidth+p.x) = c&gxColorMask; + } +} + + +// ////////////////////////////////////////////////////////////////////////// // +public void gxHLine (int x, int y, int w, in uint clr) nothrow @trusted @nogc { + if (gxIsTransparent(clr)) return; + if (!gxClipRect.clipHStripe(x, y, w)) return; + if (!GxRect(VBufWidth, VBufHeight).clipHStripe(x, y, w)) return; + if (gxIsSolid(clr)) { + immutable uint addr = y*VBufWidth+x; + if (gxIsSolidBlack(clr)) { + import core.stdc.string : memset; + memset(vglTexBuf+addr, 0, w<<2); + } else { + vglTexBuf[addr..addr+w] = clr; + } + } else { + uint* dptr = vglTexBuf+y*VBufWidth+x; + while (w-- > 0) { + mixin(GxColMixMixin!("*dptr++", "*dptr", "clr")); + } + } +} + +public void gxHLine() (in auto ref GxPoint p, in int w, in uint clr) nothrow @trusted @nogc { pragma(inline, true); gxHLine(p.x, p.y, w, clr); } + +public void gxVLine (int x, int y, int h, in uint clr) nothrow @trusted @nogc { + if (gxIsTransparent(clr)) return; + if (!gxClipRect.clipVStripe(x, y, h)) return; + if (!GxRect(VBufWidth, VBufHeight).clipVStripe(x, y, h)) return; + uint* dptr = vglTexBuf+y*VBufWidth+x; + if (gxIsSolid(clr)) { + while (h-- > 0) { *dptr = clr; dptr += VBufWidth; } + } else { + while (h-- > 0) { mixin(GxColMixMixin!("*dptr", "*dptr", "clr")); dptr += VBufWidth; } + } +} + +public void gxVLine() (in auto ref GxPoint p, in int h, in uint clr) nothrow @trusted @nogc { pragma(inline, true); gxVLine(p.x, p.y, h, clr); } + + +// ////////////////////////////////////////////////////////////////////////// // +public void gxFillRect (int x, int y, int w, int h, in uint clr) nothrow @trusted @nogc { + if (gxIsTransparent(clr)) return; + if (!gxClipRect.clipHVStripes(x, y, w, h)) return; + if (!GxRect(VBufWidth, VBufHeight).clipHVStripes(x, y, w, h)) return; + if (gxIsSolid(clr)) { + uint addr = y*VBufWidth+x; + if (gxIsSolidBlack(clr)) { + import core.stdc.string : memset; + while (h-- > 0) { + memset(vglTexBuf+addr, 0, w<<2); + addr += VBufWidth; + } + } else { + while (h-- > 0) { + vglTexBuf[addr..addr+w] = clr; + addr += VBufWidth; + } + } + } else { + uint* dptr = vglTexBuf+y*VBufWidth+x; + immutable uint dinc = VBufWidth-w; + while (h-- > 0) { + foreach (immutable _; 0..w) { + mixin(GxColMixMixin!("*dptr++", "*dptr", "clr")); + } + dptr += dinc; + } + } +} + +public void gxFillRect() (in auto ref GxRect rc, in uint clr) nothrow @trusted @nogc { + pragma(inline, true); + gxFillRect(rc.x0, rc.y0, rc.width, rc.height, clr); +} + +public void gxDrawRect (in int x, in int y, in int w, in int h, in uint clr) nothrow @trusted @nogc { + if (w < 1 || h < 1 || gxIsTransparent(clr)) return; + gxHLine(x, y, w, clr); + if (h > 1) gxHLine(x, y+h-1, w, clr); + if (h > 2) { + gxVLine(x, y+1, h-2, clr); + if (w > 1) gxVLine(x+w-1, y+1, h-2, clr); + } +} + +public void gxDrawRect() (in auto ref GxRect rc, in uint clr) nothrow @trusted @nogc { + pragma(inline, true); + gxDrawRect(rc.x0, rc.y0, rc.width, rc.height, clr); +} + + +// ////////////////////////////////////////////////////////////////////////// // +// use clip region as boundaries +public void gxDrawShadow (in GxRect winrect) nothrow @trusted @nogc { + if (winrect.empty) return; + gxWithSavedClip{ + //immutable GxRect rc = gxClipRect; + gxClipReset(); + gxFillRect(winrect.x1+1, winrect.y0+8, 8, winrect.height-8, gxRGBA!(0, 0, 0, 127)); + gxFillRect(winrect.x0+8, winrect.y1+1, winrect.width, 8, gxRGBA!(0, 0, 0, 127)); + }; +} + + +public void gxDrawWindow (in GxRect winrect, + const(char)[] title, in uint framecolor, in uint titlecolor, + in uint titlebackcolor, in uint windowcolor) nothrow @trusted +{ + import iv.egra.gfx.text; + + if (winrect.empty) return; + gxDrawShadow(winrect); + + gxFillRect(winrect, windowcolor); + gxDrawRect(winrect, framecolor); + + if (title is null) return; + if (winrect.width <= 2 || winrect.height <= 2) return; + gxWithSavedClip{ + immutable int hgt = (gxTextHeightUtf < 10 ? 10 : gxTextHeightUtf+1); + if (gxClipRect.intersect(winrect.x0+1, winrect.y0+1, winrect.width-2, hgt)) { + gxFillRect(gxClipRect, titlebackcolor); + gxDrawTextUtf(winrect.x0+1+(winrect.width-2-gxTextWidthUtf(title))/2, winrect.y0+1+(hgt-gxTextHeightUtf)/2, title, titlecolor); + } + }; +} + + +// ////////////////////////////////////////////////////////////////////////// // +public void gxDrawScrollBar() (in auto ref GxRect r, in int max, in int value) nothrow @trusted @nogc { pragma(inline, true); gxDrawScrollBar(r, 0, max, value); } + +public void gxDrawScrollBar() (in auto ref GxRect r, int min, int max, int value) nothrow @trusted @nogc { + enum FrameColor = gxRGB!(220, 220, 220); + enum EmptyColor = gxRGB!(0, 0, 0); + enum FullColor = gxRGB!(160, 160, 160); + if (r.empty) return; + if (max <= min) min = max = value = 0; + // move it to 0 + //conwriteln("00: min=", min, "; max=", max, "; value=", value); + max -= min; + value -= min; + if (value < 0) value = 0; else if (value > max) value = max; + //conwriteln("01: min=", min, "; max=", max, "; value=", value); + int sx0 = r.x0+1; + int sy0 = r.y0+1; + int wdt = r.width-2; + int hgt = r.height-2; + bool vert = (r.width < r.height); + // frame + if ((vert && wdt > 1) || (!vert && hgt > 1)) { + gxHLine(r.x0+1, r.y0+0, wdt, FrameColor); + gxVLine(r.x0+0, r.y0+1, hgt, FrameColor); + gxVLine(r.x1+0, r.y0+1, hgt, FrameColor); + gxHLine(r.x0+1, r.y1+0, wdt, FrameColor); + } else { + sx0 -= 1; + sy0 -= 1; + wdt += 2; + hgt += 2; + } + if (max <= 0) { + gxFillRect(sx0, sy0, wdt, hgt, FullColor); + return; + } + if (vert) { + int pix = hgt*value/max; + if (pix > hgt) pix = hgt; // just in case + gxFillRect(sx0, sy0, wdt, pix, FullColor); + gxFillRect(sx0, sy0+pix, wdt, hgt-pix, EmptyColor); + } else { + int pix = wdt*value/max; + if (pix > wdt) pix = wdt; // just in case + gxFillRect(sx0, sy0, pix, hgt, FullColor); + gxFillRect(sx0+pix, sy0, wdt-pix, hgt, EmptyColor); + } +} + + +// ////////////////////////////////////////////////////////////////////////// // +private int abs (int a) pure nothrow @safe @nogc { pragma(inline, true); return (a < 0 ? -a : a); } + +public void gxCircle (in int cx, in int cy, in int radius, in uint clr) nothrow @trusted @nogc { + static void plot4points (in int cx, in int cy, in int x, in int y, in uint clr) nothrow @trusted @nogc { + pragma(inline, true); + gxPutPixel(cx+x, cy+y, clr); + if (x) gxPutPixel(cx-x, cy+y, clr); + if (y) gxPutPixel(cx+x, cy-y, clr); + gxPutPixel(cx-x, cy-y, clr); + } + + if (radius <= 0 || gxIsTransparent(clr)) return; + if (radius == 1) { gxPutPixel(cx, cy, clr); return; } + int error = -radius, x = radius, y = 0; + while (x > y) { + plot4points(cx, cy, x, y, clr); + plot4points(cx, cy, y, x, clr); + error += y*2+1; + ++y; + if (error >= 0) { --x; error -= x*2; } + } + plot4points(cx, cy, x, y, clr); +} + +public void gxCircle() (in auto ref GxPoint c, in int radius, in uint clr) nothrow @trusted @nogc { pragma(inline, true); gxCircle(c.x, c.y, radius, clr); } + + +public void gxFillCircle (in int cx, in int cy, in int radius, in uint clr) nothrow @trusted @nogc { + if (radius <= 0 || gxIsTransparent(clr)) return; + if (radius == 1) { gxPutPixel(cx, cy, clr); return; } + int error = -radius, x = radius, y = 0; + while (x >= y) { + int last_y = y; + error += y; + ++y; + error += y; + gxHLine(cx-x, cy+last_y, 2*x+1, clr); + if (x != 0 && last_y != 0) gxHLine(cx-x, cy-last_y, 2*x+1, clr); + if (error >= 0) { + if (x != last_y) { + gxHLine(cx-last_y, cy+x, 2*last_y+1, clr); + if (last_y != 0 && x != 0) gxHLine(cx-last_y, cy-x, 2*last_y+1, clr); + } + error -= x; + --x; + error -= x; + } + } +} + +public void gxFillCircle() (in auto ref GxPoint c, in int radius, in uint clr) nothrow @trusted @nogc { pragma(inline, true); gxFillCircle(c.x, c.y, radius, clr); } + + +public void gxEllipse (int x0, int y0, int x1, int y1, in uint clr) nothrow @trusted @nogc { + if (gxIsTransparent(clr)) return; + if (y0 == y1) { gxHLine(x0, y0, x1-x0+1, clr); return; } + if (x0 == x1) { gxVLine(x0, y0, y1-y0+1, clr); return; } + int a = abs(x1-x0), b = abs(y1-y0), b1 = b&1; // values of diameter + long dx = 4*(1-a)*b*b, dy = 4*(b1+1)*a*a; // error increment + long err = dx+dy+b1*a*a; // error of 1.step + if (x0 > x1) { x0 = x1; x1 += a; } // if called with swapped points... + if (y0 > y1) y0 = y1; // ...exchange them + y0 += (b+1)/2; y1 = y0-b1; // starting pixel + a *= 8*a; b1 = 8*b*b; + do { + long e2; + gxPutPixel(x1, y0, clr); // I. Quadrant + gxPutPixel(x0, y0, clr); // II. Quadrant + gxPutPixel(x0, y1, clr); // III. Quadrant + gxPutPixel(x1, y1, clr); // IV. Quadrant + e2 = 2*err; + if (e2 >= dx) { ++x0; --x1; err += dx += b1; } // x step + if (e2 <= dy) { ++y0; --y1; err += dy += a; } // y step + } while (x0 <= x1); + while (y0-y1 < b) { + // too early stop of flat ellipses a=1 + gxPutPixel(x0-1, ++y0, clr); // complete tip of ellipse + gxPutPixel(x0-1, --y1, clr); + } +} + +public void gxEllipse() (in auto ref GxRect rc, in int radius, in uint clr) nothrow @trusted @nogc { pragma(inline, true); gxEllipse(rc.x0, rc.y0, rc.x1, rc.y1, clr); } + + +public void gxFillEllipse (int x0, int y0, int x1, int y1, in uint clr) nothrow @trusted @nogc { + if (gxIsTransparent(clr)) return; + if (y0 == y1) { gxHLine(x0, y0, x1-x0+1, clr); return; } + if (x0 == x1) { gxVLine(x0, y0, y1-y0+1, clr); return; } + int a = abs(x1-x0), b = abs(y1-y0), b1 = b&1; // values of diameter + long dx = 4*(1-a)*b*b, dy = 4*(b1+1)*a*a; // error increment + long err = dx+dy+b1*a*a; // error of 1.step + int prev_y0 = -1, prev_y1 = -1; + if (x0 > x1) { x0 = x1; x1 += a; } // if called with swapped points... + if (y0 > y1) y0 = y1; // ...exchange them + y0 += (b+1)/2; y1 = y0-b1; // starting pixel + a *= 8*a; b1 = 8*b*b; + do { + long e2; + if (y0 != prev_y0) { gxHLine(x0, y0, x1-x0+1, clr); prev_y0 = y0; } + if (y1 != y0 && y1 != prev_y1) { gxHLine(x0, y1, x1-x0+1, clr); prev_y1 = y1; } + e2 = 2*err; + if (e2 >= dx) { ++x0; --x1; err += dx += b1; } // x step + if (e2 <= dy) { ++y0; --y1; err += dy += a; } // y step + } while (x0 <= x1); + while (y0-y1 < b) { + // too early stop of flat ellipses a=1 + gxPutPixel(x0-1, ++y0, clr); // complete tip of ellipse + gxPutPixel(x0-1, --y1, clr); + } +} + +public void gxFillEllipse() (in auto ref GxRect rc, in int radius, in uint clr) nothrow @trusted @nogc { pragma(inline, true); gxFillEllipse(rc.x0, rc.y0, rc.x1, rc.y1, clr); } + + +// ////////////////////////////////////////////////////////////////////////// // +public void gxDrawRoundedRect (int x0, int y0, int wdt, int hgt, int radius, in uint clr) nothrow @trusted @nogc { + static void gxArcs (int cx, int cy, int wdt, int hgt, in int radius, in uint clr) nothrow @trusted @nogc { + static void plot4points (in int radius, in int wdt, in int hgt, in int cx, in int cy, in int x, in int y, in uint clr) nothrow @trusted @nogc { + pragma(inline, true); + gxPutPixel(cx+wdt-1+x, cy+hgt-1+y, clr); + gxPutPixel(cx-x, cy+hgt-1+y, clr); + gxPutPixel(cx+wdt-1+x, cy-y, clr); + gxPutPixel(cx-x, cy-y, clr); + } + + wdt -= (radius<<1); if (wdt <= 0) return; + hgt -= (radius<<1); if (hgt <= 0) return; + cx += radius; cy += radius; + int error = -radius, x = radius, y = 0; + while (x > y) { + plot4points(radius, wdt, hgt, cx, cy, x, y, clr); + plot4points(radius, wdt, hgt, cx, cy, y, x, clr); + error += y*2+1; + ++y; + if (error >= 0) { --x; error -= x*2; } + } + if (x || y != radius) plot4points(radius, wdt, hgt, cx, cy, x, y, clr); + } + + + if (wdt < 1 || hgt < 1) return; + if (radius < 1) { gxDrawRect(x0, y0, wdt, hgt, clr); return; } + if (gxIsTransparent(clr)) return; + if (hgt == 1) { gxHLine(x0, y0, wdt, clr); return; } + if (wdt == 1) { gxVLine(x0, y0, hgt, clr); return; } + // fix radius + immutable int minsz = (wdt < hgt ? wdt : hgt); + if (radius >= (minsz>>1)) { + radius = (minsz>>1)-1; + if (radius < 1) { gxDrawRect(x0, y0, wdt, hgt, clr); return; } + } + // draw the parts of the rect + gxHLine(x0+radius+1, y0, wdt-(radius<<1)-2, clr); // top + gxHLine(x0+radius+1, y0+hgt-1, wdt-(radius<<1)-2, clr); // bottom + gxVLine(x0, y0+radius+1, hgt-(radius<<1)-2, clr); // left + gxVLine(x0+wdt-1, y0+radius+1, hgt-(radius<<1)-2, clr); // right + // left arc + gxArcs(x0, y0, wdt, hgt, radius, clr); +} + +public void gxDrawRoundedRect() (in auto ref GxRect rc, in int radius, in uint clr) nothrow @trusted @nogc { + pragma(inline, true); + gxDrawRoundedRect(rc.x0, rc.y0, rc.width, rc.height, radius, clr); +} + + +// ////////////////////////////////////////////////////////////////////////// // +__gshared usize frectXCoords; // array of ints +__gshared usize frectXCSize; // in items + + +/* cyclic dependency +shared static ~this () { + if (frectXCoords) { + import core.stdc.stdlib : free; + free(cast(void*)frectXCoords); + frectXCoords = 0; + } +} +*/ + + +int* ensureXCoords (int radius) nothrow @trusted @nogc { + if (radius < 1) return null; + if (radius > 1024*1024) return null; + if (cast(usize)radius > frectXCSize) { + import core.stdc.stdlib : realloc; + immutable usize newsz = (cast(usize)radius|0x7fu)+1; + void* np = realloc(cast(void*)frectXCoords, newsz*int.sizeof); + if (np is null) return null; // out of memory + frectXCSize = newsz; + frectXCoords = cast(usize)np; + } + return cast(int*)frectXCoords; +} + + +// this is wrong, but i'm ok with it for now +public void gxFillRoundedRect (int x0, int y0, int wdt, int hgt, int radius, in uint clr) nothrow @trusted @nogc { + if (wdt < 1 || hgt < 1) return; + if (radius < 1) { gxFillRect(x0, y0, wdt, hgt, clr); return; } + if (gxIsTransparent(clr)) return; + if (hgt == 1) { gxHLine(x0, y0, wdt, clr); return; } + if (wdt == 1) { gxVLine(x0, y0, hgt, clr); return; } + // fix radius + immutable int minsz = (wdt < hgt ? wdt : hgt); + if (radius >= (minsz>>1)) { + radius = (minsz>>1)-1; + if (radius < 1) { gxFillRect(x0, y0, wdt, hgt, clr); return; } + } + + // create border coords + auto xpt = ensureXCoords(radius+1); + if (xpt is null) { gxFillRect(x0, y0, wdt, hgt, clr); return; } // do at least something + xpt[0..radius+1] = int.min; + + // create coords + { + int error = -radius, x = radius, y = 0; + while (x > y) { + if (y <= radius && xpt[y] < x) xpt[y] = x; + if (x >= 0 && x <= radius && xpt[x] < y) xpt[x] = y; + error += y*2+1; + ++y; + if (error >= 0) { --x; error -= x*2; } + } + if (y <= radius && xpt[y] < x) xpt[y] = x; + if (x >= 0 && x <= radius && xpt[x] < y) xpt[x] = y; + } + + // draw the filled rect + gxFillRect(x0, y0+radius+1, wdt, hgt-(radius<<1)-2, clr); + + // draw arc + foreach (immutable dy; 0..radius+1) { + if (xpt[dy] == int.min) continue; + immutable topy = y0+radius-dy; + immutable topx0 = x0+radius-xpt[dy]; + immutable topx1 = x0+wdt-radius-1+xpt[dy]; + //gxPutPixel(topx0, topy, clr); + //gxPutPixel(topx1, topy, clr); + gxHLine(topx0, topy, topx1-topx0+1, clr); + immutable boty = y0+hgt-radius+dy-1; + //gxPutPixel(topx0, boty, clr); + //gxPutPixel(topx1, boty, clr); + gxHLine(topx0, boty, topx1-topx0+1, clr); + } +} + +public void gxFillRoundedRect() (in auto ref GxRect rc, in int radius, in uint clr) nothrow @trusted @nogc { + pragma(inline, true); + gxFillRoundedRect(rc.x0, rc.y0, rc.width, rc.height, radius, clr); +} + + +// ////////////////////////////////////////////////////////////////////////// // +// bresenham with clipping +// the idea is that we can simply skip the right number of steps +// if the point is off the drawing area +public void gxDrawLine (int x0, int y0, int x1, int y1, in uint clr, bool lastPoint=true) nothrow @trusted @nogc { + enum swap(string a, string b) = "{immutable int tmp_="~a~";"~a~"="~b~";"~b~"=tmp_;}"; + + if (gxIsTransparent(clr)) return; + + GxRect realClip = gxClipRect; + if (!realClip.intersect(VBufWidth, VBufHeight)) return; + + // just a point? + if (x0 == x1 && y0 == y1) { + if (lastPoint) gxPutPixel(x0, y0, clr); + return; + } + + // horizontal line? + if (y0 == y1) { + if (x0 > x1) { + gxHLine(x1+(lastPoint ? 0 : 1), y0, x0-x1+(lastPoint ? 1 : 0), clr); + } else { + gxHLine(x0, y0, x1-x0+(lastPoint ? 1 : 0), clr); + } + return; + } + + // clip rectange + int wx0 = realClip.x0, wy0 = realClip.y0, wx1 = realClip.x1, wy1 = realClip.y1; + if (wx0 > wx1 || wy0 > wy1) return; // this should not happen, but... + + // vertical setup; always go from top to bottom, so we'll draw the same line regardless of the starting point + bool skipFirst = false; + if (y0 > y1) { + // swap endpoints + if (!lastPoint) skipFirst = lastPoint = true; + mixin(swap!("x0", "x1")); + mixin(swap!("y0", "y1")); + } + if (y0 > wy1 || y1 < wy0) return; // out of clip rectange + int sty = 1; // "step sign" for x axis; we still need the var, because there is a possible swap down there + + // horizontal setup + int stx = void; // "step sign" for x axis + if (x0 < x1) { + // from left to right + if (x0 > wx1 || x1 < wx0) return; // out of clip rectange + stx = 1; // going right + } else { + // from right to left + if (x1 > wx1 || x0 < wx0) return; // out of clip rectange + stx = -1; // going left + x0 = -x0; + x1 = -x1; + wx0 = -wx0; + wx1 = -wx1; + mixin(swap!("wx0", "wx1")); + } + + int dsx = x1-x0; // "length" for x axis + int dsy = y1-y0; // "length" for y axis + int xd = void, yd = void; // current coord + bool xyswapped = false; // if `true`, `xd` and `yd` are swapped + if (dsx < dsy) { + xyswapped = true; + mixin(swap!("x0", "y0")); + mixin(swap!("x1", "y1")); + mixin(swap!("dsx", "dsy")); + mixin(swap!("wx0", "wy0")); + mixin(swap!("wx1", "wy1")); + mixin(swap!("stx", "sty")); + } + xd = x0; + yd = y0; + int dx2 = 2*dsx; // "double length" for x axis + int dy2 = 2*dsy; // "double length" for y axis + int e = 2*dsy-dsx; // "error" (as in bresenham algo) + int term = x1; // termination point + bool xfixed = false; // will be set if we properly fixed x0 coord while fixing the y0 + // note that clipping can overflow for insane coords + // if you are completely sure that it can't happen, you can use `int` instead of `long` + if (y0 < wy0) { + // clip at top + immutable long temp = cast(long)dx2*(wy0-y0)-dsx; + xd += cast(int)(temp/dy2); + if (xd > wx1) return; // x is moved out of clipping rect, nothing to do + immutable int rem = cast(int)(temp%dy2); + if (xd+(rem > 0 ? 1 : 0) >= wx0) { + xfixed = true; // startx is inside the clipping rect, no need to perform left clip + yd = wy0; + e -= rem+dsx; + if (rem > 0) { ++xd; e += dy2; } + } + } + if (!xfixed && x0 < wx0) { + // clip at left + immutable long temp = cast(long)dy2*(wx0-x0); + yd += cast(int)(temp/dx2); + immutable int rem = cast(int)(temp%dx2); + if (yd > wy1 || (yd == wy1 && rem >= dsx)) return; // y is moved out of clipping rect, nothing to do + xd = wx0; + e += rem; + if (rem >= dsx) { ++yd; e -= dx2; } + } + if (y1 > wy1) { + // clip at bottom + immutable long temp = cast(long)dx2*(wy1-y0)+dsx; + term = x0+cast(int)(temp/dy2); + // it should be safe to decrement here + if (cast(int)(temp%dy2) == 0) --term; + } + if (term > wx1) term = wx1; // clip at right + + if (sty == -1) yd = -yd; + if (stx == -1) { xd = -xd; term = -term; } + dx2 -= dy2; + + if (lastPoint) term += stx; + if (skipFirst) { + if (term == xd) return; + if (e >= 0) { yd += sty; e -= dx2; } else { e += dy2; } + xd += stx; + } + + // draw it; `putPixel()` can omit checks + if (gxIsSolid(clr)) { + while (xd != term) { + // inlined putpixel + *cast(uint*)((cast(ubyte*)vglTexBuf)+(xyswapped ? xd*VBufWidth+yd : yd*VBufWidth+xd)) = clr; + // done drawing, move coords + if (e >= 0) { yd += sty; e -= dx2; } else { e += dy2; } + xd += stx; + } + } else { + while (xd != term) { + // inlined putpixel + uint* dp = cast(uint*)((cast(ubyte*)vglTexBuf)+(xyswapped ? xd*VBufWidth+yd : yd*VBufWidth+xd)); + mixin(GxColMixMixin!("*dp", "*dp", "clr")); + // done drawing, move coords + if (e >= 0) { yd += sty; e -= dx2; } else { e += dy2; } + xd += stx; + } + } +} + +public void gxDrawLine() (in auto ref GxPoint p0, in int x1, in int y1, in uint clr, in bool lastPoint=true) { pragma(inline, true); gxDrawLine(p0.x, p0.y, x1, y1, clr, lastPoint); } +public void gxDrawLine() (in int x0, in int y0, auto ref GxPoint p1, in uint clr, in bool lastPoint=true) { pragma(inline, true); gxDrawLine(x0, y0, p1.x, p1.y, clr, lastPoint); } +public void gxDrawLine() (in auto ref GxPoint p0, auto ref GxPoint p1, in uint clr, in bool lastPoint=true) { pragma(inline, true); gxDrawLine(p0.x, p0.y, p1.x, p1.y, clr, lastPoint); } diff --git a/egra/gfx/config.d b/egra/gfx/config.d new file mode 100644 index 0000000..5c86a37 --- /dev/null +++ b/egra/gfx/config.d @@ -0,0 +1,27 @@ +/* + * Simple Framebuffer Gfx/GUI lib + * + * coded by Ketmar // Invisible Vector + * Understanding is not required. Only obedience. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License ONLY. + * + * 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, see . + */ +module iv.egra.gfx.config /*is aliced*/; + +version = egfx_opengl_backend; + +version(egfx_opengl_backend) { + public enum EGfxOpenGLBackend = true; +} else { + public enum EGfxOpenGLBackend = false; +} diff --git a/egra/gfx/package.d b/egra/gfx/package.d new file mode 100644 index 0000000..7766bfd --- /dev/null +++ b/egra/gfx/package.d @@ -0,0 +1,28 @@ +/* + * Simple Framebuffer Gfx/GUI lib + * + * coded by Ketmar // Invisible Vector + * Understanding is not required. Only obedience. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License ONLY. + * + * 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, see . + */ +module iv.egra.gfx /*is aliced*/; + + +// ////////////////////////////////////////////////////////////////////////// // +public import iv.egra.gfx.config; +public import iv.egra.gfx.base; +public import iv.egra.gfx.text; + +static if (EGfxOpenGLBackend) public import iv.egra.gfx.backgl; +static if (!EGfxOpenGLBackend) public import iv.egra.gfx.backx11; diff --git a/egra/gfx/text.d b/egra/gfx/text.d new file mode 100644 index 0000000..5ec51e8 --- /dev/null +++ b/egra/gfx/text.d @@ -0,0 +1,619 @@ +/* + * Simple Framebuffer Gfx/GUI lib + * + * coded by Ketmar // Invisible Vector + * Understanding is not required. Only obedience. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License ONLY. + * + * 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, see . + */ +module iv.egra.gfx.text /*is aliced*/; +private: + +import iv.alice; +import iv.bclamp; +import iv.cmdcon; +import iv.fontconfig; +import iv.freetype; +import iv.utfutil; +import iv.vfs; + +import iv.egra.gfx.config; +import iv.egra.gfx.base; + + +// ////////////////////////////////////////////////////////////////////////// // +enum ReplacementChar = 0xFFFD; +//__gshared string chiFontName = "Arial:pixelsize=16"; +//__gshared string chiFontName = "Verdana:pixelsize=16"; +__gshared string chiFontName = "Verdana:weight=bold:pixelsize=16"; +__gshared string chiFontFile; +__gshared int fontSize; +__gshared int fontHeight; +__gshared int fontBaselineOfs; + + +// ////////////////////////////////////////////////////////////////////////// // +__gshared ubyte* ttfontdata; +__gshared uint ttfontdatasize; + +__gshared FT_Library ttflibrary; +__gshared FTC_Manager ttfcache; +__gshared FTC_CMapCache ttfcachecmap; +__gshared FTC_ImageCache ttfcacheimage; + + +shared static ~this () { + if (ttflibrary) { + if (ttfcache) { + FTC_Manager_Done(ttfcache); + ttfcache = null; + } + FT_Done_FreeType(ttflibrary); + ttflibrary = null; + } +} + + +enum FontID = cast(FTC_FaceID)1; + + +extern(C) nothrow { + void ttfFontFinalizer (void* obj) { + import core.stdc.stdlib : free; + if (obj is null) return; + auto tf = cast(iv.freetype.FT_Face)obj; + if (tf.generic.data !is ttfontdata) return; + if (ttfontdata !is null) { + version(aliced) conwriteln("TTF CACHE: freeing loaded font..."); + free(ttfontdata); + ttfontdata = null; + ttfontdatasize = 0; + } + } + + FT_Error ttfFontLoader (FTC_FaceID face_id, FT_Library library, FT_Pointer request_data, iv.freetype.FT_Face* aface) { + if (face_id == FontID) { + try { + if (ttfontdata is null) { + conwriteln("TTF CACHE: loading '", chiFontFile, "'..."); + import core.stdc.stdlib : malloc; + auto fl = VFile(chiFontFile); + auto fsz = fl.size; + if (fsz < 16 || fsz > int.max/8) throw new Exception("invalid ttf size"); + ttfontdatasize = cast(uint)fsz; + ttfontdata = cast(ubyte*)malloc(ttfontdatasize); + if (ttfontdata is null) { import core.exception : onOutOfMemoryErrorNoGC; onOutOfMemoryErrorNoGC(); } + fl.rawReadExact(ttfontdata[0..ttfontdatasize]); + } + auto res = FT_New_Memory_Face(library, cast(const(FT_Byte)*)ttfontdata, ttfontdatasize, 0, aface); + if (res != 0) throw new Exception("error loading ttf: '"~chiFontFile~"'"); + (*aface).generic.data = ttfontdata; + (*aface).generic.finalizer = &ttfFontFinalizer; + } catch (Exception e) { + if (ttfontdata !is null) { + import core.stdc.stdlib : free; + free(ttfontdata); + ttfontdata = null; + ttfontdatasize = 0; + } + version(aliced) conwriteln("ERROR loading font: ", e.msg); + return FT_Err_Cannot_Open_Resource; + } + return FT_Err_Ok; + } else { + version(aliced) conwriteln("TTF CACHE: invalid font id"); + } + return FT_Err_Cannot_Open_Resource; + } +} + + +void ttfLoad () nothrow { + if (FT_Init_FreeType(&ttflibrary)) assert(0, "can't initialize FreeType"); + if (FTC_Manager_New(ttflibrary, 0, 0, 0, &ttfFontLoader, null, &ttfcache)) assert(0, "can't initialize FreeType cache manager"); + if (FTC_CMapCache_New(ttfcache, &ttfcachecmap)) assert(0, "can't initialize FreeType cache manager"); + if (FTC_ImageCache_New(ttfcache, &ttfcacheimage)) assert(0, "can't initialize FreeType cache manager"); + { + FTC_ScalerRec fsc; + fsc.face_id = FontID; + fsc.width = 0; + fsc.height = fontSize; + fsc.pixel = 1; // size in pixels + + FT_Size ttfontsz; + if (FTC_Manager_LookupSize(ttfcache, &fsc, &ttfontsz)) assert(0, "cannot find FreeType font"); + fontHeight = cast(int)ttfontsz.metrics.height>>6; // 26.6 + fontBaselineOfs = cast(int)((ttfontsz.metrics.height+ttfontsz.metrics.descender)>>6); + if (fontHeight < 2 || fontHeight > 128) assert(0, "invalid FreeType font metrics"); + } + version(aliced) conwriteln("TTF CACHE initialized."); +} + + +void initFontEngine () nothrow { + if (ttflibrary is null) { + import std.string : fromStringz, toStringz; + if (!FcInit()) assert(0, "cannot init fontconfig"); + iv.fontconfig.FcPattern* pat = FcNameParse(chiFontName.toStringz); + if (pat is null) assert(0, "cannot parse font name"); + if (!FcConfigSubstitute(null, pat, FcMatchPattern)) assert(0, "cannot find fontconfig substitute"); + FcDefaultSubstitute(pat); + // find the font + iv.fontconfig.FcResult result; + iv.fontconfig.FcPattern* font = FcFontMatch(null, pat, &result); + if (font !is null) { + char* file = null; + if (FcPatternGetString(font, FC_FILE, 0, &file) == FcResultMatch) { + version(aliced) conwriteln("font file: [", file, "]"); + chiFontFile = file.fromStringz.idup; + } + double pixelsize; + if (FcPatternGetDouble(font, FC_PIXEL_SIZE, 0, &pixelsize) == FcResultMatch) { + version(aliced) conwriteln("pixel size: ", pixelsize); + fontSize = cast(int)pixelsize; + } + } + FcPatternDestroy(pat); + // arbitrary limits + if (fontSize < 6) fontSize = 6; + if (fontSize > 42) fontSize = 42; + ttfLoad(); + } +} + + +// ////////////////////////////////////////////////////////////////////////// // +public void utfByDChar (const(char)[] s, scope void delegate (dchar ch) nothrow @trusted dg) nothrow @trusted { + if (dg is null) return; + Utf8DecoderFast dc; + foreach (char ch; s) { + if (dc.decode(cast(ubyte)ch)) dg(dc.complete ? dc.codepoint : dc.replacement); + } +} + + +public void utfByDCharSPos (const(char)[] s, scope void delegate (dchar ch, usize stpos) nothrow @trusted dg) nothrow @trusted { + if (dg is null) return; + Utf8DecoderFast dc; + usize stpos = 0; + foreach (immutable idx, char ch; s) { + if (dc.decode(cast(ubyte)ch)) { + dg(dc.complete ? dc.codepoint : dc.replacement, stpos); + stpos = idx+1; + } + } +} + + +// ////////////////////////////////////////////////////////////////////////// // +void drawFTBitmap (int x, int y, in ref FT_Bitmap bitmap, uint clr) nothrow @trusted @nogc { + if (bitmap.pixel_mode != FT_PIXEL_MODE_MONO) return; // alas + if (bitmap.rows < 1 || bitmap.width < 1) return; // nothing to do + if (gxIsTransparent(clr)) return; // just in case + // prepare + bool topdown = true; + const(ubyte)* src = bitmap.buffer; + int spt = bitmap.pitch; + if (spt < 0) { + topdown = false; + spt = -spt; + src += spt*(bitmap.rows-1); + } + if (!gxIsSolid(clr)) { + // let this be slow + foreach (immutable int dy; 0..bitmap.rows) { + ubyte count = 0, b = 0; + auto ss = src; + foreach (immutable int dx; 0..bitmap.width) { + if (count-- == 0) { count = 7; b = *ss++; } else b <<= 1; + if (b&0x80) gxPutPixel(x+dx, y, clr); + } + ++y; + if (topdown) src += spt; else src -= spt; + } + return; + } + // check if we can use the fastest path + auto brc = GxRect(x, y, bitmap.width, bitmap.rows); + if (gxClipRect.contains(brc) && GxRect(0, 0, VBufWidth, VBufHeight).contains(brc)) { + // yay, the fastest one! + uint* dptr = vglTexBuf+y*VBufWidth+x; + foreach (immutable int dy; 0..bitmap.rows) { + ubyte count = 0, b = 0; + auto ss = src; + auto curptr = dptr; + foreach (immutable int dx; 0..bitmap.width) { + if (count-- == 0) { count = 7; b = *ss++; } else b <<= 1; + if (b&0x80) *curptr = clr; + ++curptr; + } + if (topdown) src += spt; else src -= spt; + dptr += VBufWidth; + } + } else { + // do it slow + foreach (immutable int dy; 0..bitmap.rows) { + ubyte count = 0, b = 0; + auto ss = src; + foreach (immutable int dx; 0..bitmap.width) { + if (count-- == 0) { count = 7; b = *ss++; } else b <<= 1; + if (b&0x80) gxSetPixel(x+dx, y, clr); + } + ++y; + if (topdown) src += spt; else src -= spt; + } + } +} + + +// y is baseline; returns advance +int ttfDrawGlyph (bool firstchar, int scale, int x, int y, int glyphidx, uint clr) nothrow @trusted { + enum mono = true; + if (glyphidx == 0) return 0; + + FTC_ImageTypeRec fimg; + fimg.face_id = FontID; + fimg.width = 0; + fimg.height = fontSize*scale; + static if (mono) { + fimg.flags = FT_LOAD_TARGET_MONO|(gxIsTransparent(clr) ? 0 : FT_LOAD_MONOCHROME|FT_LOAD_RENDER); + } else { + fimg.flags = (gxIsTransparent(clr) ? 0 : FT_LOAD_RENDER); + } + + FT_Glyph fg; + if (FTC_ImageCache_Lookup(ttfcacheimage, &fimg, glyphidx, &fg, null)) return 0; + + int advdec = 0; + if (!gxIsTransparent(clr)) { + if (fg.format != FT_GLYPH_FORMAT_BITMAP) return 0; + FT_BitmapGlyph fgi = cast(FT_BitmapGlyph)fg; + int x0 = x+fgi.left; + if (firstchar && fgi.bitmap.width > 0) { x0 -= fgi.left; advdec = fgi.left; } + drawFTBitmap(x0, y-fgi.top, fgi.bitmap, clr); + } + return cast(int)(fg.advance.x>>16)-advdec; +} + + +int ttfGetKerning (int scale, int gl0idx, int gl1idx) nothrow @trusted { + if (gl0idx == 0 || gl1idx == 0) return 0; + + FTC_ScalerRec fsc; + fsc.face_id = FontID; + fsc.width = 0; + fsc.height = fontSize*scale; + fsc.pixel = 1; // size in pixels + + FT_Size ttfontsz; + if (FTC_Manager_LookupSize(ttfcache, &fsc, &ttfontsz)) return 0; + if (!FT_HAS_KERNING(ttfontsz.face)) return 0; + + FT_Vector kk; + if (FT_Get_Kerning(ttfontsz.face, gl0idx, gl1idx, FT_KERNING_UNSCALED, &kk)) return 0; + if (!kk.x) return 0; + auto kadvfrac = FT_MulFix(kk.x, ttfontsz.metrics.x_scale); // 1/64 of pixel + return cast(int)((kadvfrac/*+(kadvfrac < 0 ? -32 : 32)*/)>>6); +} + + +// ////////////////////////////////////////////////////////////////////////// // +public int gxCharWidthScaled (int scale, dchar ch) nothrow @trusted { + if (scale < 1) return 0; + + initFontEngine(); + + int glyph = FTC_CMapCache_Lookup(ttfcachecmap, FontID, -1, ch); + if (glyph == 0) glyph = FTC_CMapCache_Lookup(ttfcachecmap, FontID, -1, ReplacementChar); + if (glyph == 0) return 0; + + FTC_ImageTypeRec fimg; + fimg.face_id = FontID; + fimg.width = 0; + fimg.height = fontSize*scale; + fimg.flags = FT_LOAD_TARGET_MONO; + + FT_Glyph fg; + if (FTC_ImageCache_Lookup(ttfcacheimage, &fimg, glyph, &fg, null)) return -666; + + int res = cast(int)fg.advance.x>>16; + return (res > 0 ? res : 0); +} + +public int gxCharWidth (dchar ch) nothrow @trusted { return gxCharWidthScaled(1, ch); } + +// return char width +public int gxDrawChar (int x, int y, dchar ch, uint fg, int prevcp=-1) nothrow @trusted { return gxDrawCharScaled(1, x, y, ch, fg, prevcp); } +public int gxDrawChar() (in auto ref GxPoint p, char ch, uint fg, int prevcp=-1) nothrow @trusted { return gxDrawCharScaled(1, p.x, p.y, ch, fg, prevcp); } + + +// ////////////////////////////////////////////////////////////////////////// // +// return char width +public int gxDrawCharScaled (int scale, int x, int y, dchar ch, uint clr, int prevcp=-1) nothrow @trusted { + if (scale < 1) return 0; + + int glyph = FTC_CMapCache_Lookup(ttfcachecmap, FontID, -1, ch); + if (glyph == 0) glyph = FTC_CMapCache_Lookup(ttfcachecmap, FontID, -1, ReplacementChar); + if (glyph == 0) return 0; + + int kadv = ttfGetKerning(scale, (prevcp >= 0 ? FTC_CMapCache_Lookup(ttfcachecmap, FontID, -1, prevcp) : 0), glyph); + return ttfDrawGlyph(false, scale, x+kadv, y+fontBaselineOfs*scale, glyph, clr); +} + +public int gxDrawCharScaled() (int scale, in auto ref GxPoint p, char ch, uint fg, int prevcp=-1) nothrow @trusted { return gxDrawCharScaled(scale, p.x, p.y, ch, fg, prevcp); } + + +// ////////////////////////////////////////////////////////////////////////// // +public struct GxKerning { + int prevgidx = 0; + int wdt = 0; + int lastadv = 0; + int lastcw = 0; + int tabsize = 0; + int scale = 1; + bool firstchar = true; + +nothrow @trusted: + this (int atabsize, int ascale=1, bool firstCharIsFull=false) { + initFontEngine(); + firstchar = !firstCharIsFull; + scale = ascale; + if ((tabsize = (atabsize > 0 ? atabsize : 0)) != 0) tabsize = tabsize*gxCharWidthScaled(ascale, ' '); + } + + void reset (int atabsize) { + initFontEngine(); + prevgidx = 0; + wdt = 0; + lastadv = 0; + lastcw = 0; + if ((tabsize = (atabsize > 0 ? atabsize : 0)) != 0) tabsize = tabsize*gxCharWidthScaled(scale, ' '); + firstchar = true; + } + + // tab length for current position + int tablength () { pragma(inline, true); return (tabsize > 0 ? (wdt/tabsize+1)*tabsize-wdt : 0); } + + int fixWidthPre (dchar ch) { + immutable int prevgl = prevgidx; + wdt += lastadv; + lastadv = 0; + lastcw = 0; + prevgidx = 0; + if (ch == '\t' && tabsize > 0) { + // tab + lastadv = lastcw = tablength; + firstchar = false; + } else { + initFontEngine(); + int glyph = FTC_CMapCache_Lookup(ttfcachecmap, FontID, -1, ch); + if (glyph == 0) glyph = FTC_CMapCache_Lookup(ttfcachecmap, FontID, -1, ReplacementChar); + if (glyph != 0) { + wdt += ttfGetKerning(scale, prevgl, glyph); + + FTC_ImageTypeRec fimg; + fimg.face_id = FontID; + fimg.width = 0; + fimg.height = fontSize*scale; + version(none) { + fimg.flags = FT_LOAD_TARGET_MONO; + } else { + fimg.flags = FT_LOAD_TARGET_MONO|FT_LOAD_MONOCHROME|FT_LOAD_RENDER; + } + + FT_Glyph fg; + version(none) { + if (FTC_ImageCache_Lookup(ttfcacheimage, &fimg, glyph, &fg, null) == 0) { + prevgidx = glyph; + lastadv = fg.advance.x>>16; + } + } else { + if (FTC_ImageCache_Lookup(ttfcacheimage, &fimg, glyph, &fg, null) == 0) { + int advdec = 0; + if (fg.format == FT_GLYPH_FORMAT_BITMAP) { + FT_BitmapGlyph fgi = cast(FT_BitmapGlyph)fg; + if (firstchar && fgi.bitmap.width > 0) { + lastcw = fgi.bitmap.width; + advdec = fgi.left; + if (lastcw < 1) { advdec = 0; lastcw = cast(int)fg.advance.x>>16; } + } else { + lastcw = fgi.left+fgi.bitmap.width; + if (lastcw < 1) lastcw = cast(int)fg.advance.x>>16; + } + } + prevgidx = glyph; + lastadv = (cast(int)fg.advance.x>>16)-advdec; + firstchar = false; + } + } + } + } + return wdt; + } + + @property int finalWidth () const { pragma(inline, true); return wdt+/*lastadv*/lastcw; } + + // BUGGY! + @property int nextCharOfs () const { pragma(inline, true); return wdt+lastadv; } + + @property int currOfs () const { pragma(inline, true); return wdt; } + + @property int nextOfsNoSpacing () const { pragma(inline, true); return wdt+lastcw; } +} + + +// ////////////////////////////////////////////////////////////////////////// // +public struct GxDrawTextOptions { + int tabsize = 0; + uint clr = gxTransparent; + int scale = 1; + bool firstCharIsFull = false; + +static pure nothrow @safe @nogc: + auto Color (uint aclr) { pragma(inline, true); return GxDrawTextOptions(0, aclr, 1, false); } + auto Tab (int atabsize) { pragma(inline, true); return GxDrawTextOptions(atabsize, gxTransparent, 1, false); } + auto TabColor (int atabsize, uint aclr) { pragma(inline, true); return GxDrawTextOptions(atabsize, aclr, 1, false); } + auto TabColorFirstFull (int atabsize, uint aclr, bool fcf) { pragma(inline, true); return GxDrawTextOptions(atabsize, aclr, 1, fcf); } + auto ScaleTabColor (int ascale, int atabsize, uint aclr) { pragma(inline, true); return GxDrawTextOptions(atabsize, aclr, ascale, false); } + auto ColorNFC (uint aclr) { pragma(inline, true); return GxDrawTextOptions(0, aclr, 1, true); } + auto TabColorNFC (int atabsize, uint aclr) { pragma(inline, true); return GxDrawTextOptions(atabsize, aclr, 1, true); } + auto ScaleTabColorNFC (int ascale, int atabsize, uint aclr) { pragma(inline, true); return GxDrawTextOptions(atabsize, aclr, ascale, true); } + // more ctors? +} + +public struct GxDrawTextState { + usize spos; // current codepoint starting position + usize epos; // current codepoint ending position (exclusive; i.e. *after* codepoint) + int curx; // current x (before drawing the glyph) +} + + +// delegate should return color +public int gxDrawTextUtf(R) (in auto ref GxDrawTextOptions opt, int x, int y, auto ref R srng, uint delegate (in ref GxDrawTextState state) nothrow @trusted clrdg=null) nothrow @trusted +if (Imp!"std.range.primitives".isInputRange!R && is(Imp!"std.range.primitives".ElementEncodingType!R == char)) +{ + // rely on the assumption that font face won't be unloaded while we are in this function + if (opt.scale < 1) return 0; + + initFontEngine(); + + GxDrawTextState state; + + y += fontBaselineOfs*opt.scale; + + immutable int tabpix = (opt.tabsize > 0 ? opt.tabsize*gxCharWidthScaled(opt.scale, ' ') : 0); + + FT_Size ttfontsz; + + int prevglyph = 0; + immutable int stx = x; + + bool dokern = true; + bool firstchar = !opt.firstCharIsFull; + Utf8DecoderFast dc; + + while (!srng.empty) { + immutable ubyte srbyte = cast(ubyte)srng.front; + srng.popFront(); + ++state.epos; + + if (dc.decode(srbyte)) { + int ch = (dc.complete ? dc.codepoint : dc.replacement); + int pgl = prevglyph; + prevglyph = 0; + state.curx = x; + + if (opt.tabsize > 0) { + if (ch == '\t') { + firstchar = false; + int wdt = x-stx; + state.curx = x; + x += (wdt/tabpix+1)*tabpix-wdt; + if (clrdg !is null) clrdg(state); + state.spos = state.epos; + continue; + } + } + + int glyph = FTC_CMapCache_Lookup(ttfcachecmap, FontID, -1, ch); + if (glyph == 0) glyph = FTC_CMapCache_Lookup(ttfcachecmap, FontID, -1, ReplacementChar); + if (glyph != 0) { + // kerning + int kadv = 0; + if (pgl != 0 && dokern) { + if (ttfontsz is null) { + FTC_ScalerRec fsc; + fsc.face_id = FontID; + fsc.width = 0; + fsc.height = fontSize*opt.scale; + fsc.pixel = 1; // size in pixels + if (FTC_Manager_LookupSize(ttfcache, &fsc, &ttfontsz)) { + dokern = false; + ttfontsz = null; + } else { + dokern = (FT_HAS_KERNING(ttfontsz.face) != 0); + } + } + if (dokern) { + FT_Vector kk; + if (FT_Get_Kerning(ttfontsz.face, pgl, glyph, FT_KERNING_UNSCALED, &kk) == 0) { + if (kk.x) { + auto kadvfrac = FT_MulFix(kk.x, ttfontsz.metrics.x_scale); // 1/64 of pixel + kadv = cast(int)((kadvfrac/*+(kadvfrac < 0 ? -32 : 32)*/)>>6); + } + } + } + } + x += kadv; + uint clr = opt.clr; + if (clrdg !is null) { state.curx = x; clr = clrdg(state); } + x += ttfDrawGlyph(firstchar, opt.scale, x, y, glyph, clr); + firstchar = false; + } + state.spos = state.epos; + prevglyph = glyph; + } + } + + return x-stx; +} + + +public int gxDrawTextUtf() (in auto ref GxDrawTextOptions opt, int x, int y, const(char)[] s, uint delegate (in ref GxDrawTextState state) nothrow @trusted clrdg=null) nothrow @trusted { + static struct StrIterator { + private: + const(char)[] str; + public pure nothrow @trusted @nogc: + this (const(char)[] s) { pragma(inline, true); str = s; } + @property bool empty () const { pragma(inline, true); return (str.length == 0); } + @property char front () const { pragma(inline, true); return (str.length ? str.ptr[0] : 0); } + void popFront () { if (str.length) str = str[1..$]; } + } + + return gxDrawTextUtf(opt, x, y, StrIterator(s), clrdg); +} + + +public int gxTextWidthScaledUtf (int scale, const(char)[] s, int tabsize=0, bool firstCharIsFull=false) nothrow @trusted { + if (scale < 1) return 0; + auto kern = GxKerning(tabsize, scale, firstCharIsFull); + s.utfByDChar(delegate (dchar ch) @trusted { kern.fixWidthPre(ch); }); + return kern.finalWidth; +} + + + +// ////////////////////////////////////////////////////////////////////////// // +public int gxTextHeightUtf () nothrow @trusted { initFontEngine(); return fontHeight; } +public int gxTextHeightScaledUtf (int scale) nothrow @trusted { initFontEngine(); return (scale < 1 ? 0 : fontHeight*scale); } + +public int gxTextBaseLineUtf () nothrow @trusted { initFontEngine(); return fontBaselineOfs; } +public int gxTextUnderLineUtf () nothrow @trusted { initFontEngine(); return fontBaselineOfs+2; } + +public int gxTextWidthUtf (const(char)[] s, int tabsize=0, bool firstCharIsFull=false) nothrow @trusted { return gxTextWidthScaledUtf(1, s, tabsize, firstCharIsFull); } + +public int gxDrawTextUtf() (int x, int y, const(char)[] s, uint clr) nothrow @trusted { return gxDrawTextUtf(GxDrawTextOptions.Color(clr), x, y, s); } +public int gxDrawTextUtf() (in auto ref GxPoint p, const(char)[] s, uint clr) nothrow @trusted { return gxDrawTextUtf(p.x, p.y, s, clr); } + + +public int gxDrawTextOutScaledUtf (int scale, int x, int y, const(char)[] s, uint clr, uint clrout) nothrow @trusted { + if (scale < 1) return 0; + auto opt = GxDrawTextOptions.ScaleTabColor(scale, 0, clrout); + foreach (immutable dy; -1*scale..2*scale) { + foreach (immutable dx; -1*scale..2*scale) { + if (dx || dy) gxDrawTextUtf(opt, x+dx, y+dy, s); + } + } + opt.clr = clr; + return gxDrawTextUtf(opt, x, y, s); +} diff --git a/egra/gui/dialogs.d b/egra/gui/dialogs.d new file mode 100644 index 0000000..470d1d5 --- /dev/null +++ b/egra/gui/dialogs.d @@ -0,0 +1,215 @@ +/* + * Simple Framebuffer Gfx/GUI lib + * + * coded by Ketmar // Invisible Vector + * Understanding is not required. Only obedience. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License ONLY. + * + * 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, see . + */ +module iv.egra.gui.dialogs /*is aliced*/; +private: + +import arsd.simpledisplay; + +import iv.alice; +import iv.cmdcon; +import iv.strex; +import iv.utfutil; +import iv.vfs; + +import iv.egra.gfx; +import iv.egra.gui.subwindows; +import iv.egra.gui.widgets; + + +// ////////////////////////////////////////////////////////////////////////// // +public string[] buildAutoCompletion (ConString prefix) { + import std.file : DirEntry, SpanMode, dirEntries; + + if (prefix.length == 0) return null; + + ConString path, namepfx; + + if (prefix[$-1] == '/') { + path = prefix; + namepfx = null; + } else { + auto lspos = prefix.lastIndexOf('/'); + if (lspos < 0) { + path = null; + namepfx = prefix; + } else { + path = prefix[0..lspos+1]; + namepfx = prefix[lspos+1..$]; + } + } + + //conwriteln("path=[", path, "]; namepfx=[", namepfx, "]"); + + string[] res; + foreach (DirEntry de; dirEntries(path.idup, SpanMode.shallow)) { + if (namepfx.length != 0) { + import std.path : baseName; + //conwriteln(" [", de.baseName, "]"); + if (!de.baseName.startsWith(namepfx)) continue; + } + try { + if (de.isDir) res ~= de.name~"/"; else res ~= de.name; + } catch (Exception e) {} + } + + if (res.length > 1) { + import std.algorithm : sort; + sort(res); + } + + return res; +} + + +// ////////////////////////////////////////////////////////////////////////// // +public class SelectCompletionWindow : SubWindow { + SimpleListBoxWidget lb; + string[] list; + + void delegate (string str) onSelected; + + this (const(char)[] prefix, string[] clist, bool aspath) { + createRoot(); + int xhgt = 0; + int xwdt = 0; + bool idxset = false; + lb = new SimpleListBoxWidget(rootWidget); + foreach (string s; clist) { + if (aspath && s.length) { + usize lspos = s.length; + if (s[$-1] == '/') --lspos; + while (lspos > 0 && s.ptr[lspos-1] != '/') --lspos; + s = s[lspos..$]; + } + lb.appendItem(s); + if (s == prefix) { idxset = true; lb.curidx = lb.length-1; } + if (!idxset && s.startsWith(prefix)) { idxset = true; lb.curidx = lb.length-1; } + int w = gxTextWidthUtf(s)+2; + if (xwdt < w) xwdt = w; + xhgt += gxTextHeightUtf; + } + + if (xhgt == 0) { super(); return; } + if (xhgt > screenHeight) xhgt = screenHeight-decorationSizeY; + + if (xwdt > screenWidth-decorationSizeX) xwdt = screenWidth-decorationSizeX; + + super("Select Completion", GxSize(xwdt+decorationSizeX, xhgt+decorationSizeY)); + lb.width = clientWidth; + lb.height = clientHeight; + list = clist; + + lb.onAction = delegate (self) { + if (onSelected !is null && lb.curidx >= 0 && lb.curidx < list.length) { + close(); + onSelected(list[lb.curidx]); + return; + } + vbwin.beep(); + }; + + addModal(); + } + + override bool onKeyBubble (KeyEvent event) { + if (event.pressed) { + if (event == "Escape" || event == "C-Q") { close(); return true; } + if (event == "Enter") { lb.onAction(lb); return true; } + } + return super.onKeyBubble(event); + } +} + + +// ////////////////////////////////////////////////////////////////////////// // +public class YesNoWindow : SubWindow { +public: + ButtonWidget yes, no; + + void delegate () onYes; + void delegate () onNo; + +protected: + string msgMessage; + bool defaction = true; + +public: + this (string atitle, string amessage, bool adefaction=true) { + msgMessage = amessage; + defaction = adefaction; + super(atitle); + } + + override void createWidgets () { + new SpacerWidget(rootWidget, 8); + + auto msgbox = new HBoxWidget(rootWidget); + new SpacerWidget(msgbox, 4); + with (new LabelWidget(msgbox, msgMessage, LabelWidget.HAlign.Center, LabelWidget.VAlign.Center)) { + flex = 1; + } + new SpacerWidget(msgbox, 4); + + new SpacerWidget(rootWidget, 8); + + auto btnbox = new HBoxWidget(rootWidget); + new SpacerWidget(btnbox, 2); + new SpringWidget(btnbox, 1); + yes = new ButtonExWidget(btnbox, "&Yes"); + new SpacerWidget(btnbox, 4); + no = new ButtonExWidget(btnbox, "&No"); + new SpringWidget(btnbox, 1); + new SpacerWidget(btnbox, 2); + + new SpacerWidget(rootWidget, 2); + + int bwdt = yes.width; + if (bwdt < no.width) bwdt = no.width; + if (bwdt < 96) bwdt = 96; + + yes.width = bwdt; + no.width = bwdt; + + relayoutResize(); + centerWindow(); + + if (defaction) yes.focus(); else no.focus(); + + yes.onAction = delegate (self) { + close(); + if (onYes !is null) onYes(); + }; + + no.onAction = delegate (self) { + close(); + if (onNo !is null) onNo(); + }; + } + + override void finishCreating () { + addModal(); + } + + override bool onKeyBubble (KeyEvent event) { + if (event.pressed) { + if (event == "Escape" || event == "C-Q") { no.doAction(); return true; } + } + return super.onKeyBubble(event); + } +} diff --git a/egra/gui/editor.d b/egra/gui/editor.d new file mode 100644 index 0000000..87aaa52 --- /dev/null +++ b/egra/gui/editor.d @@ -0,0 +1,1085 @@ +/* Invisible Vector Library + * simple FlexBox-based TUI engine + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License ONLY. + * + * 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, see . + */ +module iv.egra.gui.editor /*is aliced*/; + +import arsd.simpledisplay; + +import iv.alice; +import iv.cmdcon; +import iv.strex; +import iv.utfutil; +import iv.vfs; + +import iv.egeditor.editor; +//import iv.egeditor.highlighters; + +import iv.egra.gfx; + +import iv.egra.gui.subwindows; +import iv.egra.gui.widgets; +import iv.egra.gui.dialogs; + + +// ////////////////////////////////////////////////////////////////////////// // +final class ChiTextMeter : EgTextMeter { + GxKerning twkern; + + //int currofs; /// x offset for current char (i.e. the last char that was passed to `advance()` should be drawn with this offset) + //int currwdt; /// current line width (including, the last char that was passed to `advance()`), preferably without trailing empty space between chars + //int currheight; /// current text height; keep this in sync with the current state; `reset` should set it to "default text height" + + /// this should reset text width iterator (and curr* fields); tabsize > 0: process tabs as... well... tabs ;-) + override void reset (int tabsize) nothrow { + twkern.reset(tabsize); + currheight = gxTextHeightUtf; + } + + /// advance text width iterator, return x position for drawing next char + override void advance (dchar ch, in ref GapBuffer.HighState hs) nothrow { + twkern.fixWidthPre(ch); + currofs = twkern.currOfs; + currwdt = twkern.nextOfsNoSpacing; + } + + /// finish text iterator; it should NOT reset curr* fields! + /// WARNING: EditorEngine tries to call this after each `reset()`, but user code may not + override void finish () nothrow { + //return twkern.finalWidth; + } +} + + +// ////////////////////////////////////////////////////////////////////////// // +final class TextEditor : EditorEngine { + enum TEDSingleOnly; // only for single-line mode + enum TEDMultiOnly; // only for multiline mode + enum TEDEditOnly; // only for non-readonly mode + enum TEDROOnly; // only for readonly mode + + static struct TEDKey { string key; string help; bool hidden; } // UDA + + static string TEDImplX(string key, string help, string code, size_t ln) () { + static assert(key.length > 0, "wtf?!"); + static assert(code.length > 0, "wtf?!"); + string res = "@TEDKey("~key.stringof~", "~help.stringof~") void _ted_"; + int pos = 0; + while (pos < key.length) { + char ch = key[pos++]; + if (key.length-pos > 0 && key[pos] == '-') { + if (ch == 'C' || ch == 'c') { ++pos; res ~= "Ctrl"; continue; } + if (ch == 'M' || ch == 'm') { ++pos; res ~= "Alt"; continue; } + if (ch == 'S' || ch == 's') { ++pos; res ~= "Shift"; continue; } + } + if (ch == '^') { res ~= "Ctrl"; continue; } + if (ch >= 'a' && ch <= 'z') ch -= 32; + if ((ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || ch == '_') res ~= ch; else res ~= '_'; + } + res ~= ln.stringof; + res ~= " () {"~code~"}"; + return res; + } + + mixin template TEDImpl(string key, string help, string code, size_t ln=__LINE__) { + mixin(TEDImplX!(key, help, code, ln)); + } + + mixin template TEDImpl(string key, string code, size_t ln=__LINE__) { + mixin(TEDImplX!(key, "", code, ln)); + } + +protected: + KeyEvent[32] comboBuf; + int comboCount; // number of items in `comboBuf` + Widget pw; + + ChiTextMeter chiTextMeter; + +public: + this (Widget apw, int x0, int y0, int w, int h, bool asinglesine=false) { + pw = apw; + //coordsInPixels = true; + lineHeightPixels = gxTextHeightUtf; + chiTextMeter = new ChiTextMeter(); + super(x0, y0, w, h, null, asinglesine); + textMeter = chiTextMeter; + utfuck = true; + visualtabs = true; + tabsize = 4; // spaces + } + + final int lineQuoteLevel (int lidx) { + if (lidx < 0 || lidx >= lc.linecount) return 0; + int pos = lc.line2pos(lidx); + auto ts = gb.textsize; + if (pos >= ts) return 0; + if (gb[pos] != '>') return 0; + int count = 1; + ++pos; + while (pos < ts) { + char ch = gb[pos++]; + if (ch == '>') ++count; + else if (ch == '\n') break; + else if (ch != ' ') break; + } + return count; + } + + public override void drawCursor () { + if (pw.isFocused) { + int lcx, lcy; + localCursorXY(&lcx, &lcy); + drawTextCursor(/*pw.parent.isFocused*/true, x0+lcx, y0+lcy); + } + } + + // not here, 'cause this is done before text + public override void drawStatus () {} + + public final paintStatusLine () { + import core.stdc.stdio : snprintf; + int sx = x0, sy = y0+height; + gxFillRect(sx, sy, width, gxTextHeightUtf, pw.getColor("status-back")); + char[128] buf = void; + auto len = snprintf(buf.ptr, buf.length, "%04d:%04d %d", curx, cury, linecount); + gxDrawTextUtf(sx+2, sy, buf[0..len], pw.getColor("status-text")); + } + + public override void drawPage () { + fullDirty(); // HACK! + gxWithSavedClip{ + gxClipRect.intersect(GxRect(x0, y0, width, height)); + super.drawPage(); + }; + if (!singleline) paintStatusLine(); + } + + + public override void drawLine (int lidx, int yofs, int xskip) { + int x = x0-xskip; + int y = y0+yofs; + auto pos = lc.line2pos(lidx); + auto lea = lc.line2pos(lidx+1); + auto ts = gb.textsize; + bool utfucked = utfuck; + immutable int bs = bstart, be = bend; + immutable ls = pos; + + uint clr = pw.getColor("text"); + immutable uint markFgClr = pw.getColor("mark-text"); + immutable uint markBgClr = pw.getColor("mark-back"); + + if (!singleline) { + int qlevel = lineQuoteLevel(lidx); + if (qlevel) { + final switch (qlevel%2) { + case 0: clr = pw.getColor("quote0-text"); break; + case 1: clr = pw.getColor("quote1-text"); break; + } + } else { + char[1024] abuf = void; + auto aname = getAttachName(abuf[], lidx); + if (aname.length) { + try { + import std.file : exists, isFile; + clr = (aname.exists && aname.isFile ? pw.getColor("attach-file-text") : pw.getColor("attach-bad-text")); + } catch (Exception e) {} + } + } + } + + bool checkMarking = false; + if (hasMarkedBlock) { + //conwriteln("bs=", bs, "; be=", be, "; pos=", pos, "; lea=", lea); + if (pos >= bs && lea <= be) { + // full line + gxFillRect(x0, y, winw, lineHeightPixels, markBgClr); + clr = markFgClr; + } else if (pos < be && lea > bs) { + // draw block background + auto tpos = pos; + int bx0 = x, bx1 = x; + chiTextMeter.twkern.reset(visualtabs ? tabsize : 0); + while (tpos < lea) { + immutable dchar dch = dcharAtAdvance(tpos); + if (!singleline && dch == '\n') break; + chiTextMeter.twkern.fixWidthPre(dch); + if (tpos > bs) break; + } + bx0 = bx1 = x+chiTextMeter.twkern.currOfs; + bool eolhit = false; + while (tpos < lea) { + if (tpos >= be) break; + immutable dchar dch = dcharAtAdvance(tpos); + if (!singleline && dch == '\n') { eolhit = (tpos < be); break; } + chiTextMeter.twkern.fixWidthPre(dch); + if (tpos > be) break; + } + bx1 = (eolhit ? x0+width : x+chiTextMeter.twkern.finalWidth); + gxFillRect(bx0, y, bx1-bx0+1, lineHeightPixels, markBgClr); + checkMarking = true; + } + } + if (singleline && hasMarkedBlock) checkMarking = true; // let it be + + //twkern.reset(visualtabs ? tabsize : 0); + uint cc = clr; + + int epos = pos; + if (singleline) { + epos = ts; + } else { + while (epos < ts) if (gb[epos++] == '\n') { --epos; break; } + } + + //FIXME: tabsize + int wdt = gxDrawTextUtf(GxDrawTextOptions.Tab(tabsize), x, y, this[pos..epos], delegate (in ref state) nothrow @trusted { + return (checkMarking ? (pos+state.spos >= bs && pos+state.epos <= be ? markFgClr : clr) : clr); + }); + if (!singleline) { + if (epos > pos && gb[epos-1] == ' ') { + gxDrawChar(x+wdt, y, '\u2248', pw.getColor("wrap-mark-text")); + } + } + + /* + while (pos < ts) { + if (checkMarking) cc = (pos >= bs && pos < be ? markFgClr : clr); + immutable dchar dch = dcharAtAdvance(pos); + if (!singleline && dch == '\n') { + // draw "can wrap" mark + if (pos-2 >= ls && gb[pos-2] == ' ') { + int xx = x+twkern.finalWidth+1; + gxDrawChar(xx, y, '\n', gxRGB!(0, 0, 220)); + } + break; + } + if (dch == '\t' && visualtabs) { + int xx = x+twkern.fixWidthPre('\t'); + gxHLine(xx, y+lineHeightPixels/2, twkern.tablength, gxRGB!(200, 0, 0)); + } else { + gxDrawChar(x+twkern.fixWidthPre(dch), y, dch, cc); + } + } + */ + } + + // just clear line + // use `winXXX` vars to know window dimensions + public override void drawEmptyLine (int yofs) { + // nothing to do here + } + + protected enum Ecc { None, Eaten, Combo } + + // None: not valid + // Eaten: exact hit + // Combo: combo start + // comboBuf should contain comboCount keys! + protected final Ecc checkKeys (const(char)[] keys) { + foreach (immutable cidx; 0..comboCount+1) { + keys = keys.xstrip; + if (keys.length == 0) return Ecc.Combo; + usize kepos = 0; + while (kepos < keys.length && keys.ptr[kepos] > ' ') ++kepos; + if (comboBuf[cidx] != keys[0..kepos]) return Ecc.None; + keys = keys[kepos..$]; + } + return (keys.xstrip.length ? Ecc.Combo : Ecc.Eaten); + } + + // fuck! `(this ME)` trick doesn't work here + protected final Ecc doEditorCommandByUDA(ME=typeof(this)) (KeyEvent key) { + import std.traits; + bool possibleCombo = false; + // temporarily add current key to combo + comboBuf[comboCount] = key; + // check all known combos + foreach (string memn; __traits(allMembers, ME)) { + static if (is(typeof(&__traits(getMember, ME, memn)))) { + import std.meta : AliasSeq; + alias mx = AliasSeq!(__traits(getMember, ME, memn))[0]; + static if (isCallable!mx && hasUDA!(mx, TEDKey)) { + // check modifiers + bool goodMode = true; + static if (hasUDA!(mx, TEDSingleOnly)) { if (!singleline) goodMode = false; } + static if (hasUDA!(mx, TEDMultiOnly)) { if (singleline) goodMode = false; } + static if (hasUDA!(mx, TEDEditOnly)) { if (readonly) goodMode = false; } + static if (hasUDA!(mx, TEDROOnly)) { if (!readonly) goodMode = false; } + if (goodMode) { + foreach (const TEDKey attr; getUDAs!(mx, TEDKey)) { + auto cc = checkKeys(attr.key); + if (cc == Ecc.Eaten) { + // hit + static if (is(ReturnType!mx == void)) { + comboCount = 0; // reset combo + mx(); + return Ecc.Eaten; + } else { + if (mx()) { + comboCount = 0; // reset combo + return Ecc.Eaten; + } + } + } else if (cc == Ecc.Combo) { + possibleCombo = true; + } + } + } + } + } + } + // check if we can start/continue combo + if (possibleCombo) { + if (++comboCount < comboBuf.length-1) return Ecc.Combo; + } + // if we have combo prefix, eat key unconditionally + if (comboCount > 0) { + comboCount = 0; // reset combo, too long, or invalid, or none + return Ecc.Eaten; + } + return Ecc.None; + } + + bool processKey (KeyEvent key) { + final switch (doEditorCommandByUDA(key)) { + case Ecc.None: break; + case Ecc.Combo: + case Ecc.Eaten: + return true; + } + + return false; + } + + bool processChar (dchar ch) { + if (ch < ' ' || ch == 127) return false; + if (ch == ' ') { + // check if we should reformat + auto llen = linelen(cury); + if (llen >= 76) { + if (curx == 0 || gb[curpos-1] != ' ') doPutChar(' '); + auto ocx = curx; + auto ocy = cury; + // normalize ocx + { + int rpos = lc.linestart(ocy); + int qcnt = 0; + if (gb[rpos] == '>') { + while (gb[rpos] == '>') { ++qcnt; ++rpos; } + if (gb[rpos] == ' ') { ++qcnt; ++rpos; } + } + //conwriteln("origcx=", ocx, "; ncx=", ocx-qcnt, "; qcnt=", qcnt); + if (ocx < qcnt) ocx = qcnt; else ocx -= qcnt; + } + reformatFromLine!true(cury); + // position cursor + while (ocy < lc.linecount) { + int rpos = lc.linestart(ocy); + llen = linelen(ocy); + //conwriteln(" 00: ocy=", ocy, "; llen=", llen, "; ocx=", ocx); + int qcnt = 0; + if (gb[rpos] == '>') { + while (gb[rpos] == '>') { --llen; ++qcnt; ++rpos; } + if (gb[rpos] == ' ') { --llen; ++qcnt; ++rpos; } + } + //conwriteln(" 01: ocy=", ocy, "; llen=", llen, "; ocx=", ocx, "; qcnt=", qcnt); + if (ocx <= llen) { ocx += qcnt; break; } + ocx -= llen; + ++ocy; + } + gotoXY(ocx, ocy); + return true; + } + } + doPutDChar(ch); + return true; + } + + bool processClick (int x, int y, MouseEvent event) { + if (x < 0 || y < 0 || x >= winw || y >= winh) return false; + if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.left) { + int tx, ty; + widget2text(x, y, tx, ty); + gotoXY(tx, ty); + return true; + } + return false; + } + +final: + void processWordWith (scope char delegate (char ch) dg) { + if (dg is null) return; + bool undoAdded = false; + scope(exit) if (undoAdded) undoGroupEnd(); + auto pos = curpos; + if (!isWordChar(gb[pos])) return; + // find word start + while (pos > 0 && isWordChar(gb[pos-1])) --pos; + while (pos < gb.textsize) { + auto ch = gb[pos]; + if (!isWordChar(gb[pos])) break; + auto nc = dg(ch); + if (ch != nc) { + if (!undoAdded) { undoAdded = true; undoGroupStart(); } + replaceText!"none"(pos, 1, (&nc)[0..1]); + } + ++pos; + } + gotoPos(pos); + } + +final: + @TEDMultiOnly mixin TEDImpl!("Up", q{ doUp(); }); + @TEDMultiOnly mixin TEDImpl!("S-Up", q{ doUp(true); }); + @TEDMultiOnly mixin TEDImpl!("C-Up", q{ doScrollUp(); }); + @TEDMultiOnly mixin TEDImpl!("S-C-Up", q{ doScrollUp(true); }); + + @TEDMultiOnly mixin TEDImpl!("Down", q{ doDown(); }); + @TEDMultiOnly mixin TEDImpl!("S-Down", q{ doDown(true); }); + @TEDMultiOnly mixin TEDImpl!("C-Down", q{ doScrollDown(); }); + @TEDMultiOnly mixin TEDImpl!("S-C-Down", q{ doScrollDown(true); }); + + mixin TEDImpl!("Left", q{ doLeft(); }); + mixin TEDImpl!("S-Left", q{ doLeft(true); }); + mixin TEDImpl!("C-Left", q{ doWordLeft(); }); + mixin TEDImpl!("S-C-Left", q{ doWordLeft(true); }); + + mixin TEDImpl!("Right", q{ doRight(); }); + mixin TEDImpl!("S-Right", q{ doRight(true); }); + mixin TEDImpl!("C-Right", q{ doWordRight(); }); + mixin TEDImpl!("S-C-Right", q{ doWordRight(true); }); + + @TEDMultiOnly mixin TEDImpl!("PageUp", q{ doPageUp(); }); + @TEDMultiOnly mixin TEDImpl!("S-PageUp", q{ doPageUp(true); }); + @TEDMultiOnly mixin TEDImpl!("C-PageUp", q{ doTextTop(); }); + @TEDMultiOnly mixin TEDImpl!("S-C-PageUp", q{ doTextTop(true); }); + + @TEDMultiOnly mixin TEDImpl!("PageDown", q{ doPageDown(); }); + @TEDMultiOnly mixin TEDImpl!("S-PageDown", q{ doPageDown(true); }); + @TEDMultiOnly mixin TEDImpl!("C-PageDown", q{ doTextBottom(); }); + @TEDMultiOnly mixin TEDImpl!("S-C-PageDown", q{ doTextBottom(true); }); + + mixin TEDImpl!("Home", q{ doHome(); }); + mixin TEDImpl!("S-Home", q{ doHome(true, true); }); + @TEDMultiOnly mixin TEDImpl!("C-Home", q{ doPageTop(); }); + @TEDMultiOnly mixin TEDImpl!("S-C-Home", q{ doPageTop(true); }); + + mixin TEDImpl!("End", q{ doEnd(); }); + mixin TEDImpl!("S-End", q{ doEnd(true); }); + @TEDMultiOnly mixin TEDImpl!("C-End", q{ doPageBottom(); }); + @TEDMultiOnly mixin TEDImpl!("S-C-End", q{ doPageBottom(true); }); + + @TEDEditOnly mixin TEDImpl!("Backspace", q{ doBackspace(); }); + @TEDSingleOnly @TEDEditOnly mixin TEDImpl!("M-Backspace", "delete previous word", q{ doDeleteWord(); }); + @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("M-Backspace", "delete previous word or unindent", q{ doBackByIndent(); }); + + mixin TEDImpl!("Delete", q{ + doDelete(); + }); + + //mixin TEDImpl!("^Insert", "copy block to clipboard file, reset block mark", q{ if (tempBlockFileName.length == 0) return; doBlockWrite(tempBlockFileName); doBlockResetMark(); }); + + @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("Enter", q{ + auto ly = cury; + if (lineQuoteLevel(ly)) { + doLineSplit(false); // no autoindent + auto ls = lc.linestart(ly); + undoGroupStart(); + scope(exit) undoGroupEnd(); + while (gb[ls] == '>') { + ++ls; + insertText!"end"(curpos, ">"); + } + if (gb[curpos] > ' ') insertText!"end"(curpos, " "); + } else { + doLineSplit(); + } + }); + @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("M-Enter", "split line without autoindenting", q{ doLineSplit(false); }); + + + mixin TEDImpl!("F3", "start/stop/reset block marking", q{ doToggleBlockMarkMode(); }); + mixin TEDImpl!("C-F3", "reset block mark", q{ doBlockResetMark(); }); + @TEDEditOnly mixin TEDImpl!("F5", "copy block", q{ doBlockCopy(); }); + // mixin TEDImpl!("^F5", "copy block to clipboard file", q{ if (tempBlockFileName.length == 0) return; doBlockWrite(tempBlockFileName); }); + //@TEDEditOnly mixin TEDImpl!("S-F5", "insert block from clipboard file", q{ if (tempBlockFileName.length == 0) return; waitingInF5 = true; }); + @TEDEditOnly mixin TEDImpl!("F6", "move block", q{ doBlockMove(); }); + @TEDEditOnly mixin TEDImpl!("F8", "delete block", q{ doBlockDelete(); }); + + mixin TEDImpl!("C-A", "move to line start", q{ doHome(); }); + mixin TEDImpl!("C-E", "move to line end", q{ doEnd(); }); + + @TEDMultiOnly mixin TEDImpl!("M-I", "jump to previous bookmark", q{ doBookmarkJumpUp(); }); + @TEDMultiOnly mixin TEDImpl!("M-J", "jump to next bookmark", q{ doBookmarkJumpDown(); }); + @TEDMultiOnly mixin TEDImpl!("M-K", "toggle bookmark", q{ doBookmarkToggle(); }); + + @TEDEditOnly mixin TEDImpl!("M-C", "capitalize word", q{ + bool first = true; + processWordWith((char ch) { + if (first) { first = false; ch = ch.toupper; } + return ch; + }); + }); + @TEDEditOnly mixin TEDImpl!("M-Q", "lowercase word", q{ processWordWith((char ch) => ch.tolower); }); + @TEDEditOnly mixin TEDImpl!("M-U", "uppercase word", q{ processWordWith((char ch) => ch.toupper); }); + + @TEDMultiOnly mixin TEDImpl!("M-S-L", "force center current line", q{ makeCurLineVisibleCentered(true); }); + @TEDEditOnly mixin TEDImpl!("C-U", "undo", q{ doUndo(); }); + @TEDEditOnly mixin TEDImpl!("M-S-U", "redo", q{ doRedo(); }); + @TEDEditOnly mixin TEDImpl!("C-W", "remove previous word", q{ doDeleteWord(); }); + @TEDEditOnly mixin TEDImpl!("C-Y", "remove current line", q{ doKillLine(); }); + + //@TEDMultiOnly @TEDEditOnly mixin TEDImpl!("Tab", q{ doPutText(" "); }); + @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-Tab", "indent block", q{ doIndentBlock(); }); + @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-S-Tab", "unindent block", q{ doUnindentBlock(); }); + + @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K C-I", "indent block", q{ doIndentBlock(); }); + @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K C-U", "unindent block", q{ doUnindentBlock(); }); + @TEDEditOnly mixin TEDImpl!("C-K C-E", "clear from cursor to EOL", q{ doKillToEOL(); }); + @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K Tab", "indent block", q{ doIndentBlock(); }); + // @TEDEditOnly mixin TEDImpl!("^K M-Tab", "untabify", q{ doUntabify(gb.tabsize ? gb.tabsize : 2); }); // alt+tab: untabify + // @TEDEditOnly mixin TEDImpl!("^K C-space", "remove trailing spaces", q{ doRemoveTailingSpaces(); }); + // mixin TEDImpl!("C-K C-T", /*"toggle \"visual tabs\" mode",*/ q{ visualtabs = !visualtabs; }); + + @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K C-B", q{ doSetBlockStart(); }); + @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K C-K", q{ doSetBlockEnd(); }); + + @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K C-C", q{ doBlockCopy(); }); + @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K C-M", q{ doBlockMove(); }); + @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K C-Y", q{ doBlockDelete(); }); + @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K C-H", q{ doBlockResetMark(); }); + // fuckin' vt100! + @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K Backspace", q{ doBlockResetMark(); }); + + @TEDEditOnly mixin TEDImpl!("C-Q Tab", q{ doPutChar('\t'); }); + //mixin TEDImpl!("C-Q C-U", "toggle utfuck mode", q{ utfuck = !utfuck; }); // ^Q^U: switch utfuck mode + //mixin TEDImpl!("C-Q 1", "switch to koi8", q{ utfuck = false; codepage = CodePage.koi8u; fullDirty(); }); + //mixin TEDImpl!("C-Q 2", "switch to cp1251", q{ utfuck = false; codepage = CodePage.cp1251; fullDirty(); }); + //mixin TEDImpl!("C-Q 3", "switch to cp866", q{ utfuck = false; codepage = CodePage.cp866; fullDirty(); }); + mixin TEDImpl!("C-Q C-B", "go to block start", q{ if (hasMarkedBlock) gotoPos!true(bstart); lastBGEnd = false; }); + + @TEDSingleOnly @TEDEditOnly mixin TEDImpl!("C-Q Enter", "intert LF", q{ doPutChar('\n'); }); + @TEDSingleOnly @TEDEditOnly mixin TEDImpl!("C-Q M-Enter", "intert CR", q{ doPutChar('\r'); }); + + mixin TEDImpl!("C-Q C-K", "go to block end", q{ if (hasMarkedBlock) gotoPos!true(bend); lastBGEnd = true; }); + + @TEDMultiOnly @TEDROOnly mixin TEDImpl!("Space", q{ doPageDown(); }); + @TEDMultiOnly @TEDROOnly mixin TEDImpl!("S-Space", q{ doPageUp(); }); + +final: + string[] extractAttaches () { + string[] res; + int lidx = 0; + lineloop: while (lidx < lc.linecount) { + // find "@@" + auto pos = lc.linestart(lidx); + //conwriteln("checking line ", lidx, ": '", gb[pos], "'"); + while (pos < gb.textsize) { + char ch = gb[pos]; + if (ch == '@' && gb[pos+1] == '@') { pos += 2; break; } // found + if (ch > ' ' || ch == '\n') { ++lidx; continue lineloop; } // next line + ++pos; + } + //conwriteln("found \"@@\" at line ", lidx); + // skip spaces after "@@" + while (pos < gb.textsize) { + char ch = gb[pos]; + if (ch == '\n') { ++lidx; continue lineloop; } // next line + if (ch > ' ') break; + ++pos; + } + if (pos >= gb.textsize) break; // no more text + // extract file name + string fname; + while (pos < gb.textsize) { + char ch = gb[pos]; + if (ch == '\n') break; + fname ~= ch; + ++pos; + } + if (fname.length) { + //conwriteln("got fname: '", fname, "'"); + res ~= fname; + } + // remove this line + int ls = lc.linestart(lidx); + int le = lc.linestart(lidx+1); + if (ls < le) deleteText!"start"(ls, le-ls); + } + return res; + } + + char[] getAttachName (char[] dest, int lidx) { + if (lidx < 0 || lidx >= lc.linecount) return null; + // find "@@" + auto pos = lc.linestart(lidx); + while (pos < gb.textsize) { + char ch = gb[pos]; + if (ch == '@' && gb[pos+1] == '@') { pos += 2; break; } // found + if (ch > ' ' || ch == '\n') return null; // not found + ++pos; + } + // skip spaces after "@@" + while (pos < gb.textsize) { + char ch = gb[pos]; + if (ch == '\n') return null; // not found + if (ch > ' ') break; + ++pos; + } + if (pos >= gb.textsize) return null; // not found + // extract file name + usize dpos = 0; + while (pos < gb.textsize) { + char ch = gb[pos]; + if (ch == '\n') break; + if (dpos >= dest.length) return null; + dest.ptr[dpos++] = ch; + ++pos; + } + return dest.ptr[0..dpos]; + } + + // ah, who cares about speed? + void reformatFromLine(bool doUndoGroup) (int lidx) { + if (lidx < 0) lidx = 0; + if (lidx >= lc.linecount) return; + + static if (doUndoGroup) { + bool undoGroupStarted = false; + scope(exit) if (undoGroupStarted) undoGroupEnd(); + + void startUndoGroup () { + if (!undoGroupStarted) { + undoGroupStarted = true; + undoGroupStart(); + } + } + } else { + void startUndoGroup () {} + } + + void normalizeQuoting (int lidx) { + while (lidx < lc.linecount) { + auto pos = lc.linestart(lidx); + if (gb[pos] == '>') { + bool lastWasSpace = false; + auto stpos = pos; + auto afterlastq = pos; + while (pos < gb.textsize) { + char ch = gb[pos]; + if (ch == '\n') break; + if (ch == '>') { afterlastq = ++pos; continue; } + if (ch == ' ') { ++pos; continue; } + break; + } + pos = stpos; + while (pos < afterlastq) { + char ch = gb[pos]; + assert(ch != '\n'); + if (ch == ' ') { deleteText(pos, 1); --afterlastq; continue; } // remove space (thus normalizing quotes) + assert(ch == '>'); + ++pos; + } + assert(pos == afterlastq); + if (pos < gb.textsize && gb[pos] > ' ') { startUndoGroup(); insertText!("none", false)(pos, " "); } + } + ++lidx; + } + } + + // try to join two lines, if it is possible; return `true` if succeed + bool tryJoinLines (int lidx) { + assert(lidx >= 0); + if (lc.linecount == 1) return false; // nothing to do + if (lidx+1 >= lc.linecount) return false; // nothing to do + auto ql0 = lineQuoteLevel(lidx); + auto ql1 = lineQuoteLevel(lidx+1); + if (ql0 != ql1) return false; // different quote levels, can't join + auto ls = lc.linestart(lidx); + auto le = lc.lineend(lidx); + if (le-ls < 1) return false; // wtf?! + if (gb[le-1] != ' ') return false; // no trailing space -- can't join + // don't join if next line is empty one: this is prolly paragraph delimiter + if (le+1 >= gb.textsize || gb[le+1] == '\n') return false; + if (gb[le+1] == '>') { + int pp = le+1; + while (pp < gb.textsize) { + char ch = gb[pp]; + if (ch != '>' && ch != ' ') break; + ++pp; + } + if (gb[pp-1] != ' ') return false; + } + // remove newline + startUndoGroup(); + deleteText(le, 1); + // remove excessive spaces, if any + while (le > ls && gb[le-1] == ' ') --le; + assert(gb[le] == ' '); + ++le; // but leave one space + while (le < gb.textsize) { + if (gb[le] == '\n' || gb[le] > ' ') break; + deleteText(le, 1); + } + // remove quoting + if (ql0) { + assert(le < gb.textsize && gb[le] == '>'); + while (le < gb.textsize) { + char ch = gb[le]; + if (ch == '\n' || (ch != '>' && ch != ' ')) break; + deleteText(le, 1); + } + } + return true; // yay + } + + // join lines; we'll split 'em later + for (int l = lidx; l < lc.linecount; ) if (!tryJoinLines(l)) ++l; + + // make quoting consistent + normalizeQuoting(lidx); + + void conwrlinerest (int pos) { + if (pos < 0 || pos >= gb.textsize) return; + while (pos < gb.textsize) { + char ch = gb[pos++]; + if (ch == '\n') break; + conwrite(ch); + } + } + + void conwrline (int lidx) { + if (lidx < 0 || lidx >= lc.linecount) return; + conwrlinerest(lc.linestart(lidx)); + } + + // now split the lines; all lines are joined, so we can only split + lineloop: while (lidx < lc.linecount) { + auto ls = lc.linestart(lidx); + auto le = lc.lineend(lidx); + // calculate line length without trailing spaces + auto llen = linelen(lidx); + { + auto pe = le; + while (pe > ls && gb[pe-1] <= ' ') { --pe; --llen; } + } + if (llen <= 76) { ++lidx; continue; } // nothing to do here + // need to wrap it + auto pos = ls; + int curlen = 0; + // skip quotes, if any + while (gb[pos] == '>') { ++curlen; ++pos; } + // skip leading spaces + while (gb[pos] != '\n' && gb[pos] <= ' ') { ++curlen; ++pos; } + if (pos >= gb.textsize || gb[pos] == '\n') { ++lidx; continue; } // wtf?! + int lwstart = -1; + bool inword = true; + while (pos < gb.textsize) { + immutable stpos = pos; + dchar ch = dcharAtAdvance(pos); + ++curlen; + if (inword) { + if (ch <= ' ') { + if (lwstart >= 0 && curlen > 76) { + // wrap + startUndoGroup(); + insertText!("none", false)(lwstart, "\n"); + ++lwstart; + if (gb[ls] == '>') { + while (gb[ls] == '>') { + insertText!("none", false)(lwstart, ">"); + ++lwstart; + ++ls; + } + insertText!("none", false)(lwstart, " "); + } + ++lidx; + continue lineloop; + } + if (ch == '\n') break; + // not in word anymore + inword = false; + } + } else { + if (ch == '\n') break; + if (ch > ' ') { lwstart = stpos; inword = true; } + } + } + ++lidx; + } + } +} + + +// ////////////////////////////////////////////////////////////////////////// // +struct QuoteInfo { + int level; // quote level + int length; // quote prefix length, in chars +} + +QuoteInfo calcQuote(T:const(char)[]) (T s) { + static if (is(T == typeof(null))) { + return QuoteInfo(); + } else { + QuoteInfo qi; + if (s.length > 0 && s[0] == '>') { + while (qi.length < s.length) { + if (s[qi.length] != ' ') { + if (s[qi.length] != '>') break; + ++qi.level; + } + ++qi.length; + } + if (s.length-qi.length > 1 && s[qi.length] == ' ') ++qi.length; + } + return qi; + } +} + + +// ////////////////////////////////////////////////////////////////////////// // +final class EditorWidget : Widget { + TextEditor editor; + bool moveToBottom = true; + + this (Widget aparent) { + tabStop = true; + super(aparent); + editor = new TextEditor(this, 0, 0, 10, 10); + } + + void addText (const(char)[] s) { + immutable GxRect grect = globalRect; + editor.moveResize(grect.x0, grect.y0, (grect.width < 1 ? 1 : grect.width), (grect.height < 1 ? grect.height : 1)); + editor.doPasteStart(); + scope(exit) editor.doPasteEnd(); + editor.doPutTextUtf(s); + //editor.doPutChar('\n'); + } + + void reformat () { + editor.clearAndDisableUndo(); + scope(exit) editor.reinstantiateUndo(); + editor.reformatFromLine!false(0); + editor.gotoXY(0, 0); // HACK + editor.textChanged = false; + } + + string[] extractAttaches () { + return editor.extractAttaches(); + } + + private final void drawScrollBar () { + //restoreClip(); // the easiest way again + immutable GxRect grect = globalRect; + gxDrawScrollBar(GxRect(grect.x0, grect.y0, 4, height), editor.linecount-1, editor.topline+editor.visibleLinesPerWindow-1); + } + + protected override void doPaint (GxRect grect) { + if (width < 1 || height < 1) return; + editor.moveResize(grect.x0+5, grect.y0, grect.width-5*2, grect.height-gxTextHeightUtf); + + if (moveToBottom) { moveToBottom = false; editor.gotoXY(0, editor.linecount); } // HACK! + gxFillRect(grect, getColor("back")); + gxWithSavedClip { + editor.drawPage(); + }; + + drawScrollBar(); + } + + // return `true` if event was eaten + override bool onKey (KeyEvent event) { + if (!isFocused) return super.onKey(event); + if (event.pressed) { + if (editor.processKey(event)) return true; + if (event == "S-Insert") { + getClipboardText(vbwin, delegate (in char[] text) { + if (text.length) { + editor.doPasteStart(); + scope(exit) editor.doPasteEnd(); + editor.doPutTextUtf(text[]); + } + }); + return true; + } + if (event == "C-Insert") { + auto mtr = editor.markedBlockRange(); + if (mtr.length) { + char[] brng; + brng.reserve(mtr.length); + foreach (char ch; mtr) brng ~= ch; + if (brng.length > 0) { + setClipboardText(vbwin, cast(string)brng); // it is safe to cast here + setPrimarySelection(vbwin, cast(string)brng); // it is safe to cast here + } + editor.doBlockResetMark(); + } + return true; + } + + if (event == "M-Tab") { + char[1024] abuf = void; + auto attname = editor.getAttachName(abuf[], editor.cury); + if (attname.length && editor.curx >= editor.linelen(editor.cury)) { + auto cplist = buildAutoCompletion(attname); + auto atnlen = attname.length; + auto adg = delegate (string s) { + //conwriteln("attname=[", attname, "]; s=[", s, "]"); + if (s.length <= atnlen /*|| s[0..attname.length] != attname*/) return; + editor.undoGroupStart(); + scope(exit) editor.undoGroupEnd(); + editor.doPutTextUtf(s[atnlen..$]); + }; + if (cplist.length == 1) { + adg(cplist[0]); + } else { + auto acw = new SelectCompletionWindow(attname, cplist, true); + acw.onSelected = adg; + } + } + return true; + } + } + return super.onKey(event); + } + + override bool onMouse (MouseEvent event) { + if (!isFocused) return super.onMouse(event); + if (GxPoint(event.x, event.y).inside(rect.size)) { + if (editor.processClick(event.x, event.y, event)) return true; + } + return super.onMouse(event); + } + + override bool onChar (dchar ch) { + if (isFocused && editor.processChar(ch)) return true; + return super.onChar(ch); + } +} + + +// ////////////////////////////////////////////////////////////////////////// // +final class LineEditWidget : Widget { + TextEditor editor; + string title; + public int titwdt = -1; + + this (Widget aparent, string atitle=null, int atitwdt=-1) { + tabStop = true; + super(aparent); + //parent.setupClientClip(); + //ww = gxClipRect.width; + width = aparent.width; + height = gxTextHeightUtf+2; + title = atitle; + titwdt = atitwdt; + if (titwdt < 0) titwdt = gxTextWidthUtf(atitle)+2; + editor = new TextEditor(this, 0, 0, width, height, true); // singleline + } + + @property bool readonly () const nothrow { return editor.readonly; } + @property void readonly (bool v) nothrow { editor.readonly = v; } + + @property bool killTextOnChar () const nothrow { return editor.killTextOnChar; } + @property void killTextOnChar (bool v) nothrow { editor.killTextOnChar = v; } + + @property string str () { + if (editor.textsize == 0) return null; + char[] res; + res.reserve(editor.textsize); + foreach (char ch; editor[]) res ~= ch; + return cast(string)res; // it is safe to cast here + } + + @property void str (const(char)[] s) { + editor.clearAndDisableUndo(); + scope(exit) editor.reinstantiateUndo(); + editor.clear(); + editor.doPutTextUtf(s); + editor.textChanged = false; + } + + protected override void doPaint (GxRect grect) { + if (width < 1 || height < 1) return; + + int cx0 = grect.x0; + int cx1 = grect.x1; + int cy0 = grect.y0; + + if (titwdt > 0) { + gxFillRect(GxRect(GxPoint(cx0, grect.y0), GxPoint(grect.x1, grect.y1)), getColor("title-back")); + gxDrawTextUtf(cx0+titwdt-gxTextWidthUtf(title)-1, cy0+1, title, getColor("title-text")); + cx0 += titwdt; + gxClipRect.intersect(GxRect(GxPoint(cx0, grect.y0), GxPoint(grect.x1, grect.y1))); + } + gxFillRect(cx0+1, cy0, grect.width-titwdt-2, grect.height, getColor("back")); + gxClipRect.intersect(GxRect(GxPoint(cx0+2, grect.y0), GxPoint(grect.x1-2, grect.y1))); + cx0 += 2; + cx1 -= 2; + + if (cx1 > cx0) { + editor.moveResize(cx0, cy0+1, cx1-cx0+1, gxTextHeightUtf); + editor.drawPage(); + } + } + + // return `true` if event was eaten + override bool onKey (KeyEvent event) { + if (!isFocused) return super.onKey(event); + if (event.pressed) { + if (editor.processKey(event)) return true; + if (event == "S-Insert") { + getClipboardText(vbwin, delegate (in char[] text) { + if (text.length) { + editor.doPasteStart(); + scope(exit) editor.doPasteEnd(); + editor.doPutTextUtf(text[]); + } + }); + return true; + } + if (event == "C-Insert") { + auto mtr = editor.markedBlockRange(); + if (mtr.length) { + char[] brng; + brng.reserve(mtr.length); + foreach (char ch; mtr) brng ~= ch; + if (brng.length > 0) { + setClipboardText(vbwin, cast(string)brng); // it is safe to cast here + setPrimarySelection(vbwin, cast(string)brng); // it is safe to cast here + } + editor.doBlockResetMark(); + } + return true; + } + } + return super.onKey(event); + } + + override bool onMouse (MouseEvent event) { + if (!isFocused) return super.onMouse(event); + if (GxPoint(event.x, event.y).inside(rect.size)) { + if (editor.processClick(event.x, event.y, event)) return true; + } + return super.onMouse(event); + } + + override bool onChar (dchar ch) { + if (isFocused && editor.processChar(ch)) return true; + return super.onChar(ch); + } +} diff --git a/egra/gui/package.d b/egra/gui/package.d new file mode 100644 index 0000000..5905295 --- /dev/null +++ b/egra/gui/package.d @@ -0,0 +1,25 @@ +/* + * Simple Framebuffer Gfx/GUI lib + * + * coded by Ketmar // Invisible Vector + * Understanding is not required. Only obedience. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License ONLY. + * + * 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, see . + */ +module iv.egra.gui /*is aliced*/; + +public import iv.egra.gui.style; +public import iv.egra.gui.subwindows; +public import iv.egra.gui.widgets; +public import iv.egra.gui.dialogs; +public import iv.egra.gui.editor; diff --git a/egra/gui/style.d b/egra/gui/style.d new file mode 100644 index 0000000..edde2c1 --- /dev/null +++ b/egra/gui/style.d @@ -0,0 +1,848 @@ +/* + * Simple Framebuffer GUI + * + * coded by Ketmar // Invisible Vector + * Understanding is not required. Only obedience. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License ONLY. + * + * 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, see . + */ +module iv.egra.gui.style; + +import iv.egra.gfx.base; + +import iv.strex; +import iv.xcolornames; + + +// ////////////////////////////////////////////////////////////////////////// // +static immutable defaultStyleText = ` +SubWindow { + frame: rgb(255, 255, 255); + title-back: @frame; + title-text: rgb(0, 0, 0); + + .inactive { + frame: rgb(192, 192, 192); + title-back: @frame; + title-text: rgb(0, 0, 0); + } + + back: rgb(0, 0, 180); + text: rgb(255, 255, 255); + + /* + .focused { + back: rgb(0, 0, 0); + text: rgb(255, 255, 255); + } + */ + + .disabled { + text: rgb(128, 128, 128); + } +} + + +YesNoWindow { + frame: rgb(0x40, 0x70, 0xcf); + title-back: rgb(0x73, 0x96, 0xdc); + title-text: rgb(0xff, 0xff, 0xff); + + .inactive { + frame: rgb(192, 192, 192); + title-back: @frame; + title-text: rgb(0, 0, 0); + } + + back: rgb(0xbf, 0xcf, 0xef); + text: rgb(0x16, 0x2d, 0x59); +} + + +Widget { + back: @parent:; + text: @parent:; + + .focused { + back: @parent:.null; + text: @parent:.null; + } + + .disabled { + back: @parent:; + text: @parent:; + } +} + + +LabelWidget { + back: transparent; + + .focused { + back: transparent; + } + + .disabled { + back: transparent; + } +} + + +ProgressBarWidget { + rect: gray20; + back: rgb(17, 17, 0); + text: white; + stripe: rgb(52, 52, 38); + + .hishade { + back: rgb(52, 52, 38); + stripe: rgb(82, 82, 70); + } + + .full { + back: rgb(159, 71, 0); + stripe: rgb(173, 98, 38); + } + + .full-hishade { + back: rgb(173, 98, 38); + stripe: rgb(203, 128, 70); + } +} + + +ButtonWidget { + back: rgb(180, 180, 180); + text: rgb(0, 0, 0); + hotline: @text; + + .focused { + back: rgb(250, 250, 250); + text: rgb(0, 0, 60); + hotline: @text; + } + + .disabled { + back: rgb(142, 142, 142); + text: rgb(32, 32, 32); + hotline: @text; + } +} + + +ButtonExWidget { + back: rgb(0x73, 0x96, 0xdc); + text: rgb(0xff, 0xff, 0xff); + rect: rgb(0x40, 0x70, 0xcf); + shadowline: rgb(0x83, 0xa6, 0xec); + + .focused { + back: rgb(0x93, 0xb6, 0xfc); + shadowline: rgb(0xa3, 0xc6, 0xff); + } +} + + +CheckboxWidget { + back: transparent; + text: rgb(192, 192, 192); + mark: rgb(0, 192, 0); + + .focused { + back: rgb(0, 0, 64); + text: @SubWindow:; + mark: rgb(0, 255, 0); + } + + .disabled { + back: @SubWindow:; + text: @SubWindow:; + mark: @SubWindow:text; + } +} + + +SimpleListBoxWidget { + back: rgb(0, 0, 60); + text: rgb(255, 255, 0); + cursor-back: rgb(0, 110, 110); + cursor-text: rgb(255, 255, 255); + + .focused { + back:@.null; + text:@.null; + cursor-back: rgb(0, 60, 60); + cursor-text: rgb(192, 192, 192); + } +} + + +EditorWidget { + back: #007; + + status-back: white; + status-color: black; + + text: rgb(220, 220, 0); + quote0-text: rgb(128, 128, 0); + quote1-text: rgb(0, 128, 128); + wrap-mark-text: rgb(0, 90, 220); + + attach-file-text: rgb(0x6e, 0x00, 0xff); + attach-bad-text: red; + + // marked block + mark-back: rgb(0, 160, 160); + mark-text: white; +} + + +LineEditWidget : EditorWidget { + title-back: transparent; + title-text: white; + + back: black; +} +`; + +// ////////////////////////////////////////////////////////////////////////// // +struct FuiSimpleParser { +private: + const(char)[] text; + const(char)[] str; // text left + +public: + this (const(char)[] atext) nothrow @safe @nogc { pragma(inline, true); setText(atext); } + + int getCurrentLine () pure const nothrow @safe @nogc { + int res = 0; + foreach (immutable char ch; text[0..$-str.length]) if (ch == '\n') ++res; + return res; + } + + void error (string msg) const { + import std.conv : to; + version(none) { import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "===\n%.*s\n===\n", cast(uint)str.length, str.ptr); } + throw new Exception("parse error around line "~getCurrentLine.to!string~": "~msg); + } + + void setText (const(char)[] atext) nothrow @safe @nogc { pragma(inline, true); text = atext; str = atext; } + + bool isEOT () { + skipBlanks(); + return (str.length == 0); + } + + void skipBlanks () { + while (str.length) { + if (str[0] <= ' ') { str = str.xstripleft; continue; } + if (str.length < 2 || str[0] != '/') break; + // single-line comment? + if (str[1] == '/') { + str = str[2..$]; + while (str.length && str[0] != '\n') str = str[1..$]; + continue; + } + // multiline comment? + if (str[1] == '*') { + bool endFound = false; + auto svs = str; + str = str[2..$]; + while (str.length) { + if (str.length > 1 && str[0] == '*' && str[1] == '/') { + endFound = true; + str = str[2..$]; + break; + } + str = str[1..$]; + } + if (!endFound) { str = svs; error("unfinished comment"); } + continue; + } + // multiline nested comment? + if (str[1] == '+') { + bool endFound = false; + auto svs = str; + int level = 0; + while (str.length) { + if (str.length > 1) { + if (str[0] == '/' && str[1] == '+') { str = str[2..$]; ++level; continue; } + if (str[0] == '+' && str[1] == '/') { str = str[2..$]; if (--level == 0) { endFound = true; break;} continue; } + } + str = str[1..$]; + } + if (!endFound) { str = svs; error("unfinished comment"); } + continue; + } + break; + } + } + + bool checkNoEat (const(char)[] tk) { + assert(tk.length); + skipBlanks(); + return (str.length >= tk.length && str[0..tk.length] == tk); + } + + bool checkNoEat (in char ch) { + skipBlanks(); + return (str.length > 0 && str[0] == ch); + } + + bool check (const(char)[] tk) { + if (!checkNoEat(tk)) return false; + str = str[tk.length..$]; + skipBlanks(); + return true; + } + + bool check (in char ch) { + if (!checkNoEat(ch)) return false; + str = str[1..$]; + skipBlanks(); + return true; + } + + void expect (const(char)[] tk) { + skipBlanks(); + auto svs = str; + if (!check(tk)) { str = svs; error("`"~tk.idup~"` expected"); } + } + + void expect (in char ch) { + skipBlanks(); + auto svs = str; + if (!check(ch)) { str = svs; error("`"~ch~"` expected"); } + } + + const(char)[] expectIdNoCopy () { + skipBlanks(); + if (str.length == 0) error("identifier expected"); + if (!isalpha(str[0]) && str[0] != '_' && str[0] != '-') error("identifier expected"); + usize pos = 1; + while (pos < str.length) { + if (!isalnum(str[pos]) && str[pos] != '_' && str[pos] != '-') break; + ++pos; + } + const(char)[] res = str[0..pos]; + str = str[pos..$]; + skipBlanks(); + return res; + } + + string expectId () { + pragma(inline, true); + return expectIdNoCopy().idup; + } + + uint parseColor () { + skipBlanks(); + if (str.length == 0) error("color expected"); + + // html-like color? + if (check('#')) return parseHtmlColor(); + + auto svs = str; + auto id = expectIdNoCopy(); + + if (id.strEquCI("transparent")) return gxTransparent; + + // `rgb()` or `rgba()`? + bool allowAlpha; + if (id.strEquCI("rgba")) allowAlpha = true; + else if (id.strEquCI("rgb")) allowAlpha = false; + else { + auto xc = xFindColorByName(id); + if (xc is null) { str = svs; error("invalid color definition"); } + return gxrgb(xc.r, xc.g, xc.b); + } + + skipBlanks(); + if (!check('(')) { str = svs; error("invalid color definition"); } + immutable uint clr = parseColorRGB(allowAlpha); + if (!check(')')) { str = svs; error("invalid color definition"); } + return clr; + } + +private: + // '#' skipped + uint parseHtmlColor () { + auto svs = str; + skipBlanks(); + ubyte[3] rgb = 0; + // first 3 digits + foreach (immutable n; 0..3) { + while (str.length && str[0] == '_') str = str[1..$]; + if (str.length == 0) { str = svs; error("invalid color"); } + immutable int dg = digitInBase(str[0], 16); + if (dg < 0) { str = svs; error("invalid color"); } + rgb[n] = cast(ubyte)dg; + str = str[1..$]; + } + while (str.length && str[0] == '_') str = str[1..$]; + // second 3 digits? + if (str.length && digitInBase(str[0], 16) >= 0) { + foreach (immutable n; 0..3) { + while (str.length && str[0] == '_') str = str[1..$]; + if (str.length == 0) { str = svs; error("invalid color"); } + immutable int dg = digitInBase(str[0], 16); + if (dg < 0) { str = svs; error("invalid color"); } + rgb[n] = cast(ubyte)(rgb[n]*16+dg); + str = str[1..$]; + } + while (str.length && str[0] == '_') str = str[1..$]; + } else { + foreach (immutable n; 0..3) rgb[n] = cast(ubyte)(rgb[n]*16+rgb[n]); + } + skipBlanks(); + return gxrgb(rgb[0], rgb[1], rgb[2]); + } + + // "(" skipped + uint parseColorRGB (bool allowAlpha) { + auto svs = str; + ubyte[4] rgba = 0; + foreach (immutable n; 0..3+(allowAlpha ? 1 : 0)) { + if (n && !check(',')) { str = svs; error("invalid color"); } + skipBlanks(); + if (str.length == 0 || !isdigit(str[0])) { str = svs; error("invalid color"); } + uint val = 0; + uint base = 10; + if (str[0] == '0' && str.length >= 2 && (str[1] == 'x' || str[1] == 'X')) { + str = str[2..$]; + if (str.length == 0 || digitInBase(str[0], 16) < 0) { str = svs; error("invalid color"); } + base = 16; + } + while (str.length) { + if (str[0] != '_') { + immutable int dg = digitInBase(str[0], cast(int)base); + if (dg < 0) break; + val = val*base+cast(uint)dg; + if (val > 255) { str = svs; error("invalid color"); } + } + str = str[1..$]; + } + while (str.length && str[0] == '_') str = str[1..$]; + rgba[n] = cast(ubyte)val; + } + skipBlanks(); + if (allowAlpha) return gxrgba(rgba[0], rgba[1], rgba[2], rgba[3]); + return gxrgb(rgba[0], rgba[1], rgba[2]); + } +} + + +// ////////////////////////////////////////////////////////////////////////// // +class ColorStyle { +public: + struct ColorItem { + string widget; // "Widget" + string type; // "text" + string mod; // "inactive", "disabled", etc. + bool redirect; + uint color; + // redirect + string rewidget; // null means "this" + string retype; // null means "this" + string remod; // null means "this" + // loop protection + uint seenCount; + } + +protected: + ColorItem[string] colors; + string[string] parents; + uint seenCount; + + uint[string] styleCache; + char[] tempccstr; + +protected: + final void clearStyleCache () { + pragma(inline, true); + styleCache = null; + } + + final const(char)[] buildcc (const(char)[] defparent, const(char)[] ctname, const(char)[] type, const(char)[] mod) { + pragma(inline, true); + immutable usize strsize = defparent.length+1+ctname.length+1+type.length+1+mod.length; + if (tempccstr.length < strsize) tempccstr.length = (strsize|0xff)+1; + // default parent + usize spos = defparent.length; + tempccstr[0..spos] = defparent; + tempccstr[spos++] = 0; + // class name + tempccstr[spos..spos+ctname.length] = ctname; + spos += ctname.length; + tempccstr[spos++] = 0; + // type + tempccstr[spos..spos+type.length] = type; + spos += type.length; + tempccstr[spos++] = 0; + // modifier + tempccstr[spos..spos+mod.length] = mod; + // done + return tempccstr[0..spos+mod.length]; + } + + final bool findCachedColor (in TypeInfo_Class defpar, in TypeInfo_Class ct, const(char)[] type, const(char)[] mod, out uint res) { + pragma(inline, true); + if (auto cp = buildcc((defpar !is null ? defpar.name : null), (ct !is null ? ct.name : null), type, mod) in styleCache) { + res = *cp; + return true; + } + return false; + } + + final void cacheColor (in uint clr, in TypeInfo_Class defpar, in TypeInfo_Class ct, const(char)[] type, const(char)[] mod) { + pragma(inline, true); + auto key = buildcc((defpar !is null ? defpar.name : null), (ct !is null ? ct.name : null), type, mod); + if (auto cp = key in styleCache) { + *cp = clr; + } else { + styleCache[key.idup] = clr; + } + } + +protected: + final void resetSeenCount () { + seenCount = 1; + foreach (ref ColorItem ci; colors.byValue) ci.seenCount = 0; + } + + final void incSeenCount () { + pragma(inline, true); + if (++seenCount == 0) resetSeenCount(); + } + +protected: + void addColorItem (in ColorItem ci) { + clearStyleCache(); + auto nm = buildcc(null, ci.widget, ci.type, ci.mod); + if (auto cptr = nm in colors) { + cptr.redirect = ci.redirect; + cptr.color = ci.color; + cptr.rewidget = ci.rewidget; + cptr.retype = ci.retype; + cptr.remod = ci.remod; + cptr.seenCount = 0; + } else { + colors[nm.idup] = ci; + } + } + +public: + void parseStyle (const(char)[] str) { + auto par = FuiSimpleParser(str); + while (!par.isEOT) { + if (par.check('!')) { + auto cmd = par.expectIdNoCopy(); + if (cmd.strEquCI("clear-style")) { + clear(); + } else { + par.error("invalid command: '"~cmd.idup~"'"); + } + par.expect(';'); + continue; + } + string widget = par.expectId(); + string parent = null; + if (par.check(':')) { + parent = par.expectId(); + if (parent == widget) par.error("invalid parent `"~parent~"` for `"~widget~"`"); + parents[widget] = parent; + } + par.expect('{'); + string mod = null; + for (;;) { + if (par.check('}')) { + if (mod is null) break; + mod = null; + continue; + } + //{ import std.stdio; writeln("WIDGET: <", widget, ">"); } + if ((mod is null) && par.check('.')) { + mod = par.expectId(); + //{ import std.stdio; writeln("NEW MOD: <", mod, ">"); } + par.expect('{'); + continue; + } + string type = par.expectId(); + //{ import std.stdio; writeln("TYPE: <", type, ">"); } + par.expect(':'); + ColorItem ci; + ci.widget = widget; + ci.type = type; + ci.mod = mod; + // redirect? + if (par.check('@')) { + // redirect + ci.redirect = true; + if (par.checkNoEat('.')) { + // .mod + ci.rewidget = null; + ci.retype = type; + } else { + string rd0 = par.expectId(); + if (par.check(':')) { + // widget:... + ci.rewidget = rd0; + if (par.checkNoEat('.')) { + ci.retype = type; + } else { + // has type? + if (par.checkNoEat(';')) { + ci.retype = type; + } else { + ci.retype = par.expectId(); + } + } + } else { + // type... + ci.rewidget = null; + ci.retype = rd0; + } + } + if (par.check('.')) { + ci.remod = par.expectId(); + } else { + ci.remod = mod; + } + } else { + // color + ci.redirect = false; + ci.color = par.parseColor(); + } + par.expect(';'); + if (ci.mod.strEquCI("none") || ci.mod.strEquCI("null")) ci.mod = null; + if (ci.remod.strEquCI("none") || ci.remod.strEquCI("null")) ci.remod = null; + version(none) { + import std.stdio; + write("SRC: <", ci.widget, ":", ci.type, ".", ci.mod, ">"); + if (ci.redirect) { + writeln(" --> <", ci.rewidget, ":", ci.retype, ".", ci.remod, ">"); + } else { + writeln(" : rgba(", gxGetRed(ci.color), ",", gxGetGreen(ci.color), ",", gxGetBlue(ci.color), ",", gxGetAlpha(ci.color), ")"); + } + } + addColorItem(ci); + } + } + } + +public: + this () {} + + void cloneFrom (ColorStyle st) { + if (st is null || st is this) return; + colors = null; + foreach (const ref ColorItem ci; st.colors.byValue) addColorItem(ci); + parents = null; + foreach (auto s; st.parents.byKeyValue) parents[s.key] = s.value; + styleCache = null; + resetSeenCount(); + seenCount = 0; + } + + void clear () { + colors = null; + parents = null; + styleCache = null; + seenCount = 0; + } + + protected static struct BaseInfo { + TypeInfo_Class defaultParent = void; + TypeInfo_Class ctsrc = void; + } + + protected uint findColorIntr (ref BaseInfo nfo, TypeInfo_Class ctwdt, + const(char)[] type, const(char)[] mod=null, bool* found=null) + { + if (ctwdt is null) { + ctwdt = nfo.defaultParent; + if (ctwdt is null) { + if (found !is null) *found = false; + return gxUnknown; + } + } + + string sname = classShortName(ctwdt); + + ColorItem *mcit = void; + auto nm = buildcc(null, sname, type, mod); + mcit = (nm in colors); + // if there is a modifier, try without it + if (mcit is null && mod.length) { + nm = buildcc(null, sname, type, null); + mcit = (nm in colors); + } + + if (mcit !is null) { + if (!mcit.redirect) { + // not a redirect + mcit.seenCount = seenCount; + if (found !is null) *found = true; + return mcit.color; + } + + // check for infinite loop + if (mcit.seenCount == seenCount) { + // infinite loop + if (found !is null) *found = false; + return gxUnknown; + } + mcit.seenCount = seenCount; + + // empty redirect widget name means "source widget" + if (mcit.rewidget.length == 0) { + return findColorIntr(ref nfo, nfo.ctsrc, mcit.retype, mcit.remod, found); + } + + // special "parent" redirect widget name + if (mcit.rewidget == "parent") { + if (auto pp = sname in parents) { + return findColorIntr(ref nfo, findWidgetClass(*pp), mcit.retype, mcit.remod, found); + } else { + return findColorIntr(ref nfo, ctwdt.base, mcit.retype, mcit.remod, found); + } + } + + // normal-named redirect + return findColorIntr(ref nfo, findWidgetClass(mcit.rewidget), mcit.retype, mcit.remod, found); + } + + // try parent + if (auto pp = sname in parents) { + return findColorIntr(ref nfo, findWidgetClass(*pp), type, mod, found); + } + + // try inheritance parent + return findColorIntr(ref nfo, ctwdt.base, type, mod, found); + } + + final uint findColor (TypeInfo_Class defparent, TypeInfo_Class ctsrc, const(char)[] type, const(char)[] mod=null, bool* foundp=null) { + if (defparent is ctsrc) defparent = null; + + if (ctsrc is null) { + if (defparent is null) { + if (foundp !is null) *foundp = false; + return gxUnknown; + } + ctsrc = defparent; + defparent = null; + } + + uint clr; + if (findCachedColor(defparent, ctsrc, type, mod, out clr)) { + if (foundp !is null) *foundp = true; + return clr; + } + + incSeenCount(); + BaseInfo nfo; + nfo.defaultParent = defparent; + nfo.ctsrc = ctsrc; + clr = findColorIntr(ref nfo, ctsrc, type, mod, foundp); + cacheColor(clr, defparent, ctsrc, type, mod); + + return clr; + } + + final uint findColor (in Object defpar, in Object obj, const(char)[] type, const(char)[] mod=null, bool* foundp=null) { + pragma(inline, true); + TypeInfo_Class dp = (defpar !is null ? cast(TypeInfo_Class)typeid(defpar) : null); + TypeInfo_Class ct = (obj !is null ? cast(TypeInfo_Class)typeid(obj) : null); + return findColor(dp, ct, type, mod, foundp); + } + +static: + __gshared TypeInfo_Class[string] classNameCache; + + static bool isGoodClassType (TypeInfo_Class ct) pure nothrow @trusted @nogc { + while (ct !is null) { + if (ct.name == "iv.egra.gui.subwindows.SubWindow" || + ct.name == "iv.egra.gui.widgets.Widget") + { + return true; + } + ct = ct.base; + } + return false; + } + + static string classShortName (in TypeInfo_Class ct) nothrow @trusted @nogc { + pragma(inline, true); + if (ct is null) return null; + string name = ct.name; + auto dpos = name.lastIndexOf('.'); + return (dpos < 0 ? name : name[dpos+1..$]); + } + + static TypeInfo_Class findWidgetClass (string cname) { + if (cname.length == 0) return null; + if (auto ctp = cname in classNameCache) { + version(none) { import core.stdc.stdio : printf; + printf("findWidgetClass<%.*s>: CACHE HIT! %p\n", cast(uint)cname.length, cname.ptr, *ctp); + } + return *ctp; + } + foreach (ModuleInfo* m; ModuleInfo) { + string mname = m.name; + if (mname.startsWith("std.") || + mname.startsWith("core.") || + mname.startsWith("rt.") || + mname.startsWith("gc.") || + mname.startsWith("arsd.") || + false) + { + continue; + } + foreach (TypeInfo_Class ct; m.localClasses()) { + if (!ct.name.endsWith(cname)) continue; + version(none) { import core.stdc.stdio : printf; + printf("findWidgetClass<%.*s>: checking <%.*s>\n", + cast(uint)cname.length, cname.ptr, + cast(uint)ct.name.length, ct.name.ptr); + } + if (ct.name.length == cname.length || ct.name[$-cname.length-1] == '.') { + version(none) { import core.stdc.stdio : printf; + printf("findWidgetClass<%.*s>: found <%.*s>\n", + cast(uint)cname.length, cname.ptr, + cast(uint)ct.name.length, ct.name.ptr); + } + // final check + if (isGoodClassType(ct)) { + // cache it + classNameCache[cname.idup] = ct; + return ct; + } + } + } + } + version(none) { import core.stdc.stdio : printf; + printf("findWidgetClass<%.*s>: NOT FOUND!\n", cast(uint)cname.length, cname.ptr); + } + // cache "unknown" + classNameCache[cname] = null; + return null; + } +} + + +__gshared ColorStyle defaultColorStyle; + +shared static this () { + defaultColorStyle = new ColorStyle; + defaultColorStyle.parseStyle(defaultStyleText); +} diff --git a/egra/gui/subwindows.d b/egra/gui/subwindows.d new file mode 100644 index 0000000..377e8e8 --- /dev/null +++ b/egra/gui/subwindows.d @@ -0,0 +1,1065 @@ +/* + * Simple Framebuffer Gfx/GUI lib + * + * coded by Ketmar // Invisible Vector + * Understanding is not required. Only obedience. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License ONLY. + * + * 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, see . + */ +module iv.egra.gui.subwindows /*is aliced*/; +private: + +import core.time; + +import arsd.simpledisplay; + +import iv.alice; +import iv.cmdcon; +import iv.flexlay2; +import iv.strex; +import iv.utfutil; + +import iv.egra.gfx; +public import iv.egra.gui.style; +import iv.egra.gui.widgets : Widget, RootWidget; + + +// ////////////////////////////////////////////////////////////////////////// // +public __gshared SimpleWindow vbwin; // main window; MUST be set! +public __gshared bool vbfocused = false; + + +// ////////////////////////////////////////////////////////////////////////// // +__gshared public int lastMouseXUnscaled = 10000, lastMouseYUnscaled = 10000; +__gshared /*MouseButton*/public int lastMouseButton; + +public int lastMouseX () nothrow @trusted @nogc { pragma(inline, true); return lastMouseXUnscaled/screenEffScale; } +public int lastMouseY () nothrow @trusted @nogc { pragma(inline, true); return lastMouseYUnscaled/screenEffScale; } + +public bool lastMouseLeft () nothrow @trusted @nogc { pragma(inline, true); return ((lastMouseButton&MouseButton.left) != 0); } +public bool lastMouseRight () nothrow @trusted @nogc { pragma(inline, true); return ((lastMouseButton&MouseButton.right) != 0); } +public bool lastMouseMiddle () nothrow @trusted @nogc { pragma(inline, true); return ((lastMouseButton&MouseButton.middle) != 0); } + + +// ////////////////////////////////////////////////////////////////////////// // +public class ScreenRebuildEvent {} +public class ScreenRepaintEvent {} +public class QuitEvent {} +public class CursorBlinkEvent {} +public class HideMouseEvent {} + +__gshared ScreenRebuildEvent evScrRebuild; +__gshared ScreenRepaintEvent evScreenRepaint; +__gshared CursorBlinkEvent evCurBlink; +__gshared HideMouseEvent evHideMouse; + +shared static this () { + evScrRebuild = new ScreenRebuildEvent(); + evScreenRepaint = new ScreenRepaintEvent(); + evCurBlink = new CursorBlinkEvent(); + evHideMouse = new HideMouseEvent(); +} + + +// ////////////////////////////////////////////////////////////////////////// // +public void postScreenRebuild () { if (vbwin !is null && !vbwin.eventQueued!ScreenRebuildEvent) vbwin.postEvent(evScrRebuild); } +public void postScreenRepaint () { if (vbwin !is null && !vbwin.eventQueued!ScreenRepaintEvent && !vbwin.eventQueued!ScreenRebuildEvent) vbwin.postEvent(evScreenRepaint); } +public void postScreenRepaintDelayed () { if (vbwin !is null && !vbwin.eventQueued!ScreenRepaintEvent && !vbwin.eventQueued!ScreenRebuildEvent) vbwin.postTimeout(evScreenRepaint, 35); } + +public void postCurBlink () { + if (vbwin !is null && !vbwin.eventQueued!CursorBlinkEvent) { + //conwriteln("curblink posted!"); + vbwin.postTimeout(evCurBlink, 100); + //vbwin.postTimeout(evCurBlink, 500); + } +} + + +// ////////////////////////////////////////////////////////////////////////// // +__gshared MonoTime lastMouseMove; +__gshared uint MouseHideTime = 3000; + + +shared static this () { + conRegVar("mouse_hide_time", "mouse cursor hiding time (in milliseconds); 0 to not hide it", + delegate (self) { + return MouseHideTime; + }, + delegate (self, uint nv) { + if (MouseHideTime != nv) { + if (MouseHideTime == 0) mouseMoved(); + MouseHideTime = nv; + conwriteln("mouse hiding time: ", nv); + } + }, + ); +} + + +//========================================================================== +// +// mshtime_dbg +// +//========================================================================== +public int mshtime_dbg () { + if (MouseHideTime > 0) { + auto ctt = MonoTime.currTime; + auto mt = (ctt-lastMouseMove).total!"msecs"; + return cast(int)mt; + } else { + return 0; + } +} + + +//========================================================================== +// +// isMouseVisible +// +//========================================================================== +public bool isMouseVisible () { + if (MouseHideTime > 0) { + auto ctt = MonoTime.currTime; + return ((ctt-lastMouseMove).total!"msecs" < MouseHideTime+500); + } else { + return true; + } +} + + +//========================================================================== +// +// mouseAlpha +// +//========================================================================== +public float mouseAlpha () { + if (MouseHideTime > 0) { + auto ctt = MonoTime.currTime; + auto msc = (ctt-lastMouseMove).total!"msecs"; + if (msc >= MouseHideTime+500) return 0.0f; + if (msc < MouseHideTime) return 1.0f; + msc -= MouseHideTime; + return 1.0f-msc/500.0f; + } else { + return 1.0f; + } +} + + +//========================================================================== +// +// repostHideMouse +// +// returns `true` if mouse should be redrawn +// +//========================================================================== +public bool repostHideMouse () { + if (vbwin is null || vbwin.eventQueued!HideMouseEvent) return false; + if (MouseHideTime > 0) { + auto ctt = MonoTime.currTime; + auto tms = (ctt-lastMouseMove).total!"msecs"; + if (tms >= MouseHideTime) { + if (tms >= MouseHideTime+500) return true; // hide it + vbwin.postTimeout(evHideMouse, 50); + return true; // fade it + } + vbwin.postTimeout(evHideMouse, cast(int)(MouseHideTime-tms)); + } + return false; +} + + +//========================================================================== +// +// mouseMoved +// +//========================================================================== +public void mouseMoved () { + if (MouseHideTime > 0) { + lastMouseMove = MonoTime.currTime; + if (vbwin !is null && !vbwin.eventQueued!HideMouseEvent) vbwin.postTimeout(evHideMouse, MouseHideTime); + } +} + + +//========================================================================== +// +// drawTextCursor +// +//========================================================================== +public void drawTextCursor (bool active, int x, int y, int hgt=-666) { + if (hgt == -666) hgt = gxTextHeightUtf; + if (hgt < 1) return; + if (active) { + auto ctt = (MonoTime.currTime.ticks*1000/MonoTime.ticksPerSecond)/100; + int doty = ctt%(hgt*2-1); + if (doty >= hgt) doty = hgt*2-doty-1; + gxVLine(x, y, hgt, (ctt%10 < 5 ? gxRGB!(255, 255, 255) : gxRGB!(200, 200, 200))); + gxPutPixel(x, y+doty, gxRGB!(0, 255, 255)); + postCurBlink(); + } else { + gxVLine(x, y, hgt, gxRGB!(170, 170, 170)); + } +} + + +// ////////////////////////////////////////////////////////////////////////// // +private __gshared SubWindow subwinLast; +private __gshared bool ignoreSubWinChar = false; +// make a package and move that to package +private __gshared SubWindow subwinDrag = null; +private __gshared int subwinDragXSpot, subwinDragYSpot; + + +public @property bool isSubWinDragging () nothrow @trusted @nogc { pragma(inline, true); return (subwinDrag !is null); } +public @property bool isSubWinDraggingKeyboard () nothrow @trusted @nogc { pragma(inline, true); return (subwinDrag !is null && subwinDragXSpot == int.min && subwinDragYSpot == int.min); } + + +//========================================================================== +// +// eguiLostGlobalFocus +// +//========================================================================== +public void eguiLostGlobalFocus () { + ignoreSubWinChar = false; + subwinDrag = null; + SubWindow aw = getActiveSubWindow(); + if (aw !is null) aw.releaseWidgetGrab(); +} + + +private bool insertSubWindow (SubWindow nw) { + if (nw is null || nw.mClosed) return false; + assert(nw.mPrev is null); + assert(nw.mNext is null); + assert(!nw.mInWinList); + nw.releaseWidgetGrab(); + SubWindow law = getActiveSubWindow(); + if (nw.mType == SubWindow.Type.OnBottom) { + SubWindow w = subwinLast; + if (w !is null) { + while (w.mPrev !is null) w = w.mPrev; + nw.mNext = w; + if (w !is null) w.mPrev = nw; else subwinLast = nw; + } else { + subwinLast = nw; + } + nw.mInWinList = true; + if (law !is null && law !is getActiveSubWindow()) law.releaseWidgetGrab(); + return true; + } + SubWindow aw = getActiveSubWindow(); + assert(aw !is nw); + if (nw.mType == SubWindow.Type.OnTop || aw is null) { + nw.mPrev = subwinLast; + if (subwinLast !is null) subwinLast.mNext = nw; + subwinLast = nw; + nw.mInWinList = true; + if (law !is null && law !is getActiveSubWindow()) law.releaseWidgetGrab(); + return true; + } + if (aw.mModal && !nw.mModal) return false; // can't insert normal windows while modal window is active + // insert after aw + nw.mPrev = aw; + nw.mNext = aw.mNext; + aw.mNext = nw; + if (nw.mNext !is null) nw.mNext.mPrev = nw; + if (aw is subwinLast) subwinLast = nw; + nw.mInWinList = true; + if (law !is null && law !is getActiveSubWindow()) law.releaseWidgetGrab(); + return true; +} + + +private bool removeSubWindow (SubWindow nw) { + if (nw is null || !nw.mInWinList) return false; + nw.releaseWidgetGrab(); + SubWindow law = getActiveSubWindow(); + if (nw.mPrev !is null) nw.mPrev.mNext = nw.mNext; + if (nw.mNext !is null) nw.mNext.mPrev = nw.mPrev; + if (nw is subwinLast) subwinLast = nw.mPrev; + nw.mPrev = null; + nw.mNext = null; + nw.mInWinList = false; + if (law !is null && law !is getActiveSubWindow()) law.releaseWidgetGrab(); + return true; +} + + +//========================================================================== +// +// mouse2xy +// +//========================================================================== +public void mouse2xy (MouseEvent event, out int mx, out int my) nothrow @trusted @nogc { + mx = event.x/screenEffScale; + my = event.y/screenEffScale; +} + + +//========================================================================== +// +// subWindowAt +// +//========================================================================== +public SubWindow subWindowAt (in GxPoint p) nothrow @trusted @nogc { + for (SubWindow w = subwinLast; w !is null; w = w.mPrev) { + if (!w.closed) { + if (w.mMinimized) { + if (p.x >= w.winminx && p.y >= w.winminy && p.x < w.winminx+w.MinSizeX && p.y < w.winminy+w.MinSizeY) return w; + } else { + if (p.inside(w.winrect)) return w; + } + } + } + return null; +} + + +public SubWindow subWindowAt (int mx, int my) nothrow @trusted @nogc { return subWindowAt(GxPoint(mx, my)); } + + +//========================================================================== +// +// subWindowAt +// +//========================================================================== +public SubWindow subWindowAt (MouseEvent event) nothrow @trusted @nogc { + pragma(inline, true); + return subWindowAt(event.x/screenEffScale, event.y/screenEffScale); +} + + +//========================================================================== +// +// getActiveSubWindow +// +//========================================================================== +public SubWindow getActiveSubWindow () nothrow @trusted @nogc { + for (SubWindow w = subwinLast; w !is null; w = w.mPrev) { + if (!w.mClosed && w.type != SubWindow.Type.OnTop && !w.mMinimized) return w; + } + return null; +} + + +//========================================================================== +// +// dispatchEvent +// +//========================================================================== +public bool dispatchEvent (KeyEvent event) { + bool res = false; + if (isSubWinDragging) { + if (isSubWinDraggingKeyboard) { + if (!event.pressed) return true; + if (event == "Left") subwinDrag.x0 = subwinDrag.x0-1; + else if (event == "Right") subwinDrag.x0 = subwinDrag.x0+1; + else if (event == "Up") subwinDrag.y0 = subwinDrag.y0-1; + else if (event == "Down") subwinDrag.y0 = subwinDrag.y0+1; + else if (event == "C-Left") subwinDrag.x0 = subwinDrag.x0-8; + else if (event == "C-Right") subwinDrag.x0 = subwinDrag.x0+8; + else if (event == "C-Up") subwinDrag.y0 = subwinDrag.y0-8; + else if (event == "C-Down") subwinDrag.y0 = subwinDrag.y0+8; + else if (event == "Home") subwinDrag.x0 = 0; + else if (event == "End") subwinDrag.x0 = screenWidth-subwinDrag.width; + else if (event == "PageUp") subwinDrag.y0 = 0; + else if (event == "PageDown") subwinDrag.y0 = screenHeight-subwinDrag.height; + else if (event == "Escape" || event == "Enter") subwinDrag = null; + postScreenRebuild(); + return true; + } + } else if (auto aw = getActiveSubWindow()) { + res = aw.onKeyEvent(event); + if (res) postScreenRebuild(); + } + return res; +} + + +//========================================================================== +// +// dispatchEvent +// +//========================================================================== +public bool dispatchEvent (MouseEvent event) { + __gshared SubWindow lastHover = null; + + if (subwinLast is null) { postScreenRepaint(); return false; } + + int mx = lastMouseX; + int my = lastMouseY; + auto aw = getActiveSubWindow(); + auto msw = subWindowAt(event); + scope(exit) { + if (msw !is lastHover) { + lastHover = msw; + postScreenRebuild(); + } + } + bool curIsModal = (aw !is null && aw.mModal); + + // switch window by button press + if (event.type == MouseEventType.buttonReleased && msw !is aw && !curIsModal) { + if (msw !is null && msw.mType == SubWindow.Type.Normal) { + if (aw !is null) aw.releaseWidgetGrab(); + msw.releaseWidgetGrab(); + msw.bringToFront(); + postScreenRepaint(); + return true; + } + } + + // drag + if (isSubWinDragging) { + subwinDrag.releaseWidgetGrab(); // just in case + if (!isSubWinDraggingKeyboard) { + subwinDrag.x0 = mx+subwinDragXSpot; + subwinDrag.y0 = my+subwinDragYSpot; + // stop drag? + if (event.type == MouseEventType.buttonReleased && event.button == MouseButton.left) subwinDrag = null; + postScreenRebuild(); + } else { + postScreenRepaint(); + } + return true; + } + + if (msw is null || msw !is aw || msw.mMinimized) { + if (msw is null || !msw.onTop) { + postScreenRepaint(); + return false; + } + } + assert(msw !is null); + + if (msw.onMouseEvent(event)) { + postScreenRebuild(); + return true; + } + + postScreenRepaint(); + return false; +} + + +//========================================================================== +// +// dispatchEvent +// +//========================================================================== +public bool dispatchEvent (dchar ch) { + if (ignoreSubWinChar) { ignoreSubWinChar = false; return (subwinLast !is null); } + bool res = false; + if (!isSubWinDragging) { + if (auto aw = getActiveSubWindow()) { + res = aw.onCharEvent(ch); + if (res) postScreenRebuild(); + } + } + return res; +} + + +//========================================================================== +// +// paintSubWindows +// +//========================================================================== +public void paintSubWindows () { + // get first window + SubWindow firstWin = subwinLast; + if (firstWin is null) return; + while (firstWin.mPrev !is null) firstWin = firstWin.mPrev; + + SubWindow firstMin, firstNormal, firstTop; + SubWindow lastMin, lastNormal, lastTop; + + //gxClipReset(); + + void doDraw (SubWindow w) { + if (w !is null) { + gxClipReset(); + w.onPaint(); + if (w is subwinDrag) { gxClipReset(); gxFillRect(w.x0, w.y0, w.width, w.height, gxRGBA!(255, 127, 0, 176)); } + } + } + + // paint background windows + for (SubWindow w = firstWin; w !is null; w = w.mNext) { + if (w.mClosed) continue; + if (w.mMinimized) { + if (firstMin is null) firstMin = w; + lastMin = w; + } else if (w.mType == SubWindow.Type.Normal) { + if (firstNormal is null) firstNormal = w; + lastNormal = w; + } else if (w.mType == SubWindow.Type.OnTop) { + if (firstTop is null) firstTop = w; + lastTop = w; + } else if (w.mType == SubWindow.Type.OnBottom) { + doDraw(w); + } + } + + // paint minimized windows + for (SubWindow w = firstMin; w !is null; w = w.mNext) { + if (!w.mClosed && w.mMinimized) doDraw(w); + if (w is lastMin) break; + } + + // paint normal windows + for (SubWindow w = firstNormal; w !is null; w = w.mNext) { + if (!w.mClosed && !w.mMinimized && w.mType == SubWindow.Type.Normal) doDraw(w); + if (w is lastNormal) break; + } + + // paint ontop windows + for (SubWindow w = firstTop; w !is null; w = w.mNext) { + if (!w.mClosed && !w.mMinimized && w.mType == SubWindow.Type.OnTop) doDraw(w); + if (w is lastTop) break; + } + + // paint hint for minimized window + if (auto msw = subWindowAt(lastMouseX, lastMouseY)) { + if (!msw.mClosed && msw.mMinimized && msw.title.length) { + auto wdt = gxTextWidthUtf(msw.title)+2; + auto hgt = gxTextHeightUtf+2; + int y = msw.winminy-hgt; + int x; + if (wdt >= screenWidth) { + x = (screenWidth-wdt)/2; + } else { + x = (msw.winminx+msw.MinSizeX)/2-wdt/2; + if (x < 0) x = 0; + } + gxClipReset(); + gxFillRect(x, y, wdt, hgt, gxRGB!(255, 255, 255)); + gxDrawTextUtf(x+1, y+1, msw.title, gxRGB!(0, 0, 0)); + } + } +} + + +// ////////////////////////////////////////////////////////////////////////// // +public class SubWindow { +protected: + enum Type { + Normal, + OnTop, + OnBottom, + } + +protected: + SubWindow mPrev, mNext; + Type mType = Type.Normal; + bool mMinimized; + bool mInWinList; + bool mModal; + bool mClosed; + int awidx; // active widget index + RootWidget mRoot; + + ColorStyle colorStyle; + +public: + int winminx, winminy; + GxRect winrect; + string title; + + GxSize minWinSize; + GxSize maxWinSize; + +public: + // color getters + final uint getStyleColor (in Object obj, const(char)[] type, const(char)[] mod=null) { + pragma(inline, true); + ColorStyle st = colorStyle; + if (st is null) st = defaultColorStyle; + return st.findColor(this, obj, type, mod); + } + + final uint getColor (const(char)[] type, const(char)[] mod) { + return getStyleColor(this, type, mod); + } + + final uint getColor (const(char)[] type) { + return getStyleColor(this, type, (active ? null : "inactive")); + } + +protected: + void createRoot () { + if (mRoot is null) setRoot(new RootWidget(this)); else fixRoot(); + } + + void fixRoot () { + if (mRoot) { + mRoot.rect.pos = GxPoint(0, 0); + mRoot.rect.size = GxSize(clientWidth, clientHeight); + } + } + + void setStyle (ColorStyle stl) { + colorStyle = stl; + } + + void finishConstruction () { + winrect.size.sanitize(); + createRoot(); + createStyle(); + createWidgets(); + finishCreating(); + } + +public: + this () { + winrect.pos = GxPoint(0, 0); + winrect.size = GxSize(0, 0); + finishConstruction(); + } + + this (string atitle) { + title = atitle; + winrect.pos = GxPoint(0, 0); + winrect.size = GxSize(0, 0); + finishConstruction(); + } + + this (string atitle, in GxPoint apos, in GxSize asize) { + title = atitle; + winrect.pos = apos; + winrect.size = asize; + finishConstruction(); + } + + this (string atitle, in GxSize asize) { + title = atitle; + winrect.size = asize; + winrect.size.sanitize(); + winrect.pos.x = (screenWidth-winrect.width)/2; + winrect.pos.y = (screenHeight-winrect.height)/2; + finishConstruction(); + } + + // this doesn't perform relayouting + void setRoot (RootWidget w) { + mRoot = w; + fixRoot(); + } + + // this is called from constructor + void createStyle () { + } + + // this is called from constructor + void createWidgets () { + } + + // this is called from constructor + // you can call `addModal()` here, for example + void finishCreating () { + add(); + } + + void relayout (bool resizeWindow) { + if (mRoot is null) return; + + FuiFlexLayouter!Widget lay; + + lay.isValidBoxId = delegate bool (Widget id) { return (id !is null); }; + + lay.firstChild = delegate Widget (Widget id) { return id.firstChild; }; + lay.nextSibling = delegate Widget (Widget id) { return id.nextSibling; }; + + lay.getMinSize = delegate int (Widget id, in bool horiz) { return (horiz ? id.minSize.w : id.minSize.h); }; + lay.getMaxSize = delegate int (Widget id, in bool horiz) { return (horiz ? id.maxSize.w : id.maxSize.h); }; + lay.getPrefSize = delegate int (Widget id, in bool horiz) { return (horiz ? id.prefSize.w : id.prefSize.h); }; + + lay.isHorizBox = delegate bool (Widget id) { return (id.childDir == GxDir.Horiz); }; + + lay.getFlex = delegate int (Widget id) { return id.flex; }; + + lay.getSize = delegate int (Widget id, in bool horiz) { return (horiz ? id.boxsize.w : id.boxsize.h); }; + lay.setSize = delegate void (Widget id, in bool horiz, int val) { if (horiz) id.boxsize.w = val; else id.boxsize.h = val; }; + + lay.getFinalSize = delegate int (Widget id, in bool horiz) { return (horiz ? id.finalSize.w : id.finalSize.h); }; + lay.setFinalSize = delegate void (Widget id, in bool horiz, int val) { if (horiz) id.finalSize.w = val; else id.finalSize.h = val; }; + + lay.setFinalPos = delegate void (Widget id, in bool horiz, int val) { if (horiz) id.finalPos.x = val; else id.finalPos.y = val; }; + + if (winrect.size.empty) { + if (maxWinSize.empty) maxWinSize = GxSize(screenWidth-decorationSizeX, screenHeight-decorationSizeY); + resizeWindow = true; + } + + if (winrect.size.w > screenWidth) winrect.size.w = screenWidth; + if (winrect.size.h > screenHeight) winrect.size.h = screenHeight; + + if (resizeWindow) { + mRoot.maxSize = maxWinSize; + } else { + mRoot.maxSize = winrect.size-GxSize(decorationSizeX, decorationSizeY); + if (mRoot.maxSize.w <= 0) mRoot.maxSize.w = maxWinSize.w; + if (mRoot.maxSize.h <= 0) mRoot.maxSize.h = maxWinSize.h; + } + + mRoot.minSize = minWinSize; + if (mRoot.minSize.w > screenWidth-decorationSizeX) mRoot.minSize.w = screenWidth-decorationSizeX; + if (mRoot.minSize.h > screenHeight-decorationSizeY) mRoot.minSize.h = screenHeight-decorationSizeY; + + if (title.length) { + if (mRoot.boxsize.w <= 0) mRoot.boxsize.w = gxTextWidthUtf(title)+2; + } + mRoot.prefSize = mRoot.boxsize; + + mRoot.forEachDepth((Widget w) { w.preLayout(); }); + + lay.layout(mRoot); + + winrect.size = mRoot.boxsize+GxSize(decorationSizeX, decorationSizeY); + if (winrect.x1 >= screenWidth) winrect.pos.x -= winrect.x1-screenWidth+1; + if (winrect.y1 >= screenHeight) winrect.pos.y -= winrect.y1-screenHeight+1; + if (winrect.pos.x < 0) winrect.pos.x = 0; + if (winrect.pos.y < 0) winrect.pos.y = 0; + + mRoot.forEachDepth((Widget w) { w.postLayout(); }); + } + + void relayoutResize () { relayout(true); } + + // this clones the style + void appendStyle (const(char)[] str) { + if (colorStyle is null) { + colorStyle = new ColorStyle; + colorStyle.cloneFrom(defaultColorStyle); + } + colorStyle.parseStyle(str); + } + + final @property Widget rootWidget () pure nothrow @safe @nogc { pragma(inline, true); return mRoot; } + + final @property int x0 () const pure nothrow @safe @nogc { return (mMinimized ? winminx : winrect.pos.x); } + final @property int y0 () const pure nothrow @safe @nogc { return (mMinimized ? winminy : winrect.pos.y); } + final @property int width () const pure nothrow @safe @nogc { return (mMinimized ? MinSizeX : winrect.size.w); } + final @property int height () const pure nothrow @safe @nogc { return (mMinimized ? MinSizeY : winrect.size.h); } + + final @property void x0 (int v) pure nothrow @safe @nogc { if (mMinimized) winminx = v; else winrect.pos.x = v; } + final @property void y0 (int v) pure nothrow @safe @nogc { if (mMinimized) winminy = v; else winrect.pos.y = v; } + final @property void width (int v) nothrow @safe @nogc { winrect.size.w = v; } + final @property void height (int v) nothrow @safe @nogc { winrect.size.h = v; } + + final void setSize (int awidth, int aheight) { + bool changed = false; + if (awidth > 0 && awidth != winrect.size.w) { changed = true; winrect.size.w = awidth; } + if (aheight > 0 && aheight != winrect.size.w) { changed = true; winrect.size.h = aheight; } + if (changed) fixRoot(); + } + + final void setClientSize (int awidth, int aheight) { + bool changed = false; + if (awidth > 0 && awidth+decorationSizeX != winrect.size.w) { changed = true; winrect.size.w = awidth+decorationSizeX; } + if (aheight > 0 && aheight+decorationSizeY != winrect.size.h) { changed = true; winrect.size.h = aheight+decorationSizeY; } + if (changed) fixRoot(); + } + + final void centerWindow () nothrow @trusted @nogc { + winrect.pos.x = (screenWidth-winrect.size.w)/2; + winrect.pos.y = (screenHeight-winrect.size.h)/2; + } + + final @property SubWindow prev () pure nothrow @safe @nogc { return mPrev; } + final @property SubWindow next () pure nothrow @safe @nogc { return mNext; } + + final @property Type type () const pure nothrow @safe @nogc { return mType; } + final @property bool onTop () const pure nothrow @safe @nogc { return (mType == Type.OnTop); } + final @property bool onBottom () const pure nothrow @safe @nogc { return (mType == Type.OnBottom); } + + final @property bool inWinList () const pure nothrow @safe @nogc { return mInWinList; } + + final @property bool modal () const pure nothrow @safe @nogc { return mModal; } + final @property bool closed () const pure nothrow @safe @nogc { return mClosed; } + + final @property bool active () const nothrow @trusted @nogc { + if (!mInWinList || mClosed || mMinimized) return false; + return (getActiveSubWindow is this); + } + + @property int decorationSizeX () const nothrow @safe { return 2*2; } + @property int decorationSizeY () const nothrow @safe { return (gxTextHeightUtf < 10 ? 10 : gxTextHeightUtf+2)+1+2; } + + @property int clientOffsetX () const nothrow @safe { return 2; } + @property int clientOffsetY () const nothrow @safe { return (gxTextHeightUtf < 10 ? 10 : gxTextHeightUtf+2)+1; } + + final @property int clientWidth () const nothrow @safe { return winrect.size.w-decorationSizeX; } + final @property int clientHeight () const nothrow @safe { return winrect.size.h-decorationSizeY; } + + protected void drawWidgets () { + setupClientClip(); + if (mRoot !is null) mRoot.onPaint(); + } + + // draw window frame and background in "normal" state + protected void drawWindowNormal () { + setupClip(); + immutable string act = (active ? null : "inactive"); + gxDrawWindow(winrect, title, + getStyleColor(this, "frame", act), + getStyleColor(this, "title-text", act), + getStyleColor(this, "title-back", act), + getStyleColor(this, "back", act)); + } + + // draw window frame and background in "minimized" state + protected void drawWindowMinimized () { + gxClipRect.x0 = winminx; + gxClipRect.y0 = winminy; + gxClipRect.x1 = winminx+MinSizeX-1; + gxClipRect.y1 = winminy+MinSizeY-1; + immutable string act = (active ? null : "inactive"); + gxFillRect(winminx, winminy, MinSizeX, MinSizeY, getStyleColor(this, "back", act)); + gxDrawRect(winminx, winminy, MinSizeX, MinSizeY, getStyleColor(this, "frame", act)); + } + + void releaseWidgetGrab () { + if (mRoot !is null) mRoot.releaseGrab(); + } + + // event in our local coords + void startMouseDrag (MouseEvent event) { + releaseWidgetGrab(); + subwinDrag = this; + subwinDragXSpot = -event.x; + subwinDragYSpot = -event.y; + postScreenRebuild(); + } + + void startKeyboardDrag () { + releaseWidgetGrab(); + subwinDrag = this; + subwinDragXSpot = int.min; + subwinDragYSpot = int.min; + postScreenRebuild(); + } + + void stopDrag () { + if (subwinDrag is this) { + releaseWidgetGrab(); + subwinDrag = null; + postScreenRebuild(); + } + } + + void onPaint () { + if (closed) return; + gxWithSavedClip { + if (!mMinimized) { + drawWindowNormal(); + drawWidgets(); + } else { + drawWindowMinimized(); + } + }; + } + + + bool onKeySink (KeyEvent event) { + return false; + } + + bool onKeyBubble (KeyEvent event) { + // global window hotkeys + if (event.pressed) { + if (event == "C-F5") { startKeyboardDrag(); return true; } + if (event == "M-M" && !mModal) { minimize(); return true; } + } + return false; + } + + bool onKeyEvent (KeyEvent event) { + if (closed) return false; + if (mMinimized) return false; + if (onKeySink(event)) return true; + if (mRoot !is null && mRoot.dispatchKey(event)) return true; + return onKeyBubble(event); + } + + + bool onMouseSink (MouseEvent event) { + // start drag? + if (subwinDrag is null && event.type == MouseEventType.buttonPressed && event.button == MouseButton.left) { + if (event.x >= 0 && event.y >= 0 && + event.x < width && event.y < (!mMinimized ? gxTextHeightUtf+2 : height)) + { + startMouseDrag(event); + return true; + } + } + + if (mMinimized) return false; + + if (event.type == MouseEventType.buttonReleased && event.button == MouseButton.right) { + if (event.x >= 0 && event.y >= 0 && + event.x < winrect.size.w && event.y < gxTextHeightUtf) + { + if (!mModal && mType == Type.Normal) minimize(); + return true; + } + } + + return false; + } + + bool onMouseBubble (MouseEvent event) { + return false; + } + + bool onMouseEvent (MouseEvent event) { + if (!active) return false; + if (closed) return false; + + int mx, my; + event.mouse2xy(mx, my); + + MouseEvent ev = event; + ev.x = mx-x0; + ev.y = my-y0; + if (onMouseSink(ev)) return true; + + if (mRoot !is null) { + ev = event; + ev.x = mx-(x0+clientOffsetX)-mRoot.rect.x0; + ev.y = my-(y0+clientOffsetY)-mRoot.rect.y0; + if (mRoot.dispatchMouse(ev)) return true; + } + + ev = event; + ev.x = mx-x0; + ev.y = my-y0; + return onMouseBubble(ev); + } + + + bool onCharSink (dchar ch) { + return false; + } + + bool onCharBubble (dchar ch) { + return false; + } + + bool onCharEvent (dchar ch) { + if (!active) return false; + if (closed) return false; + if (mMinimized) return false; + if (onCharSink(ch)) return true; + if (mRoot !is null && mRoot.dispatchChar(ch)) return true; + return onCharBubble(ch); + } + + void setupClip () { + gxClipRect.intersect(winrect); + } + + final void setupClientClip () { + setupClip(); + gxClipRect.intersect(GxRect( + GxPoint(winrect.pos.x+clientOffsetX, winrect.pos.y+clientOffsetY), + GxPoint(winrect.pos.x+clientOffsetX+clientWidth-1, winrect.pos.y+clientOffsetY+clientHeight-1))); + } + + void close () { + mClosed = true; + if (removeSubWindow(this)) postScreenRebuild(); + } + + protected bool addToSubwinList (bool asModal, bool fromKeyboard) { + if (fromKeyboard) ignoreSubWinChar = true; + if (mInWinList) return true; + mModal = asModal; + if (insertSubWindow(this)) { + postScreenRebuild(); + return true; + } + return false; + } + + void add (bool fromKeyboard=false) { addToSubwinList(false, fromKeyboard); } + + void addModal (bool fromKeyboard=false) { addToSubwinList(true, fromKeyboard); } + + void bringToFront () { + if (mClosed || !mInWinList) return; + auto aw = getActiveSubWindow(); + if (aw is this) { mMinimized = false; return; } + if (aw !is null && aw.mModal) return; // alas + removeSubWindow(this); + mMinimized = false; + insertSubWindow(this); + if (subwinDrag !is this) subwinDrag = null; + postScreenRebuild(); + } + + @property bool minimized () const { return mMinimized; } + + @property void minimized (bool v) { + if (v == mMinimized) return; + if (v) minimize(); else bringToFront(); + } + + void minimize () { + if (mClosed || mMinimized) return; + if (!mInWinList) { mMinimized = true; return; } + if (mModal) return; + assert(subwinLast !is null); + releaseWidgetGrab(); + findMinimizedPos(winminx, winminy); + auto aw = getActiveSubWindow(); + if (aw is this) subwinDrag = null; + mMinimized = true; + postScreenRebuild(); + } + + void restore () { + releaseWidgetGrab(); + bringToFront(); + } + +public: + enum MinSizeX = 16; + enum MinSizeY = 16; + enum MinMarginX = 3; + enum MinMarginY = 3; + +protected: + static findMinimizedPos (out int wx, out int wy) { + static bool isOccupied (int x, int y) { + for (SubWindow w = subwinLast; w !is null; w = w.mPrev) { + if (w.mInWinList && !w.closed && w.mMinimized) { + if (x >= w.winminx && y >= w.winminy && x < w.winminx+MinSizeX && y < w.winminy+MinSizeY) return true; + } + } + return false; + } + + int txcount = screenWidth/(MinSizeX+MinMarginX); + //int tycount = screenHeight/(MinSizeY+MinMarginY); + if (txcount < 1) txcount = 1; + //if (tycount < 1) tycount = 1; + foreach (immutable int n; 0..6/*5535*/) { + int x = (n%txcount)*(MinSizeX+MinMarginX)+1; + int y = screenHeight-MinSizeY-(n/txcount)*(MinSizeY+MinMarginY); + //conwriteln("trying (", x, ",", y, ")"); + if (!isOccupied(x, y)) { wx = x; wy = y; /*conwriteln(" HIT!");*/ return; } + } + } +} diff --git a/egra/gui/widgets.d b/egra/gui/widgets.d new file mode 100644 index 0000000..f05cae4 --- /dev/null +++ b/egra/gui/widgets.d @@ -0,0 +1,1155 @@ +/* + * Simple Framebuffer Gfx/GUI lib + * + * coded by Ketmar // Invisible Vector + * Understanding is not required. Only obedience. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License ONLY. + * + * 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, see . + */ +module iv.egra.gui.widgets /*is aliced*/; +private: + +import arsd.simpledisplay; + +import iv.egra.gfx; + +import iv.alice; +import iv.cmdcon; +import iv.strex; +import iv.utfutil; + +import iv.egra.gui.subwindows; + + +// ////////////////////////////////////////////////////////////////////////// // +public enum WidgetStringPropertyMixin(string propname, string fieldName) = ` + @property string `~propname~` () const nothrow @safe @nogc { return `~fieldName~`; } + @property void `~propname~`(T:const(char)[]) (T v) nothrow @trusted { + static if (is(T == typeof(null))) { + `~fieldName~` = null; + } else static if (is(T == immutable(char)[])) { + `~fieldName~` = v; + } else { + `~fieldName~` = v.idup; + } + } +`; + + +// ////////////////////////////////////////////////////////////////////////// // +public abstract class Widget { +public: + Widget parent; + Widget firstChild; + Widget nextSibling; + + GxRect rect; // relative to parent + bool tabStop; + // set this to `true` to skip painting; useful for spring and spacer widgets + // such widgets will not receive any events too (including sink/bubble) + // note that there is no reason to add children to such widgets, because + // non-visual widgets cannot be painted, and cannot be focused + bool nonVisual; + + // this is for flexbox layouter + enum BoxHAlign { + Expand, + Left, + Center, + Right, + } + + enum BoxVAlign { + Expand, + Top, + Center, + Bottom, + } + + GxSize minSize; // minimum size + GxSize maxSize = GxSize(int.max, int.max); // maximum size + GxSize prefSize = GxSize(int.min, int.min); // preferred (initial) size; will be taken from widget size + + GxSize boxsize; /// box size + GxSize finalSize; /// final size (can be bigger than `size)` + GxPoint finalPos; /// final position (for the final size); relative to the parent origin + + GxDir childDir = GxDir.Horiz; /// children orientation + int flex; /// <=0: not flexible + + BoxHAlign boxHAlign = BoxHAlign.Expand; + BoxVAlign boxVAlign = BoxVAlign.Expand; + +public: + // for layouter + void preLayout () { + if (prefSize.w == int.min) prefSize = rect.size; else rect.size = prefSize; + } + + void postLayout () { + rect.pos = finalPos; + rect.size = boxsize; + final switch (boxHAlign) with (BoxHAlign) { + case Expand: rect.size.w = finalSize.w; break; + case Left: break; + case Center: rect.pos.x = finalPos.x+(finalSize.w-boxsize.w)/2; break; + case Right: rect.pos.x = finalPos.x+finalSize.w-boxsize.w; break; + } + final switch (boxVAlign) with (BoxVAlign) { + case Expand: rect.size.h = finalSize.h; break; + case Top: break; + case Center: rect.pos.y = finalPos.y+(finalSize.h-boxsize.h)/2; break; + case Bottom: rect.pos.y = finalPos.y+finalSize.h-boxsize.h; break; + } + } + +protected: + Widget mActive; // do not change directly! + string mHotkey; + + bool isMyHotkey (KeyEvent event) { + return (mHotkey.length && event == mHotkey); + } + +public: + void delegate (Widget self) onAction; + + protected void doDefaultAction () {} + + void doAction () { + if (onAction !is null) onAction(this); else doDefaultAction(); + } + +public: + void addChild (Widget w) { + //{ import std.stdio; writefln("addChild(0x%08x); w=0x%08x", cast(uint)cast(void*)this, cast(uint)cast(void*)w); } + if (w is null) return; + assert (w !is this); + assert(w.parent is null); + if (cast(RootWidget)w) throw new Exception("root widget cannot be child of anything"); + if (nonVisual) throw new Exception("non-visual widget cannot have children"); + w.parent = this; + Widget lc = firstChild; + if (lc is null) { + firstChild = w; + } else { + while (lc.nextSibling) lc = lc.nextSibling; + lc.nextSibling = w; + } + //if (mActive is null && w.canAcceptFocus(w)) mActive = w; + } + + static template isFEDGoodDelegate(DG) { + import std.traits; + enum isFEDGoodDelegate = + (is(ReturnType!DG == void) || is(ReturnType!DG == Widget)) && + (is(typeof((inout int=0) { DG dg = void; Widget w; Widget res = dg(w); })) || + is(typeof((inout int=0) { DG dg = void; Widget w; dg(w); }))); + } + + static template isFEDResDelegate(DG) { + import std.traits; + enum isFEDResDelegate = is(ReturnType!DG == Widget); + } + + // Widget delegate (Widget w) + final Widget forEachDepth(DG) (scope DG dg) if (isFEDGoodDelegate!DG) { + for (Widget w = firstChild; w !is null; w = w.nextSibling) { + Widget res = w.forEachDepth(dg); + if (res !is null) return res; + } + static if (isFEDResDelegate!DG) { + return dg(this); + } else { + dg(this); + return null; + } + } + + // Widget delegate (Widget w) + final Widget forEachVisualDepth(DG) (scope DG dg) if (isFEDGoodDelegate!DG) { + if (nonVisual) return null; + for (Widget w = firstChild; w !is null; w = w.nextSibling) { + if (!w.nonVisual) { + Widget res = w.forEachVisualDepth(dg); + if (res !is null) return res; + } + } + static if (isFEDResDelegate!DG) { + return dg(this); + } else { + dg(this); + return null; + } + } + +public: + this (Widget aparent) { + if (aparent !is null) aparent.addChild(this); + } + + // returns `true` if passed `this` + final bool isMyChild (in Widget w) pure const nothrow @safe @nogc { + pragma(inline, true); + return + w is null ? false : + w is this ? true : + isMyChild(w.parent); + } + + // should be overriden in root widget + // should be called only for widgets without a parent + bool isOwnerFocused () nothrow @safe @nogc { + if (parent) return parent.isOwnerFocused(); + return true; + } + + // should be overriden in the root widget + void releaseGrab () { + } + + // should be overriden in the root widget + GxPoint getGlobalOffset () nothrow @safe { + return GxPoint(0, 0); + } + + // returns focused widget; it doesn't matter if root widget's owner is focused + // never returns `null` + @property Widget focusedWidget () nothrow @safe @nogc { + if (parent) return parent.focusedWidget; + Widget res = this; + while (res.mActive !is null) res = res.mActive; + return res; + } + + // it doesn't matter if root widget's owner is focused + //FIXME: ask all parents too? + final @property bool focus () { + Widget oldFocus = focusedWidget; + if (oldFocus is this) return true; + if (!canAcceptFocus()) return false; + if (!oldFocus.canRemoveFocus()) return false; + releaseGrab(); + oldFocus.onBlur(); + Widget w = this; + while (w.parent !is null) { + w.parent.mActive = w; + w = w.parent; + } + onFocus(); + return true; + } + + // this returns `false` if root widget's parent is not focused + final @property bool isFocused () nothrow @safe @nogc { + pragma(inline, true); + return (isOwnerFocused() && this is focusedWidget); + } + + // returns point translated to the topmost parent coords + // with proper root widget, returns global screen coordinates (useful for drawing) + final @property GxPoint point2Global (GxPoint pt) nothrow @safe { + if (parent is null) return pt+getGlobalOffset; + return parent.point2Global(pt+rect.pos); + } + + // returns rectangle in topmost parent coords + // with proper root widget, returns global screen coordinates (useful for drawing) + final @property GxRect globalRect () nothrow @safe { + GxPoint pt = point2Global(GxPoint(0, 0)); + return GxRect(pt, rect.size); + } + + // this need to be called if you did automatic layouting, and then changed size or position + // it's not done automatically because it is rarely required + final void rectChanged () nothrow @safe @nogc { + pragma(inline, true); + prefSize = GxSize(int.min, int.min); + } + + final @property ref inout(GxSize) size () inout pure nothrow @safe @nogc { pragma(inline, true); return rect.size; } + final @property void size (in GxSize sz) nothrow @safe @nogc { pragma(inline, true); rect.size = sz; } + + final @property ref inout(GxPoint) pos () inout pure nothrow @safe @nogc { pragma(inline, true); return rect.pos; } + final @property void pos (in GxPoint p) nothrow @safe @nogc { pragma(inline, true); rect.pos = p; } + + final @property int width () const pure nothrow @safe @nogc { pragma(inline, true); return rect.size.w; } + final @property int height () const pure nothrow @safe @nogc { pragma(inline, true); return rect.size.h; } + + final @property void width (in int v) nothrow @safe @nogc { pragma(inline, true); rect.size.w = v; } + final @property void height (in int v) nothrow @safe @nogc { pragma(inline, true); rect.size.h = v; } + + final @property int posx () const pure nothrow @safe @nogc { pragma(inline, true); return rect.pos.x; } + final @property int posy () const pure nothrow @safe @nogc { pragma(inline, true); return rect.pos.y; } + + final @property void posx (in int v) nothrow @safe @nogc { pragma(inline, true); rect.pos.x = v; } + final @property void posy (in int v) nothrow @safe @nogc { pragma(inline, true); rect.pos.y = v; } + + // this never returns `null`, even for out-of-bound coords + // for out-of-bounds case simply return `this` + final Widget childAt (GxPoint pt) pure nothrow @safe @nogc { + if (!pt.inside(rect.size)) return this; + Widget bestChild; + for (Widget w = firstChild; w !is null; w = w.nextSibling) { + if (!w.nonVisual && pt.inside(w.rect)) bestChild = w; + } + if (bestChild is null) return this; + pt -= bestChild.rect.pos; + return bestChild.childAt(pt); + } + + final Widget childAt (in int x, in int y) pure nothrow @safe @nogc { + pragma(inline, true); + return childAt(GxPoint(x, y)); + } + + // this is called with set clip + // passed rectangle is global rect + // clip rect is set to the widget area + // `doPaint()` is called before painting children + // `doPaintPost()` is called after painting children + // it is safe to change clip rect there, it will be restored + // `grect` is in global screen coordinates + protected void doPaint (GxRect grect) {} + protected void doPaintPost (GxRect grect) {} + + void onPaint () { + if (nonVisual || rect.empty) return; // nothing to paint here + if (gxClipRect.empty) return; // totally clipped away + gxWithSavedClip { + immutable GxRect grect = globalRect; + if (!gxClipRect.intersect(grect)) return; + // before-children painter + gxWithSavedClip { doPaint(grect); }; + // paint children + for (Widget w = firstChild; w !is null; w = w.nextSibling) { + if (!w.nonVisual) gxWithSavedClip { w.onPaint(); }; + } + // after-children painter + gxWithSavedClip { doPaintPost(grect); }; + }; + } + + // return `false` to disable focus change + bool canRemoveFocus () { return true; } + + // return `false` to disable focus change + bool canAcceptFocus () { return (!nonVisual && tabStop); } + + // called before removing the focus + void onBlur () {} + + // called after setting the focus + void onFocus () {} + + // return `true` if the event was eaten + // coordinates are adjusted to the widget origin + bool onKey (KeyEvent event) { return false; } + bool onMouse (MouseEvent event) { return false; } + bool onChar (dchar ch) { return false; } + + // coordinates are adjusted to the widget origin (NOT dest!) + bool onKeySink (Widget dest, KeyEvent event) { return false; } + bool onMouseSink (Widget dest, MouseEvent event) { return false; } + bool onCharSink (Widget dest, dchar ch) { return false; } + + // coordinates are adjusted to the widget origin (NOT dest!) + bool onKeyBubble (Widget dest, KeyEvent event) { return false; } + bool onMouseBubble (Widget dest, MouseEvent event) { return false; } + bool onCharBubble (Widget dest, dchar ch) { return false; } + +public: + // color getters + uint getStyleColor (in Object obj, const(char)[] type, const(char)[] mod=null) { + if (parent is null) return gxUnknown; + return parent.getStyleColor(obj, type, mod); + } + + final uint getColor (const(char)[] type, const(char)[] mod) { + return getStyleColor(this, type, mod); + } + + final uint getColor (const(char)[] type) { + return getStyleColor(this, type, (isFocused ? "focused" : null)); + } +} + + +// ////////////////////////////////////////////////////////////////////////// // +// this is root widget that is used for subwindow client area +public class RootWidget : Widget { + SubWindow owner; + uint mButtons; // used for grab + + @disable this (); + + this (SubWindow aowner) { + childDir = GxDir.Vert; + super(null); + owner = aowner; + if (aowner !is null) aowner.setRoot(this); + } + + override bool isOwnerFocused () nothrow @safe @nogc { + if (owner is null) return false; + return owner.active; + } + + // can be called by the owner + // this also resets pressed mouse buttons + override void releaseGrab () { + mButtons = 0; + } + + override GxPoint getGlobalOffset () nothrow @safe { + if (owner is null) return super.getGlobalOffset(); + return GxPoint(owner.x0+owner.clientOffsetX, owner.y0+owner.clientOffsetY)+rect.pos; + } + + void checkGrab () { + if (mButtons && !isOwnerFocused) releaseGrab(); + } + + enum EventPhase { + Sink, + Mine, + Bubble, + } + + // dispatch event to this widget + // it implements sink/bubble model + // delegate is: + // bool delegate (Widget curr, Widget dest, EventPhase phase); + // returns "event eaten" flag (which stops propagation) + // `dest` must be a good child, and cannot be `null` + final bool dispatchTo(DG) (Widget dest, scope DG dg) + if (is(typeof((inout int=0) { DG dg = void; Widget w; EventPhase ph; immutable bool res = dg(w, w, ph); }))) + { + // as we're walking from the bottom, first call it recursively, and then call delegate + bool sinkPhase() (Widget curr) { + if (curr is null) return false; + if (sinkPhase(curr.parent)) return true; + return dg(curr, dest, EventPhase.Sink); + } + + if (dest is null || !isMyChild(dest)) return false; + + if (sinkPhase(dest.parent)) return true; + if (dg(dest, dest, EventPhase.Mine)) return true; + for (Widget w = dest.parent; w !is null; w = w.parent) { + if (dg(w, dest, EventPhase.Bubble)) return true; + } + + return false; + } + + void doShiftTab () { + Widget pf = null; + Widget fc = focusedWidget; + forEachVisualDepth(delegate Widget (Widget w) { + if (w is fc) return w; + if (w.canAcceptFocus()) { + pf = w; + if (fc is null) return w; + } + return null; + }); + + if (pf is null) { + // find last + forEachVisualDepth((Widget w) { + if (w.canAcceptFocus()) pf = w; + }); + } + + if (pf !is null) pf.focus(); + } + + void doTab () { + Widget fc = focusedWidget; + bool seenFC = (fc is null); + Widget nf = forEachVisualDepth(delegate Widget (Widget w) { + if (!seenFC) { + seenFC = (w is fc); + } else { + if (w.canAcceptFocus()) return w; + } + return null; + }); + + if (nf is null) { + // find first + nf = forEachVisualDepth(delegate Widget (Widget w) { + if (w.canAcceptFocus()) return w; + return null; + }); + } + + if (nf !is null) nf.focus(); + } + + override bool onKeyBubble (Widget dest, KeyEvent event) { + if (event.pressed) { + if (event == "Tab" || event == "C-Tab") { doTab(); return true; } + if (event == "S-Tab" || event == "C-S-Tab") { doShiftTab(); return true; } + + // check hotkeys + Widget hk = forEachVisualDepth(delegate Widget (Widget w) { + if (w.isMyHotkey(event) && w.canAcceptFocus()) return w; + return null; + }); + if (hk !is null) { + hk.focus(); + hk.doAction(); + return true; + } + } + + return super.onKeyBubble(dest, event); + } + + bool dispatchKey (KeyEvent event) { + checkGrab(); + return dispatchTo(focusedWidget, delegate bool (Widget curr, Widget dest, EventPhase phase) { + if (curr.nonVisual) return false; + final switch (phase) { + case EventPhase.Sink: return curr.onKeySink(dest, event); + case EventPhase.Mine: return curr.onKey(event); + case EventPhase.Bubble: return curr.onKeyBubble(dest, event); + } + assert(0); // just in case + }); + } + + // this is quite complicated... + protected Widget getMouseDestination (in MouseEvent event) { + checkGrab(); + + // still has a grab? + if (mButtons) { + // if some mouse buttons are still down, route everything to the focused widget + // also, release a grab here if we can (grab flag is not used anywhere else) + if (event.type == MouseEventType.buttonReleased) mButtons &= ~cast(uint)event.button; + return focusedWidget; + } + + Widget dest = childAt(event.x, event.y); + assert(dest !is null); + + // if mouse button is pressed, and there were no pressed buttons before, + // find the child, and check if it can grab events + if (event.type == MouseEventType.buttonPressed) { + if (dest !is focusedWidget) dest.focus(); + if (dest is focusedWidget) { + // this activates the grab + mButtons = cast(uint)event.button; + } else { + releaseGrab(); + } + } else { + // release grab, if necessary (it shouldn't be necessary here, but just in case...) + if (mButtons && event.type == MouseEventType.buttonReleased) { + mButtons &= ~cast(uint)event.button; + } + } + + // route to the proper child + return dest; + } + + // mouse event coords should be relative to our rect + bool dispatchMouse (MouseEvent event) { + Widget dest = getMouseDestination(event); + assert(dest !is null); + // convert event to global + immutable GxRect grect = globalRect; + event.x += grect.pos.x; + event.y += grect.pos.y; + return dispatchTo(dest, delegate bool (Widget curr, Widget dest, EventPhase phase) { + if (curr.nonVisual) return false; + // fix coordinates + immutable GxRect wrect = curr.globalRect; + MouseEvent ev = event; + ev.x -= wrect.pos.x; + ev.y -= wrect.pos.y; + final switch (phase) { + case EventPhase.Sink: return curr.onMouseSink(dest, ev); + case EventPhase.Mine: return curr.onMouse(ev); + case EventPhase.Bubble: return curr.onMouseBubble(dest, ev); + } + assert(0); // just in case + }); + } + + bool dispatchChar (dchar ch) { + checkGrab(); + return dispatchTo(focusedWidget, delegate bool (Widget curr, Widget dest, EventPhase phase) { + if (curr.nonVisual) return false; + final switch (phase) { + case EventPhase.Sink: return curr.onCharSink(dest, ch); + case EventPhase.Mine: return curr.onChar(ch); + case EventPhase.Bubble: return curr.onCharBubble(dest, ch); + } + assert(0); // just in case + }); + } + +public: + // color getters + override uint getStyleColor (in Object obj, const(char)[] type, const(char)[] mod=null) { + if (owner !is null) return owner.getStyleColor(obj, type, mod); + return super.getStyleColor(obj, type, mod); + } +} + + +// ////////////////////////////////////////////////////////////////////////// // +public class SpacerWidget : Widget { + this (Widget aparent, in int size) { + assert(aparent !is null); + super(aparent); + tabStop = false; + nonVisual = true; + if (aparent.childDir == GxDir.Horiz) rect.size.w = size; else rect.size.h = size; + } +} + +public class SpringWidget : SpacerWidget { + this (Widget aparent, in int aflex) { + super(aparent, 0); + flex = aflex; + } +} + + +// ////////////////////////////////////////////////////////////////////////// // +public class BoxWidget : Widget { + this (Widget aparent) { + super(aparent); + tabStop = false; + } +} + +public class HBoxWidget : BoxWidget { + this (Widget aparent) { + super(aparent); + childDir = GxDir.Horiz; + } +} + +public class VBoxWidget : BoxWidget { + this (Widget aparent) { + super(aparent); + childDir = GxDir.Vert; + } +} + + +// ////////////////////////////////////////////////////////////////////////// // +public class LabelWidget : Widget { +public: + enum HAlign { + Left, + Center, + Right, + } + + enum VAlign { + Top, + Center, + Bottom, + } + +private: + string mText; + +public: + HAlign halign = HAlign.Left; + VAlign valign = VAlign.Center; + int hpad = 0; + int vpad = 0; + +public: + this(T:const(char)[]) (Widget aparent, T atext, HAlign horiz=HAlign.Left, VAlign vert=VAlign.Center) { + super(aparent); + text = atext; + rect.size.w = gxTextWidthUtf(mText); + rect.size.h = gxTextHeightUtf; + tabStop = false; + halign = horiz; + valign = vert; + } + + mixin(WidgetStringPropertyMixin!("text", "mText")); + + protected void drawLabel (GxRect grect) { + if (mText.length == 0) return; + int x; + final switch (halign) { + case HAlign.Left: x = grect.x0+hpad; break; + case HAlign.Center: x = grect.x0+(grect.width-gxTextWidthUtf(mText))/2; break; + case HAlign.Right: x = grect.x0+grect.width-hpad-gxTextWidthUtf(mText); break; + } + int y; + final switch (valign) { + case VAlign.Top: y = grect.y0+vpad; break; + case VAlign.Center: y = grect.y0+(grect.height-gxTextHeightUtf)/2; break; + case VAlign.Bottom: y = grect.y0+grect.height-vpad-gxTextHeightUtf; break; + } + //gxDrawTextUtf(x0+(width-gxTextWidthUtf(mText))/2, y0+(height-gxTextHeightUtf)/2, mText, parent.clrWinText); + gxDrawTextUtf(x, y, mText, getColor("text")); + //{ import core.stdc.stdio : printf; printf("LBL: 0x%08x\n", getColor("text")); } + } + + protected override void doPaint (GxRect grect) { + gxFillRect(grect, getColor("back")); + drawLabel(grect); + } +} + + +// ////////////////////////////////////////////////////////////////////////// // +public class ProgressBarWidget : LabelWidget { +protected: + int mMin = 0; + int mMax = 100; + int mCurrent = 0; + int lastWidth = int.min; + int lastPxFull = int.min; + +private: + final bool updateLast () nothrow @safe @nogc { + bool res = false; + if (width != lastWidth) { + lastWidth = width; + res = true; + } + if (lastWidth < 1) { + if (lastPxFull != 0) { + res = true; + lastPxFull = 0; + } + } else { + int pxFull; + if (mMin == mMax) { + pxFull = lastWidth; + } else { + pxFull = cast(int)(cast(long)lastWidth*cast(long)(mCurrent-mMin)/cast(long)(mMax-mMin+1)); + } + if (pxFull != lastPxFull) { + res = true; + lastPxFull = pxFull; + } + } + return res; + } + +public: + this(T:const(char)[]) (Widget aparent, T atext, HAlign horiz=HAlign.Center, VAlign vert=VAlign.Center) { + super(aparent, atext, horiz, vert); + height = height+4; + } + + final void setMinMax (int amin, int amax) nothrow @safe @nogc { + if (amin > amax) { immutable int tmp = amin; amin = amax; amax = tmp; } + mMin = amin; + mMax = amax; + if (mCurrent < mMin) mCurrent = mMin; + if (mCurrent > mMax) mCurrent = mMax; + updateLast(); + } + + final @property int current () const nothrow @safe @nogc { + pragma(inline, true); + return mCurrent; + } + + final @property void current (int val) nothrow @safe @nogc { + pragma(inline, true); + if (val < mMin) val = mMin; + if (val > mMax) val = mMax; + mCurrent = val; + updateLast(); + } + + // returns `true` if need to repaint + final bool setCurrentTotal (int val, int total) nothrow @safe @nogc { + if (total < 0) total = 0; + setMinMax(0, total); + if (val < 0) val = 0; + if (val > total) val = total; + mCurrent = val; + return updateLast(); + } + + protected void drawStripes (GxRect rect, string modnorm, string modshade) { + if (rect.empty) return; + + immutable int sty = rect.pos.y; + immutable int sth = rect.size.h; + + GxRect uprc = rect; + if (rect.height > 4) uprc.size.h = 2; + else if (rect.height > 2) uprc.size.h = 1; + else uprc.size.h = 0; + if (uprc.size.h > 0) { + rect.pos.y += uprc.size.h; + rect.size.h -= uprc.size.h; + } + + if (!uprc.empty) gxFillRect(uprc, getColor("back", modshade)); + gxFillRect(rect, getColor("back", modnorm)); + + immutable uint clrOk = getColor("stripe", modnorm); + immutable uint clrHi = getColor("stripe", modshade); + immutable int wend = rect.size.w+32+sth; + for (int x0 = 0; x0 < wend; x0 += 32) { + foreach (int y0; 0..sth) { + gxHLine(rect.pos.x+x0-y0, sty+y0, 16, (y0 < uprc.size.h ? clrHi : clrOk)); + } + } + } + + protected override void doPaint (GxRect grect) { + immutable uint clrRect = getColor("rect"); + + gxDrawRect(grect, clrRect); + gxClipRect.shrinkBy(1, 1); + grect.shrinkBy(1, 1); + if (grect.empty) return; + + if (lastWidth != width) updateLast(); + immutable int pxFull = lastPxFull; + + if (pxFull > 0) { + gxWithSavedClip{ + GxRect rc = GxRect(grect.pos, pxFull, grect.height); + if (gxClipRect.intersect(rc)) drawStripes(rc, "full", "full-hishade"); + }; + } + + if (pxFull < grect.width) { + gxWithSavedClip{ + GxRect rc = grect; + rc.pos.x += pxFull; + if (gxClipRect.intersect(rc)) drawStripes(grect, null, "hishade"); + }; + } + + if (grect.height > 2) grect.moveLeftTopBy(0, 1); + drawLabel(grect); + } +} + + +// ////////////////////////////////////////////////////////////////////////// // +public class ButtonWidget : Widget { +protected: + string mTitle; + string mHotch; + int hotxpos; + +public: + this(T0:const(char)[], T1:const(char)[]) (Widget aparent, T0 atitle, T1 ahotkey=null) { + tabStop = true; + super(aparent); + title = atitle; + hotkey = ahotkey; + if (mTitle.length > 1) { + auto umpos = mTitle.indexOf('&'); + if (umpos >= 0 && umpos < mTitle.length-1) { + mHotch = mTitle[umpos+1..$]; + while (mHotch.utflen > 1) mHotch = mHotch.utfchop; + mTitle = mTitle[0..umpos]~mTitle[umpos+1..$]; + hotxpos = gxTextWidthUtf(mTitle[0..umpos]); + if (umpos > 0) hotxpos += 1; + if (mHotkey is null) mHotkey = "M-"~mHotch; + } + } + rect.size.w = gxTextWidthUtf(mTitle)+6; + rect.size.h = gxTextHeightUtf+4; + } + + mixin(WidgetStringPropertyMixin!("title", "mTitle")); + mixin(WidgetStringPropertyMixin!("hotkey", "mHotkey")); + + protected override void doPaint (GxRect grect) { + uint bclr = getColor("text"); + uint fclr = getColor("back"); + gxFillRect(grect.x0+1, grect.y0+1, grect.width-2, grect.height-2, bclr); + gxHLine(grect.x0+1, grect.y0+0, grect.width-2, bclr); + gxHLine(grect.x0+1, grect.y1+0, grect.width-2, bclr); + gxVLine(grect.x0+0, grect.y0+1, grect.height-2, bclr); + gxVLine(grect.x1+0, grect.y0+1, grect.height-2, bclr); + gxClipRect.shrinkBy(1, 1); + int tx = grect.x0+(width-gxTextWidthUtf(mTitle))/2; + int ty = grect.y0+(height-gxTextHeightUtf)/2; + gxDrawTextUtf(tx, ty, mTitle, fclr); + if (mHotch.length) { + gxHLine(tx+hotxpos, ty+gxTextUnderLineUtf, gxTextWidthUtf(mHotch), getColor("hotline")); + } + } + + override bool onKey (KeyEvent event) { + if (!event.pressed || !isFocused) return super.onKey(event); + if (event == "Enter" || event == "Space") { + doAction(); + return true; + } + return super.onKey(event); + } + + override bool onMouse (MouseEvent event) { + if (!isFocused) return super.onMouse(event); + if (GxPoint(event.x, event.y).inside(rect.size)) { + if (event.type == MouseEventType.buttonReleased && event.button == MouseButton.left) { + //{ import std.stdio; writeln("BTN(", mTitle, "): !!!"); } + doAction(); + } + return true; + } + return super.onMouse(event); + } +} + + +// ////////////////////////////////////////////////////////////////////////// // +public class ButtonExWidget : ButtonWidget { +public: + this(T0:const(char)[], T1:const(char)[]) (Widget aparent, T0 atitle, T1 ahotkey=null) { + super(aparent, atitle, ahotkey); + rect.size.h = rect.size.h+1; + } + + protected override void doPaint (GxRect grect) { + /* + uint bclr = (focused ? gxRGB!(0x93, 0xb6, 0xfc) : gxRGB!(0x73, 0x96, 0xdc)); + uint hclr = (focused ? gxRGB!(0xa3, 0xc6, 0xff) : gxRGB!(0x83, 0xa6, 0xec)); + uint fclr = (focused ? gxRGB!(0xff, 0xff, 0xff) : gxRGB!(0xff, 0xff, 0xff)); + uint brc = gxRGB!(0x40, 0x70, 0xcf); + */ + uint bclr = getColor("back"); + uint hclr = getColor("shadowline"); + uint fclr = getColor("text"); + uint brc = getColor("rect"); + + gxFillRect(grect.x0+1, grect.y0+1, grect.width-2, grect.height-2, bclr); + gxDrawRect(grect.x0, grect.y0, grect.width, grect.height, brc); + gxHLine(grect.x0+1, grect.y0+1, grect.width-2, hclr); + + gxClipRect.shrinkBy(1, 1); + int tx = grect.x0+(width-gxTextWidthUtf(mTitle))/2; + int ty = grect.y0+(height-gxTextHeightUtf)/2; + gxDrawTextUtf(tx, ty, mTitle, fclr); + if (mHotch.length) { + gxHLine(tx+hotxpos, ty+gxTextUnderLineUtf, gxTextWidthUtf(mHotch), getColor("hotline")); + } + } +} + + +// ////////////////////////////////////////////////////////////////////////// // +public class CheckboxWidget : ButtonWidget { +protected: + bool mChecked; + bool mEnabled = true; + +public: + this(T0:const(char)[], T1:const(char)[]) (Widget aparent, T0 atitle, T1 ahotkey=null) { + tabStop = true; + super(aparent, atitle, ahotkey); + width = gxTextWidthUtf(mTitle)+6+gxTextWidthUtf("[")+gxTextWidthUtf("x")+gxTextWidthUtf("]")+8; + height = gxTextHeightUtf+4; + } + + @property bool checked () const nothrow @safe @nogc { return mChecked; } + @property void checked (bool v) nothrow @safe @nogc { mChecked = v; } + + @property bool enabled () const nothrow @safe @nogc { return mEnabled; } + @property void enabled (bool v) nothrow @safe @nogc { mEnabled = v; tabStop = v; } + + override bool canAcceptFocus () { return (tabStop && mEnabled); } + + protected override void doPaint (GxRect grect) { + uint fclr = getColor("text"); + uint bclr = getColor("back"); + uint xclr = getColor("mark"); + + if (!mEnabled) { + fclr = getColor("text", "disabled"); + bclr = getColor("back", "disabled"); + xclr = getColor("mark", "disabled"); + } + + gxFillRect(grect, bclr); + + gxClipRect.shrinkBy(1, 1); + int tx = grect.x0+3; + int ty = grect.y0+(grect.height-gxTextHeightUtf)/2; + + gxDrawTextUtf(tx, ty, "[", fclr); + tx += gxTextWidthUtf("["); + + if (mChecked) gxDrawTextUtf(tx, ty, "x", xclr); + tx += gxTextWidthUtf("x"); + + gxDrawTextUtf(tx, ty, "]", fclr); + tx += gxTextWidthUtf("]")+8; + + gxDrawTextUtf(tx, ty, mTitle, fclr); + if (mHotch.length) gxHLine(tx+hotxpos, ty+gxTextUnderLineUtf, gxTextWidthUtf(mHotch), fclr); + } + + protected override void doDefaultAction () { + mChecked = !mChecked; + } + + override bool onMouse (MouseEvent event) { + if (!isFocused) return super.onMouse(event); + if (GxPoint(event.x, event.y).inside(rect.size)) { + if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.left) { + doAction(); + } + return true; + } + return super.onMouse(event); + } +} + + +// ////////////////////////////////////////////////////////////////////////// // +public final class SimpleListBoxWidget : Widget { +private: + string[] mItems; + Object[] mItemData; + int mTopIdx; + int mCurIdx; + +public: + this (Widget aparent) { + super(aparent); + } + + @property int curidx () const nothrow @safe @nogc { return (mCurIdx >= 0 && mCurIdx < mItems.length ? mCurIdx : 0); } + + @property void curidx (int idx) nothrow @safe @nogc { + if (mItems.length == 0) return; + if (idx < 0) idx = 0; + if (idx >= mItems.length) idx = cast(int)(mItems.length-1); + mCurIdx = idx; + } + + @property int length () const nothrow @safe @nogc { return cast(int)mItems.length; } + @property string opIndex (usize idx) const nothrow @safe @nogc { return (idx < mItems.length ? mItems[idx] : null); } + @property Object itemData (usize idx) nothrow @safe @nogc { return (idx < mItems.length ? mItemData[idx] : null); } + + void appendItem(T:const(char)[]) (T s, Object o=null) { + //conwriteln("new item: ", s); + static if (is(T == typeof(null))) { + mItems ~= null; + } else static if (is(T == string)) { + mItems ~= s; + } else { + mItems ~= s.idup; + } + mItemData ~= o; + } + + @property int visibleItemsCount () const nothrow @trusted { + pragma(inline, true); + return (height < gxTextHeightUtf*2 ? 1 : height/gxTextHeightUtf); + } + + void makeCursorVisible () { + if (mItems.length == 0) return; + if (mCurIdx < 0) mCurIdx = 0; + if (mCurIdx >= mItems.length) mCurIdx = cast(int)(mItems.length-1); + if (mCurIdx < mTopIdx) { mTopIdx = mCurIdx; return; } + int icnt = visibleItemsCount-1; + if (mTopIdx+icnt < mCurIdx) mTopIdx = mCurIdx-icnt; + } + + protected override void doPaint (GxRect grect) { + makeCursorVisible(); + uint bclr = getColor("back"); + uint tclr = getColor("text"); + uint cbclr = getColor("cursor-back"); + uint ctclr = getColor("cursor-text"); + gxFillRect(grect, bclr); + int y = 0; + int idx = mTopIdx; + while (idx < mItems.length && y < grect.height) { + if (idx >= 0) { + uint clr = tclr; + if (idx == mCurIdx) { + gxFillRect(grect.x0, grect.y0+y, grect.width, gxTextHeightUtf, cbclr); + clr = ctclr; + } + gxWithSavedClip { + gxClipRect.intersect(GxRect(grect.pos, GxPoint(grect.x1-1, grect.y1))); + gxDrawTextUtf(grect.x0+1, grect.y0+y, mItems[idx], clr); + }; + } + ++idx; + y += gxTextHeightUtf; + } + } + + override bool onKey (KeyEvent event) { + if (!event.pressed || !isFocused) return super.onKey(event); + if (event == "Up") { curidx = curidx-1; return true; } + if (event == "Down") { curidx = curidx+1; return true; } + if (event == "Home") { curidx = 0; return true; } + if (event == "End") { curidx = cast(int)(mItems.length-1); return true; } + if (event == "PageUp") { + makeCursorVisible(); + if (curidx > mTopIdx) { + curidx = mTopIdx; + } else { + curidx = curidx-(visibleItemsCount-1); + } + return true; + } + if (event == "PageDown") { + makeCursorVisible(); + int icnt = visibleItemsCount-1; + if (icnt) { + if (mTopIdx+icnt < curidx) { + curidx = mTopIdx+icnt; + } else { + curidx = curidx+icnt; + } + } + return true; + } + return super.onKey(event); + } + + override bool onMouse (MouseEvent event) { + if (!isFocused) return super.onMouse(event); + if (GxPoint(event.x, event.y).inside(rect.size)) { + int mx = event.x, my = event.y; + makeCursorVisible(); + int idx = mTopIdx+my/gxTextHeightUtf; + if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.left) { + if (curidx == idx) doAction(); else curidx = idx; + } else if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.wheelUp) { + curidx = curidx-1; + } else if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.wheelDown) { + curidx = curidx+1; + } + return true; + } + return super.onMouse(event); + } +} diff --git a/egra/package.d b/egra/package.d new file mode 100644 index 0000000..71cdfe6 --- /dev/null +++ b/egra/package.d @@ -0,0 +1,22 @@ +/* + * Simple Framebuffer Gfx/GUI lib + * + * coded by Ketmar // Invisible Vector + * Understanding is not required. Only obedience. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License ONLY. + * + * 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, see . + */ +module iv.egra /*is aliced*/; + +public import iv.egra.gfx; +public import iv.egra.gui; diff --git a/egra/test.d b/egra/test.d new file mode 100644 index 0000000..2c45887 --- /dev/null +++ b/egra/test.d @@ -0,0 +1,638 @@ +/* E-Mail Client + * coded by Ketmar // Invisible Vector + * Understanding is not required. Only obedience. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License ONLY. + * + * 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, see . + */ +module test /*is aliced*/; + +import arsd.simpledisplay; + +import iv.cmdcon; +import iv.cmdcongl; +import iv.strex; +import iv.utfutil; +import iv.vfs.io; +import iv.vfs.util; + +import iv.egra; + + +// ////////////////////////////////////////////////////////////////////////// // +static immutable string TestStyle = ` +MainPaneWindow { + grouplist-divline: white; + grouplist-back: #222; + + threadlist-divline: white; + threadlist-back: #222; +} +`; + + +// ////////////////////////////////////////////////////////////////////////// // +__gshared ubyte vbufNewScale = 1; // new window scale +__gshared int lastWinWidth, lastWinHeight; +__gshared bool vbufVSync = false; +__gshared bool vbufEffVSync = false; + + +// ////////////////////////////////////////////////////////////////////////// // +public class DoConsoleCommandsEvent {} +__gshared DoConsoleCommandsEvent evDoConCommands; + +shared static this () { + evDoConCommands = new DoConsoleCommandsEvent(); +} + +public void postDoConCommands () { if (vbwin !is null && !vbwin.eventQueued!DoConsoleCommandsEvent) vbwin.postEvent(evDoConCommands); } + + +// ////////////////////////////////////////////////////////////////////////// // +public class TitlerWindow : SubWindow { +public: + LineEditWidget edtTitle; + LineEditWidget fromName; + LineEditWidget fromMail; + +protected: + string name; + string mail; + string folder; + string title; + + bool delegate (const(char)[] name, const(char)[] mail, const(char)[] folder, const(char)[] title) onSelected; + + override void createWidgets () { + fromName = new LineEditWidget(rootWidget, "Name:"); + fromName.flex = 1; + + fromMail = new LineEditWidget(rootWidget, "Mail:"); + fromMail.flex = 1; + + edtTitle = new LineEditWidget(rootWidget, "Title:"); + edtTitle.flex = 1; + + fromName.str = name; + fromMail.str = mail; + edtTitle.str = title; + + int twdt = fromName.titwdt; + if (twdt < fromMail.titwdt) twdt = fromMail.titwdt; + if (twdt < edtTitle.titwdt) twdt = edtTitle.titwdt; + fromName.titwdt = twdt; + fromMail.titwdt = twdt; + edtTitle.titwdt = twdt; + + relayoutResize(); + centerWindow(); + + edtTitle.focus(); + } + + this (string aname, string amail, string afolder, string atitle) { + string caption = "Title for "~aname~" <"~amail~">"; + name = aname; + mail = amail; + folder = afolder; + title = atitle; + super(caption); + } + + override bool onKeyBubble (KeyEvent event) { + if (event.pressed) { + if (event == "Escape" || event == "C-Q") { close(); return true; } + if (event == "Enter") { + if (onSelected !is null) { + if (!onSelected(fromName.str, fromMail.str, folder, edtTitle.str)) return true; + } + close(); + return true; + } + } + return super.onKeyBubble(event); + } +} + + +// ////////////////////////////////////////////////////////////////////////// // +public class TagOptionsWindow : SubWindow { + LabelWidget optPath; // real path + LineEditWidget optMonthes; + CheckboxWidget optThreaded; + CheckboxWidget optAttaches; + string tagname; + + void delegate (const(char)[] tagname) onUpdated; + + this (string atagname) { + tagname = atagname; + string caption = "options for '"~atagname~"'"; + super(caption); + } + + override void createWidgets () { + optPath = new LabelWidget(rootWidget, "", LabelWidget.HAlign.Center); + optPath.flex = 1; + + optMonthes = new LineEditWidget(rootWidget, "Monthes:"); + optMonthes.flex = 1; + optMonthes.width = optMonthes.titwdt+64; + + optThreaded = new CheckboxWidget(rootWidget, "&Threaded"); + optThreaded.flex = 1; + + optAttaches = new CheckboxWidget(rootWidget, "&Attaches"); + optAttaches.flex = 1; + + optMonthes.focus(); + + optPath.text = "booPath"; + optMonthes.str = "666"; + + optThreaded.enabled = false; + optAttaches.enabled = true; + + optMonthes.killTextOnChar = true; + + relayoutResize(); + centerWindow(); + } + + override bool onKeyBubble (KeyEvent event) { + if (event.pressed) { + if (event == "Escape" || event == "C-Q") { close(); return true; } + if (event == "Enter") { + if (onUpdated !is null) onUpdated(tagname); + close(); + return true; + } + } + return super.onKeyBubble(event); + } +} + +// ////////////////////////////////////////////////////////////////////////// // +void initConsole () { + //conRegVar!vbufNewScale(1, 4, "v_scale", "window scale: [1..3]"); + + conRegVar!bool("v_vsync", "sync to video refresh rate?", + (ConVarBase self) => vbufVSync, + (ConVarBase self, bool nv) { + static if (EGfxOpenGLBackend) { + if (vbufVSync != nv) { + vbufVSync = nv; + postScreenRepaint(); + } + } + }, + ); + + + // //////////////////////////////////////////////////////////////////// // + conRegFunc!(() { + import core.memory : GC; + conwriteln("starting GC collection..."); + GC.collect(); + GC.minimize(); + conwriteln("GC collection complete."); + })("gc_collect", "force GC collection cycle"); + + + // //////////////////////////////////////////////////////////////////// // + conRegFunc!(() { + auto qww = new YesNoWindow("Quit?", "Do you really want to quit?", true); + qww.onYes = () { concmd("quit"); }; + qww.addModal(); + })("quit_prompt", "quit with prompt"); + + conRegFunc!(() { + new TitlerWindow("name", "mail", "folder", "title"); + })("window_titler", "titler window test"); + + conRegFunc!(() { + new TagOptionsWindow("xtag"); + })("window_tagoptions", "tag options window test"); +} + + +// ////////////////////////////////////////////////////////////////////////// // +__gshared MainPaneWindow mainPane; + + +final class MainPaneWindow : SubWindow { + ProgressBarWidget pbar; + + this () { + super(null, GxPoint(0, 0), GxSize(screenWidth, screenHeight)); + mType = Type.OnBottom; + + pbar = new ProgressBarWidget(rootWidget, "progress bar"); + pbar.setMinMax(0, 100); + pbar.width = clientWidth-64; + pbar.current = 50; + pbar.posy = clientHeight-pbar.height-8; + pbar.posx = (clientWidth-pbar.width)/2; + + add(); + } + + // //////////////////////////////////////////////////////////////////// // + override void onPaint () { + if (closed) return; + + enum guiGroupListWidth = 128; + enum guiThreadListHeight = 520; + + gxFillRect(0, 0, guiGroupListWidth, screenHeight, getColor("grouplist-back")); + gxVLine(guiGroupListWidth, 0, screenHeight, getColor("grouplist-divline")); + + gxFillRect(guiGroupListWidth+1, 0, screenWidth, guiThreadListHeight, getColor("threadlist-back")); + gxHLine(guiGroupListWidth+1, guiThreadListHeight, screenWidth, getColor("threadlist-divline")); + + version(test_round_rect) { + gxClipReset(); + gxFillRoundedRect(lastMouseX-16, lastMouseY-16, 128, 96, rrad, /*gxSolidWhite*/gxRGBA!(0, 255, 0, 127)); + //gxDrawRoundedRect(lastMouseX-16, lastMouseY-16, 128, 96, rrad, gxRGBA!(0, 255, 0, 127)); + } + + drawWidgets(); + } + + version(test_round_rect) { + int rrad = 16; + } + + override bool onKeyBubble (KeyEvent event) { + if (event.pressed) { + version(test_round_rect) { + if (event == "Plus") { ++rrad; return true; } + if (event == "Minus") { --rrad; return true; } + } + if (event == "C-Q") { concmd("quit_prompt"); return true; } + if (event == "1") { concmd("window_titler"); return true; } + if (event == "2") { concmd("window_tagoptions"); return true; } + //if (dbg_dump_keynames) conwriteln("key: ", event.toStr, ": ", event.modifierState&ModifierState.windows); + //foreach (const ref kv; mainAppKeyBindings.byKeyValue) if (event == kv.key) concmd(kv.value); + } + return false; + } + + // returning `false` to avoid screen rebuilding by dispatcher + override bool onMouseBubble (MouseEvent event) { + //FIXME: use window coordinates + int mx, my; + event.mouse2xy(mx, my); + // button press + if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.left) { + } + // wheel + if (event.type == MouseEventType.buttonPressed && (event.button == MouseButton.wheelUp || event.button == MouseButton.wheelDown)) { + } + // button release + if (event.type == MouseEventType.buttonReleased && event.button == MouseButton.left) { + } + if (event.type == MouseEventType.motion) { + //auto uidx = findUrlIndexAt(mx, my); + //if (uidx != lastUrlIndex) { lastUrlIndex = uidx; postScreenRebuild(); return false; } + } + + if (event.type == MouseEventType.buttonPressed || event.type == MouseEventType.buttonReleased) { + postScreenRebuild(); + } else { + // for OpenGL, this rebuilds the whole screen anyway + postScreenRepaint(); + } + + return false; + } +} + + +// ////////////////////////////////////////////////////////////////////////// // +// call this from both backend handlers +void egfxRepaint (SimpleWindow w, immutable bool fromGLHandler=false) { + if (w is null || w.closed) return; + + bool resizeWin = false; + + { + consoleLock(); + scope(exit) consoleUnlock(); + + if (!conQueueEmpty()) postDoConCommands(); + + if (vbufNewScale != screenEffScale) { + // window scale changed + resizeWin = true; + } + if (vbufEffVSync != vbufVSync) { + vbufEffVSync = vbufVSync; + w.vsync = vbufEffVSync; + } + } + + if (resizeWin) { + w.resize(screenWidthScaled, screenHeightScaled); + glconResize(screenWidthScaled, screenHeightScaled); + //vglResizeBuffer(screenWidth, screenHeight, vbufNewScale); + } + + gxClipReset(); + gxClearScreen(0); + paintSubWindows(); + vglUpdateTexture(); // this does nothing for X11, but required for OpenGL + + static if (EGfxOpenGLBackend) { + if (!fromGLHandler) w.setAsCurrentOpenGlContext(); + } + scope(exit) { + static if (EGfxOpenGLBackend) { + if (!fromGLHandler) w.releaseCurrentOpenGlContext(); + } + } + + static if (EGfxOpenGLBackend) { + vglBlitTexture(); + } else { + vglBlitTexture(w); + } + + if (vArrowTextureId) { + if (isMouseVisible) { + int px = lastMouseX; + int py = lastMouseY; + static if (EGfxOpenGLBackend) { + glColor4f(1, 1, 1, mouseAlpha); + } + vglBlitArrow(px, py); + } + } + + static if (EGfxOpenGLBackend) { + glconDrawWindow = null; + glconDrawDirect = false; + glconDraw(); + } else { + { + auto painter = w.draw(); + vglBlitTexture(w); + glconDrawWindow = w; + glconDrawDirect = false; + glconDraw(); + glconDrawWindow = null; + } + } + + if (isQuitRequested()) w.postEvent(new QuitEvent()); +} + + +void rebuildScreen (SimpleWindow w) { + if (w !is null && !w.closed && !w.hidden) { + static if (EGfxOpenGLBackend) { + w.redrawOpenGlSceneNow(); + } else { + egfxRepaint(w); + } + } +} + + +void repaintScreen (SimpleWindow w) { + if (w !is null && !w.closed && !w.hidden) { + static if (EGfxOpenGLBackend) { + w.redrawOpenGlSceneNow(); + } else { + static __gshared bool lastvisible = false; + bool curvisible = isConsoleVisible; + if (lastvisible != curvisible || curvisible) { + lastvisible = curvisible; + rebuildScreen(w); + return; + } + } + } +} + + +// ////////////////////////////////////////////////////////////////////////// // +void main (string[] args) { + defaultColorStyle.parseStyle(TestStyle); + + glconAllowOpenGLRender = false; + + sdpyWindowClass = "EGUITest"; + //glconShowKey = "M-Grave"; + + initConsole(); + + conProcessQueue(); + conProcessArgs!true(args); + + screenEffScale = vbufNewScale; + vbufEffVSync = vbufVSync; + + lastWinWidth = screenWidthScaled; + lastWinHeight = screenHeightScaled; + + static if (EGfxOpenGLBackend) { + vbwin = new SimpleWindow(lastWinWidth, lastWinHeight, "Chiroptera", OpenGlOptions.yes, Resizability.allowResizing); + vbwin.hideCursor(); + glconAllowOpenGLRender = true; + } else { + vbwin = new SimpleWindow(lastWinWidth, lastWinHeight, "Chiroptera", OpenGlOptions.no, Resizability.allowResizing); + } + + vbwin.onFocusChange = delegate (bool focused) { + vbfocused = focused; + if (!focused) { + lastMouseButton = 0; + eguiLostGlobalFocus(); + } + }; + + vbwin.windowResized = delegate (int wdt, int hgt) { + // TODO: fix gui sizes + if (vbwin.closed) return; + + if (lastWinWidth == wdt && lastWinHeight == hgt) return; + glconResize(wdt, hgt); + + if (wdt < vbufNewScale*32) wdt = vbufNewScale; + if (hgt < vbufNewScale*32) hgt = vbufNewScale; + int newwdt = (wdt+vbufNewScale-1)/vbufNewScale; + int newhgt = (hgt+vbufNewScale-1)/vbufNewScale; + + lastWinWidth = wdt; + lastWinHeight = hgt; + + vglResizeBuffer(newwdt, newhgt, vbufNewScale); + + mouseMoved(); + + rebuildScreen(vbwin); + }; + + vbwin.addEventListener((DoConsoleCommandsEvent evt) { + bool sendAnother = false; + bool prevVisible = isConsoleVisible; + { + consoleLock(); + scope(exit) consoleUnlock(); + conProcessQueue(); + sendAnother = !conQueueEmpty(); + } + if (sendAnother) postDoConCommands(); + if (vbwin.closed) return; + if (isQuitRequested) { vbwin.close(); return; } + if (prevVisible || isConsoleVisible) postScreenRepaintDelayed(); + }); + + vbwin.addEventListener((HideMouseEvent evt) { + if (vbwin.closed) return; + if (isQuitRequested) { vbwin.close(); return; } + if (repostHideMouse) { + repaintScreen(vbwin); + } + }); + + vbwin.addEventListener((ScreenRebuildEvent evt) { + if (vbwin.closed) return; + if (isQuitRequested) { vbwin.close(); return; } + rebuildScreen(vbwin); + if (isConsoleVisible) postScreenRepaintDelayed(); + }); + + vbwin.addEventListener((ScreenRepaintEvent evt) { + if (vbwin.closed) return; + if (isQuitRequested) { vbwin.close(); return; } + repaintScreen(vbwin); + if (isConsoleVisible) postScreenRepaintDelayed(); + }); + + vbwin.addEventListener((CursorBlinkEvent evt) { + if (vbwin.closed) return; + rebuildScreen(vbwin); + }); + + vbwin.addEventListener((QuitEvent evt) { + if (vbwin.closed) return; + if (isQuitRequested) { vbwin.close(); return; } + vbwin.close(); + }); + + + vbwin.redrawOpenGlScene = delegate () { + if (vbwin.closed) return; + egfxRepaint(vbwin, fromGLHandler:true); + }; + + static if (is(typeof(&vbwin.closeQuery))) { + vbwin.closeQuery = delegate () { concmd("quit"); postDoConCommands(); }; + } + + void firstTimeInit () { + static bool firstTimeInited = false; + if (firstTimeInited) return; + firstTimeInited = true; + + static if (EGfxOpenGLBackend) { + import iv.glbinds; + vbwin.setAsCurrentOpenGlContext(); + vbwin.vsync = vbufEffVSync; + } + vbufEffVSync = vbufVSync; + + vglResizeBuffer(screenWidth, screenHeight); + vglCreateArrowTexture(); + + glconInit(screenWidthScaled, screenHeightScaled); + + rebuildScreen(vbwin); + } + + vbwin.visibleForTheFirstTime = delegate () { + firstTimeInit(); + }; + + mainPane = new MainPaneWindow(); + + postScreenRebuild(); + repostHideMouse(); + + vbwin.eventLoop(1000*10, + delegate () { + scope(exit) if (!conQueueEmpty()) postDoConCommands(); + if (vbwin.closed) return; + { + consoleLock(); + scope(exit) consoleUnlock(); + conProcessQueue(); + } + if (isQuitRequested) { vbwin.close(); return; } + }, + delegate (KeyEvent event) { + scope(exit) if (!conQueueEmpty()) postDoConCommands(); + if (vbwin.closed) return; + if (isQuitRequested) { vbwin.close(); return; } + if (glconKeyEvent(event)) { + postScreenRepaint(); + return; + } + if ((event.modifierState&ModifierState.numLock) == 0) { + switch (event.key) { + case Key.Pad0: event.key = Key.Insert; break; + case Key.Pad1: event.key = Key.End; break; + case Key.Pad2: event.key = Key.Down; break; + case Key.Pad3: event.key = Key.PageDown; break; + case Key.Pad4: event.key = Key.Left; break; + //case Key.Pad5: event.key = Key.Insert; break; + case Key.Pad6: event.key = Key.Right; break; + case Key.Pad7: event.key = Key.Home; break; + case Key.Pad8: event.key = Key.Up; break; + case Key.Pad9: event.key = Key.PageUp; break; + case Key.PadEnter: event.key = Key.Enter; break; + case Key.PadDot: event.key = Key.Delete; break; + default: break; + } + } else { + if (event.key == Key.PadEnter) event.key = Key.Enter; + } + if (dispatchEvent(event)) return; + //postScreenRepaint(); // just in case + }, + delegate (MouseEvent event) { + scope(exit) if (!conQueueEmpty()) postDoConCommands(); + if (vbwin.closed) return; + lastMouseXUnscaled = event.x; + lastMouseYUnscaled = event.y; + if (event.type == MouseEventType.buttonPressed) lastMouseButton |= event.button; + else if (event.type == MouseEventType.buttonReleased) lastMouseButton &= ~event.button; + mouseMoved(); + if (dispatchEvent(event)) return; + }, + delegate (dchar ch) { + if (vbwin.closed) return; + scope(exit) if (!conQueueEmpty()) postDoConCommands(); + if (glconCharEvent(ch)) { + postScreenRepaint(); + return; + } + if (dispatchEvent(ch)) return; + }, + ); + + flushGui(); + conProcessQueue(int.max/4); +} -- 2.11.4.GIT