//----------------------------------------------------------------------------- // Helper functions for the text-based browser window. // // Copyright 2008-2013 Jonathan Westhues. //----------------------------------------------------------------------------- #include "solvespace.h" namespace SolveSpace { class Button { public: virtual std::string Tooltip() = 0; virtual void Draw(UiCanvas *uiCanvas, int x, int y, bool asHovered) = 0; virtual int AdvanceWidth() = 0; virtual void Click() = 0; }; class SpacerButton : public Button { public: std::string Tooltip() override { return ""; } void Draw(UiCanvas *uiCanvas, int x, int y, bool asHovered) override { // Draw a darker-grey spacer in between the groups of icons. uiCanvas->DrawRect(x, x + 4, y, y - 24, /*fillColor=*/{ 45, 45, 45, 255 }, /*outlineColor=*/{}); } int AdvanceWidth() override { return 12; } void Click() override {} }; class ShowHideButton : public Button { public: bool *variable; std::string tooltip; std::string iconName; std::shared_ptr icon; ShowHideButton(bool *variable, std::string iconName, std::string tooltip) : variable(variable), tooltip(tooltip), iconName(iconName) {} std::string Tooltip() override { return ((*variable) ? "Hide " : "Show ") + tooltip; } void Draw(UiCanvas *uiCanvas, int x, int y, bool asHovered) override { if(icon == NULL) { icon = LoadPng("icons/text-window/" + iconName + ".png"); } uiCanvas->DrawPixmap(icon, x, y - 24); if(asHovered) { uiCanvas->DrawRect(x - 2, x + 26, y + 2, y - 26, /*fillColor=*/{ 255, 255, 0, 75 }, /*outlineColor=*/{}); } if(!*(variable)) { int s = 0, f = 24; RgbaColor color = { 255, 0, 0, 150 }; uiCanvas->DrawLine(x+s, y-s, x+f, y-f, color, 2); uiCanvas->DrawLine(x+s, y-f, x+f, y-s, color, 2); } } int AdvanceWidth() override { return 32; } void Click() override { SS.GW.ToggleBool(variable); } }; class FacesButton : public ShowHideButton { public: FacesButton() : ShowHideButton(&(SS.GW.showFaces), "faces", "") {} std::string Tooltip() override { if(*variable) { return "Don't make faces selectable with mouse"; } else { return "Make faces selectable with mouse"; } } }; class OccludedLinesButton : public Button { public: std::shared_ptr visibleIcon; std::shared_ptr stippledIcon; std::shared_ptr invisibleIcon; std::string Tooltip() override { switch(SS.GW.drawOccludedAs) { case GraphicsWindow::DrawOccludedAs::INVISIBLE: return "Stipple occluded lines"; case GraphicsWindow::DrawOccludedAs::STIPPLED: return "Draw occluded lines"; case GraphicsWindow::DrawOccludedAs::VISIBLE: return "Don't draw occluded lines"; default: ssassert(false, "Unexpected mode"); } } void Draw(UiCanvas *uiCanvas, int x, int y, bool asHovered) override { if(visibleIcon == NULL) { visibleIcon = LoadPng("icons/text-window/occluded-visible.png"); } if(stippledIcon == NULL) { stippledIcon = LoadPng("icons/text-window/occluded-stippled.png"); } if(invisibleIcon == NULL) { invisibleIcon = LoadPng("icons/text-window/occluded-invisible.png"); } std::shared_ptr icon; switch(SS.GW.drawOccludedAs) { case GraphicsWindow::DrawOccludedAs::INVISIBLE: icon = invisibleIcon; break; case GraphicsWindow::DrawOccludedAs::STIPPLED: icon = stippledIcon; break; case GraphicsWindow::DrawOccludedAs::VISIBLE: icon = visibleIcon; break; } uiCanvas->DrawPixmap(icon, x, y - 24); if(asHovered) { uiCanvas->DrawRect(x - 2, x + 26, y + 2, y - 26, /*fillColor=*/{ 255, 255, 0, 75 }, /*outlineColor=*/{}); } } int AdvanceWidth() override { return 32; } void Click() override { switch(SS.GW.drawOccludedAs) { case GraphicsWindow::DrawOccludedAs::INVISIBLE: SS.GW.drawOccludedAs = GraphicsWindow::DrawOccludedAs::STIPPLED; break; case GraphicsWindow::DrawOccludedAs::STIPPLED: SS.GW.drawOccludedAs = GraphicsWindow::DrawOccludedAs::VISIBLE; break; case GraphicsWindow::DrawOccludedAs::VISIBLE: SS.GW.drawOccludedAs = GraphicsWindow::DrawOccludedAs::INVISIBLE; break; } SS.GenerateAll(); InvalidateGraphics(); SS.ScheduleShowTW(); } }; static SpacerButton spacerButton; static ShowHideButton workplanesButton = { &(SS.GW.showWorkplanes), "workplane", "workplanes from inactive groups" }; static ShowHideButton normalsButton = { &(SS.GW.showNormals), "normal", "normals" }; static ShowHideButton pointsButton = { &(SS.GW.showPoints), "point", "points" }; static ShowHideButton constraintsButton = { &(SS.GW.showConstraints), "constraint", "constraints and dimensions" }; static FacesButton facesButton; static ShowHideButton shadedButton = { &(SS.GW.showShaded), "shaded", "shaded view of solid model" }; static ShowHideButton edgesButton = { &(SS.GW.showEdges), "edges", "edges of solid model" }; static ShowHideButton outlinesButton = { &(SS.GW.showOutlines), "outlines", "outline of solid model" }; static ShowHideButton meshButton = { &(SS.GW.showMesh), "mesh", "triangle mesh of solid model" }; static OccludedLinesButton occludedLinesButton; static Button *buttons[] = { &workplanesButton, &normalsButton, &pointsButton, &constraintsButton, &facesButton, &spacerButton, &shadedButton, &edgesButton, &outlinesButton, &meshButton, &spacerButton, &occludedLinesButton, }; const TextWindow::Color TextWindow::fgColors[] = { { 'd', RGBi(255, 255, 255) }, { 'l', RGBi(100, 100, 255) }, { 't', RGBi(255, 200, 0) }, { 'h', RGBi( 90, 90, 90) }, { 's', RGBi( 40, 255, 40) }, { 'm', RGBi(200, 200, 0) }, { 'r', RGBi( 0, 0, 0) }, { 'x', RGBi(255, 20, 20) }, { 'i', RGBi( 0, 255, 255) }, { 'g', RGBi(160, 160, 160) }, { 'b', RGBi(200, 200, 200) }, { 0, RGBi( 0, 0, 0) } }; const TextWindow::Color TextWindow::bgColors[] = { { 'd', RGBi( 0, 0, 0) }, { 't', RGBi( 34, 15, 15) }, { 'a', RGBi( 25, 25, 25) }, { 'r', RGBi(255, 255, 255) }, { 0, RGBi( 0, 0, 0) } }; void TextWindow::MakeColorTable(const Color *in, float *out) { int i; for(i = 0; in[i].c != 0; i++) { int c = in[i].c; ssassert(c >= 0 && c <= 255, "Unexpected color index"); out[c*3 + 0] = in[i].color.redF(); out[c*3 + 1] = in[i].color.greenF(); out[c*3 + 2] = in[i].color.blueF(); } } void TextWindow::Init() { ClearSuper(); } void TextWindow::ClearSuper() { HideEditControl(); // Cannot use *this = {} here because TextWindow instances // are 2.4MB long; this causes stack overflows in prologue // when built with MSVC, even with optimizations. memset(this, 0, sizeof(*this)); MakeColorTable(fgColors, fgColorTable); MakeColorTable(bgColors, bgColorTable); ClearScreen(); Show(); } void TextWindow::HideEditControl() { editControl.colorPicker.show = false; HideTextEditControl(); } void TextWindow::ShowEditControl(int col, const std::string &str, int halfRow) { if(halfRow < 0) halfRow = top[hoveredRow]; editControl.halfRow = halfRow; editControl.col = col; int x = LEFT_MARGIN + CHAR_WIDTH*col; int y = (halfRow - SS.TW.scrollPos)*(LINE_HEIGHT/2); ShowTextEditControl(x, y + 18, str); } void TextWindow::ShowEditControlWithColorPicker(int col, RgbaColor rgb) { SS.ScheduleShowTW(); editControl.colorPicker.show = true; editControl.colorPicker.rgb = rgb; editControl.colorPicker.h = 0; editControl.colorPicker.s = 0; editControl.colorPicker.v = 1; ShowEditControl(col, ssprintf("%.2f, %.2f, %.2f", rgb.redF(), rgb.greenF(), rgb.blueF())); } void TextWindow::ClearScreen() { int i, j; for(i = 0; i < MAX_ROWS; i++) { for(j = 0; j < MAX_COLS; j++) { text[i][j] = ' '; meta[i][j].fg = 'd'; meta[i][j].bg = 'd'; meta[i][j].link = NOT_A_LINK; } top[i] = i*2; } rows = 0; } void TextWindow::Printf(bool halfLine, const char *fmt, ...) { va_list vl; va_start(vl, fmt); if(rows >= MAX_ROWS) return; int r, c; r = rows; top[r] = (r == 0) ? 0 : (top[r-1] + (halfLine ? 3 : 2)); rows++; for(c = 0; c < MAX_COLS; c++) { text[r][c] = ' '; meta[r][c].link = NOT_A_LINK; } char fg = 'd'; char bg = 'd'; RgbaColor bgRgb = RGBi(0, 0, 0); int link = NOT_A_LINK; uint32_t data = 0; LinkFunction *f = NULL, *h = NULL; c = 0; while(*fmt) { char buf[1024]; if(*fmt == '%') { fmt++; if(*fmt == '\0') goto done; strcpy(buf, ""); switch(*fmt) { case 'd': { int v = va_arg(vl, int); sprintf(buf, "%d", v); break; } case 'x': { unsigned int v = va_arg(vl, unsigned int); sprintf(buf, "%08x", v); break; } case '@': { double v = va_arg(vl, double); sprintf(buf, "%.2f", v); break; } case '2': { double v = va_arg(vl, double); sprintf(buf, "%s%.2f", v < 0 ? "" : " ", v); break; } case '3': { double v = va_arg(vl, double); sprintf(buf, "%s%.3f", v < 0 ? "" : " ", v); break; } case '#': { double v = va_arg(vl, double); sprintf(buf, "%.3f", v); break; } case 's': { char *s = va_arg(vl, char *); memcpy(buf, s, min(sizeof(buf), strlen(s)+1)); break; } case 'c': { // 'char' is promoted to 'int' when passed through '...' int v = va_arg(vl, int); if(v == 0) { strcpy(buf, ""); } else { sprintf(buf, "%c", v); } break; } case 'E': fg = 'd'; // leave the background, though link = NOT_A_LINK; data = 0; f = NULL; h = NULL; break; case 'F': case 'B': { char cc = fmt[1]; // color code RgbaColor *rgbPtr = NULL; switch(cc) { case 0: goto done; // truncated directive case 'p': cc = (char)va_arg(vl, int); break; case 'z': rgbPtr = va_arg(vl, RgbaColor *); break; } if(*fmt == 'F') { fg = cc; } else { bg = cc; if(rgbPtr) bgRgb = *rgbPtr; } fmt++; break; } case 'L': if(fmt[1] == '\0') goto done; fmt++; if(*fmt == 'p') { link = va_arg(vl, int); } else { link = *fmt; } break; case 'f': f = va_arg(vl, LinkFunction *); break; case 'h': h = va_arg(vl, LinkFunction *); break; case 'D': { unsigned int v = va_arg(vl, unsigned int); data = (uint32_t)v; break; } case '%': strcpy(buf, "%"); break; } } else { utf8_iterator it2(fmt), it1 = it2++; strncpy(buf, fmt, it2 - it1); buf[it2 - it1] = '\0'; } for(utf8_iterator it(buf); *it; ++it) { for(size_t i = 0; i < BitmapFont::Builtin()->GetWidth(*it); i++) { if(c >= MAX_COLS) goto done; text[r][c] = (i == 0) ? *it : ' '; meta[r][c].fg = fg; meta[r][c].bg = bg; meta[r][c].bgRgb = bgRgb; meta[r][c].link = link; meta[r][c].data = data; meta[r][c].f = f; meta[r][c].h = h; c++; } } fmt++; } while(c < MAX_COLS) { meta[r][c].fg = fg; meta[r][c].bg = bg; meta[r][c].bgRgb = bgRgb; c++; } done: va_end(vl); } void TextWindow::Show() { if(SS.GW.pending.operation == GraphicsWindow::Pending::NONE) SS.GW.ClearPending(); SS.GW.GroupSelection(); auto const &gs = SS.GW.gs; // Make sure these tests agree with test used to draw indicator line on // main list of groups screen. if(SS.GW.pending.description) { // A pending operation (that must be completed with the mouse in // the graphics window) will preempt our usual display. HideEditControl(); ShowHeader(false); Printf(false, ""); Printf(false, "%s", SS.GW.pending.description); Printf(true, "%Fl%f%Ll(cancel operation)%E", &TextWindow::ScreenUnselectAll); } else if((gs.n > 0 || gs.constraints > 0) && shown.screen != Screen::PASTE_TRANSFORMED) { if(edit.meaning != Edit::TTF_TEXT) HideEditControl(); ShowHeader(false); DescribeSelection(); } else { if(edit.meaning == Edit::TTF_TEXT) HideEditControl(); ShowHeader(true); switch(shown.screen) { default: shown.screen = Screen::LIST_OF_GROUPS; // fall through case Screen::LIST_OF_GROUPS: ShowListOfGroups(); break; case Screen::GROUP_INFO: ShowGroupInfo(); break; case Screen::GROUP_SOLVE_INFO: ShowGroupSolveInfo(); break; case Screen::CONFIGURATION: ShowConfiguration(); break; case Screen::STEP_DIMENSION: ShowStepDimension(); break; case Screen::LIST_OF_STYLES: ShowListOfStyles(); break; case Screen::STYLE_INFO: ShowStyleInfo(); break; case Screen::PASTE_TRANSFORMED: ShowPasteTransformed(); break; case Screen::EDIT_VIEW: ShowEditView(); break; case Screen::TANGENT_ARC: ShowTangentArc(); break; } } Printf(false, ""); // Make sure there's room for the color picker if(editControl.colorPicker.show) { int pickerHeight = 25; int halfRow = editControl.halfRow; if(top[rows-1] - halfRow < pickerHeight && rows < MAX_ROWS) { rows++; top[rows-1] = halfRow + pickerHeight; } } InvalidateText(); } void TextWindow::TimerCallback() { tooltippedButton = hoveredButton; InvalidateText(); } void TextWindow::DrawOrHitTestIcons(UiCanvas *uiCanvas, TextWindow::DrawOrHitHow how, double mx, double my) { int width, height; GetTextWindowSize(&width, &height); int x = 20, y = 33 + LINE_HEIGHT; y -= scrollPos*(LINE_HEIGHT/2); if(how == PAINT) { int top = y - 28, bot = y + 4; uiCanvas->DrawRect(0, width, top, bot, /*fillColor=*/{ 30, 30, 30, 255 }, /*outlineColor=*/{}); } Button *oldHovered = hoveredButton; if(how != PAINT) { hoveredButton = NULL; } for(Button *button : buttons) { if(how == PAINT) { button->Draw(uiCanvas, x, y, (button == hoveredButton)); } else if(mx > x - 2 && mx < x + 26 && my < y + 2 && my > y - 26) { // The mouse is hovered over this icon, so do the tooltip // stuff. if(button != tooltippedButton) { oldMousePos = Point2d::From(mx, my); } if(button != oldHovered || how == CLICK) { SetTimerFor(1000); } hoveredButton = button; if(how == CLICK) { button->Click(); } } x += button->AdvanceWidth(); } if(how != PAINT && hoveredButton != oldHovered) { InvalidateText(); } if(tooltippedButton && !tooltippedButton->Tooltip().empty()) { if(how == PAINT) { std::string tooltip = tooltippedButton->Tooltip(); int ox = (int)oldMousePos.x, oy = (int)oldMousePos.y - LINE_HEIGHT; ox += 3; oy -= 3; int tw = (tooltip.length() + 1) * (CHAR_WIDTH - 1); ox = min(ox, (width - 25) - tw); oy = max(oy, 5); uiCanvas->DrawRect(ox, ox+tw, oy, oy+LINE_HEIGHT, /*fillColor=*/{ 255, 255, 150, 255 }, /*outlineColor=*/{ 0, 0, 0, 255 }); uiCanvas->DrawBitmapText(tooltip, ox+5, oy-3+LINE_HEIGHT, { 0, 0, 0, 255 }); } else { if(!hoveredButton || (hoveredButton != tooltippedButton)) { tooltippedButton = NULL; InvalidateGraphics(); } // And if we're hovered, then we've set a timer that will cause // us to show the tool tip later. } } } //---------------------------------------------------------------------------- // Given (x, y, z) = (h, s, v) in [0,6), [0,1], [0,1], return (x, y, z) = // (r, g, b) all in [0, 1]. //---------------------------------------------------------------------------- Vector TextWindow::HsvToRgb(Vector hsv) { if(hsv.x >= 6) hsv.x -= 6; Vector rgb; double hmod2 = hsv.x; while(hmod2 >= 2) hmod2 -= 2; double x = (1 - fabs(hmod2 - 1)); if(hsv.x < 1) { rgb = Vector::From(1, x, 0); } else if(hsv.x < 2) { rgb = Vector::From(x, 1, 0); } else if(hsv.x < 3) { rgb = Vector::From(0, 1, x); } else if(hsv.x < 4) { rgb = Vector::From(0, x, 1); } else if(hsv.x < 5) { rgb = Vector::From(x, 0, 1); } else { rgb = Vector::From(1, 0, x); } double c = hsv.y*hsv.z; double m = 1 - hsv.z; rgb = rgb.ScaledBy(c); rgb = rgb.Plus(Vector::From(m, m, m)); return rgb; } std::shared_ptr TextWindow::HsvPattern2d(int w, int h) { std::shared_ptr pixmap = Pixmap::Create(Pixmap::Format::RGB, w, h); for(size_t j = 0; j < pixmap->height; j++) { size_t p = pixmap->stride * j; for(size_t i = 0; i < pixmap->width; i++) { Vector hsv = Vector::From(6.0*i/(pixmap->width-1), 1.0*j/(pixmap->height-1), 1); Vector rgb = HsvToRgb(hsv); rgb = rgb.ScaledBy(255); pixmap->data[p++] = (uint8_t)rgb.x; pixmap->data[p++] = (uint8_t)rgb.y; pixmap->data[p++] = (uint8_t)rgb.z; } } return pixmap; } std::shared_ptr TextWindow::HsvPattern1d(double hue, double sat, int w, int h) { std::shared_ptr pixmap = Pixmap::Create(Pixmap::Format::RGB, w, h); for(size_t i = 0; i < pixmap->height; i++) { size_t p = i * pixmap->stride; for(size_t j = 0; j < pixmap->width; j++) { Vector hsv = Vector::From(6*hue, sat, 1.0*(pixmap->width - 1 - j)/pixmap->width); Vector rgb = HsvToRgb(hsv); rgb = rgb.ScaledBy(255); pixmap->data[p++] = (uint8_t)rgb.x; pixmap->data[p++] = (uint8_t)rgb.y; pixmap->data[p++] = (uint8_t)rgb.z; } } return pixmap; } void TextWindow::ColorPickerDone() { RgbaColor rgb = editControl.colorPicker.rgb; EditControlDone(ssprintf("%.2f, %.2f, %.3f", rgb.redF(), rgb.greenF(), rgb.blueF()).c_str()); } bool TextWindow::DrawOrHitTestColorPicker(UiCanvas *uiCanvas, DrawOrHitHow how, bool leftDown, double x, double y) { bool mousePointerAsHand = false; if(how == HOVER && !leftDown) { editControl.colorPicker.picker1dActive = false; editControl.colorPicker.picker2dActive = false; } if(!editControl.colorPicker.show) return false; if(how == CLICK || (how == HOVER && leftDown)) InvalidateText(); static const RgbaColor BaseColor[12] = { RGBi(255, 0, 0), RGBi( 0, 255, 0), RGBi( 0, 0, 255), RGBi( 0, 255, 255), RGBi(255, 0, 255), RGBi(255, 255, 0), RGBi(255, 127, 0), RGBi(255, 0, 127), RGBi( 0, 255, 127), RGBi(127, 255, 0), RGBi(127, 0, 255), RGBi( 0, 127, 255), }; int width, height; GetTextWindowSize(&width, &height); int px = LEFT_MARGIN + CHAR_WIDTH*editControl.col; int py = (editControl.halfRow - SS.TW.scrollPos)*(LINE_HEIGHT/2); py += LINE_HEIGHT + 5; static const int WIDTH = 16, HEIGHT = 12; static const int PITCH = 18, SIZE = 15; px = min(px, width - (WIDTH*PITCH + 40)); int pxm = px + WIDTH*PITCH + 11, pym = py + HEIGHT*PITCH + 7; int bw = 6; if(how == PAINT) { uiCanvas->DrawRect(px, pxm+bw, py, pym+bw, /*fillColor=*/{ 50, 50, 50, 255 }, /*outlineColor=*/{}); uiCanvas->DrawRect(px+(bw/2), pxm+(bw/2), py+(bw/2), pym+(bw/2), /*fillColor=*/{ 0, 0, 0, 255 }, /*outlineColor=*/{}); } else { if(x < px || x > pxm+(bw/2) || y < py || y > pym+(bw/2)) { return false; } } px += (bw/2); py += (bw/2); int i, j; for(i = 0; i < WIDTH/2; i++) { for(j = 0; j < HEIGHT; j++) { Vector rgb; RgbaColor d; if(i == 0 && j < 8) { d = SS.modelColor[j]; rgb = Vector::From(d.redF(), d.greenF(), d.blueF()); } else if(i == 0) { double a = (j - 8.0)/3.0; rgb = Vector::From(a, a, a); } else { d = BaseColor[j]; rgb = Vector::From(d.redF(), d.greenF(), d.blueF()); if(i >= 2 && i <= 4) { double a = (i == 2) ? 0.2 : (i == 3) ? 0.3 : 0.4; rgb = rgb.Plus(Vector::From(a, a, a)); } if(i >= 5 && i <= 7) { double a = (i == 5) ? 0.7 : (i == 6) ? 0.4 : 0.18; rgb = rgb.ScaledBy(a); } } rgb = rgb.ClampWithin(0, 1); int sx = px + 5 + PITCH*(i + 8) + 4, sy = py + 5 + PITCH*j; if(how == PAINT) { uiCanvas->DrawRect(sx, sx+SIZE, sy, sy+SIZE, /*fillColor=*/RGBf(rgb.x, rgb.y, rgb.z), /*outlineColor=*/{}); } else if(how == CLICK) { if(x >= sx && x <= sx+SIZE && y >= sy && y <= sy+SIZE) { editControl.colorPicker.rgb = RGBf(rgb.x, rgb.y, rgb.z); ColorPickerDone(); } } else if(how == HOVER) { if(x >= sx && x <= sx+SIZE && y >= sy && y <= sy+SIZE) { mousePointerAsHand = true; } } } } int hxm, hym; int hx = px + 5, hy = py + 5; hxm = hx + PITCH*7 + SIZE; hym = hy + PITCH*2 + SIZE; if(how == PAINT) { uiCanvas->DrawRect(hx, hxm, hy, hym, /*fillColor=*/editControl.colorPicker.rgb, /*outlineColor=*/{}); } else if(how == CLICK) { if(x >= hx && x <= hxm && y >= hy && y <= hym) { ColorPickerDone(); } } else if(how == HOVER) { if(x >= hx && x <= hxm && y >= hy && y <= hym) { mousePointerAsHand = true; } } hy += PITCH*3; hxm = hx + PITCH*7 + SIZE; hym = hy + PITCH*1 + SIZE; // The one-dimensional thing to pick the color's value if(how == PAINT) { uiCanvas->DrawPixmap(HsvPattern1d(editControl.colorPicker.h, editControl.colorPicker.s, hxm-hx, hym-hy), hx, hy); int cx = hx+(int)((hxm-hx)*(1.0 - editControl.colorPicker.v)); uiCanvas->DrawLine(cx, hy, cx, hym, { 0, 0, 0, 255 }); } else if(how == CLICK || (how == HOVER && leftDown && editControl.colorPicker.picker1dActive)) { if(x >= hx && x <= hxm && y >= hy && y <= hym) { editControl.colorPicker.v = 1 - (x - hx)/(hxm - hx); Vector rgb = HsvToRgb(Vector::From( 6*editControl.colorPicker.h, editControl.colorPicker.s, editControl.colorPicker.v)); editControl.colorPicker.rgb = RGBf(rgb.x, rgb.y, rgb.z); editControl.colorPicker.picker1dActive = true; } } // and advance our vertical position hy += PITCH*2; hxm = hx + PITCH*7 + SIZE; hym = hy + PITCH*6 + SIZE; // Two-dimensional thing to pick a color by hue and saturation if(how == PAINT) { uiCanvas->DrawPixmap(HsvPattern2d(hxm-hx, hym-hy), hx, hy); int cx = hx+(int)((hxm-hx)*editControl.colorPicker.h), cy = hy+(int)((hym-hy)*editControl.colorPicker.s); uiCanvas->DrawLine(cx - 5, cy, cx + 4, cy, { 255, 255, 255, 255 }); uiCanvas->DrawLine(cx, cy - 5, cx, cy + 4, { 255, 255, 255, 255 }); } else if(how == CLICK || (how == HOVER && leftDown && editControl.colorPicker.picker2dActive)) { if(x >= hx && x <= hxm && y >= hy && y <= hym) { double h = (x - hx)/(hxm - hx), s = (y - hy)/(hym - hy); editControl.colorPicker.h = h; editControl.colorPicker.s = s; Vector rgb = HsvToRgb(Vector::From( 6*editControl.colorPicker.h, editControl.colorPicker.s, editControl.colorPicker.v)); editControl.colorPicker.rgb = RGBf(rgb.x, rgb.y, rgb.z); editControl.colorPicker.picker2dActive = true; } } SetMousePointerToHand(mousePointerAsHand); return true; } void TextWindow::Paint() { #if !defined(HEADLESS) int width, height; GetTextWindowSize(&width, &height); Camera camera = {}; camera.width = width; camera.height = height; camera.LoadIdentity(); camera.offset.x = -(double)camera.width / 2.0; camera.offset.y = -(double)camera.height / 2.0; OpenGl1Renderer canvas = {}; canvas.camera = camera; canvas.BeginFrame(); canvas.UpdateProjection(); UiCanvas uiCanvas = {}; uiCanvas.canvas = &canvas; uiCanvas.flip = true; halfRows = camera.height / (LINE_HEIGHT/2); int bottom = top[rows-1] + 2; scrollPos = min(scrollPos, bottom - halfRows); scrollPos = max(scrollPos, 0); // Let's set up the scroll bar first MoveTextScrollbarTo(scrollPos, top[rows - 1] + 1, halfRows); // Now paint the window. int r, c, a; for(a = 0; a < 2; a++) { for(r = 0; r < rows; r++) { int ltop = top[r]; if(ltop < (scrollPos-1)) continue; if(ltop > scrollPos+halfRows) break; for(c = 0; c < min((width/CHAR_WIDTH)+1, (int) MAX_COLS); c++) { int x = LEFT_MARGIN + c*CHAR_WIDTH; int y = (ltop-scrollPos)*(LINE_HEIGHT/2) + 4; int fg = meta[r][c].fg; int bg = meta[r][c].bg; // On the first pass, all the background quads; on the next // pass, all the foreground (i.e., font) quads. if(a == 0) { RgbaColor bgRgb = meta[r][c].bgRgb; int bh = LINE_HEIGHT, adj = 0; if(bg == 'z') { bh = CHAR_HEIGHT; adj += 2; } else { bgRgb = RgbaColor::FromFloat(bgColorTable[bg*3+0], bgColorTable[bg*3+1], bgColorTable[bg*3+2]); } if(bg != 'd') { // Move the quad down a bit, so that the descenders // still have the correct background. uiCanvas.DrawRect(x, x + CHAR_WIDTH, y + adj, y + adj + bh, /*fillColor=*/bgRgb, /*outlineColor=*/{}); } } else if(a == 1) { RgbaColor fgRgb = RgbaColor::FromFloat(fgColorTable[fg*3+0], fgColorTable[fg*3+1], fgColorTable[fg*3+2]); if(text[r][c] != ' ') { uiCanvas.DrawBitmapChar(text[r][c], x, y + CHAR_HEIGHT, fgRgb); } // If this is a link and it's hovered, then draw the // underline if(meta[r][c].link && meta[r][c].link != 'n' && (r == hoveredRow && c == hoveredCol)) { int cs = c, cf = c; while(cs >= 0 && meta[r][cs].link && meta[r][cs].f == meta[r][c].f && meta[r][cs].data == meta[r][c].data) { cs--; } cs++; while( meta[r][cf].link && meta[r][cf].f == meta[r][c].f && meta[r][cf].data == meta[r][c].data) { cf++; } // But don't underline checkboxes or radio buttons while(((text[r][cs] >= 0xe000 && text[r][cs] <= 0xefff) || text[r][cs] == ' ') && cs < cf) { cs++; } // Always use the color of the rightmost character // in the link, so that underline is consistent color fg = meta[r][cf-1].fg; fgRgb = RgbaColor::FromFloat(fgColorTable[fg*3+0], fgColorTable[fg*3+1], fgColorTable[fg*3+2]); int yp = y + CHAR_HEIGHT; uiCanvas.DrawLine(LEFT_MARGIN + cs*CHAR_WIDTH, yp, LEFT_MARGIN + cf*CHAR_WIDTH, yp, fgRgb); } } } } } // The line to indicate the column of radio buttons that indicates the // active group. SS.GW.GroupSelection(); auto const &gs = SS.GW.gs; // Make sure this test agrees with test to determine which screen is drawn if(!SS.GW.pending.description && gs.n == 0 && gs.constraints == 0 && shown.screen == Screen::LIST_OF_GROUPS) { int x = 29, y = 70 + LINE_HEIGHT; y -= scrollPos*(LINE_HEIGHT/2); RgbaColor color = RgbaColor::FromFloat(fgColorTable['t'*3+0], fgColorTable['t'*3+1], fgColorTable['t'*3+2]); uiCanvas.DrawLine(x, y, x, y+40, color); } // The header has some icons that are drawn separately from the text DrawOrHitTestIcons(&uiCanvas, PAINT, 0, 0); // And we may show a color picker for certain editable fields DrawOrHitTestColorPicker(&uiCanvas, PAINT, false, 0, 0); canvas.EndFrame(); canvas.Clear(); #endif } void TextWindow::MouseEvent(bool leftClick, bool leftDown, double x, double y) { if(TextEditControlIsVisible() || GraphicsEditControlIsVisible()) { if(DrawOrHitTestColorPicker(NULL, leftClick ? CLICK : HOVER, leftDown, x, y)) { return; } if(leftClick) { HideEditControl(); HideGraphicsEditControl(); } else { SetMousePointerToHand(false); } return; } DrawOrHitTestIcons(NULL, leftClick ? CLICK : HOVER, x, y); GraphicsWindow::Selection ps = SS.GW.hover; SS.GW.hover.Clear(); int prevHoveredRow = hoveredRow, prevHoveredCol = hoveredCol; hoveredRow = 0; hoveredCol = 0; // Find the corresponding character in the text buffer int c = (int)((x - LEFT_MARGIN) / CHAR_WIDTH); int hh = (LINE_HEIGHT)/2; y += scrollPos*hh; int r; for(r = 0; r < rows; r++) { if(y >= top[r]*hh && y <= (top[r]+2)*hh) { break; } } if(r >= 0 && c >= 0 && r < rows && c < MAX_COLS) { SetMousePointerToHand(false); hoveredRow = r; hoveredCol = c; const auto &item = meta[r][c]; if(leftClick) { if(item.link && item.f) { (item.f)(item.link, item.data); Show(); InvalidateGraphics(); } } else { if(item.link) { SetMousePointerToHand(true); if(item.h) { (item.h)(item.link, item.data); } } else { SetMousePointerToHand(false); } } } if((!ps.Equals(&(SS.GW.hover))) || prevHoveredRow != hoveredRow || prevHoveredCol != hoveredCol) { InvalidateGraphics(); InvalidateText(); } } void TextWindow::MouseLeave() { tooltippedButton = NULL; hoveredButton = NULL; hoveredRow = 0; hoveredCol = 0; InvalidateText(); } void TextWindow::ScrollbarEvent(int newPos) { if(TextEditControlIsVisible()) return; int bottom = top[rows-1] + 2; newPos = min(newPos, bottom - halfRows); newPos = max(newPos, 0); if(newPos != scrollPos) { scrollPos = newPos; MoveTextScrollbarTo(scrollPos, top[rows - 1] + 1, halfRows); InvalidateText(); } } }